Skip to Content
How-To GuidesGoAdding a New Agent to a Go Golem Component

Adding a New Agent to a Go Golem Component

Overview

An agent is a durable, stateful unit of computation in Golem. In Go each agent is split across two packages:

  • a state-free definition (package <name>) — the agent’s identity, its method descriptors, and the input/output types. Other agents import this package to call it.
  • an implementation (package impl, in a nested impl/ subpackage) — the private state and the method handlers. Importing this package registers the agent with the SDK.

This split lets agents call one another without a Go import cycle, and keeps state private to the implementation.

Steps

  1. Create the definition — add agents/<name>/<name>.go with package <name>.
  2. Declare identity + types — an ID struct (its fields are the constructor parameters), plus any input/output structs.
  3. Define the agent and its methods — golem.DefineAgent + one Agent.Method descriptor per method.
  4. Create the implementation — add agents/<name>/impl/impl.go with package impl: a private state, Agent.Implement, and one impl.Handle per method.
  5. Wire it in — blank-import the impl package in main.go.
  6. Build — run golem build.

Definition (agents/counter/counter.go)

// Package counter is the DEFINITION of the counter agent. package counter import "github.com/golemcloud/golem/sdks/go/golem" // ID holds the constructor parameters, and is the type-level identity used by // cross-agent calls. Two agents with the same ID are the same agent. type ID struct{ Name string } // AddIn is a method's parameter list: one WIT parameter per exported field. type AddIn struct{ By int64 } // Agent is the state-free definition. Mode defaults to Durable. var Agent = golem.DefineAgent[ID](golem.Spec{ Name: "CounterAgent", Description: "A simple durable counter", }) // Method descriptors are package-level vars: the same value drives the published // schema, the implementation binding, and calls from other agents. var ( Increment = Agent.Method[golem.Unit, int64]("increment", golem.Description("Increase the count by one")) Add = Agent.Method[AddIn, int64]("add", golem.Description("Add to the count")) Value = Agent.Method[golem.Unit, int64]("value", golem.Description("Return the current value")) )

golem.Unit is the empty parameter/result placeholder (a method with no input or no output).

golem.Description(...) describes a method and golem.PromptHint(...) tells AI callers when to use it; Spec.PromptHint does the same for creating the agent.

Spec.Dependencies lists other agents of the same component this one calls (Dependencies: []golem.AgentDependency{billing.Agent}); they are published with the agent’s metadata.

Implementation (agents/counter/impl/impl.go)

// Package impl is the IMPLEMENTATION of the counter agent. package impl import ( "myapp/agents/counter" "github.com/golemcloud/golem/sdks/go/golem" ) // state is the agent's private, durable state — invisible to callers. type state struct{ value int64 } func (s *state) current() int64 { return s.value } // Implement binds the constructor and returns a handle; Handle registers each // method on it. var agent = counter.Agent.Implement(func(counter.ID) *state { return &state{} }) func init() { agent.Handle(counter.Increment, func(ctx *golem.Context[state], _ golem.Unit) int64 { ctx.State.value++ return ctx.State.value }) agent.Handle(counter.Add, func(ctx *golem.Context[state], in counter.AddIn) int64 { ctx.State.value += in.By return ctx.State.value }) // A handler can also be an ordinary Go method, bound with a method expression. agent.Handle(counter.Value, golem.Adapt0((*state).current)) }

The handler receives *golem.Context[state]; ctx.State is your *state. golem.Adapt0 adapts a no-input (*state) method into a handler; golem.Adapt adapts a one-input method.

Wiring: main.go

main.go (package main) blank-imports each agent’s impl package so its registration runs on import, plus the SDK runtime:

package main import ( _ "myapp/agents/counter/impl" _ "github.com/golemcloud/golem/sdks/go/golem" ) func main() {}

Adding another agent = another agents/<name>/ folder + one more blank import here.

Custom Types

Parameter and result types are Go structs; each exported field becomes one WIT field, named by the Go name in lowerCamelCase with each initialism as one word (AmountCents → amountCents, UserID → userId). Two fields whose names map to the same schema name are a definition error.

  • Use sized integers (int64, uint64, …). Bare int/uint are rejected — their width is platform-dependent.
  • time.Time, time.Duration and golem.UUID are datetime, duration and uuid; golem.ParseUUID reads one from text.
  • A *T field means option<T>. A nil slice/map is an empty list/map, never “absent” — use *[]T if you must distinguish.
  • Variants/enums are declared with golem.DefineVariant / golem.DefineEnum. A variant case type is its payload; a case with no payload is an empty struct (type Cash struct{}).
  • When the payload cannot carry the variant’s marker method without changing what it is — a time.Time, a golem.Text, an Option, another variant — wrap it in a one-field struct and register it with golem.WrappedCase. type At time.Time would publish a different schema; type At struct{ Value time.Time } with WrappedCase[At]("at") publishes at(datetime). Unions have golem.WrappedBranch for the same reason.

Unstructured and Multimodal Content

For content a model or a person reads, use the content types. Tools and UIs recognise them by their role:

type EnOrDe struct{} func (EnOrDe) Languages() []string { return []string{"en", "de"} } type SummarizeIn struct { Doc golem.UnstructuredText[EnOrDe] // golem.AnyLanguage for any Photo golem.UnstructuredBinary[golem.AnyMimeType] // or a marker with MimeTypes() Media golem.Multimodal // text and binary items } doc := golem.UnstructuredText[EnOrDe]{Text: "hallo", Language: "de"} // inline ref := golem.UnstructuredBinary[golem.AnyMimeType]{URL: "https://…/a.png"} // by URL media := golem.Multimodal{golem.TextModality{Value: …}, golem.BinaryModality{Value: …}}
  • A non-empty URL refers to the content; otherwise Text or Data carries it inline.
  • golem.MultimodalOf[T] is a list of your own modalities, where T is a variant registered with golem.DefineVariant.

Restricting Values

A golem struct tag restricts a field, on a method’s parameters or on a record. The restriction is published in the schema, and the platform rejects a value outside it before the method runs:

type ResizeIn struct { Width uint32 `golem:"min=1,max=4096,unit=px"` Name golem.Text `golem:"languages=en|de,maxLength=40,regex=^[a-z][a-z0-9-]*$"` Photo golem.Binary `golem:"mime=image/png|image/jpeg,maxBytes=1048576"` Out golem.Path `golem:"direction=output,kind=file,extensions=png"` Source golem.URL `golem:"schemes=https"` Limit golem.Option[uint8] `golem:"max=9"` // an option or a list is restricted through to its values }
  • Numbers take min, max and unit. A quantity takes min and max, such as 0.5 in its base unit or 20kg.
  • golem.Text takes languages, minLength, maxLength and regex. A plain string carries no restrictions.
  • golem.Binary takes mime, minBytes and maxBytes. golem.Path takes direction, kind, mime and extensions. golem.URL takes schemes and hosts.
  • Lists are separated by |. A regex runs to the end of the tag, so put it last.
  • A tag that does not fit the field’s type is reported as a definition error.

Returning Failures

Handlers return only their output value. Signal failure two ways:

  • Uncaught errors — a panic (e.g. from golem.Must(...) on an unexpected error) traps the component, as in Rust: the agent becomes failed and the caller gets an error, not a result. Inside durability.Atomically the invocation is instead retried per the agent’s retry policy.
  • Domain errors the caller should observe as a value — model them in the method’s output type with golem.Result[Ok, Err]:
// definition var Record = Agent.Method[RecordIn, golem.Result[int64, string]]("record") // handler agent.Handle(Record, func(ctx *golem.Context[state], in RecordIn) golem.Result[int64, string] { if in.Amount < 0 { return golem.Err[int64, string]("cannot record a negative amount") } ctx.State.total += in.Amount return golem.Ok[int64, string](ctx.State.total) })

Returning golem.Err(...) completes the invocation successfully — the caller receives the error as a value (and can unwrap with .MustOk()). Panicking instead fails the whole agent.

Key Constraints

  • Method parameters/results are passed by value; wrap the parameter list in a struct (one field per WIT parameter).
  • A golem.Principal field in a method’s input struct (or the ID) is filled by the host with the invocation’s principal and is not a parameter callers pass; ctx.Principal() is the principal the agent was initialized with (see golem-add-http-auth-go).
  • ID must be a struct; its fields are the agent identity. Agents are created implicitly on first invocation. An ID with no fields (type ID struct{}) is a singleton — exactly one instance of the agent.
  • Invocations run sequentially in a single thread — no concurrency within one agent, no locks needed.
  • Method descriptors must be package-level vars in the definition package (the same value is used by the schema, the binding, and callers).
  • State lives only in the impl package (unexported) — callers never see it.
  • Target is WASM only: no raw sockets; outgoing HTTP goes through the SDK’s WASI transport (see golem-make-http-request-go).
GuideDescription
golem-call-another-agent-goThis agent needs to call another agent (RPC)
golem-add-http-endpoint-goExpose this agent’s methods over HTTP
golem-multi-instance-agent-goAddress many instances of an agent by ID
golem-custom-snapshot-goCustomize how the agent’s state is snapshotted