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 nestedimpl/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
- Create the definition — add
agents/<name>/<name>.gowithpackage <name>. - Declare identity + types — an
IDstruct (its fields are the constructor parameters), plus any input/output structs. - Define the agent and its methods —
golem.DefineAgent+ oneAgent.Methoddescriptor per method. - Create the implementation — add
agents/<name>/impl/impl.gowithpackage impl: a privatestate,Agent.Implement, and oneimpl.Handleper method. - Wire it in — blank-import the impl package in
main.go. - 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, …). Bareint/uintare rejected — their width is platform-dependent. time.Time,time.Durationandgolem.UUIDaredatetime,durationanduuid;golem.ParseUUIDreads one from text.- A
*Tfield meansoption<T>. A nil slice/map is an empty list/map, never “absent” — use*[]Tif 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, agolem.Text, anOption, another variant — wrap it in a one-field struct and register it withgolem.WrappedCase.type At time.Timewould publish a different schema;type At struct{ Value time.Time }withWrappedCase[At]("at")publishesat(datetime). Unions havegolem.WrappedBranchfor 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
URLrefers to the content; otherwiseTextorDatacarries it inline. golem.MultimodalOf[T]is a list of your own modalities, whereTis a variant registered withgolem.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,maxandunit. A quantity takesminandmax, such as0.5in its base unit or20kg. golem.Texttakeslanguages,minLength,maxLengthandregex. A plainstringcarries no restrictions.golem.Binarytakesmime,minBytesandmaxBytes.golem.Pathtakesdirection,kind,mimeandextensions.golem.URLtakesschemesandhosts.- Lists are separated by
|. Aregexruns 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. fromgolem.Must(...)on an unexpected error) traps the component, as in Rust: the agent becomes failed and the caller gets an error, not a result. Insidedurability.Atomicallythe 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.Principalfield in a method’s input struct (or theID) 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 (seegolem-add-http-auth-go). IDmust be a struct; its fields are the agent identity. Agents are created implicitly on first invocation. AnIDwith 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).
Related Guides
| Guide | Description |
|---|---|
golem-call-another-agent-go | This agent needs to call another agent (RPC) |
golem-add-http-endpoint-go | Expose this agent’s methods over HTTP |
golem-multi-instance-agent-go | Address many instances of an agent by ID |
golem-custom-snapshot-go | Customize how the agent’s state is snapshotted |