Skip to Content
How-To GuidesGoCalling Another Agent (RPC) in Go

Calling Another Agent (RPC) in Go

Overview

Agents talk to each other through the callee’s definition package. You get a typed client from Agent.Get(id) and call a method descriptor on it. The caller imports only the callee’s definition package (its ID, its method descriptors) — never its impl — so the two implementations never form a Go import cycle.

Calls are durable: results are recorded in the oplog and replayed after a restart rather than re-issued.

Steps

  1. Import the callee’s definition package (e.g. myapp/agents/ledger).
  2. Get a client for a specific instance: ledger.Agent.Get(ledger.ID{Region: "eu"}).
  3. Call a method descriptor on the client: ledger.Record.MustCall(client, ledger.RecordIn{Amount: 10}), or Call to get (Out, error) back.
  4. If the method returns a golem.Result, unwrap it (.MustOk() or inspect it).

Synchronous call

package impl import ( "myapp/agents/ledger" "myapp/agents/rpccaller" "github.com/golemcloud/golem/sdks/go/golem" ) type state struct{} var agent = rpccaller.Agent.Implement(func(rpccaller.ID) *state { return &state{} }) func init() { agent.Handle(rpccaller.Call, func(_ *golem.Context[state], in rpccaller.CallIn) int64 { // Get a client for the ledger instance identified by region. c := ledger.Agent.Get(ledger.ID{Region: in.Region}) // MustCall returns the method's Out; here Out is a golem.Result, unwrapped with MustOk. return ledger.Record.MustCall(c, ledger.RecordIn{Amount: in.Amount}).MustOk() }) }

Call and MustCall block until the callee returns. Because a single agent processes invocations sequentially, a synchronous self-call (calling back into the same instance) would deadlock — use Trigger/Schedule for self-messaging (see golem-recurring-task-go).

Plain vs Result returns

MustCall returns the method’s declared output type directly. Only unwrap when that type is a golem.Result — don’t append .MustOk() to a plain return:

// greeter.Hello returns a plain string → use it directly greeting := greeter.Hello.MustCall(c, greeter.HelloIn{Name: "alice"}) // ledger.Record returns golem.Result[int64, string] → unwrap total := ledger.Record.MustCall(c, ledger.RecordIn{Amount: 10}).MustOk()

Handling a failed call

Call returns (Out, error). The error is a *golem.RemoteCallError when the callee failed, could not be reached after the runtime’s retries, or returned a value that does not decode as Out. Its Kind classifies the failure, and a remote custom error stays reachable through errors.As:

total, err := ledger.Record.Call(c, ledger.RecordIn{Amount: 10}) if err != nil { var remote *golem.RemoteCallError if errors.As(err, &remote) && remote.Kind == golem.RemoteNotFound { return "the ledger is not deployed" } panic(err) }

MustCall panics with the same error instead. A panic in a method traps the component, as in Rust: the calling agent fails, or, inside durability.Atomically, the region is retried.

Asynchronous call

Start the call, do other work, then await:

fut := ledger.Record.CallAsync(c, ledger.RecordIn{Amount: in.Amount}) // *golem.Future[Out] // ... other work ... total := fut.MustGet().MustOk() // MustGet() blocks for the result; Get() returns (Out, error)

Launch several and await them to fan out work across other agents.

Unwrapping a golem.Result

When a method’s output is golem.Result[Ok, Err]:

res := ledger.Record.MustCall(c, in) // golem.Result[int64, string] switch { case res.IsErr(): // handle the domain error value msg := res.Err() _ = msg default: total := res.Ok() _ = total } // Or fail loud (panic on Err, which fails the calling agent): total := ledger.Record.MustCall(c, in).MustOk()

Import-cycle rule (important)

  • impl packages never import each other. All cross-agent references go through the callee’s definition package, which imports only the SDK.
  • This keeps the import graph a DAG no matter how cyclic the call graph is (A↔B mutual calls are fine as long as each impl imports the other’s def, not its impl).

Calling an agent in another component

The definition package only exists inside the component that defines the agent. To call an agent that lives in another component — in any language — use the guest client golem build generates for it.

Declare the dependency on the calling component in golem.yaml:

components: example:caller: dir: caller templates: go dependencies: agents: - example:weather/WeatherAgent

golem build then generates golem-temp/bridge-sdk/go/internal/weather-agent-guest-client, a Go module named golem.local/bridge/weather-agent-guest-client. Point the calling component’s go.mod at it:

require golem.local/bridge/weather-agent-guest-client v0.0.0 replace golem.local/bridge/weather-agent-guest-client => ../golem-temp/bridge-sdk/go/internal/weather-agent-guest-client

Import it and call through the typed client:

import weather "golem.local/bridge/weather-agent-guest-client" client, err := weather.GetWeatherAgent(weather.WeatherAgentID{City: "London"}) if err != nil { return err.Error() // the weather component is not deployed } forecast, err := client.Forecast(3) // (Out, error) forecast = client.MustForecast(3) // panics on a failed call receipt := client.MustTriggerForecast(3) // enqueue, don't wait pending := client.ForecastAsync(3) // *golem.Future[Out]
  • Get<Agent> and NewPhantom<Agent> return an error when the host cannot resolve the target, as the callee lives in another component that may not be deployed. MustGet<Agent> and MustNewPhantom<Agent> panic instead.
  • Each method has the forms of a same-component MethodDef: <M> (MethodDef.Call: (Out, error), only error without output), Trigger<M>, Schedule<M>(at, …), each with a Must form, and <M>Async (CallAsync). A failed call returns the SDK’s own *golem.RemoteCallError, exactly as a same-component call does. Methods that take or return a stream have no trigger or schedule form.
  • The .local module path can never be fetched from a module proxy, so a missing replace fails the build rather than silently resolving something else.
  • The client’s types are the callee’s, generated from its schema: records become structs, variants sealed interfaces with one type per case, enums integer constants with a String() method.
  • Methods that take or return streams are included, with each stream spelled golem.AgentStream[T] (see golem-streaming-agent-go).
  • Get<Agent> and NewPhantom<Agent> take golem.ClientOpts. When the callee declares local configuration, the client also has a <Agent>Config struct, with one values.Option field per config path (["limits", "max-items"] → LimitsMaxItems), and a With<Agent>Config option. Only the fields you set are sent; the rest keep their provisioned values:
counter, err := counters.GetCounterAgent(counters.CounterAgentID{Name: "fast"}, counters.WithCounterAgentConfig(counters.CounterAgentConfig{Step: golem.Some[uint32](5)}))

Key Constraints

  • You call Method.Call(client, in) (not client.Call(method, in)): the method descriptor carries the input/output types. Same for Trigger, Schedule, CallAsync and the Must forms.
  • The client comes from the definition (Def.Agent.Get(id)); the ID is encoded from the ID type, so the caller does not need the callee locally registered.
  • Don’t hold a *golem.Future across invocations — await it (Get() / MustGet()) within the same handler.
GuideDescription
golem-fire-and-forget-goEnqueue a call without awaiting a result (Trigger)
golem-recurring-task-goAn agent messages itself / schedules future work
golem-multi-instance-agent-goAddress a specific instance (or a phantom) by ID
golem-add-agent-goDefine the agent being called
golem-agent-reflection-goCall agents without their Go definition (runtime discovery, remote declarations, agent ids)