Calling Agents with Runtime Reflection (Go)
Normal RPC through the callee’s definition package or a generated guest client (see golem-call-another-agent-go) is the baseline. Reflection adds three more levels, which compose through agent ids:
| Level | Use it when | Go |
|---|---|---|
| Caller-defined, method-only | Many agent types share a method shape; you have an id | golem.DefineAgentClient[Shape]() + Method[In, Out], Bind(id) |
| Caller-defined, full | You own Go types for one target type, without its definition | golem.DefineFullAgentClient[Id](name, spec) + Method[In, Out] |
| Discovered | The type or method is chosen at runtime and should be validated | reflection.DiscoverAgentType → Get / Bind → Call |
| Fully dynamic | Values are already packed; no schema authority kept | reflection.BindAgentID(id) → Call |
Every level uses the same verbs: Call waits for the result, Trigger does not, Schedule runs it later. Discovered and dynamic clients live in the reflection package (github.com/golemcloud/golem/sdks/go/golem/reflection).
Method-Only Clients
A method-only client declares methods and nothing else, so it calls any existing agent that has them, whatever its type:
type Pingable struct{} // the shape's identity type
var (
Pinger = golem.DefineAgentClient[Pingable]()
Ping = Pinger.Method[golem.Unit, string]("ping")
)
func pingAll(ids []string) {
for _, id := range ids {
c, err := Pinger.Bind(id)
if err != nil {
continue
}
reply, err := Ping.Call(c, golem.Unit{})
fmt.Println(id, reply, err)
}
}Bind checks nothing about the target, which the client does not describe; a target that is not deployed is reported by Bind, and a mismatch by the host or when the result is decoded. Binding does not create the agent, but the first call to a durable id does.
Full Clients
A full client also declares the target type’s name, constructor and lifecycle, so it creates and addresses instances like the agent’s own definition:
type CounterID struct{ Name string }
type AddIn struct{ By int64 }
var (
Counter = golem.DefineFullAgentClient[CounterID]("CounterAgent", golem.AgentClientSpec{})
Add = Counter.Method[AddIn, int64]("add")
)
c, err := Counter.Get(CounterID{Name: "main"}) // an error when the target is not deployed
total, err := Add.Call(c, AddIn{By: 5})
c, err = Counter.Bind(agentID) // the id must name CounterAgent with a CounterID constructor
id, err := Counter.AgentID(CounterID{Name: "main"}, golem.None[golem.UUID]()) // without creating itGet,NewPhantom,BindandAgentIDwork the same on an agent’s own definition (DefineAgent), except that thereGet,NewPhantomandAgentIDpanic instead of returning an error: the target is in the same component, so a failure is a programming mistake.golem.AgentClientSpec{Mode: golem.Ephemeral}declares an ephemeral target: it has no durable identity, soGetandBindare refused andNewPhantomaddresses it.golem.DefineConfiguredFullAgentClient[Id, Cfg]declares the target’s configuration type, soGet(id, client.WithConfig(cfg))passes typed creation-time overrides, checked at compile time againstCfg.
Generated guest clients are built on full clients.
Discover and Inspect
for _, t := range reflection.DiscoverAgentTypes() {
fmt.Println(t.Name(), t.Mode(), t.SourceLanguage())
for _, m := range t.Methods() {
fmt.Println(" ", m.Name(), m.Description(), m.ReadOnly())
}
}
counter, found := reflection.DiscoverAgentType("CounterAgent")
if !found {
panic("CounterAgent is not deployed in this environment")
}
add, known := counter.Method("add")
if !known {
panic("CounterAgent has no method add")
}
input := add.Input() // a schema.Ref of the parameter record
fmt.Println(golem.Must(input.ToJSONSchema(true))) // e.g. for an LLM
if output, hasOutput := add.Output().Get(); hasOutput {
fmt.Println(golem.Must(output.ToJSONSchema(true)))
}Input() returns a schema.Ref for the parameter record (without host-filled fields such as the principal). Output() returns a golem.Option[schema.Ref]: golem.None[schema.Ref]() when there is no result. Use Get() and check its boolean before accessing the result schema. A schema.Ref from github.com/golemcloud/golem/sdks/go/core/schema packs and unpacks canonical JSON and renders JSON Schema. Constructor().Input() describes the constructor the same way. A discovery miss (not deployed, not visible to the caller) reports false. A snapshot never refreshes itself; discover again when a newer deployment matters.
Call a Discovered Agent
client, err := counter.Get(map[string]any{"name": "main"})
if err != nil {
panic(err)
}
total, invocation, err := client.Call("add", map[string]any{"by": "5"}) // s64 is a string
if err != nil {
var rce *golem.RemoteCallError
if errors.As(err, &rce) {
fmt.Println(rce.Kind, rce.Message)
}
panic(err)
}
fmt.Println(client.AgentID(), total, invocation.InvocationID)Gettakes the constructor arguments by name and creates the agent if it does not exist;reflection.WithPhantomID(uuid)addresses a phantom instance andNewPhantomallocates one.Bind(agentID)addresses an existing id of this type.reflection.WithConfigJSON(path, value)overrides one declared configuration value at creation, as canonical JSON (reflection.WithConfigValue(path, schema.SchemaValue)takes a schema-model value);Get,NewPhantomandBindaccept it. Undeclared paths, secrets and invalid values are refused before anything is sent. An agent that already exists keeps the configuration it was created with.Triggerreturns the invocation’sgolem.InvocationMetadata, and the*golem.ScheduledInvocationthatSchedulereturns holds it asMetadata.AgentIDnames the agent that runs the invocation — for an ephemeral agent, the only record of which instance ran — andInvocationIDis agolem.InvocationID, whoseString()is<agent-id>.<uuid>;golem.ParseInvocationIDreads that form back.Callreturns the metadata as a*golem.InvocationMetadata, which is nil when Golem rejected the call; the error then says why.CallAsyncreturns a*reflection.PendingCall: itsMetadatais known at once,Get()returns the result (MustGet()panics on an error, and a secondGetreturns the same result), andCancel()stops waiting (the call may already have run;Getthen reportsgolem.ErrCallCancelled). It behaves like the*golem.Futurethat a typedCallAsyncreturns. A scheduled call is cancelled through the*golem.ScheduledInvocationthatSchedulereturns.- Missing, unexpected or invalid arguments fail with
*schema.ValidationError, listing every problem, before anything is sent. - Remote failures are
*golem.RemoteCallError; for a remote agent error, itsCauseis a*golem.AgentError.
Agent IDs
typed := golem.Must(golem.ParseAgentID[CounterID](agentID))
fmt.Println(typed.TypeName, typed.ID.Name, typed.PhantomID.IsSome())
raw := golem.Must(reflection.ParseRawAgentID(agentID)) // no Go type needed
fmt.Println(raw.TypeName, golem.Must(raw.ConstructorJSON()))
t, found := reflection.DiscoverAgentTypeByID(agentID) // the type behind an existing agent
id := golem.Must(t.AgentID(map[string]any{"name": "other"}, golem.None[golem.UUID]()))Parsing is strict and reports malformed ids; lookup by id never creates the agent. Agent ids are scoped to the caller’s environment.
Fully Dynamic
A dynamic client sends values the caller packed itself and validates nothing. Keep a discovered snapshot beside it for validation:
t, _ := reflection.DiscoverAgentType(agentType)
m, _ := t.Method(method)
input := golem.Must(m.Input().PackJSON(args))
client := golem.Must(reflection.BindAgentID(agentID))
out, _, err := client.Call(method, input)
if err != nil {
panic(err)
}
if v, has := out.Get(); has {
if result, declared := m.Output().Get(); declared {
fmt.Println(golem.Must(result.UnpackJSON(v)))
}
}A dynamic client also has CallAsync, Trigger and Schedule. reflection.MakeAgentID(typeName, constructor, phantom) builds an id from a packed constructor, so a dynamic caller can address any instance; the first call to it creates a durable agent.
Canonical JSON
- Integers up to 32 bits are JSON numbers;
s64,u64, duration nanoseconds and quantity mantissas are base-10 strings ("5"). A duration is{"nanoseconds": "…"}. - An absent option is
null; a result is{"ok": …}or{"err": …}; a variant case is its name, or{name: payload}; a map is[[k, v], …]; flags are an array of names; binary is unpadded base64url. - Streams, futures and other capabilities cannot be packed as JSON; call such methods through a typed client.
Related Guides
| Guide | Description |
|---|---|
golem-agent-reflection | Choosing between client approaches, identity rules |
golem-call-another-agent-go | Typed RPC through definitions and guest clients |
golem-call-tool-go | Calling tools the same ways |
golem-multi-instance-agent-go | Agent identities and phantoms |