Documentation / guides
Overview
Freeze one agent's model, instructions, tools, limits, modes, and delegation policy.
A loop.Definition is one agent’s immutable design-time contract. It carries the
inference client and model, system text, declared tool factories, access policy,
limits, context policy, output schema, modes, and delegate names. It contains no
conversation, goroutine, lease, or session ID. A Rig
owns the graph of definitions and binds them when it creates a session.
How it works
The public lifecycle has three deliberately different values:
| Value | Created by | Owns | May mutate live state? |
|---|---|---|---|
loop.Definition | loop.Define(opts ...loop.Option) | validated, frozen policy | no |
loop.BoundDefinition | Definition.Bind(ctx, tool.Bindings) | fresh tool instances and resolved modes | no; read-only view |
loop.Handle / loop.Controller | a session runtime | live loop identity and actor commands | Controller only, at turn boundaries |
Binding is a separate step because tool factories receive session and loop IDs.
The definition can therefore be reused by several sessions without sharing tool
instances or mutable slices. The public package does not expose the internal
loopruntime.Loop; use session.Session and session.SessionController for
conversation, events, persistence, and shutdown.
%%{init: {"theme":"dark"}}%%
flowchart LR
O[loop.Option values] --> D[loop.Define]
D -->|validated| DF[immutable loop.Definition]
DF -->|Bind(ctx, tool.Bindings)| B[loop.BoundDefinition]
B --> H[live loop Handle]
H --> C[loop.Controller]
C --> T[turn-boundary changes]
Configure a Loop
The smallest definition has a name and a valid provider-neutral model. The
inference client is an inference.Client; the model is model.Model.
assistant, err := loop.Define(
loop.WithName("assistant"),
loop.WithInference(client, selectedModel), // client is an inference.Client.
loop.WithSystem("You help the user inspect and change a project."),
loop.WithTools(readFile, grep, editFile), // tool.Definition values.
)
if err != nil {
var definitionErr *loop.DefinitionError
if errors.As(err, &definitionErr) {
log.Printf("definition rejected: %s", definitionErr.Kind)
}
return fmt.Errorf("define loop: %w", err)
}
Every option is applied once for singleton settings. WithTools,
WithToolMiddlewares, WithDelegates, and WithModes are additive. A gated
or middleware-enabled loop must include a nonempty WithPolicyRevision; this
opaque revision gives restore a stable identity for function-valued policy.
Context counting requires a counter, an inference capability, and exactly one
of observation or compaction; see Context Observation
for the policy split and Compaction Policy.
Lifecycle
The Rig checks that delegate names exist and that every registered loop is reachable from a primer. A session then binds each definition, creates a live loop actor, and routes input through a Turn. A Turn may contain several conceptual Steps. A controller change is validated and committed at a turn boundary; it does not mutate the immutable definition or retroactively change a running step.
Public boundary
The sealed BoundDefinition interface exposes read-only accessors such as
Model, EffectiveSystem, Tools, Mode, ContextCounter, and
RuntimeIdentity. It returns defensive copies for models, slices, and output
schemas. Handle exposes only ID, Mode, and Model; Controller embeds it
and adds SetMode, Change, and subtree-scoped Interrupt.
Do not type-assert a bound value to an internal runtime or construct an actor directly. That would bypass session ownership, durable events, and shutdown ordering.