Calling Go Agents from Outside Golem
Overview
Agents are reached from outside the platform in four ways:
- A generated Go bridge client — a typed Go module per agent type, calling the Golem REST API. The usual choice for a Go program.
- The
golemCLI —golem agent invokecalls a method and waits for the result. - HTTP endpoints — if the agent declares an HTTP mount, any HTTP client calls its methods over plain HTTP.
- The worker REST API — invoke methods and complete promises directly over HTTP against the Golem server.
The bridge client’s language is independent of the agent’s, so a Go program can call agents written in any language.
Throughout, the example is a CounterAgent whose constructor takes a name string, with methods increment (returns a count), add, and value.
Approach 1: a generated Go bridge client
Step 1: enable bridge generation
Add a bridge section to golem.yaml:
bridge:
go:
external:
agents: "*" # Generate for all agents
# Or list specific agents:
# agents:
# - CounterAgent
# - my-app:billing
outputDir: ./bridge-sdk/go # Optional custom output directorygolem build --yes then writes one Go module per agent type — e.g. counter-agent-client/ — into the output directory (default golem-temp/bridge-sdk/go/), and keeps it in sync as agents change. Prefer this over running golem generate-bridge by hand.
Step 2: require the generated module
Each client is its own module, named golem.local/bridge/<client-dir>. That path can never resolve on a module proxy, so the consuming program points a replace at the generated directory:
require golem.local/bridge/counter-agent-client v0.0.0
replace golem.local/bridge/counter-agent-client => ../golem-temp/bridge-sdk/go/counter-agent-clientThen go mod tidy. The client depends only on github.com/golemcloud/golem/sdks/go/bridge (the REST runtime) and github.com/golemcloud/golem/sdks/go/core (the shared value types) — not on the guest SDK, so it builds as an ordinary native Go program.
Step 3: call the agent
import (
"context"
"fmt"
"time"
"github.com/golemcloud/golem/sdks/go/bridge"
counter "golem.local/bridge/counter-agent-client"
)
func run(ctx context.Context) error {
// Once per process: which server, app and environment to talk to.
if err := bridge.Configure(bridge.Configuration{
Server: bridge.Local(), // or bridge.Cloud(token), bridge.Custom(url, token)
AppName: "my-app",
EnvName: "local",
}); err != nil {
return err
}
// The id struct holds the constructor arguments that identify an instance.
c, err := counter.GetCounterAgent(counter.CounterAgentID{Name: "my-counter"})
if err != nil {
return err
}
n, err := c.Increment(ctx) // awaits the result
if err != nil {
return err
}
fmt.Println("count:", n)
// Fire-and-forget, or scheduled for later: both return a bridge.Receipt.
if _, err := c.TriggerAdd(ctx, 5); err != nil {
return err
}
_, err = c.ScheduleAdd(ctx, time.Now().Add(time.Hour), 5)
return err
}Every method has three forms — M(ctx, …), TriggerM(ctx, …) and ScheduleM(ctx, when, …) — and every call returns an error: a transport failure, an error status (*bridge.Error, carrying the status and body) and a result that does not decode are all reported, never panicked.
GetCounterAgent (and NewPhantom<Agent>) takes options:
| Option | Effect |
|---|---|
bridge.WithConfiguration(cfg) | Use this server/app/env instead of the one set with bridge.Configure |
bridge.WithPhantomID(id) | Address the phantom instance with this id |
bridge.WithNewPhantomID() | Address a fresh phantom instance, under a random id |
With<Agent>Config(<Agent>Config{…}) | Override agent configuration, typed: generated when the agent declares local configuration, one values.Option field per config path (["limits", "max-items"] → LimitsMaxItems); only set fields are sent |
bridge.WithConfig(bridge.ConfigEntry{Path: []string{"retries"}, Value: 3}) | Override agent configuration by path; values are plain JSON |
An ephemeral agent has no durable instance to get: its client is constructed with NewPhantom<Agent>(id, opts...), every call runs in a fresh instance, and an awaited call returns bridge.InvocationResult[T] (Value, plus the AgentID and IdempotencyKey of the instance that ran) — or a bridge.Receipt for a method without a result.
Generated types
Parameter and return types are generated in the client package, using the same Go spelling a Go agent would: records are structs, enums are uint32 constants (StatusInTransit), flags are structs of bool, and variants and unions are sealed interfaces with one struct per case (EventNote{Value: "hi"}; a case without a payload is an empty struct). Options, results and tuples are values.Option[T], values.Result[T, E] and values.Tuple2[A, B]… from github.com/golemcloud/golem/sdks/go/core/values; text, char, binary, path, url and uuid are values.Text, values.Char, values.Binary, values.Path, values.URL, values.UUID; datetimes and durations are time.Time and time.Duration.
Unstructured and multimodal content uses the shared content types: values.UnstructuredText[L], values.UnstructuredBinary[M] (with generated language and media-type markers such as LanguagesEnDe, or values.AnyLanguage / values.AnyMimeType), values.Multimodal, and values.MultimodalOf[T] for a custom set of modalities. A quantity is values.Quantity[U], where U is a generated unit marker named after the base unit (values.Quantity[UnitKg]). An empty Unit sends the base unit.
Streaming methods
A method that takes or returns a stream spells each stream bridge.AgentStream[T] and runs over an invocation session, a WebSocket the bridge manages. It has only the awaited form, M(ctx, …), which returns once the result arrives. Streams in the result keep reading from the session after that:
// Send a stream: known values, a producer goroutine, or a writer you feed.
out, err := c.Transform(ctx, bridge.StreamOf[uint32](1, 2, 3))
if err != nil {
return err
}
for v, err := range out.All(ctx) { // or out.Next(ctx), out.Collect(ctx)
if err != nil {
return err // *bridge.StreamError (producer failed), *bridge.StreamCancelled
}
fmt.Println(v)
}
upload := bridge.ProduceStream(func(w *bridge.AgentStreamWriter[[]byte]) error {
return w.WriteAll(chunks...) // a returned error is the agent's "source unavailable"
})
_, err = c.Upload(ctx, upload)- A stream is read once. Passing it to a method gives it away; reading it afterwards fails with
bridge.ErrStreamConsumed. Close()a returned stream you stop reading early. The agent then knows nobody is listening, and the session can finish.- Cancelling
ctxbefore the result arrives cancels the call and the streams it sent. - If the connection drops, the session reconnects and resumes on its own: unacknowledged input is resent, and output continues after the last item you read.
- A rejected call or a failed invocation is a
*bridge.SessionErrorwith the protocol’s error code.
Approach 2: the CLI
golem agent invoke calls a method on a deployed agent and blocks for the result. The agent is created automatically on first invocation. Both golem and golem-cli work for every command.
golem agent invoke <AGENT_ID> <FUNCTION_NAME> [ARGUMENTS...]Agent ID format
The agent ID is the agent type name plus its constructor parameters, in declaration order (for a Go agent, the fields of its ID struct):
AgentTypeName(param1, param2, ...)CounterAgent takes one name, so its ID is CounterAgent("my-counter"). A singleton (type ID struct{}) uses empty parentheses: CounterAgent(). IDs may be prefixed with env/, app/env/, or account/app/env/.
Examples
# No-parameter method
golem agent invoke 'CounterAgent("my-counter")' increment
# add takes one parameter (the By field of AddIn), passed positionally
golem agent invoke 'CounterAgent("my-counter")' add 5
# Read the current value
golem agent invoke 'CounterAgent("my-counter")' value
# In a specific environment
golem agent invoke 'staging/CounterAgent("my-counter")' value
# With an explicit idempotency key
golem agent invoke -i my-unique-key 'CounterAgent("my-counter")' increment
# Machine-readable output
golem agent invoke --format json 'CounterAgent("my-counter")' valueCLI options
| Option | Description |
|---|---|
-t, --trigger | Only trigger the invocation without waiting (fire-and-forget) |
-i, --idempotency-key <KEY> | Set a specific idempotency key; use "-" for auto-generated |
--no-stream | Disable live streaming of agent stdout/stderr/log |
--schedule-at <DATETIME> | Schedule the invocation (requires --trigger; ISO 8601) |
--format json / --format yaml | Machine-readable output |
Method arguments use Go syntax, read against the agent’s schema: strings quoted ("my-counter"), integers bare (5), booleans true/false, structs and lists as composite literals without their type ({zone: "a", level: 2}, {"x", "y"}), maps { "k" => 1 }, options as the value or nil, results Ok(v)/Err(e), enum and variant cases by name. Each exported field of a Go input struct is one positional parameter, named in lowerCamelCase with each initialism as one word (AmountCents → amountCents, UserID → userId, APIKey → apiKey); see golem-invoke-agent-go for the full syntax.
Approach 3: HTTP endpoints
If the agent declares an HTTP mount, its methods are reachable over plain HTTP from any client. Declare the mount on the Spec and per-method routes with golem.HTTP:
var Agent = golem.DefineAgent[ID](golem.Spec{
Name: "CounterAgent",
HTTP: &golem.Mount{Path: "/counters/{name}"}, // {name} binds the ID field
})
var Add = Agent.Method[AddIn, int64]("add", golem.HTTP(golem.POST("/add?by={by}")))The agent must also be declared in the manifest’s httpApi deployment. External clients then POST to the deployed route — from a Go program with the standard library:
resp, err := http.Post(
"https://my-golem.example.com/counters/my-counter/add?by=5",
"application/json", nil,
)See golem-add-http-endpoint-go for the full mount/route model.
Approach 4: the agent REST API
Invoke methods (or complete promises) directly against the Golem server over HTTP — usable from any language, including external Go. Constructor and method parameters are records in the REST wire form ({"kind": ..., "value": ...}; 64-bit integers are strings):
# Invoke CounterAgent("my-counter").add(5) and await the result
curl -X POST "$GOLEM_URL/v1/agents/invoke-agent" \
-H "Authorization: Bearer $GOLEM_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"appName": "my-app",
"envName": "local",
"agentTypeName": "CounterAgent",
"parameters": {"kind": "record", "value": {"fields": [{"kind": "string", "value": "my-counter"}]}},
"methodName": "add",
"methodParameters": {"kind": "record", "value": {"fields": [{"kind": "s64", "value": "5"}]}},
"mode": "await"
}'
# Complete a promise an agent is awaiting (see golem-wait-for-external-input-go)
curl -X POST \
"$GOLEM_URL/v1/components/$COMPONENT_ID/workers/$AGENT_ID/complete" \
-H "Authorization: Bearer $GOLEM_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"oplogIdx": <oplog_index>, "data": [<bytes>]}'"mode": "schedule" triggers the invocation without awaiting it. For promise completion, $AGENT_ID is the same AgentTypeName(params) string the CLI uses; a promise’s PromiseID (AgentID.ComponentID, AgentID.AgentID, OplogIndex) supplies the component, the agent and the oplog index. The generated external Go client (Approach 1) builds these requests for you.
Key Constraints
golem agent invokeblocks until the agent returns; use-t/--triggerfor fire-and-forget.- Every invocation carries an idempotency key (auto-generated if not supplied), giving at-most-once execution even if the client retries.
- Constructor arguments in the agent ID must match the Go
IDstruct fields in order. - A generated Go client is its own module (
golem.local/bridge/<client-dir>); the consuminggo.modneeds therequireand areplacepointing at the generated directory. - A stream-bearing method has no
Trigger/Scheduleform: a trigger could not hand back a stream. - HTTP-endpoint access requires both a
Spec.HTTPmount and anhttpApideployment declaration.
Related Guides
| Guide | Description |
|---|---|
golem-invoke-agent-go | Full golem agent invoke reference and options |
golem-add-http-endpoint-go | Expose the agent’s methods over HTTP |
golem-wait-for-external-input-go | Complete a promise from outside via REST |
golem-add-agent-go | Define the agent being called |