Forking Agents
Explanation
Golem agents are single threaded. To achieve parallel execution, it is possible to spawn child agents and communicate with them using RPC, as described on the Agent to Agent communication page.
A simpler way is to use the fork API. The fork API consists of a single host function, defined as the following:
declare module 'golem:api/host@1.1.7' {
// ...
/**
* Indicates which agent the code is running on after `fork`
*/
export type ForkResult = "original" | "forked";
/**
* Forks the current agent at the current execution point. The new agent gets the `new-name` agent ID,
* and this agent continues running as well. The return value is going to be different in this agent and
* the forked agent.
*/
export function fork(newName: string): ForkResult;
}
When called, a new agent is created using the given name, with exactly the same state as the one that called the fork
function. The execution continues in both the original and the new agents, with a different fork-result
result value in each.
This is a low-level API that requires passing the new agent ID as a string. This ID has to be a valid agent-id, consisting of the agent type (same as the agent type of the current agent) and valid constructor parameters. However, the constructor parameters must contain an element distinguishing the new agent from the original one.
One simple way to do that is to use an extra parameter to the agent constructor that is not used by the agent itself, but is used to distinguish the new agent from the original one; it needs to have a clear default value (like 0, or empty string, etc.) to be able to find the root agent.
Future Golem versions will provide a more user-friendly, higher level API to implement this functionality.
Usage
Using this fork
function from a component that was created from Golem's built-in templates is straightforward because access to the Golem specific host functions is already set up.
The following code snippet demonstrates calling fork
and continuing on two different parallel branches based on its result value:
import { BaseAgent, agent } from '@golemcloud/golem-ts-sdk';
import { fork, type ForkResult } from "golem:api/host@1.1.7"
@agent()
class ExampleAgent extends BaseAgent {
name: string;
constructor(name: string, mode: "root" | "fork") {
super();
this.name = name;
}
run() {
switch (fork(`example-agent("${this.name}",fork)`)) {
case "original": {
// ...
break;
}
case "forked": {
// ...
break;
}
}
}
}
Join
Once the agent has been forked, the two agent instances are running simultaneously and independently. Golem promises provide a way to resynchronize the two agents.
The high level idea is the following:
- The original agent creates a Golem promise and stores the resulting
PromiseId
in a variable. - Then forks the new agent. Both the new and the old agents have the promise id.
- When the new agent is done with the work it was forked for, it completes the promise and includes some arbitrary payload it wants to send back to the original agent.
- The original agent, after doing some additional work in parallel to the forked one, can suspend its execution to wait for the promise to be completed. When the promise is completed, the original agent resumes execution and receives the payload sent by the forked agent.
The following code snippet demonstrates this pattern:
import { BaseAgent, agent } from '@golemcloud/golem-ts-sdk';
import { awaitPromise, completePromise, createPromise, PromiseId, fork, type ForkResult } from "golem:api/host@1.1.7"
@agent()
class ExampleAgent extends BaseAgent {
name: string;
constructor(name: string, mode: "root" | "fork") {
super();
this.name = name;
}
async run() {
const promiseId: PromiseId = createPromise();
switch (fork(`example-agent("${this.name}",fork)`)) {
case "original": {
const localResult = ...; // ... do some more work;
const rawForkResult: UInt8Array = await awaitPromise(promiseId);
const forkResult = JSON.parse(new TextDecoder().decode(rawForkResult));
// Merge localResult and forkResult and continue running
break;
}
case "forked": {
const result = ...; // do some work in parallel to the original agent
const rawResult: UInt8Array = new TextEncoder().encode(JSON.stringify(result));
completePromise(promiseId, rawResult);
// Stop execution
break;
}
}
}
}
Note that Golem Promises are NOT JavaScript Promises, and the API to create, complete and await them is currently blocking.