Agent
An agent is a node that calls a model with a typed output contract. It is the second flavor of node in weave — reactors subscribe to scrolls, agents ask models. Both register the same way and land the same kind of events.
The LLM-calling node
Every node in weave is either driven by events (a reactor)
or driven by a single call that returns a structured result
(an agent). An agent wraps the mechanics of the model round
trip: build the messages, send the request, commit
the response, decode into a typed struct. That
whole loop is an ai.request paired with an ai.response on the scroll.
The output is not a free-form string. Every agent declares
an output schema via WithOutput, and the
runtime constrains the model to produce JSON that fits it.
This is what makes agents composable — downstream reactors
and projections can treat the result as data without a
parsing step.
// An agent is a node that calls a model with a typed output contract.
type RumorTags struct {
Tags []string `json:"tags"`
}
var rumorTagger = weave.Agent("rumor-tagger",
weave.WithSystem(`Classify this tavern rumor into 1-3 short tags (e.g. "dragon-sighting", "missing-heir", "haunted-ruin").`),
weave.WithPrompt(func(ctx weave.Context) string {
return "Rumor:\n\n" + ctx["rumor"].(string)
}),
weave.WithOutput(RumorTags{}),
)
// Register with the weave engine alongside reactors and other agents.
if err := w.Register(rumorTagger); err != nil { return err }Anatomy
An agent has five moving parts. Only the name is required; everything else is a sensible default you override when you mean to.
- Name. A stable identifier. Used in the scroll, in traces, and to look up the agent in the registry.
- System prompt. The model's instructions. Usually loaded from a
.tmplfile at init. - User prompt. A string with
{{key}}interpolation, or afunc(Context) stringfor dynamic rendering. - Output schema. A zero-value struct with json tags. The runtime derives a JSON schema and constrains the model to produce matching output.
- Tools. Zero or more. Zero means a single-shot call; one or more turns the agent into a tool loop.
- Model. Optional per-agent override via
WithModel. Empty falls back to the Weave-instance default.
Decomposition is the load-bearing move
The instinct to build one big agent with a rich output struct is the instinct to resist. A 12-field output is a 12-axis prompt, and all 12 axes fail together, cache together, and are opaque to debug together. The production pattern that scales is to decompose into the smallest useful unit and fan out.
Nano
gpt-5-nanoOne structured output. Scoring, classification, single-enum generation, one-sentence synthesis.
Rule of thumb. Default for enrichment. Output struct has 1–3 fields. If you need more, you need more nanos.
Mini
gpt-5-miniMulti-step reasoning inside a bounded domain. Reconcilers, extractors, categorizers.
Rule of thumb. Opt in with WithModel. Output still a typed verdict — reasoning lives in the chain of thought, not the schema.
Full
latest frontierOnly for the one conversational agent that holds the user loop. Multi-turn, tool-calling, open-ended.
Rule of thumb. There is usually exactly one of these per product. Everything else should decompose.
// Three tiers — one output per agent is the load-bearing rule.
// Nano — single structured output. The default for enrichment and scoring.
type ThreatResult struct {
Threat string `json:"threat" jsonschema:"description=trivial|minor|major|deadly"`
}
var assessThreat = weave.Agent("assess-monster-threat",
weave.WithSystem(threatPrompt),
weave.WithPrompt(renderEncounter),
weave.WithOutput(ThreatResult{}),
weave.WithModel(GPT5Nano), // or inherit from w.SetDefaultModel
)
// Mini — multi-step reasoning inside a bounded domain (reconcilers, extractors).
type Verdict struct {
Kind string `json:"kind"` // accept | reject | duplicate | supersede
Reason string `json:"reason"`
}
var reconcileQuest = weave.Agent("reconcile-quest",
weave.WithSystem(dungeonMasterPrompt),
weave.WithPrompt(renderProposalAndAccepted),
weave.WithOutput(Verdict{}),
weave.WithModel(GPT5Mini),
)
// Full — reserved for the one conversational agent that holds the user loop.
// Tool-calling, multi-turn, open-ended.
var innkeeper = weave.Agent("innkeeper",
weave.WithSystem(innkeeperSystemPrompt),
weave.WithPrompt(func(ctx weave.Context) string { return innkeeperPrompt }),
weave.WithTool(proposeQuest),
weave.WithTool(proposeReward),
weave.WithModel(GPT5Full),
)The tiers are a convention, not a runtime distinction. They are useful because nano is cheap, parallelizable, and cacheable in a way that mega-agents never are. Ship nanos by default; reach for mini only when a single output needs real reasoning; reserve full for the one conversation that genuinely needs it.
Executing — inputs go in, typed output comes out
An agent does nothing until w.Execute is
called. You pass an inputs bag and a pointer to the output
struct; the runtime hydrates the prompt, calls the model,
decodes into the pointer, and records everything on a
scroll.
// Executing an agent — pass an inputs bag and a pointer to the output struct.
var result RumorTags
_, err := w.Execute(ctx, rumorTagger,
weave.Context{"rumor": rumor},
&result,
)
if err != nil { return err }
// result.Tags is now populated, the call landed on the scroll, and
// replay will skip the network round-trip if the ai.response is recorded.The scroll is the unit of replay. Execute the same agent
with the same inputs against a scroll that already has the ai.response and the runtime returns the
recorded answer — no network call, deterministic output,
identical test fixture every run.
Fan-out, not mega-prompts
The fan-out pattern is the counterpart to decomposition. Inside a
reactor, launch N nanos in parallel with a sync.WaitGroup, gate each one on a rate limiter,
and log-and-continue on per-call failure. One axis timing out
shouldn't sink the other eleven.
// Fan-out: four nanos in parallel, one atomic event per result.
var wg sync.WaitGroup
for _, axis := range encounterAxes { // threat, terrain, loot, xp
wg.Add(1)
go func(axis enrichAxis) {
defer wg.Done()
if err := WaitNano(ctx); err != nil { // rate-limit against the nano tier
return
}
var out axis.resultType
_, err := w.Execute(ctx, axis.agent,
weave.Context{"encounter": encounter},
&out,
)
if err != nil {
slog.Warn("nano failed", "agent", axis.agent.Name(), "err", err)
return // one axis failing does not block the siblings
}
axis.appendResult(ctx, dungeonScroll, encounter.ID, out)
}(axis)
}
wg.Wait()Each result lands as its own atomic event on the domain scroll. Retrying a single failed axis is a one-line re-enqueue, not a re-run of the whole mega-prompt. This is the operational payoff of nano decomposition.
Tool loops, briefly
When an agent has WithTool attached, its
execution becomes a loop rather than a single shot. The
model may emit a tool call; the runtime commits a tool.dispatch, the handler commits a tool.result, and the loop continues until the
model returns a final response. The agent still has a
typed output contract — the loop ends when the output fits.
Tool loops are the one place where full-tier models earn their slot. Everything else — enrichment, classification, extraction, reconciliation — should be decomposed into single-output nanos.
Not to be confused with
- A chain. A chain implies a linear sequence of model calls inside one abstraction. An agent is one call (or one tool loop). You compose agents with reactors and folds — the composition layer is the scroll, not an in-memory pipeline object.
- A prompt template. A prompt template is a
string. An agent is a name plus a contract plus a call. The
template is a detail of
WithPrompt; the rest of the agent is what makes the call meaningful. - An autonomous worker. An agent is a pure function of its inputs — one call returns one typed output. Autonomy comes from reactors subscribing to scrolls and driving agents over time, not from agents running themselves.
Status
The agent primitive is shipped and exercised in production (tavern, examples). The rate-limiter tier plumbing and name-indexed Execute are the remaining edges that surface once a pipeline has many agents.