Skip to Content
DevelopAgent to Agent Communication

Agent to Agent communication

See Agent communication for an overview of how agents call, wait for, and stream to each other.

Code-first approach

When defining an agent in code, this automatically defines a client interface for calling remote agents as well. This way an agent of an agent type can create other agents and invoke methods on them. The target agent can be of any agent type available for the caller agent in code.

The simplest way to achieve this is to keep all the agent types that need to call each other in the same component. In languages that separate an agent’s definition from its implementation, you can instead extract the definition to a shared library imported by both components.

Creating a client

The first step of calling a remote agent is creating a client for it by specifying which agent type it is and identifying the agent by providing values for its constructor parameters. If the agent identified by the parameters has not been existing yet, it is going to be created and its constructor is executed remotely. If it has been already created, the client just points to it and no action is taken until an agent method is called using it.

In the following example, we have two agents defined; a weather agent with a constructor parameter identifying a location, and another example agent creating and calling this weather agent.

import { z } from 'zod'; import { defineAgent, method } from '@golemcloud/golem-ts-sdk'; export const WeatherAgent = defineAgent({ name: 'WeatherAgent', id: { location: z.string() }, methods: { currentWeather: method({ input: {}, returns: z.string(), description: "Gets the current weather", }), }, }); export const WeatherAgentImpl = WeatherAgent.implement({ init: () => ({}), methods: { currentWeather() { // ... return ""; }, }, }); export const ExampleAgent = defineAgent({ name: 'ExampleAgent', id: {}, methods: { run: method({ input: {}, returns: z.string() }), }, }); export const ExampleAgentImpl = ExampleAgent.implement({ init: () => ({}), methods: { async run() { const weatherInLondon = WeatherAgent.client.get({ location: "London" }); // ... return ""; }, }, });

Here weatherInLondon is the client for calling a remote WeatherAgent, obtained from WeatherAgent.client with the target agent’s id record - it exposes the agent methods, such as currentWeather as its own methods.

Calling an agent in another component

When the caller and target agents belong to different components, use the pattern for your language:

Export a plain shared schema/spec object. The provider calls defineAgent(weatherSpec).implement(...); the caller uses defineAgentClient(weatherSpec).client.get(...). Import defineAgentClient from @golemcloud/golem-ts-sdk; unlike defineAgent, it does not register the type in the caller component.

Phantom agents

Calling get gets or creates a durable agent identified by its constructor parameters. Durable agent types have a default instance and can have arbitrary phantom instances distinguished by an additional phantom ID UUID. Ephemeral clients expose only phantom factories.

When using agent IDs (for example in APIs or the CLI), phantom IDs are appended to the agent ID in square brackets:

  • Primary instance of weather-agent with parameter London:
    • weather-agent("London")
  • A phantom instance of the same agent:
    • weather-agent("London")[43d9d7ab-e1d7-47e2-a213-d635eaa317a2]

Phantom agents are created through Agent-to-Agent communication or forking. To create a new phantom agent via RPC, use the alternative client constructor method:

const { client: phantomAgent, agentId, phantomId } = WeatherAgent.client.newPhantom({ location: "London" });

If we know the phantom ID of an agent, we can create a client targeting an existing phantom agent:

const { client: phantomAgent, agentId, phantomId } = WeatherAgent.client.newPhantom({ location: "London" }); const samePhantomAgent = WeatherAgent.client.getPhantom({ location: "London" }, phantomId);

Phantom agents through TypeScript reflection

When the target type is selected at runtime, its reflected client exposes the same phantom operations as a definition client:

import { getReflectedAgentType } from "@golemcloud/golem-ts-sdk"; const weatherType = getReflectedAgentType("WeatherAgent"); if (!weatherType) throw new Error("WeatherAgent is not registered"); if (weatherType.mode === "durable") { const phantom = weatherType.client.newPhantom({ location: "London", }); if (!("client" in phantom)) throw new Error("Expected a durable phantom"); const { client, agentId, phantomId } = phantom; const samePhantom = weatherType.client.getPhantom( { location: "London" }, phantomId, ); }

For an ephemeral reflected type, newPhantom returns the reflected client directly rather than a reusable { client, agentId, phantomId } wrapper. The final one-shot agent ID and idempotency key are returned by each invocation:

const requestType = getReflectedAgentType("RequestAgent"); if (!requestType || requestType.mode !== "ephemeral") { throw new Error("RequestAgent must be ephemeral"); } const request = requestType.client.newPhantom({ route: "summarize" }); if ("client" in request) throw new Error("unexpected durable phantom wrapper"); const result = await request.method("run").invoke({ text: "hello" }); console.log(result.metadata.agentId, result.metadata.idempotencyKey);

Ephemeral reflected types also expose getPhantom when the caller already has the phantom ID. That explicit handle does not make a final, already-invoked ephemeral agent ID reusable.

See Calling Agents with Runtime Reflection for discovery, schema inspection, concrete AgentId binding, and error handling.

Reflection and dynamic clients in Effect TypeScript

Effect TypeScript supports both schema-aware reflection and schema-native dynamic invocation. Reflection discovers a deployed type and validates JSON-shaped inputs:

import { Effect } from "effect" import { Reflection } from "@golemcloud/effect-golem" const reflected = Effect.scoped( Effect.gen(function* () { const type = yield* Reflection.getAgentType("WeatherAgent") if (type === undefined || type.mode !== "durable") return undefined const client = yield* type.client.get({ location: "London" }) const method = yield* client.method("currentWeather") return yield* method.invoke({}) }), )

When you already have an encoded durable agent identity and do not need discovery, parse it and bind a dynamic client. Dynamic methods accept native SchemaValueTree inputs, so the caller must supply values matching the deployed method contract:

import { AgentIdentity } from "@golemcloud/effect-golem" const dynamic = Effect.scoped( Effect.gen(function* () { const identity = yield* AgentIdentity.parse(encodedAgentId) const client = yield* identity.dynamicClient() return yield* client.method("currentWeather").invoke(inputTree) }), )

Calling a remote agent method

Once we have a client for a remote agent, it is possible to call its methods just as if it would be a local instance.

const currentWeather = await weatherInLondon.currentWeather();

Triggering execution of an agent method

It is possible to trigger the remote execution of an agent method without awaiting it. This is useful for spawning background tasks, for example.

To trigger an agent method and return immediately, use the trigger method exposed on each remote method in the client:

const remoteAgent = BackgroundTaskAgent.client.get({ jobId: backgroundJobId }); remoteAgent.runTask.trigger({ message: "hello", count: 1234 });

Scheduling an agent method

An advanced case of triggering the execution of an agent method is to schedule it to be executed at a later time.

Similar to .trigger, there is a .schedule method as well on each remote agent method in the client:

const remoteAgent = BackgroundTaskAgent.client.get({ jobId: backgroundJobId }); remoteAgent.runTask.schedule({ seconds: 60n, nanoseconds: 0 }, { message: "hello", count: 1234 });

Here the first parameter is a Datetime object, with seconds and nanoseconds fields representing the UNIX Epoch time when the method should be executed. In TypeScript, seconds is a bigint. The rest of the parameters are the same as for the original method.