Help us shape the future of Go! Take the 2026 Go Developer Survey

agent

package
v0.4.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 22, 2026 License: MIT Imports: 27 Imported by: 1

Documentation

Overview

Package agent is the single home of the agent model:

  • Definition — the serializable document form used by deployment documents: card, tool allow-list, engine selection (EngineRef), resource bindings, and lifecycle hooks by slot (prepare / observe / referees / commit).
  • Agent — the assembled runtime form built from a Definition by the deployment layer: identity, card, tools, the constructed engine (Engine), and the attached hooks (Preparer / Observer / Referee / Committer). core/deploy returns this type instead of defining its own bound-agent shape.
  • Board / BoardSnapshot — the engine execution blackboard and its serialisable snapshot, shared by engines and checkpointing.
  • Checkpoint / CheckpointStore — the resume record envelope and the host-side persistence contract. Concrete stores are resource implementations under checkpoint/ (e.g. checkpoint/workspace, registered as checkpoint.Store/workspace).
  • Runtime — Execute (the turn harness), Run / Request / Result / Identity, Host (host-side capabilities), Board, and the lifecycle contracts.

Index

Constants

View Source
const (
	HookSlotPreparer  = "prepare"
	HookSlotObserver  = "observe"
	HookSlotReferee   = "referee"
	HookSlotCommitter = "commit"
)

Hook slot names. A hook factory registers under ("hook."+slot, type); the document lists hooks under the matching top-level field (prepare / observe / referees / commit). The four slots mirror the lifecycle stages in core/agent: Preparer, Observer, Referee, Committer.

View Source
const BoardRefMarker = "${board:"

BoardRefMarker is the literal prefix of every board reference.

View Source
const BoardRefPrefix = "board"

BoardRefPrefix is the scheme-style name of the board reference namespace ("${board:path}"), shared by every layer that resolves references against the agent board at execution time (graph node configs, the script bridge's board.resolve, ...).

View Source
const MainChannel = "__main_channel"

MainChannel is the default message channel key.

Channels are an engine-level primitive: they let nodes/steps share ordered message sequences without going through Vars. Convention-level keys for "the chat transcript", "the answer", etc. belong to the agent layer; this package only provides the channel mechanism.

Naming rule: any board key (var or channel) reserved by the engine itself uses the "__" prefix. User-domain code MUST NOT introduce channel or var names beginning with "__"; doing so risks colliding with a future engine-managed slot. Existing reserved names besides MainChannel include graph-level vars VarInterruptedNode and VarToolCalls (see core/graph).

View Source
const SubjectPrefix = "agent.run."

SubjectPrefix is the fixed root every engine envelope subject MUST start with. Exposed as a constant so consumers can check strings.HasPrefix without re-deriving it.

Variables

This section is empty.

Functions

func CapabilityFromHost

func CapabilityFromHost[T any](h Host) (T, bool)

CapabilityFromHost finds an optional capability implemented by h or by an inner Host explicitly exposed through HostUnwrapper. Typed-nil hosts and capabilities are treated as unsupported.

Only decorators explicitly implementing HostUnwrapper are traversed. A wrapper can stop traversal by returning nil when it replaces the surface that authorises access, and a wrapper that scopes exactly one capability withholds it with HostCapabilityMask instead of taking the rest of the chain down with it.

func ContainsBoardRef added in v0.1.15

func ContainsBoardRef(v any) bool

ContainsBoardRef reports whether v — a decoded JSON value or a plain string — contains at least one live (unescaped) "${board:*}" reference.

func ContextWithHost

func ContextWithHost(ctx context.Context, h Host) context.Context

ContextWithHost returns a derived context carrying h. Engines that dispatch to extension points which were not designed to receive the Host directly (core/tool's Tool.Execute signature, custom plugin callbacks, …) call ContextWithHost before invoking those extensions so the extension can recover the Host via HostFromContext.

The intended consumer pattern:

// engine side: just before invoking the tool registry
ctx = agent.ContextWithHost(ctx, host)
results := reg.ExecuteAll(ctx, calls)

// tool side
host, ok := agent.HostFromContext(ctx)
if ok {
    reply, err := host.AskUser(ctx, prompt)
}

nil h is allowed and a no-op (returns ctx unchanged) so callers can plumb a possibly-nil host without conditional branches.

Engines MUST NOT use the host stashed here as a substitute for the host argument they receive in Engine.Execute — the argument is the contract; the context-carried copy is purely a transport for downstream extensions that lack a Host parameter.

func EmitStreamDelta

func EmitStreamDelta(ctx context.Context, pub Publisher, runID, stepActor string, payload StreamDeltaPayload) error

EmitStreamDelta is the low-level form of the EmitStreamX helpers. Custom nodes that need to set fields outside the type-specific helpers (e.g. a forward-compatible Type the SDK does not yet ship a helper for) build the payload themselves and pass it here. Required per-Type fields are validated to mirror the contract enforced by DecodeStreamDelta on the consumer side, so a malformed delta is caught at publish time instead of silently flowing to subscribers.

stepActor follows the contract documented at the top of subjects.go: it MUST start with the executing agent.id (so [PatternRunAgentStream] can fan-in by agent) and MAY append an engine-private suffix (graph runner: ".node.<nodeID>"; iterative engine: ".iter<N>"). Both runID and stepActor are sanitised by SanitiseID so caller-supplied values cannot fragment the resulting subject.

The envelope is stamped with HeaderRunID. The agent identifier is derived from the stepActor segment ahead of any optional ".node." / ".iter" suffix — it goes onto HeaderAgentID. For header-routed subscribers that key off the node id, the HeaderNodeID is populated whenever stepActor carries the graph runner's "<agent>.node.<nodeID>" form so the two transports stay aligned.

Publish errors are returned to the caller (unlike the executor's fire-and-forget convention) so node authors can decide whether to retry or surface the failure; in practice most callers just discard the error because stream deltas are observability, not control flow.

func EmitStreamPart

func EmitStreamPart(ctx context.Context, pub Publisher, runID, stepActor string, part message.Part) error

EmitStreamPart publishes one assistant-token delta on the canonical stream subject. See EmitStreamDelta for the stepActor format requirement.

Use this from any node that produces incremental textual output — for example a custom RAG retriever streaming its working notes, or a post-processing node turning structured data into prose. content may be empty (callers that want "still alive" heartbeats should typically mark them differently); empty content is published as-is so the helper stays predictable. EmitStreamPart publishes one canonical output part — text, image, audio, video, file, data, tool_call, tool_result, or reasoning — as a stream delta. part must be non-nil and valid (see message.Part.Validate). See EmitStreamDelta for the stepActor format requirement.

func EventBusFromHost

func EventBusFromHost(h Host) (event.Bus, bool)

EventBusFromHost returns h's borrowed event bus when h implements EventBusProvider. It treats nil interfaces, typed-nil hosts, and typed-nil buses as unsupported. Built-in Host decorators preserve the optional capability without claiming EventBusProvider themselves.

func ExtractBoardRefs added in v0.1.15

func ExtractBoardRefs(v any) []string

ExtractBoardRefs returns the sorted, deduplicated board paths referenced anywhere inside v. Escaped references and defaults are not included.

func GetTyped

func GetTyped[T any](b *Board, key string) (T, bool)

GetTyped retrieves a typed value from the Board's vars. It is a generic helper rather than a method because Go does not allow type parameters on methods.

func Interrupted

func Interrupted(intr Interrupt) error

Interrupted wraps an Interrupt as an error that satisfies errdefs.IsInterrupted. The recommended usage from an engine is:

case intr := <-h.Interrupts():
    return engine.Interrupted(intr)

Hosts inspecting the result use the standard errdefs / errors.As idiom:

if errdefs.IsInterrupted(err) {
    var ie engine.InterruptedError
    if errors.As(err, &ie) {
        switch ie.Cause {
        case engine.CauseUserInput: ...
        }
    }
}

A zero-value Interrupt still produces a well-formed error so callers don't need to special-case CauseUnknown.

func IsResumable

func IsResumable(eng Engine) bool

IsResumable reports whether eng implements Resumer. Use the two-value variant when you want the typed handle:

if r, ok := engine.AsResumer(eng); ok { _ = r.CanResume(cp) }

func IsStreamDelta

func IsStreamDelta(s event.Subject) bool

IsStreamDelta reports whether s is a stream-delta subject. Cheap (string-only) so consumers can filter envelopes before the more expensive payload decode.

Implementation note: matches subjects shaped like "engine.run.<runID>.stream.<stepActor>.delta" — i.e. the prefix is SubjectPrefix, contains ".stream." and ends with ".delta".

func MergeInterrupts

func MergeInterrupts(ctx context.Context, sources ...<-chan Interrupt) <-chan Interrupt

MergeInterrupts fans-in N independent interrupt channels into a single output channel — the natural shape sandbox / pod hosts need to combine "user cancel", "SIGTERM", "budget exceeded", "graceful stop", … into the one channel an Engine reads from [Host.Interrupts].

Behaviour:

  • The returned channel is closed once ctx is cancelled OR every non-nil source channel has been closed. Engines reading from it therefore see EOF when "everyone is done", matching how a single cooperative-interrupt channel behaves today.
  • Nil source channels are skipped silently — matches the documented "nil means never fires" semantic in Interrupter.Interrupts and keeps callers from having to filter their slice.
  • When zero non-nil sources are supplied the returned channel is a never-fires channel that is closed when ctx is cancelled. This keeps the helper total — engines selecting on it stay correct even in the trivial case.
  • Order of forwarded interrupts is the natural runtime arrival order across the source channels. No de-duplication: a host that fires "shutdown" through two distinct sources will surface both.

Forwarding goroutines exit promptly when ctx is cancelled, so the helper is safe to use in long-lived hosts that get re-created across reloads.

func PatternAllRuns

func PatternAllRuns() event.Pattern

PatternAllRuns returns the wildcard pattern matching every event from every run.

engine.run.>

func PatternRun

func PatternRun(runID string) event.Pattern

PatternRun returns the wildcard pattern matching every event of one run, regardless of engine implementation or engine-private extension.

engine.run.<runID>.>

func PatternRunSteps

func PatternRunSteps(runID string) event.Pattern

PatternRunSteps returns the wildcard pattern matching every step lifecycle event (start / complete / error and any engine-private step.* extension such as graph runner's "skipped") of one run.

engine.run.<runID>.step.>

func PatternRunStream

func PatternRunStream(runID string) event.Pattern

PatternRunStream returns the wildcard pattern matching every stream delta of one run. Use this when you want LLM token / tool deltas but not the step lifecycle events.

Agent-level fan-in ("only agent X's events in this run") is NOT available as a NATS wildcard because the stepActor segment is collapsed into one token by SanitiseID (see the file header). Consumers route by agent through the event.HeaderAgentID envelope header instead — subscribe with PatternRun(runID) and filter on env.AgentID() in the consumer.

engine.run.<runID>.stream.>

func SanitiseID

func SanitiseID(id string) string

SanitiseID escapes characters that would corrupt an event.Subject when the input is concatenated into one. event.Subject segments are separated by '.', and '*' / '>' are reserved by event.Pattern for wildcards; any of these in a runID / stepActor would either fragment the subject or turn it into an unintended pattern. SanitiseID replaces each occurrence with '_'.

Empty input becomes "_" so the resulting subject keeps a constant segment count even when the engine forgot to mint an id.

Engines are expected to call SanitiseID on every user-supplied fragment they splice into a subject. The Subject* / Pattern* builders in this file already do so for their parameters; engine implementations only need it when constructing private extensions of their own.

func SignalToError

func SignalToError(sig *ScriptSignal) error

SignalToError maps a script control signal into a Go error that fulfils the appropriate errdefs / engine classification, so host code (scriptnode, future scriptengine, …) can share one consistent translation step instead of branching on sig.Type by hand.

Mapping rules:

  • nil signal, Type "done", or unrecognised Type return nil.
  • Type "error": errdefs wrapper chosen by Kind; unknown / empty Kind degrades to errdefs.Internal (the value is preserved in the wrapped message so observability surfaces the typo).
  • Type "interrupt": Interrupted with Cause taken from Kind (unknown values fall through to CauseCustom) and Detail taken from Message.

Returning nil for an unknown Type matches the existing scriptnode behaviour — only "error" / "interrupt" are observable side-effects.

func SubjectParallelFork

func SubjectParallelFork(runID string) event.Subject

SubjectParallelFork returns the subject an engine publishes when a parallel fan-out wave begins, before any branch starts.

engine.run.<runID>.parallel.fork

func SubjectParallelJoin

func SubjectParallelJoin(runID string) event.Subject

SubjectParallelJoin returns the subject an engine publishes when a parallel fan-out wave has merged back into the shared state.

engine.run.<runID>.parallel.join

func SubjectRunEnd

func SubjectRunEnd(runID string) event.Subject

SubjectRunEnd returns the subject every engine MUST publish exactly once when Engine.Execute returns, regardless of outcome (clean completion, interrupt, cancel, failure).

engine.run.<runID>.end

func SubjectRunStart

func SubjectRunStart(runID string) event.Subject

SubjectRunStart returns the subject every engine MUST publish exactly once when Engine.Execute begins.

engine.run.<runID>.start

func SubjectStepComplete

func SubjectStepComplete(runID, stepActor string) event.Subject

SubjectStepComplete returns the subject every engine MUST publish when one step finishes successfully. See SubjectStepStart for the stepActor format.

engine.run.<runID>.step.<stepActor>.complete

func SubjectStepError

func SubjectStepError(runID, stepActor string) event.Subject

SubjectStepError returns the subject every engine MUST publish when one step fails (i.e. when it would normally cause Execute to return a non-nil non-interrupt error). See SubjectStepStart for the stepActor format.

engine.run.<runID>.step.<stepActor>.error

func SubjectStepStart

func SubjectStepStart(runID, stepActor string) event.Subject

SubjectStepStart returns the subject every engine MUST publish when it begins executing one step. stepActor identifies the unit of work; it MUST start with the executing agent.id (so consumers can reconstruct the agent identity from the subject when the envelope header is unavailable). See the file header for the per-engine suffix conventions (graph: ".node.<id>"; iterative engine: ".iter<N>") and for why agent-level NATS wildcard fan-in goes through the event.HeaderAgentID header instead of the subject.

engine.run.<runID>.step.<stepActor>.start

func SubjectStreamDelta

func SubjectStreamDelta(runID, stepActor string) event.Subject

SubjectStreamDelta returns the subject every engine MUST use when emitting an in-flight increment from the step identified by stepActor — the canonical example is one LLM token, one dispatched tool call, or one tool result. See SubjectStepStart for the stepActor format.

Payload MUST decode to a StreamDeltaPayload; see its docs for the per-Type field requirements.

engine.run.<runID>.stream.<stepActor>.delta

func SuggestCheckpoint

func SuggestCheckpoint(e Engine) error

SuggestCheckpoint asks the engine for a voluntary checkpoint when the engine implements CheckpointSuggester; otherwise it is a no-op. Returns the engine's error directly so the host can log / retry; nil is returned both for "engine does not support suggestion" and for "engine accepted the suggestion".

func WithResumeContext

func WithResumeContext(ctx context.Context, rc ResumeContext) context.Context

WithResumeContext returns a derived context carrying rc. Engines that publish telemetry attribs (attempt count, resume signal) pull from here instead of plumbing extra parameters.

func WithRunInfo

func WithRunInfo(ctx context.Context, info RunInfo) context.Context

WithRunInfo returns a derived context carrying info. Run identity is AMBIENT: deep components (script bindings, stream adapters, helpers far below the engine's call sites) read it from the context instead of receiving it through every intermediate signature. Engines inject it once at the run boundary.

Types

type Agent

type Agent struct {
	ID       string      `json:"id"`
	Card     AgentCard   `json:"card,omitzero"`
	Tools    []string    `json:"tools,omitempty"`
	Policy   Policy      `json:"-"`
	Engine   Engine      `json:"-"`
	Prepare  []Preparer  `json:"-"`
	Observe  []Observer  `json:"-"`
	Referees []Referee   `json:"-"`
	Commit   []Committer `json:"-"`
}

Agent is the assembled, runnable form of a Definition: identity, card, tool allow-list, the constructed engine, and the attached lifecycle hooks. The deployment layer builds it from a Definition; hosts and runtimes consume it. The hook slices map 1:1 to the four slots declared on Definition.

Execute can be driven either by Agent.Engine (assembled agents) or by an engine passed explicitly to Execute (which wins).

func (*Agent) Close added in v0.1.11

func (a *Agent) Close() error

Close releases the resources held by the agent's engine and lifecycle hooks. Components that do not implement io.Closer are skipped, and close errors are aggregated with errors.Join. Idempotency of each component is that component's responsibility. Close is safe on a nil receiver and on nil/typed-nil components.

type AgentCapabilities

type AgentCapabilities struct {
	Streaming              bool `json:"streaming,omitempty"`
	PushNotifications      bool `json:"pushNotifications,omitempty"`
	StateTransitionHistory bool `json:"stateTransitionHistory,omitempty"`
}

AgentCapabilities declares which optional A2A protocol features the agent supports. JSON keys exactly match the A2A spec — note the plural PushNotifications and the longer StateTransitionHistory.

type AgentCard

type AgentCard struct {
	Name               string            `json:"name"`
	Description        string            `json:"description,omitempty"`
	Skills             []Skill           `json:"skills,omitempty"`
	DefaultInputModes  []string          `json:"defaultInputModes,omitempty"`
	DefaultOutputModes []string          `json:"defaultOutputModes,omitempty"`
	Capabilities       AgentCapabilities `json:"capabilities,omitempty"`
}

AgentCard describes an agent's capabilities for discovery. Field names and JSON tags are a proper subset of the A2A AgentCard specification: every field marshals to the exact key A2A readers expect, and AgentCard never collides with an A2A field by using a different name.

What is intentionally NOT here: url / version / provider / documentationUrl / authentication — these belong to "how the agent is exposed as a service", a deployment concern owned by the protocol layer.

func (AgentCard) Validate

func (c AgentCard) Validate() error

Validate checks the card invariants: a name and well-formed skills.

type Artifact

type Artifact struct {
	Name  string         `json:"name"`
	Parts []message.Part `json:"parts,omitempty"`
}

Artifact is a named bundle of typed parts produced during a run (e.g. "summary", "tool_output_image"). Engines that write artifacts store them in a board channel; agent collects channel contents into Artifacts on the way out.

type BaseObserver

type BaseObserver struct{}

BaseObserver provides no-op default implementations of every Observer method. Embed it in custom observers that only care about a subset of the lifecycle:

type metricsObserver struct {
    agent.BaseObserver
    metrics Metrics
}

func (o *metricsObserver) OnRunEnd(ctx context.Context, id agent.Identity, res *agent.Result) {
    o.metrics.Record(res)
}

func (BaseObserver) OnInterrupt

OnInterrupt is a no-op.

func (BaseObserver) OnRunEnd

OnRunEnd is a no-op.

func (BaseObserver) OnRunRevise

func (BaseObserver) OnRunRevise(context.Context, Identity, *Result, int)

OnRunRevise is a no-op.

func (BaseObserver) OnRunStart

func (BaseObserver) OnRunStart(context.Context, Identity, *Request)

OnRunStart is a no-op.

type BaseReferee

type BaseReferee struct{}

BaseReferee provides no-op default implementations of every Referee method. Embed it in custom referees that only override After.

func (BaseReferee) After

After is a no-op that returns a zero Decision (no opinion).

type Board

type Board struct {
	// contains filtered or unexported fields
}

Board is the engine execution blackboard: typed message channels plus untyped control vars, both protected by the same mutex.

Thread safety: every public method takes a mutex. Concurrent reads use RLock; mutations use Lock. The contract matches the original graph.Board it replaces — callers that previously held graph.Board across goroutines do not need to add any new locking.

Ownership: a message that reaches a channel is immutable from then on. Every mutating path clones on the way in, and nothing in core rewrites a message already on a channel, which is what lets read paths share the stored value instead of paying for a defensive copy — that is how Board.ChannelView serves its caller and what the script-side channel() projection reads through. A caller that wants to keep editing a message it appended, or one it read back, must clone it first.

Board is intentionally ignorant of agent concepts. It does not know what "messages", "answer" or "run id" mean; those names are established by callers. Per-execution metadata (ID, Attributes, Deps) belongs on Run, not here.

func LoadAndResume

func LoadAndResume(
	ctx context.Context,
	eng Engine,
	host Host,
	store CheckpointStore,
	run Run,
	board *Board,
	opts ...LoadAndResumeOption,
) (*Board, error)

LoadAndResume is the canonical host-side helper for "either continue the existing run or start fresh". It:

  1. Loads the most recent checkpoint for run.RunID from store.
  2. If a checkpoint exists, validates it against the engine's Resumer (if implemented) — invalid checkpoints surface immediately rather than after a partial Execute.
  3. Populates run.ResumeFrom and threads a ResumeContext onto ctx so the engine, observers and middleware see consistent replay metadata.
  4. Calls eng.Execute and returns its result.

store may be nil; that is treated as "no checkpoints persisted" and is equivalent to a fresh-start with WithFreshStartAllowed honoured. board is the bootstrap board used on fresh starts and when the engine wishes to keep the host-supplied initial state (the engine itself decides whether to override with ResumeFrom.Board per the Run.ResumeFrom contract).

LoadAndResume is intentionally a one-shot helper, not a retry loop: a supervisor that wants exponential backoff or budget enforcement composes LoadAndResume with its own retry policy.

func NewBoard

func NewBoard() *Board

NewBoard creates an empty Board with an initialised main channel so callers can Board.AppendChannelMessage without a nil-check.

func RestoreBoard

func RestoreBoard(snap *BoardSnapshot) *Board

RestoreBoard reconstructs a Board from a snapshot. Passing nil returns a fresh empty board so resume code can use this unconditionally.

func (*Board) AppendChannelMessage

func (b *Board) AppendChannelMessage(name string, msg message.Message)

AppendChannelMessage appends a message to a channel, creating the channel on demand.

func (*Board) AppendChannelMessages added in v0.4.7

func (b *Board) AppendChannelMessages(name string, msgs []message.Message)

AppendChannelMessages appends msgs to a channel in order, creating the channel on demand. The batch lands under one lock, so a concurrent reader sees either none of it or all of it, and every message is copied: later mutations by the caller do not affect the Board.

func (*Board) AppendSliceVar

func (b *Board) AppendSliceVar(key string, value any) error

AppendSliceVar atomically appends a value to a slice stored in a board variable. It returns an error if the existing value is not a []any (instead of silently overwriting), so callers cannot lose data by typo'ing a key already used for a non-slice value.

func (*Board) Channel

func (b *Board) Channel(name string) []message.Message

Channel returns a copy of messages for the given channel. An empty or missing channel returns a nil slice (not a zero-length slice) so callers can use len() == 0 uniformly.

func (*Board) ChannelLen added in v0.4.7

func (b *Board) ChannelLen(name string) int

ChannelLen returns the number of messages on a channel without copying them. Callers that only need the count should use it: Board.Channel deep-copies the whole channel, which a len() check pays for nothing.

func (*Board) ChannelTail added in v0.4.7

func (b *Board) ChannelTail(name string, count int) []message.Message

ChannelTail returns a copy of the last count messages of a channel, in channel order. A non-positive count, an empty channel and a missing channel all yield nil; a count past the channel's length yields the whole channel. Only the tail is copied, so inspecting the end of a long channel does not pay for its head.

func (*Board) ChannelView added in v0.4.7

func (b *Board) ChannelView(name string) []message.Message

ChannelView returns a read-only view of a channel's messages: the returned slice belongs to the caller (assigning, reordering and slicing its elements is safe), while the messages and their parts are shared with the Board and must never be mutated.

It exists for read paths that hand the messages straight to a projection (encoding, marshalling, size accounting) where Board.Channel's deep copy is pure overhead. Anything that retains what it reads, mutates it, or hands it to third-party callers must use Board.Channel instead.

func (*Board) ChannelsCopy

func (b *Board) ChannelsCopy() map[string][]message.Message

ChannelsCopy returns a deep copy of all channel message lists. Used by parallel branch execution to give each branch an independent view that can later be merged.

func (*Board) Clone

func (b *Board) Clone() *Board

Clone returns an independent copy of the board: channels and vars are deep-copied, the new board has its own mutex, and mutations on either board do not affect the other. Use it from a Preparer that wants to extend an existing board's state without mutating the previous link's output.

func (*Board) DeleteVar

func (b *Board) DeleteVar(key string)

DeleteVar removes a board-level variable. Concurrent-safe.

func (*Board) GetVar

func (b *Board) GetVar(key string) (any, bool)

GetVar retrieves a board-level variable.

func (*Board) GetVarString

func (b *Board) GetVarString(key string) string

GetVarString retrieves a board variable as a string. It returns "" when the key is missing or the stored value is not a string.

func (*Board) LastMessage added in v0.4.7

func (b *Board) LastMessage(name string) (message.Message, bool)

LastMessage returns a copy of the last message on a channel. The boolean is false for an empty or missing channel, matching Board.PopChannelMessage. Use it instead of reading the whole channel to look at its tail.

func (*Board) PopChannelMessage added in v0.2.1

func (b *Board) PopChannelMessage(name string) (message.Message, bool)

PopChannelMessage removes and returns the last message on a channel. The boolean reports whether a message was removed; an empty or missing channel yields the zero Message and false. The returned message is the channel's stored copy and must not be mutated.

func (*Board) Resolve added in v0.1.15

func (b *Board) Resolve(v any) (any, error)

Resolve expands board references throughout v, a decoded JSON value (strings, []any, map[string]any, or leaf scalars). References inside object keys are rejected: config keys are part of the node type's contract and must stay constant.

func (*Board) ResolveString added in v0.1.15

func (b *Board) ResolveString(s string) (any, error)

ResolveString expands board references in s. When s is exactly one reference, the variable's typed value (or typed default) is returned; otherwise the interpolated string is returned. Missing references fail with a validation error unless a default is given.

func (*Board) RestoreFrom

func (b *Board) RestoreFrom(snap *BoardSnapshot)

RestoreFrom overwrites this board from a snapshot. Used by retry / rollback paths inside an executor: the executor takes a snapshot before a risky step, then restores the same Board (preserving its identity for nodes that captured a pointer to it) on failure.

func (*Board) SetChannel

func (b *Board) SetChannel(name string, msgs []message.Message)

SetChannel replaces the entire message list for a channel. The input slice is copied; later mutations by the caller do not affect the Board.

func (*Board) SetVar

func (b *Board) SetVar(key string, value any)

SetVar sets a board-level variable. Concurrent-safe.

func (*Board) Snapshot

func (b *Board) Snapshot() *BoardSnapshot

Snapshot returns a serialisable copy of the current state. Vars are deep-copied so the snapshot is safe to retain after further Board mutations; values implementing Cloneable are duplicated through their Clone method, otherwise reflection is used.

func (*Board) UpdateSliceVarItem

func (b *Board) UpdateSliceVarItem(key string, match func(any) bool, update func(any) any)

UpdateSliceVarItem finds and updates the first matching item in a slice variable. Missing keys and non-slice values are silently ignored — the typical use is "update the entry I just appended", and the caller has already verified the slice exists.

func (*Board) Vars

func (b *Board) Vars() map[string]any

Vars returns a shallow copy of all board-level variables. Mutations to the returned map do not affect the Board.

type BoardSnapshot

type BoardSnapshot struct {
	Vars     map[string]any               `json:"vars"`
	Channels map[string][]message.Message `json:"channels,omitempty"`
}

BoardSnapshot is a serialisable representation of board state, used for resume / checkpoint flows. It carries no live mutex and is safe to JSON-encode.

func (*BoardSnapshot) Clone

func (s *BoardSnapshot) Clone() *BoardSnapshot

Clone returns an independent deep copy of the snapshot. Use it when retaining a snapshot after the producing board mutates further, or when a checkpoint store needs caller-owned copies on Save/Load.

type Capabilities

type Capabilities struct {
	// SupportsResume reports whether Execute can honour
	// Run.ResumeFrom. Engines that always return errdefs.NotAvailable
	// for a non-nil ResumeFrom MUST leave this false; deployments
	// enforcing a restart-always policy need the true case to recover
	// mid-run state without losing partial work.
	SupportsResume bool `json:"supports_resume,omitempty" yaml:"supports_resume,omitempty"`

	// EmitsUserPrompt reports whether the engine may call
	// Host.AskUser during Execute. Hosts deploying in headless /
	// batch mode use this to refuse engines that would block waiting
	// for a reply that nobody can supply.
	EmitsUserPrompt bool `json:"emits_user_prompt,omitempty" yaml:"emits_user_prompt,omitempty"`

	// EmitsCheckpoint reports whether the engine writes Checkpoints
	// during Execute (independently of SupportsResume — an engine
	// can write checkpoints that only an external tool can replay).
	// Hosts use this to decide whether to attach a CheckpointStore.
	EmitsCheckpoint bool `json:"emits_checkpoint,omitempty" yaml:"emits_checkpoint,omitempty"`
}

Capabilities describes the optional features an engine kind declares to its host. The declaration is made once, statically, when the engine kind is registered — engine factories expose it through a capability interface that core/deploy and hosts assert — never probed per instance at run time. Hosts read capabilities to:

  • validate a deployment spec at Apply time (e.g. a restart policy requiring resume is rejected when the engine kind does not claim SupportsResume);
  • decide which host capabilities to wire (attach a CheckpointStore only when EmitsCheckpoint is claimed);
  • refuse to run an engine that needs user prompts in a headless deployment (EmitsUserPrompt=true on a host without an interactive channel becomes a fail-fast).

All fields default to "do not claim the capability" (zero value).

type Cause

type Cause string

Cause classifies why a run was asked to stop. The agent layer maps these onto its higher-level commit/discard policy (e.g. discard the partial output on user_input, commit it on host_shutdown).

Engines should NEVER branch on Cause for control flow — Cause is metadata for the host, not a directive for the engine. The engine's only correct response to any cause is "stop cleanly and return".

const (
	// CauseUnknown is the zero value. Hosts should avoid sending it;
	// it exists so a zero-value Interrupt is recognisable.
	CauseUnknown Cause = ""

	// CauseUserCancel is a user-initiated cancel ("stop talking",
	// "abort this turn"). Output is typically discarded.
	CauseUserCancel Cause = "user_cancel"

	// CauseUserInput is a barge-in: the user spoke / typed and the
	// agent should yield to fresh input. Output is typically discarded.
	CauseUserInput Cause = "user_input"

	// CauseHostShutdown is a graceful shutdown from the host.
	// Output should typically be committed if any was produced.
	CauseHostShutdown Cause = "host_shutdown"

	// CauseCustom carries a host-defined cause in [Interrupt.Detail].
	CauseCustom Cause = "custom"
)

type Checkpoint

type Checkpoint struct {
	// ExecID identifies the engine execution this checkpoint belongs
	// to. MUST equal the producing [Run.RunID].
	ExecID string `json:"exec_id"`

	// Steps is an opaque, engine-defined marker set locating "where"
	// the run is. For graph it is the most recently completed wave —
	// the resume anchor (the frontier is rebuilt from it) and the
	// "paused at" display (last element). A single-node wave degrades
	// to a one-element slice; empty means "no position recorded".
	// The host treats these as opaque strings.
	Steps []string `json:"steps,omitempty"`

	// Iteration is an optional monotonic counter for engines that
	// loop (graph re-entry counter, scheduler tick, …). Zero is fine
	// when the engine doesn't track iterations.
	Iteration int `json:"iteration,omitempty"`

	// Board is the Board state at the boundary. Always non-nil.
	Board *BoardSnapshot `json:"board"`

	// Payload is engine-specific extra state the engine wants to
	// persist alongside the Board. Treated as opaque JSON by the
	// store; the producing engine is the only consumer that knows
	// how to decode it.
	Payload json.RawMessage `json:"payload,omitempty"`

	// Attributes mirrors [Run.Attributes] at the time the checkpoint
	// was produced (run id at the agent layer, tenant, graph id, …).
	// Stores may use these for indexing/lookup.
	Attributes map[string]string `json:"attributes,omitempty"`

	// Timestamp is the wall-clock time the engine produced the
	// checkpoint. Hosts may overwrite when they actually persist.
	Timestamp time.Time `json:"timestamp"`

	// OriginalStartedAt is the wall-clock time the original (fresh)
	// run started. Stays constant across resumes so dashboards
	// computing total wall time (e.g. SLO budget burn) don't reset
	// every time a host re-loads the checkpoint and resumes.
	//
	// Engines SHOULD copy this from [ResumeContext.StartedAt] (when
	// resuming) or from the time they began the fresh run (when
	// producing the first checkpoint). Hosts driving resume via
	// [LoadAndResume] thread the value through automatically — the
	// helper reads OriginalStartedAt off the loaded checkpoint and
	// stamps it back on the next ResumeContext.
	//
	// Zero time means "not recorded" (engines that ship before this
	// field was added, or that don't track wall time). Consumers
	// should fall back to Timestamp in that case.
	OriginalStartedAt time.Time `json:"original_started_at,omitzero"`

	// SpecVersion identifies the engine's spec / definition version
	// at the time the checkpoint was produced. The format is
	// engine-defined: graph runner uses the graph definition's
	// Version (falling back to its Name when Version is empty); a
	// script engine could store a content hash; a host application
	// could compose a spec document version.
	//
	// CanResume implementations compare this against the engine's
	// current version: a mismatch means the underlying spec has
	// drifted (graph re-edited, script reloaded, application spec
	// reapplied with a new agent definition) and silently resuming
	// would replay against semantics the original run never saw.
	// Engines that detect drift surface errdefs.NotAvailable from
	// CanResume so the host can either fall back to a fresh run or
	// surface the mismatch to the operator.
	//
	// Empty means "no version recorded" — older checkpoints, or
	// engines that have no concept of versioned spec. CanResume
	// MUST treat empty as "skip drift check" rather than "always
	// fail" so old checkpoints stay loadable.
	SpecVersion string `json:"spec_version,omitempty"`
}

Checkpoint is the engine-agnostic persistence record produced at a safe boundary during execution. Each engine decides what its own step marker / payload looks like; this struct only owns the common envelope shape.

Engines populate Checkpoint and hand it to Checkpointer.Checkpoint (the host method). The host is responsible for writing it durably; engines must not assume the call has persisted anything.

func (Checkpoint) Clone

func (cp Checkpoint) Clone() Checkpoint

Clone returns an independent deep copy of cp. Stores should clone on Save (the caller keeps ownership of cp) and again on Load (the returned record is owned by the caller), so concurrent callers can never observe each other's mutations.

func (Checkpoint) Validate

func (cp Checkpoint) Validate() error

Validate checks the envelope invariants every engine and every durable store can rely on. Engine-specific fields (Steps, Iteration, Payload shape, SpecVersion) are intentionally left to the engine's own Resumer implementation so engines can keep minimal or strict admission policies.

type CheckpointDeleter

type CheckpointDeleter interface {
	Delete(ctx context.Context, execID string) error
}

CheckpointDeleter optionally extends CheckpointStore with the ability to delete a single execution's checkpoints. Used by the host when a run completes successfully and its checkpoints are no longer needed. Implementations must be safe for concurrent use.

type CheckpointLister

type CheckpointLister interface {
	List(ctx context.Context) ([]string, error)
}

CheckpointLister optionally extends CheckpointStore with the ability to enumerate persisted exec ids. Stores that support listing satisfy this interface; agent-level resume / dashboard code can type-assert to it. Implementations must be safe for concurrent use. The returned slice is caller-owned.

type CheckpointStore

type CheckpointStore interface {
	Save(ctx context.Context, cp Checkpoint) error
	Load(ctx context.Context, execID string) (*Checkpoint, error)
}

CheckpointStore is the host-side persistence contract. The host's Checkpointer.Checkpoint implementation typically delegates to a CheckpointStore. The interface is intentionally narrow: Save persists; Load returns the most-recent persisted record for the given exec id, or (nil, nil) if absent.

Save must atomically replace the record for cp.ExecID; when two Save calls for the same exec id overlap, the later call wins. The store must treat cp as caller-owned and deep-copy before persisting. Load must return a caller-owned copy: mutating the returned checkpoint must not affect the store or other Load results. All methods must be safe for concurrent use.

type CheckpointSuggester

type CheckpointSuggester interface {
	SuggestCheckpoint() error
}

CheckpointSuggester is the optional engine-side interface a host uses to ask the engine to write a Checkpoint at the next safe boundary — typically before a voluntary restart, scale-down, or reschedule.

Semantics (advisory, not synchronous):

  • The engine SHOULD call its host's Checkpointer at the next point in execution where Checkpoint.Steps is well-defined. It is NOT obligated to interrupt itself; SuggestCheckpoint returns immediately with no guarantee that the checkpoint has been written by the time it returns.
  • The host typically pairs SuggestCheckpoint with a follow-up Interrupt after a grace period: "save what you can, then stop".
  • Engines that have no notion of a step boundary (purely memory-resident, sub-second runs) MAY treat this as a no-op.

Unlike Capabilities this is a *behavioural* interface — the engine has to do something — so it stays an interface assertion rather than a spec field.

type Checkpointer

type Checkpointer interface {
	Checkpoint(ctx context.Context, cp Checkpoint) error
}

Checkpointer persists a checkpoint at a safe boundary the engine has reached. The host decides whether to actually write; engines must not assume durability.

Hosts without configured checkpointing should make this a no-op (return nil) rather than an error so engines can call it unconditionally.

type Cloneable

type Cloneable interface {
	Clone() any
}

Cloneable may be implemented by values stored in Board vars to provide a type-safe deep copy instead of the reflection fallback used by Board.Snapshot and RestoreBoard.

type CommitView

type CommitView struct {
	// LastBoard is materialized into the shallow Result copy passed to
	// Committers. It must be non-nil.
	LastBoard *Board
}

CommitView is the board projection a CommitViewProvider exposes only to Committer hooks. The engine-produced board remains authoritative for the Result returned to the caller and delivered to Observer.

type CommitViewProvider

type CommitViewProvider interface {
	CommitView(ctx context.Context, id Identity, req *Request, res *Result) (CommitView, error)
}

CommitViewProvider builds a Committer-only projection after the final Referee decision accepts the result and immediately before the Committer chain runs.

Providers run only when at least one Committer is registered and the final Result is committed. They MUST honor ctx cancellation and MUST NOT mutate req or res. Returning an error, or a CommitView with a nil LastBoard, skips every Committer and fails finalization.

type CommitViewProviderFunc

type CommitViewProviderFunc func(ctx context.Context, id Identity, req *Request, res *Result) (CommitView, error)

CommitViewProviderFunc adapts a function to CommitViewProvider.

func (CommitViewProviderFunc) CommitView

func (f CommitViewProviderFunc) CommitView(
	ctx context.Context,
	id Identity,
	req *Request,
	res *Result,
) (CommitView, error)

CommitView calls f.

type Committer

type Committer interface {
	Commit(ctx context.Context, id Identity, req *Request, res *Result) error
}

Committer persists or durably enqueues the final accepted result of a run. It is the reliable side-effect boundary between Referee and Observer:

  • Referees decide whether the final result is accepted.
  • Committers make an accepted result durable and may fail the run.
  • Observers react best-effort after commitment has succeeded or failed.

Committers run synchronously in registration order and only when the final Result has Committed set. When a CommitViewProvider is configured, Committers receive a shallow Result copy materialized from its projected board; callers and Observers retain the engine result. Revise attempts and discarded or non-completed results are never committed. Implementations MUST treat Identity.RunID as the operation's idempotency key because a caller may retry after an ambiguous storage or transport failure. A Referee's Revise request that the configured budget does not honor is advisory and does not by itself clear Committed; Referees that require the output to be withheld must also set DiscardOutput.

A Committer MUST honor ctx cancellation and MUST NOT mutate req or res. Multiple Committers do not form a transaction: if a later Committer fails, earlier ones may already have succeeded. Prefer one Committer that writes to a transactional store or durable outbox when atomicity across side effects matters.

type CommitterFunc

type CommitterFunc func(ctx context.Context, id Identity, req *Request, res *Result) error

CommitterFunc adapts a function to Committer.

func (CommitterFunc) Commit

func (f CommitterFunc) Commit(ctx context.Context, id Identity, req *Request, res *Result) error

Commit calls f.

type Decision

type Decision struct {
	// AcceptOutput, when true, positively accepts the engine-produced
	// output regardless of Status. This is primarily used to retain
	// useful partial output from a cooperative interruption through the
	// normal Committer chain.
	//
	// DiscardOutput has final precedence when both directives are
	// present, including when they come from different Referees.
	AcceptOutput bool

	// DiscardOutput, when true, instructs Run to mark Result.Committed
	// = false regardless of Status. Committers are skipped for a
	// discarded run.
	//
	// Setting DiscardOutput on a StatusCompleted run is allowed and
	// useful for moderation hooks ("the answer violates policy, do
	// not persist it").
	DiscardOutput bool

	// Revise asks agent.Execute to discard this attempt's output and
	// re-invoke Execute with a fresh board (re-seeded from
	// the original Request). Honoured ONLY when the per-call
	// [WithMaxRevise] budget allows another attempt; the option
	// defaults to 0, so by default Revise is recorded as a
	// finalize_reason but does NOT trigger another engine call —
	// callers must opt in explicitly to avoid runaway loops on
	// faulty Referees. An unhonored Revise does not change Committed;
	// set DiscardOutput as well when the current output must not be
	// persisted.
	//
	// When honoured the lifecycle is:
	//
	//   1. Referee returns Revise=true (and optionally Reason).
	//   2. Run fires [Observer.OnRunRevise] with the about-to-be-
	//      discarded Result and the next attempt index.
	//   3. Board is re-seeded via the Preparer chain; the
	//      same Run identifier is reused so observers that
	//      key by run id can correlate attempts.
	//   4. Execute runs again. The Referee chain runs again
	//      on the new Result.
	//   5. The loop exits when either Revise=false or the attempt
	//      counter reaches WithMaxRevise. The final Result.Attempts
	//      reflects how many Execute calls were made.
	//
	// The Reason field is exposed via [Result.State]["finalize_reason"]
	// regardless of whether the Revise was honoured, so callers
	// can audit why a particular attempt did not commit.
	Revise bool

	// Reason, when non-empty, is propagated into
	// [Result.State]["finalize_reason"] and is also returned to the
	// caller through the same field. Referees that want to attribute
	// a decision in logs (e.g. "moderation:violation") set this
	// rather than relying on log scraping.
	//
	// Composition rule: the first non-empty Reason wins. This means
	// Referees later in the chain should only set Reason if they
	// have something more specific to add.
	Reason string

	// State carries optional structured metadata the referee wants
	// attached to [Result.State] (e.g. a handoff event). Referees MUST
	// NOT mutate res directly; this is the only channel for adding
	// result metadata. Entries are merged after the referee chain, with
	// later referees winning on key conflicts.
	State map[string]any
}

Decision is the return type of Referee.After. The zero value means "no opinion" — agent applies its defaults.

Defaults agent.Run uses when no Referee returns a directive:

  • StatusCompleted runs are committed.
  • StatusInterrupted / StatusCanceled / StatusAborted / StatusFailed runs are NOT committed (their partial output is dropped from the transcript view). This matches the conservative behaviour Round A had hard-coded; round B simply makes it overridable.

func (Decision) IsZero

func (d Decision) IsZero() bool

IsZero reports whether no referee expressed any opinion.

type Definition

type Definition struct {
	Card     AgentCard `json:"card,omitzero"`
	Tools    []string  `json:"tools,omitempty"`
	Engine   EngineRef `json:"engine,omitzero"`
	Policy   *Policy   `json:"policy,omitempty"`
	Prepare  []Hook    `json:"prepare,omitempty"`
	Observe  []Hook    `json:"observe,omitempty"`
	Referees []Hook    `json:"referees,omitempty"`
	Commit   []Hook    `json:"commit,omitempty"`
}

Definition is the document form of an agent: the identity card, the tool allow-list, the engine selection (itself a resource), the resource bindings, and the lifecycle hooks by slot. The assembled runtime form is Agent, built from a Definition by the deployment layer.

func (Definition) Validate

func (d Definition) Validate() error

Validate checks the definition DTO.

type DiscardOnInterruptCauses

type DiscardOnInterruptCauses struct {
	Reason string
	// contains filtered or unexported fields
}

DiscardOnInterruptCauses is a Referee factory: when ANY of the named causes fires, DiscardOutput is set so a barge-in (or equivalent host-side abort) keeps partial assistant output out of the persistent transcript.

Construct it with NewDiscardOnInterruptCauses and register as one of Agent.Referees or via WithReferee.

func NewDiscardOnInterruptCauses

func NewDiscardOnInterruptCauses(reason string, causes ...Cause) *DiscardOnInterruptCauses

NewDiscardOnInterruptCauses builds a Referee that marks a run discarded whenever its interrupt cause matches one of causes. Reason is the string used as Decision.Reason when discarding fires, and is also surfaced in Result.State["finalize_reason"].

func (*DiscardOnInterruptCauses) After

After implements Referee. It inspects res.Status and the interrupt cause attached to the run, returning a discarding Decision when both are present and the cause matches.

type Engine

type Engine interface {
	Execute(ctx context.Context, run Run, host Host, board *Board) (*Board, error)
}

Engine is the deliberately thin contract every local execution engine satisfies so the agent layer can drive it through a uniform shape. Concrete engines (core/graph DAG executor, future script- based engines, …) usually expose richer APIs in addition to this method.

Contract:

  • Execute MUST run to completion, until interrupted, or until ctx is cancelled.

  • On clean completion, return the final Board (often the same pointer as the input — engines mutate in place by default) and a nil error.

  • On cooperative interrupt (host sent through host.Interrupts()), return the (partial) Board together with the result of Interrupted. The error then satisfies errdefs.IsInterrupted(err) and can be destructured into an InterruptedError for the cause.

  • On ctx cancellation, return the (partial) Board and ctx.Err().

  • On any other failure, return the (partial) Board together with a domain error (preferably classified via errdefs). Returning a non-nil board on error lets the host decide whether to commit / discard / persist.

  • When run.ResumeFrom is non-nil, Execute resumes from that checkpoint instead of starting fresh. See Run.ResumeFrom for the resume contract; engines that do not support resume MUST return an errdefs.NotAvailable-classified error rather than silently restarting.

Engines MUST NOT close any host-owned channel and MUST NOT mutate run.Attributes or run.ResumeFrom.

type EngineFunc

type EngineFunc func(ctx context.Context, run Run, host Host, board *Board) (*Board, error)

EngineFunc adapts a plain function to the Engine interface. Useful for test doubles and trivial engines.

func (EngineFunc) Execute

func (f EngineFunc) Execute(ctx context.Context, run Run, host Host, board *Board) (*Board, error)

Execute satisfies Engine.

type EngineRef

type EngineRef struct {
	Kind     resource.Kind   `json:"kind"`
	Impl     string          `json:"impl,omitempty"`
	Deps     resource.Deps   `json:"deps,omitempty"`
	Settings json.RawMessage `json:"settings,omitempty"`
}

EngineRef selects the agent engine resource and its bindings. It is the document-layer reference: the runtime execution contract will be a separate Engine interface (not yet in core), so the reference deliberately does not reuse that name.

func (EngineRef) Validate

func (e EngineRef) Validate() error

Validate checks the engine reference DTO.

type ErrorKind

type ErrorKind string

ErrorKind enumerates the errdefs categories scripts may set on signal.error({ kind, message }). Only categories that can plausibly originate from script logic are exposed — auth, rate-limit, timeout and similar wire-level classifications stay in the Go layer where they are produced.

Unknown kinds passed by a script degrade to ErrorKindInternal in SignalToError so a typo in script land cannot escape as an unclassified error.

const (
	// ErrorKindValidation marks user-input / config validation
	// failures. Maps to [errdefs.Validation] (HTTP 400).
	ErrorKindValidation ErrorKind = "validation"

	// ErrorKindNotFound marks a lookup the script performed that
	// returned nothing. Maps to [errdefs.NotFound] (HTTP 404).
	ErrorKindNotFound ErrorKind = "not_found"

	// ErrorKindBudgetExceeded marks a per-run / per-tenant budget
	// (token, cost, iteration count) the script self-detected.
	// Maps to [errdefs.BudgetExceeded] (HTTP 429).
	ErrorKindBudgetExceeded ErrorKind = "budget_exceeded"

	// ErrorKindPolicyDenied marks a policy-layer refusal raised from
	// inside the script (tool allow-list, role check, …). Maps to
	// [errdefs.PolicyDenied] (HTTP 403).
	ErrorKindPolicyDenied ErrorKind = "policy_denied"

	// ErrorKindNotAvailable marks a transient unavailability of a
	// dependency the script needed (e.g. a knowledge store down).
	// Maps to [errdefs.NotAvailable] (HTTP 503).
	ErrorKindNotAvailable ErrorKind = "not_available"

	// ErrorKindInternal is the default fallback for uncategorised
	// script-side errors. Maps to [errdefs.Internal] (HTTP 500).
	ErrorKindInternal ErrorKind = "internal"
)

type EventBusProvider

type EventBusProvider interface {
	EventBus() event.Bus
}

EventBusProvider is an optional Host capability for consumers that need to subscribe to the same event surface used by Publisher.

The Host (or its owning engine/runtime) owns the returned bus and its lifecycle. Consumers only borrow it: they may subscribe and publish through the Host, but must not close the bus.

EventBusProvider is intentionally not part of Host. Hosts that only publish events do not need to manufacture a subscribable bus, and NoopHost deliberately does not implement this capability.

type ExecuteOption

type ExecuteOption func(*execConfig)

ExecuteOption configures one Run invocation. Options are stateless and may be reused across calls.

func WithArtifactChannels

func WithArtifactChannels(channels ...string) ExecuteOption

WithArtifactChannels names the Board channels Run should harvest into Result.Artifacts on the way out. One Artifact per listed channel: Artifact.Name = channel name; Artifact.Parts = flat concatenation of every Part across every Message in the channel (in board-write order). Channels that hold no messages after the run produce no Artifact entry — empty channels do not pollute the result with empty bundles.

This is the writer side of the Result.Artifacts contract that contract-audit #6 flagged: the godoc had been promising "engines store them in a board channel; agent collects channel contents into Artifacts on the way out" since v0.1, but no agent.Run code path actually performed the collection so the field was permanently nil for every caller.

MUST NOT include MainChannel — Run already promotes that channel into Result.Messages and a duplicate harvest would surface the same payload twice with confusing semantics (Messages keeps role + tool metadata; Artifacts is the modality-bundle view). Run silently skips MainChannel if the caller mistakenly passes it.

Example: an engine that writes a "summary" markdown blob and an "audio" TTS clip to dedicated channels:

res, _ := agent.Execute(ctx, ag, eng, req,
    agent.WithArtifactChannels("summary", "audio"))
for _, a := range res.Artifacts {
    switch a.Name { ... }
}

Multiple WithArtifactChannels calls accumulate (deduped at collection time) so per-agent and per-call lists compose. nil / empty input is a no-op.

func WithAttributes

func WithAttributes(extra map[string]string) ExecuteOption

WithAttributes adds extra attributes that flow into Run.Attributes alongside the well-known agent_id / run_id / task_id / context_id keys. Caller-supplied keys win on conflict; agent does not overwrite.

Engines that need caller-supplied metadata read Run.Attributes via the same codepath as the well-known keys. Callers serialise any non-string values at the call site and pass the resulting map[string]string here.

func WithCommitViewProvider

func WithCommitViewProvider(p CommitViewProvider) ExecuteOption

WithCommitViewProvider installs the Committer-only result projection provider for this call. It runs after the final Referee decision and only when an accepted Result has at least one Committer to invoke.

Multiple calls use last-write-wins semantics. A final nil or typed-nil provider disables an earlier provider for this call.

func WithCommitter

func WithCommitter(c Committer) ExecuteOption

WithCommitter registers a Committer for this run. Committers run in registration order after any [Agent.Committers], and only for the final accepted result. The first error stops the chain and is returned from Execute.

func WithHost

func WithHost(h Host) ExecuteOption

WithHost installs the Host passed to the

Host is the single extension point for every host-side capability the engine needs: event publishing (Publisher), interrupt injection (Interrupter), user prompting (UserPrompter), checkpoint persistence (Checkpointer), and token-usage reporting (UsageReporter). Composing your own host is how you wire any of those — agent does not provide narrow shortcuts because most non-trivial deployments share state across capabilities (a single metric client, a single OTel tracer, a single request-scoped logger) and a host implementation is the cleanest place to keep that state.

A host that also supports event subscriptions may implement the optional EventBusProvider. The host or engine owns that bus and closes it at the end of its lifecycle; engine consumers only borrow it and must not close it.

Embed NoopHost in your host struct and override only the methods you actually need:

type myHost struct {
    NoopHost
    bus    event.Bus
    intrCh <-chan Interrupt
}
func (h *myHost) Publish(ctx context.Context, e event.Envelope) error {
    return h.bus.Publish(ctx, e)
}
func (h *myHost) Interrupts() <-chan Interrupt { return h.intrCh }

When omitted, agent falls back to NoopHost, which silently drops envelopes, never fires interrupts, refuses AskUser, drops checkpoints, and discards usage. That default is appropriate for fire-and-forget batch runs and tests — anything else needs a real host.

func WithMaxRevise

func WithMaxRevise(n int) ExecuteOption

WithMaxRevise sets the upper bound on Execute invocations per Run call when a Referee returns Decision{Revise: true}.

  • n <= 1 (default 0) disables the revise loop entirely. A Referee asking to revise records its Reason but Run still returns after the first attempt — the safe default avoids surprise infinite loops on misconfigured After.

  • n >= 2 caps total attempts at n. The loop exits as soon as either no Referee asks for revise OR the attempt counter reaches n. The final Result.Attempts is the actual number of Execute calls made.

Revise restarts re-seed the board from the original Request via the configured Preparer, so the engine sees fresh inputs. Run.ResumeFrom is dropped after the first attempt — Revise means "retry from scratch", not "replay a checkpoint".

Negative values are treated as 0 (disabled). Callers that want the engine to drive its own retry policy (rate-limit backoff, transient LLM errors, …) MUST keep WithMaxRevise at the default; the revise loop is the agent-policy layer, not the engine-transport one.

func WithObserver

func WithObserver(o Observer) ExecuteOption

WithObserver registers a Observer for this run. Multiple observers can be registered; they fire in registration order, after any [Agent.Observers] declared on the agent value. Panics inside an observer are caught and dropped.

func WithParentRunID

func WithParentRunID(id string) ExecuteOption

WithParentRunID stamps every Run this call dispatches with the supplied parent run id (Run.ParentRunID). Use it when one agent.Run is spawned by another (multi-agent call chain, handoff, sub-agent dispatch) so dashboards / pod controllers can reconstruct the call tree and apply loop-detection / depth budgets against a stable correlation key.

The empty string is a no-op; passing the parent's runID (typically obtained from agent.Identity.RunID inside an Observer / Referee on the parent run) is the canonical use. agent.Execute does NOT auto-derive ParentRunID from any ambient context — explicit is the only contract that survives ctx propagation rewrites and cross-process dispatch (application runtimes, A2A bridges).

Engines / hosts that don't read ParentRunID are unaffected. The field is also surfaced under telemetry.AttrParentRunID by engine telemetry (the graph runner stamps it onto the run span).

func WithPreparer

func WithPreparer(p Preparer) ExecuteOption

WithPreparer appends a Preparer to this run's chain. The chain runs after agent applies any [Agent.Preparers] declared on the agent value, so a call-site Preparer can build on top of agent-level preparation (history load, system prompt, etc.).

When no Preparer is registered anywhere, agent seeds a default board that appends req.Message to MainChannel and copies req.Inputs into board vars.

func WithReferee

func WithReferee(d Referee) ExecuteOption

WithReferee registers a Referee for this run. Multiple deciders can be registered; they fire in registration order, after any Agent.Referees declared on the agent value. Their decisions are merged via OR over boolean fields; the first non-empty Reason wins.

func WithResumeFrom

func WithResumeFrom(cp *Checkpoint) ExecuteOption

WithResumeFrom replays an interrupted run from a previously captured Checkpoint. The agent threads cp into Run.ResumeFrom and overrides the run id to cp.ExecID so the underlying engine's Resumer.CanResume sees ExecID == Run.RunID (cross-run checkpoints are programmer errors and surface as errdefs.Validation from the engine).

Typical use: a host loaded a checkpoint via its CheckpointStore, possibly after a process restart, and wants the agent to keep going from that point rather than start fresh. The host still passes the ORIGINAL agent.Request (same task id, same inputs); the engine restores board state from the checkpoint so the re-seeded inputs are effectively overwritten by the resumed state. Engines without Resumer surface NotAvailable (per the Engine contract); resume against an unsupported engine is a configuration error, not silent fall-through.

nil cp is a no-op (= fresh start). Multiple WithResumeFrom calls last-write-wins; agent does not attempt to merge checkpoints.

func WithRunTimeout added in v0.4.1

func WithRunTimeout(d time.Duration) ExecuteOption

WithRunTimeout bounds the wall-clock duration of one Execute call — seeder, every revise attempt, Referees and Committers included.

Use it when an engine's own timeout is per-Execute: that budget restarts on every attempt, so with a revise budget of N the logical run can take up to N × engine-timeout. WithRunTimeout caps the sum. The engine timeout still applies; the shorter deadline wins.

  • d > 0 installs the bound;
  • d <= 0 is a no-op (no run-level bound; the caller's context rules). This is the default.

A resume is a separate Execute call and therefore gets a fresh window; Policy.RunTimeout declares the same bound on the document form and is overridden by this option.

func WithToolAllowList

func WithToolAllowList(names []string) ExecuteOption

WithToolAllowList overrides Agent.Tools for this call — the caller-supplied list wins wholesale (no merge), mirroring the "caller-supplied wins" rule WithAttributes documents. Pass it from tests or from power-user flows that narrow an agent's tool gate for one specific turn.

type Hook

type Hook struct {
	Type     string          `json:"type"`
	Deps     resource.Deps   `json:"deps,omitempty"`
	Settings json.RawMessage `json:"settings,omitempty"`
}

Hook is one agent lifecycle hook entry: the hook type (looked up in the resource registry under "hook.<slot>"), its resource bindings, and its opaque settings. All four slots share the same data shape.

func (Hook) Validate

func (h Hook) Validate() error

Validate checks the hook DTO.

type Host

Host is the contract a runtime exposes to a running engine.

Host is intentionally a *composition* of small, single-method interfaces — Publisher, Interrupter, UserPrompter, Checkpointer. The composition exists to keep Engine.Execute readable; downstream code (graph nodes, tools, …) should depend on the smallest interface it actually needs:

// A pure-mapping node only emits events:
func (n *MapNode) Execute(ctx, board, pub engine.Publisher) error

// A streaming LLM node also wants the interrupt channel:
func (n *LLMNode) Execute(ctx, board,
    pub engine.Publisher, intr engine.Interrupter) error

Host implementations must be safe for concurrent use. The engine may invoke any method from any goroutine.

func ComposeHost

func ComposeHost(base Host, mws ...HostMiddleware) Host

ComposeHost returns base wrapped by every middleware in mws, in declaration order (first = outermost). Returns base unchanged when mws is empty.

func HostFromContext

func HostFromContext(ctx context.Context) (Host, bool)

HostFromContext returns the Host attached to ctx by a previous ContextWithHost call, plus an "ok" flag. The ok=false branch means either no engine wired the host into ctx (the extension is running outside an engine) or the caller passed a nil Host.

Extensions that require host capabilities should treat ok=false as a usage error and surface it via the extension's own error contract (e.g. ask_user surfaces errdefs.NotAvailable so LLMs see "I cannot prompt the user" instead of crashing).

type HostCapabilityMask added in v0.4.6

type HostCapabilityMask interface {
	// MaskedCapability is the optional capability type withheld from consumers
	// reached through this decorator's inner Host, or nil when the decorator
	// withholds nothing. Implementations name an interface type:
	//
	//	func (h steerlessHost) MaskedCapability() reflect.Type {
	//		return reflect.TypeFor[SteerSource]()
	//	}
	MaskedCapability() reflect.Type
}

HostCapabilityMask is the opt-in contract for Host decorators that withhold one optional capability from CapabilityFromHost while leaving the rest of the chain traversable.

HostUnwrapper covers the other shape: a decorator that replaces a surface and therefore hides everything below it. A capability that is scoped to one run rather than replaced has no such boundary — withholding it must not cost consumers the capabilities they still legitimately read (delegation service, event bus, …). A decorator that owns such a capability names it here, and traversal keeps unwrapping while the named capability resolves as unsupported.

The mask applies to the inner chain: the decorator's own method set is consulted first, exactly as for any other Host, so a decorator that means to withhold a capability must not implement it itself. Masks do not cancel — once a decorator withholds a capability, no decorator further inside can re-expose it.

type HostFuncs

type HostFuncs struct {
	Inner Host

	PublishFn     func(ctx context.Context, env event.Envelope) error
	InterruptsFn  func() <-chan Interrupt
	AskUserFn     func(ctx context.Context, prompt UserPrompt) (UserReply, error)
	CheckpointFn  func(ctx context.Context, cp Checkpoint) error
	ReportUsageFn func(ctx context.Context, usage inference.Usage) error
}

HostFuncs is the func-field adapter that lets a middleware decorate just the Host methods it cares about while delegating the rest to an underlying Host. Construct one with the inner host as Inner and override only the func fields you need:

wrapped := agent.HostFuncs{
    Inner: base,
    ReportUsageFn: func(ctx context.Context, u inference.Usage) error {
        // budget enforcement here
        return base.ReportUsage(ctx, u)
    },
}

Every nil func field falls through to Inner so partial decorators stay short. Inner MUST be non-nil; a nil Inner is a programming bug and triggers a panic at the first delegated call.

func (HostFuncs) AskUser

func (h HostFuncs) AskUser(ctx context.Context, prompt UserPrompt) (UserReply, error)

AskUser routes through AskUserFn or Inner.

func (HostFuncs) Checkpoint

func (h HostFuncs) Checkpoint(ctx context.Context, cp Checkpoint) error

Checkpoint routes through CheckpointFn or Inner.

func (HostFuncs) Interrupts

func (h HostFuncs) Interrupts() <-chan Interrupt

Interrupts routes through InterruptsFn or Inner.

func (HostFuncs) Publish

func (h HostFuncs) Publish(ctx context.Context, env event.Envelope) error

Publish routes through PublishFn or Inner.

func (HostFuncs) ReportUsage

func (h HostFuncs) ReportUsage(ctx context.Context, usage inference.Usage) error

ReportUsage routes through ReportUsageFn or Inner.

func (HostFuncs) UnwrapHost

func (h HostFuncs) UnwrapHost() Host

UnwrapHost preserves optional capabilities only when Publish still delegates to Inner. A custom publisher may use a different event surface, so PublishFn is an authoritative boundary that stops capability traversal.

type HostMiddleware

type HostMiddleware func(Host) Host

HostMiddleware decorates a Host with policy / observability / resource-management behaviour. The pod / agent layer typically stacks several middlewares (audit → rate-limit → budget → secret-resolve) around a base Host built from a PodSpec; this type and the ComposeHost helper exist so the stack can be declared as a slice instead of N levels of struct embedding.

Convention:

  • Middleware ordering matches the slice order: ComposeHost(base, A, B, C) returns C(B(A(base))). The first middleware in the slice is the OUTERMOST wrapper and therefore runs first when an engine calls a Host method. This matches how HTTP middleware stacks are normally declared.
  • Each middleware MUST return a Host value that delegates unchanged for any sub-interface it does not specifically decorate. The HostFuncs adapter is provided to make this easy: zero-value func fields delegate to the wrapped Host.
  • Middlewares are invoked from any goroutine — implementations must be safe for concurrent use, mirroring the Host contract.

func TracingMiddleware

func TracingMiddleware() HostMiddleware

TracingMiddleware returns a HostMiddleware that wraps every Host method call in an OTel span. It is the default observability layer for engines that emit envelopes, checkpoint, or talk back to users — making the cross-process surface visible in any trace UI without per-engine instrumentation.

Spans created (one per Host call):

  • engine.host.publish — attribs: messaging.destination (subject), event.id, event.run_id (from envelope headers when present)
  • engine.host.checkpoint — attribs: checkpoint.run_id, checkpoint.node_id, checkpoint.seq
  • engine.host.ask_user — attribs: prompt.kind
  • engine.host.report_usage — attribs: llm.provider, llm.model, llm.tokens.input, llm.tokens.output

Errors set the span status to Error and record the error.

Span lifetimes are tight (the wrapped call) so this middleware does NOT decorate Interrupts() — that returns a long-lived channel and a span around it would either be a single point-in- time event (uninteresting) or last for the entire run (better modeled by the engine's own outer span).

Compose it with other middlewares using ComposeHost; place it near the OUTER end of the stack so its spans wrap the work done by inner middlewares (rate limiting, budget enforcement, etc.).

type HostUnwrapper

type HostUnwrapper interface {
	UnwrapHost() Host
}

HostUnwrapper is the opt-in contract for Host decorators that preserve access to optional capabilities on an inner Host.

UnwrapHost is read-only: callers may inspect the returned Host for capabilities but do not replace the decorator's inner Host. Decorators must return nil when crossing their boundary would bypass an authoritative replacement surface. For example, HostFuncs does not unwrap when PublishFn replaces the inner publisher.

type Identity

type Identity struct {
	// AgentID identifies the [Agent] producing this run. Engines
	// stamp it onto envelope headers (event.HeaderAgentID) and use
	// it as the agent segment of step subjects.
	AgentID string `json:"agent_id,omitempty"`

	// RunID is the unique identifier of this execution. The host
	// generates it and keeps it stable across resume / checkpoint
	// cycles; revise attempts reuse the same RunID so observers see
	// one logical run with N attempts.
	RunID string `json:"run_id,omitempty"`

	// ParentRunID identifies the parent run when this run was
	// dispatched by another agent (multi-agent call chains). Empty
	// for top-level runs. Typed (rather than an attribute) so
	// loop / depth detection at the pod layer can rely on the
	// contract.
	ParentRunID string `json:"parent_run_id,omitempty"`

	// TaskID scopes this run to a long-running A2A task. Empty when
	// the caller did not supply one.
	TaskID string `json:"task_id,omitempty"`

	// ConversationID names the thread-of-interaction scope (A2A
	// contextId semantics). Empty when the turn is unscoped.
	ConversationID string `json:"conversation_id,omitempty"`
}

Identity is the strongly-typed identity bundle for one execution. It is a typed value (rather than loose attributes) so the downstream executor sees the same identity the harness minted.

type Interrupt

type Interrupt struct {
	Cause  Cause
	Detail string
}

Interrupt is the value the host sends through Host.Interrupts() to ask the running engine to stop. It is also the payload of the Interrupted error so the host can introspect why.

Interrupt is a plain value — copy it freely.

type InterruptedError

type InterruptedError = interruptedErr

InterruptedError is the concrete error type returned by Interrupted. It is exported so hosts can use errors.As to destructure it; the preferred way to produce one is Interrupted, not direct construction.

Implements errdefs interrupted-marker so errdefs.IsInterrupted returns true on any error wrapping (or equal to) one of these.

type Interrupter

type Interrupter interface {
	Interrupts() <-chan Interrupt
}

Interrupter exposes the host's cooperative-interrupt channel.

Engines select on the returned channel between steps:

select {
case intr := <-h.Interrupts():
    return engine.Interrupted(intr)
case <-ctx.Done():
    return ctx.Err()
default:
}

A nil channel means "no cooperative interrupt available"; engines should treat it as "never fires" — receiving on nil blocks forever, which is the correct semantic.

type LoadAndResumeOption

type LoadAndResumeOption func(*loadAndResumeOpts)

LoadAndResumeOption tunes LoadAndResume behaviour.

func WithFreshStartAllowed

func WithFreshStartAllowed(allowed bool) LoadAndResumeOption

WithFreshStartAllowed controls behaviour when the store has no checkpoint for run.RunID. Default true — execute fresh. Pass false to require an existing checkpoint and surface errdefs.NotFound when none is present (useful for "resume only" admin commands).

func WithResumeAttempt

func WithResumeAttempt(n int) LoadAndResumeOption

WithResumeAttempt sets ResumeContext.Attempt. The default of 1 is correct on fresh runs; supervisors implementing retry loops should bump this for each re-attempt.

func WithResumeSignal

func WithResumeSignal(s string) LoadAndResumeOption

WithResumeSignal sets ResumeContext.Signal for the about-to-run execution. Defaults to "manual".

func WithResumeStartedAt

func WithResumeStartedAt(t time.Time) LoadAndResumeOption

WithResumeStartedAt sets ResumeContext.StartedAt. Default is time.Now() at the moment LoadAndResume is invoked. Pass an earlier timestamp when continuing a long-running run so dashboard "total wall time" remains accurate.

type NoopCheckpointStore

type NoopCheckpointStore struct{}

NoopCheckpointStore drops every checkpoint and reports no state. Use as a default when checkpointing is not configured.

func (NoopCheckpointStore) Load

Load satisfies CheckpointStore.

func (NoopCheckpointStore) Save

Save satisfies CheckpointStore.

type NoopHost

type NoopHost struct{}

NoopHost is a zero-cost Host implementation that discards events, never reports interrupts, refuses user prompts, and skips checkpoints. It is meant for tests and embedded scenarios where an engine is invoked outside any real runtime.

func (NoopHost) AskUser

AskUser returns errdefs.NotAvailable so engines can detect that user interaction is unsupported in this host.

func (NoopHost) Checkpoint

func (NoopHost) Checkpoint(context.Context, Checkpoint) error

Checkpoint discards the checkpoint.

func (NoopHost) Interrupts

func (NoopHost) Interrupts() <-chan Interrupt

Interrupts returns nil so engines that select on it block forever on that case (i.e. interrupts never fire under NoopHost).

func (NoopHost) Publish

Publish discards the envelope.

func (NoopHost) ReportUsage

func (NoopHost) ReportUsage(context.Context, inference.Usage) error

ReportUsage discards the usage report. NoopHost has no budget so always returns nil — engines never see BudgetExceeded under it.

type Observer

type Observer interface {
	// OnRunStart fires after Run prepared the engine inputs but
	// before Execute is invoked. id carries the immutable
	// identification fields agreed for this turn.
	OnRunStart(ctx context.Context, id Identity, req *Request)

	// OnInterrupt fires only when the engine returned an interrupt
	// error. It runs before OnRunEnd. intr carries the structured
	// reason supplied by the host.
	OnInterrupt(ctx context.Context, id Identity, intr Interrupt)

	// OnRunRevise fires when a Referee asked agent.Execute to re-invoke
	// Execute (Decision{Revise: true}) AND the
	// per-call WithMaxRevise budget allows another attempt. It
	// runs after the discarded attempt's classification but BEFORE
	// the next OnRunStart, so observers see the lifecycle as:
	//
	//	OnRunStart → Execute → OnRunRevise → OnRunStart → Execute → OnRunEnd
	//
	// prevRes is the (about-to-be-replaced) Result from the failed
	// attempt — observers MUST treat it as read-only. nextAttempt
	// is the 1-indexed attempt number the next Execute will
	// be (== prevRes.Attempts + 1).
	//
	// OnRunRevise fires zero times for runs that complete on the first
	// attempt or whose Referee never asks for revise.
	OnRunRevise(ctx context.Context, id Identity, prevRes *Result, nextAttempt int)

	// OnRunEnd fires after Execute returned and Run finished
	// classifying the outcome. res is the same pointer Run is about
	// to return; observers MUST treat it as read-only.
	OnRunEnd(ctx context.Context, id Identity, res *Result)
}

Observer is a read-only lifecycle hook that lets callers react to stages of a Run without affecting its outcome. It is the plumbing behind agent's "metric emit on start", "transcript snapshot on interrupt", notifications, and similar best-effort patterns, none of which agent hard-codes any more.

Design rules:

  1. Observers MUST NOT change the Result returned by Run. agent intentionally exposes the Result to OnRunEnd by pointer because it is the same value the caller will receive — observers may stash references to it (for logging, async append, …) but mutating it leaves agent's caller staring at the mutation. Treat this surface as advisory.

  2. Observer methods MUST NOT return an error. Failures inside an observer are the observer's problem; they MUST NOT propagate into Run. Use a Committer for durable side effects that must report failure, or a Referee for policy decisions that alter whether a turn is accepted.

  3. Observer methods are called synchronously from Run on the caller's goroutine. Blocking inside them blocks the run. Long-running side effects MUST be dispatched asynchronously by the observer itself.

  4. Run guarantees the call sequence: OnRunStart fires exactly once before Execute; OnInterrupt fires at most once and ONLY when the engine returned an InterruptedError (foreign-shape errors that merely satisfy errdefs.IsInterrupted still classify the run as interrupted but skip OnInterrupt); OnRunEnd fires exactly once after Execute returns, regardless of outcome.

Embed BaseObserver to satisfy the interface with no-op defaults when only a subset of the lifecycle is interesting:

type metricsObserver struct {
    agent.BaseObserver
    metrics Metrics
}

func (o *metricsObserver) OnRunEnd(ctx context.Context, id agent.Identity, res *agent.Result) {
    o.metrics.Record(res)
}

type Policy

type Policy struct {
	// MaxRevise bounds how many times a Referee may ask Execute to
	// re-invoke the engine. Zero means one attempt (no revise).
	MaxRevise int `json:"max_revise,omitempty"`
	// RunTimeout, when set, bounds the wall clock of one Execute call
	// (every revise attempt included) as a Go duration string, e.g.
	// "10m". Empty means no run-level bound. Engines keep their own
	// per-Execute timeouts; the shorter deadline wins.
	RunTimeout string `json:"run_timeout,omitempty"`
	// ArtifactChannels names board channels collected into
	// Result.Artifacts.
	ArtifactChannels []string `json:"artifact_channels,omitempty"`
}

Policy is the per-call harness policy declared on the document form and applied by Execute unless overridden per call.

type Preparer

type Preparer interface {
	Before(ctx context.Context, id Identity, req *Request, prev *Board) (*Board, error)
}

Preparer builds the initial Board for a run, building on the board left by the previous link in the chain.

A chain of Preparers is a linear pipeline: agent runs them in registration order, threading the board through them. The first link in the chain receives a board freshly seeded with req.Message on MainChannel and req.Inputs as board vars; each subsequent link receives the board its predecessor returned. Every Preparer must return a fresh *Board (do not mutate and re-yield the input) so the engine and downstream chain links can rely on immutability of the previous board.

It is the single extension point for "anything that should be on the board before the engine sees it":

  • conversation history (load, summarise, window);
  • retrieved long-term memory and knowledge-base hits;
  • system prompts and persona text;
  • request-scoped board vars (form fields, parameters, tool allow-lists);
  • any combination of the above.

Run guarantees:

  • The chain is called exactly once per Run attempt, before Execute and before any Observer's OnRunStart. Revise attempts re-run the chain from the beginning with the same Request so boards are not stale across retries.
  • The returned board is mutated by the engine; Preparers MUST return a fresh value each call.
  • The returned board MUST be non-nil. Returning nil is a Run infrastructure error.

Preparers may perform bounded I/O such as transcript loading or retrieval. They run synchronously on the caller's goroutine, MUST honor ctx cancellation and deadlines, and SHOULD apply their own tighter timeout when calling a dependency whose latency is not already bounded. A Preparer MUST NOT detach untracked background work from the run.

type PreparerFunc

type PreparerFunc func(ctx context.Context, id Identity, req *Request, prev *Board) (*Board, error)

PreparerFunc is the function-typed adapter for Preparer.

Useful when the seed logic is a single closure over a transcript loader or retriever:

agent.WithPreparer(agent.PreparerFunc(func(ctx context.Context, id agent.Identity, req *agent.Request, prev *agent.Board) (*agent.Board, error) {
    b := prev.Clone()
    b.SetChannel("memory", retrieved)
    return b, nil
}))

func (PreparerFunc) Before

func (f PreparerFunc) Before(ctx context.Context, id Identity, req *Request, prev *Board) (*Board, error)

Before calls f.

type ProviderOutputEnvelope added in v0.1.3

type ProviderOutputEnvelope struct {
	Provider  string          `json:"provider"`
	Extension string          `json:"extension"`
	Value     json.RawMessage `json:"value"`
}

ProviderOutputEnvelope carries one provider-owned observational output on a StreamDeltaProviderOutputs delta: the provider / extension identity plus the raw output value. Value is opaque to the stream protocol; consumers decode it against the provider extension's schema when they recognize the family.

type Publisher

type Publisher interface {
	Publish(ctx context.Context, env event.Envelope) error
}

Publisher emits a single event envelope.

Subject schema is NOT owned by this package: the host decides what the routing keys look like. Engines simply produce envelopes whose subject they construct from whatever convention their host has agreed with the consumer side.

Publish errors MUST NOT cause the producing engine to fail the run by default; the engine should log/record and continue. Backpressure or transport failures are an observability concern, not a control- flow signal.

Implementations MUST observe ctx.Done and return promptly. Graph-level publish timeouts are bounded only for implementations that honor this cooperative cancellation contract.

type Referee

type Referee interface {
	// After fires after Execute returns. The Referee
	// inspects res (read-only) and the original req, and returns a
	// Decision that Run merges with other Referees' decisions.
	//
	// id carries the immutable identification fields agreed for
	// this turn. The Referee MUST NOT mutate res; agent will surface
	// the merged decision via [Result.Committed] and (when a Reason
	// was supplied) [Result.State]["finalize_reason"].
	After(ctx context.Context, id Identity, req *Request, res *Result) (Decision, error)
}

Referee is a decision-making lifecycle hook that can influence what agent.Execute does at well-defined boundaries. It is the read-write counterpart of Observer:

  • Committers persist accepted results and may return errors.
  • Observers see what happened and emit best-effort side effects.
  • Referees return a structured decision agent.Run interprets.

Referees expose one decision point — Referee.After — which fires after Execute returned but before Run invokes Committer and Observer. This covers two real cases:

  1. Disposition: a barge-in cause means the assistant was cut off mid-thought; the half-baked output should not appear in the persistent transcript. A Referee returns Decision{DiscardOutput: true}.

  2. (Reserved) Revise: the natural answer fails some quality bar (no citations, policy violation, refusal-without-reason); the Referee asks for one more model pass. The wire field is present for forward compatibility — agent does not yet honour it, and engines will need explicit support before it has any effect.

Composition

Multiple Referees may be registered (Agent-scoped + per-call). They run in registration order. The merged decision is the OR over boolean fields: any Referee asking to accept, discard, or revise is retained. Discard has final precedence when Run applies the merged decision. The first non-empty Reason wins, so callers can attribute the decision in logs.

Error contract

A Referee returning a non-nil error short-circuits the merge and causes Run to return (Result, decider-error). agent does NOT swap the error class — it surfaces the Referee's own error so callers can classify with errdefs. The Result is still populated (including the engine's output) so the caller can decide what to do next.

Embed BaseReferee to satisfy the interface with no-op defaults.

type Request

type Request struct {
	// TaskID identifies a long-lived task the request is part of.
	// Empty when the caller is not tracking tasks. Maps to A2A's
	// "taskId".
	TaskID string `json:"taskId,omitempty"`

	// ContextID identifies the conversation / session. Used as the
	// conversation key passed to History.Load / Append. Empty means
	// "no persistent transcript for this turn". Maps to A2A's
	// "contextId".
	ContextID string `json:"contextId,omitempty"`

	// RunID is the host-supplied execution id. When empty Run mints
	// one. The same value is propagated as Run.RunID and as the
	// run id attribute on emitted events. Not part of the A2A wire
	// schema — it is an internal correlation key, kept camelCase for
	// stylistic consistency.
	RunID string `json:"runId,omitempty"`

	// ParentRunID identifies the parent run when this turn was
	// dispatched by another agent (multi-agent call chains). Empty for
	// top-level runs. WithParentRunID wins when both are supplied.
	// Not part of the A2A wire schema — internal correlation key.
	ParentRunID string `json:"parentRunId,omitempty"`

	// Attributes carries host-supplied metadata for this turn that
	// should flow into Run.Attributes (and from there into telemetry
	// spans and event headers): tenant id, feature flags, delegation
	// lineage, ... WithAttributes wins per key on conflict.
	Attributes map[string]string `json:"attributes,omitempty"`

	// Message is the user's turn input (text, parts, attachments).
	Message message.Message `json:"message"`

	// Inputs are arbitrary structured inputs the engine reads off
	// the Board (form fields, parameters, …). They are written under
	// their map keys as Board vars before the engine starts.
	Inputs map[string]any `json:"inputs,omitempty"`

	// Config carries per-request preferences (output modes, …).
	// Maps to A2A's "configuration".
	Config *RequestConfig `json:"configuration,omitempty"`
}

Request is one agent turn submitted to Execute.

Field names and JSON tags mirror the A2A protocol's MessageSendParams schema (camelCase: taskId, contextId, …) so requests can be serialised across the protocol without translation. The notable absence vs core/agent.Request is that Request does not carry a RuntimeID (Run is now a stateless function) and does not carry a Strategy hint (the engine is supplied directly to Run).

type RequestConfig

type RequestConfig struct {
	// AcceptedOutputModes constrains what modalities the caller can
	// receive (e.g. ["text/plain"], ["audio/wav"], …). Engines that
	// can produce multiple modalities consult it to pick one. Maps
	// to A2A's "acceptedOutputModes".
	AcceptedOutputModes []string `json:"acceptedOutputModes,omitempty"`
}

RequestConfig holds per-request preferences. Optional knobs are added here rather than on Request to keep Request stable across minor versions. JSON keys mirror the A2A MessageSendConfiguration schema so requests can flow across the protocol without translation.

type Result

type Result struct {
	// TaskID echoes the input Request.TaskID for correlation.
	// Matches the A2A taskId casing.
	TaskID string `json:"taskId,omitempty"`

	// RunID echoes the (possibly auto-generated) execution id Run
	// used to drive the
	RunID string `json:"runId,omitempty"`

	// Status classifies the outcome.
	Status Status `json:"status"`

	// Cause is set when Status == StatusInterrupted: it carries the
	// Cause the host signalled. Empty otherwise.
	Cause Cause `json:"cause,omitempty"`

	// Messages is the slice of NEW messages produced this turn —
	// excluding the input request and any history loaded before the
	// turn. Suitable for streaming to a UI or appending to the
	// persistent transcript (which Run already did).
	Messages []message.Message `json:"messages,omitempty"`

	// Artifacts collects named, multi-modal bundles the engine
	// emitted via dedicated board channels.
	Artifacts []Artifact `json:"artifacts,omitempty"`

	// Committed reports whether this turn's output was accepted and,
	// when Committers are registered, durably committed. Agent first
	// derives commit eligibility from the Referee chain on top of its
	// defaults:
	//
	//   - StatusCompleted defaults to Committed=true.
	//   - All non-completed statuses default to Committed=false.
	//   - Any Referee returning AcceptOutput=true makes the result
	//     eligible for the normal Committer chain.
	//   - Any Referee returning DiscardOutput=true forces
	//     Committed=false, overriding AcceptOutput.
	//   - A Committer failure forces Committed=false and Execute
	//     returns the Result together with that error.
	//
	// Independent of Committed, Result.Messages always reflects the
	// engine's actual output. When no Committer is registered,
	// Committed reflects acceptance alone.
	Committed bool `json:"committed"`

	// State is a free-form bag carrying run-specific metadata. agent
	// puts a few well-known keys (run_id, board, interrupted_node,
	// …) here but does not enforce a schema beyond that.
	State map[string]any `json:"state,omitempty"`

	// Err is the engine's underlying error when Status indicates a
	// non-completed outcome. Callers that want classification call
	// errdefs.IsXxx on it; the JSON tag is "-" because errors do not
	// JSON-marshal usefully.
	Err error `json:"-"`

	// LastBoard is the engine's final Board (possibly partial when
	// Status != StatusCompleted). agent does not persist it; the
	// host can choose to checkpoint via engine's Checkpointer.
	LastBoard *Board `json:"-"`

	// Attempts is the number of Execute invocations Run made
	// before settling on this Result. 1 for fresh (single-shot)
	// runs; >1 only when WithMaxRevise was enabled and at least
	// one Referee returned Decision{Revise: true}.
	//
	// Attempts is the post-loop count, not "remaining budget":
	// Attempts == 2 means the engine was invoked twice. Hooks
	// reading res.Attempts in OnRunEnd see the final value.
	//
	// Zero is reserved for "Run never reached Execute"
	// (infrastructure error). Real runs always have Attempts >= 1.
	Attempts int `json:"attempts,omitempty"`
}

Result is what Run returns after one turn. The contract:

  • Run() returns (res, nil) for ALL business outcomes — completion, interrupt, cancel, abort, failure. Caller inspects Status to branch.

  • Run() returns (nil, err) ONLY for infrastructure failures the caller cannot reasonably recover from (e.g. history append refused, factory returned nil engine).

This mirrors core/agent.Result's "W-5" rule and avoids the double-encoding pattern where errors are also carried by Status.

func Execute

func Execute(
	ctx context.Context,
	ag Agent,
	eng Engine,
	req Request,
	opts ...ExecuteOption,
) (*Result, error)

Execute executes one turn of ag against eng with the given req.

Execute is intentionally minimalist: it owns identifier minting, board assembly, hook dispatch, and result classification — and nothing else. Anything that looks like "policy" (load conversation history, run RAG retrieval, write transcripts after a turn, emit metrics, route engine envelopes to a bus, accumulate token usage, …) lives outside Execute on:

  • Observer / Referee / Committer for lifecycle hooks;
  • Preparer for engine-input shaping;
  • the caller-supplied Host (see WithHost) for every host-side capability the engine needs (event publishing, interrupt injection, user prompting, checkpoint persistence, usage reporting). When omitted, agent falls back to NoopHost — which is fine for trivial / test runs but gives up every observability and HITL capability.

Wiring sequence

  1. Mint a RunID (req.RunID wins, else autogenerate).
  2. Build an Board using either a caller-supplied Preparer (WithPreparer) or the default seeder, which simply appends req.Message to MainChannel and copies req.Inputs to board vars.
  3. Resolve the Host — caller-supplied via WithHost, else NoopHost.
  4. Build a Identity and notify all registered Hooks via OnRunStart.
  5. Call eng.Execute. The engine mutates the board in place per its contract.
  6. If the engine returned an interrupt, fire OnInterrupt with the destructured cause/detail.
  7. Translate the engine outcome into Status and assemble Result.
  8. Run the Referee chain (Referee.After) and merge the decisions; this fixes Result.Committed and any finalize_reason metadata.
  9. If the final result is committed and Committers exist, resolve any per-call CommitViewProvider, then invoke the Committer chain with that view.
  10. Fire Observer.OnRunEnd before returning.

Error contract

Execute returns (res, nil) for every business outcome — completion, interrupt, cancel, abort, failure. (nil, err) is reserved for infrastructure failures the caller cannot reasonably recover from: nil engine, empty Agent.ID, or a Preparer, Referee, CommitViewProvider, or Committer that returned an error.

Observers MUST NOT cause Execute to return an error; they are advisory. Referee and Committer errors surface back to the caller — agent does not swap their error class, so callers can classify with errdefs.

func (*Result) Text

func (r *Result) Text() string

Text returns the last assistant text message in Result.Messages, or "" if none. Convenience for chat-style callers; multi-modal callers should walk Messages directly.

type ResumeContext

type ResumeContext struct {
	// Attempt is 1-based: the very first attempt (fresh run, no
	// resume) is 1; the first re-execution after a checkpoint is
	// 2; and so on. Hosts that limit retry budget read this field.
	Attempt int

	// StartedAt is the wall-clock time the original [Run] began.
	// Stays constant across resumes so dashboards can compute
	// total wall time (e.g. SLO budget burn).
	StartedAt time.Time

	// Signal identifies the trigger that produced THIS resume.
	// Convention (extensible — engines/middleware MUST treat
	// unknown values as opaque):
	//
	//   - "manual"           — operator clicked Resume / CLI
	//   - "interrupt-recovery" — host re-executed after a host.Interrupts() Stop
	//   - "schedule"         — cron / queue replayer
	//   - "crash"            — supervisor restarted after process exit
	//   - "retry"            — automated retry on classified failure
	Signal string

	// CheckpointAt is when the checkpoint that fuels this resume
	// was originally produced (the engine's view of "paused at").
	// Empty time.Time on fresh starts.
	CheckpointAt time.Time
}

ResumeContext is the auxiliary metadata about the *current* resume attempt. Engines, observers and middleware read it from context.Context to differentiate fresh starts from resumes, count attempts in retry loops, and surface "this is replay" indicators in trace UIs.

ResumeContext is distinct from Run.ResumeFrom: the checkpoint describes WHERE the engine should pick up; ResumeContext describes WHY the resume is happening (who triggered it, which attempt this is, how long the original run has been alive). Keep them separate so hosts can drive replay + retry semantics without mutating the checkpoint payload.

func ResumeContextFromContext

func ResumeContextFromContext(ctx context.Context) (ResumeContext, bool)

ResumeContextFromContext returns the ResumeContext attached to ctx, plus an "ok" flag. The ok=false branch means the engine is running a fresh start (or the host did not bother to populate the context — engines should treat both cases identically).

type Resumer

type Resumer interface {
	// CanResume returns nil if the given checkpoint is resumable by
	// this engine, or a classified error explaining why not:
	//
	//   - errdefs.Validation: checkpoint shape is wrong (engine kind
	//     mismatch, missing required Payload fields, ExecID
	//     conflict).
	//   - errdefs.NotAvailable: engine recognises the checkpoint but
	//     cannot resume it (incompatible engine version, removed
	//     node type, etc.).
	//
	// Implementations MUST be cheap (no I/O, no LLM calls); the
	// probe runs synchronously on the host's resume path.
	CanResume(cp Checkpoint) error
}

Resumer is the optional capability an Engine may advertise to signal it can drive Run.ResumeFrom and to short-circuit obvious mismatches *before* the host spins up an execution.

The interface is intentionally minimal — a single CanResume probe — so engines opt in cheaply and hosts can build resume tooling (admin UIs, CLI commands, supervised retry loops) without a full dry-run.

Engines that do NOT implement Resumer remain fully spec-compliant: the Run.ResumeFrom contract still applies and they MUST return an errdefs.NotAvailable-classified error if asked to resume. The helpers below (IsResumable, LoadAndResume) treat the absence of Resumer as "engine handles resume opaquely; trust Execute to surface the right error".

func AsResumer

func AsResumer(eng Engine) (Resumer, bool)

AsResumer is the typed counterpart of IsResumable. It walks wrappers that expose Unwrap() Engine (errors.As-style) so a Resumer hidden under decorator wrappers still surfaces correctly.

type Run

type Run struct {
	// Identity is the run's identity envelope. All identity
	// dimensions are typed fields here — read run.AgentID /
	// run.RunID / run.TaskID directly. (They resolve through the
	// embedded struct, e.g. run.RunID == run.Identity.RunID.)
	Identity

	// Attributes carries arbitrary host-supplied metadata that should
	// flow into telemetry spans and event headers (tenant id, engine
	// kind, feature flags, …). Identity dimensions do NOT live here
	// — they are typed fields on [Identity].
	//
	// Convention: keys SHOULD use the constants in core/telemetry
	// (`telemetry.AttrTenantID`, `telemetry.AttrEngineKind`, …) so
	// cross-package consumers (dashboards, log queries) can filter
	// without per-package translation rules.
	Attributes map[string]string

	// ToolAllowList is the policy gate naming the tool ids this run
	// is permitted to call. Engines that invoke tools honour it;
	// empty means "no agent-level restriction" (the engine's own
	// default applies). It is policy, not wiring — tool
	// *implementations* are bound at engine assembly time (see
	// [Factory] / [Config]), never per run.
	ToolAllowList []string

	// ResumeFrom, when non-nil, instructs the engine to continue
	// execution from the provided checkpoint instead of starting a
	// fresh run. The engine is the sole interpreter of
	// [Checkpoint.Steps] and [Checkpoint.Payload]; the host treats
	// them as opaque.
	//
	// Contract:
	//
	//   - When ResumeFrom is non-nil the engine SHOULD prefer
	//     ResumeFrom.Board over the board parameter passed to
	//     [Engine.Execute]; passing both is allowed but the
	//     checkpoint's board takes precedence as it represents the
	//     state at the boundary the run paused on.
	//
	//   - When ResumeFrom.ExecID differs from [Run.RunID] the engine
	//     MUST return an errdefs.Validation-classified error: forking
	//     a run requires a fresh execution id, not a resume.
	//
	//   - Engines that do not support resume MUST return an
	//     errdefs.NotAvailable-classified error when they observe a
	//     non-nil ResumeFrom rather than silently restarting from
	//     scratch.
	//
	// Hosts that drive resumption typically [CheckpointStore.Load]
	// the most recent checkpoint, set ResumeFrom, and call
	// [Engine.Execute] again with the same Run.RunID.
	ResumeFrom *Checkpoint
}

Run is the per-execution input bundle an engine receives alongside the host. It is a plain data struct — no methods, no builder, no hidden state — assembled once by the host and passed to Engine.Execute read-only.

All fields are conceptually immutable for the duration of the run. Engines may read freely; they MUST NOT mutate the maps in place nor mutate the referenced ResumeFrom checkpoint.

func (Run) Attribute

func (r Run) Attribute(key string) string

Attribute returns the value for the given attribute key, or "" if absent. A small convenience over `r.Attributes[key]` that handles a nil Attributes map.

func (Run) Info

func (r Run) Info() RunInfo

Info returns the node-facing projection of r.

type RunEndPublishError

type RunEndPublishError struct {
	Err error
}

RunEndPublishError reports that an engine could not publish its required terminal run envelope. Consumers that use run-end as a delivery barrier must stop waiting and detach instead.

func (*RunEndPublishError) Error

func (e *RunEndPublishError) Error() string

func (*RunEndPublishError) Unwrap

func (e *RunEndPublishError) Unwrap() error

type RunInfo

type RunInfo struct {
	Identity

	// Attributes mirrors [Run.Attributes] (host-supplied metadata:
	// tenant id, engine kind, feature flags, …).
	Attributes map[string]string

	// ToolAllowList mirrors [Run.ToolAllowList]: the tool ids this run
	// is permitted to call. Empty means "no agent-level restriction".
	ToolAllowList []string
}

RunInfo is the read-only, node-facing projection of Run.

Engines hand it to step/node implementations instead of the full Run: everything a step legitimately needs (identity, attributes, tool policy) is here, while engine-internal machinery such as Run.ResumeFrom stays invisible. The projection is owned by this package — when Run grows a new field, the author decides here whether steps should see it, keeping every engine's step API stable.

func RunInfoFromContext

func RunInfoFromContext(ctx context.Context) (RunInfo, bool)

RunInfoFromContext returns the RunInfo attached to ctx, plus an "ok" flag. ok=false means the caller is outside an engine run (or the engine did not populate the context) — consumers should treat the zero RunInfo as "identity unknown" rather than an error.

func (RunInfo) Attribute

func (ri RunInfo) Attribute(key string) string

Attribute returns the value for the given attribute key, or "" if absent, tolerating a nil Attributes map.

type ScriptEnv

type ScriptEnv struct {
	// Config is script-level configuration, accessible as a global in the script.
	Config map[string]any

	// Bindings maps names to host objects injected into the script's global scope.
	// Each value is typically a map[string]any of Go functions callable from the script.
	Bindings map[string]any
}

ScriptEnv carries per-execution configuration and host bindings.

type ScriptRuntime

type ScriptRuntime interface {
	Exec(ctx context.Context, name, source string, env *ScriptEnv) (*ScriptSignal, error)
}

ScriptRuntime executes scripts with injected host bindings. Implementations must be safe for concurrent use.

type ScriptSignal

type ScriptSignal struct {
	Type    string         `json:"type"`
	Kind    string         `json:"kind,omitempty"`
	Message string         `json:"message,omitempty"`
	Detail  map[string]any `json:"detail,omitempty"`
}

ScriptSignal represents a control signal from a script back to the host.

Type is always one of "interrupt" / "error" / "done" — the three outcomes a script can choose to surface besides "ran to completion".

Kind is a per-Type sub-classifier:

  • For Type "error" it carries an errdefs category name (see ErrorKind). SignalToError maps it onto the matching errdefs wrapper; unknown values degrade to ErrorKindInternal so a typo in script land cannot escape as an unclassified error.
  • For Type "interrupt" it carries an Cause string. Unknown values degrade to CauseCustom for the same reason.
  • For Type "done" Kind is unused.

Message is the human-readable detail. Detail is freeform structured metadata scripts may attach; it is preserved across host translation but is not inspected by SignalToError today.

type Skill

type Skill struct {
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	Description string   `json:"description,omitempty"`
	Tags        []string `json:"tags,omitempty"`
	Examples    []string `json:"examples,omitempty"`
	InputModes  []string `json:"inputModes,omitempty"`
	OutputModes []string `json:"outputModes,omitempty"`
}

Skill is a single capability unit declared on an AgentCard. Field names mirror A2A's skill object so cards round-trip cleanly through /.well-known/agent-card.json.

type Status

type Status string

Status is the terminal classification of a Run outcome. agent does NOT use Status as a control-flow signal — once Run returns, the caller decides what to do based on Status. The values mirror the A2A task-status enum so they can be serialised across protocol boundaries without translation.

const (
	// StatusCompleted means the engine finished cleanly and produced
	// the messages / artifacts in [Result].
	StatusCompleted Status = "completed"

	// StatusInterrupted means the engine was stopped by a cooperative
	// interrupt (host-injected). Result.Cause carries the reason.
	// By default the partial output is NOT committed (Result.Committed
	// is false); register a [Referee] (or rely on the default
	// disposition) to override.
	StatusInterrupted Status = "interrupted"

	// StatusCanceled means ctx was cancelled before the engine
	// finished.
	StatusCanceled Status = "canceled"

	// StatusFailed means the engine returned a domain error not
	// classified as interrupted / aborted.
	StatusFailed Status = "failed"

	// StatusAborted means the engine reported errdefs.IsAborted —
	// an unrecoverable internal halt. Distinguished from
	// StatusFailed so callers can apply different retry policy.
	StatusAborted Status = "aborted"
)

type SteerSource added in v0.4.6

type SteerSource interface {
	// DrainSteer returns the messages queued for this run and empties
	// the queue. It MUST NOT block and MUST be safe for concurrent
	// calls; nil or empty means nothing is queued.
	DrainSteer() []message.Message
}

SteerSource is the optional Host capability for handing messages to a running turn — the inbound counterpart of Interrupter. It is not part of Host: the presence of this interface is exactly the answer to "does this run accept steer", and a Host that never accepts steer MUST NOT implement it (NoopHost does not, like EventBusProvider).

The producer is the runtime's session.Turn.Steer, which owns the queue; this interface is the consumer side, and DrainSteer reads that queue. A decorator that scopes a queue to one run withholds the capability from its inner chain with HostCapabilityMask — the delegation service does exactly that when a delegated run inherits its caller's host, because the queue belongs to the caller's turn.

A steered message is plain conversation content submitted while the turn is running. It reaches the engine at a boundary that the consuming deployment chose, never mid-stream: engines drain where they are already between steps, and for the graph engine that boundary is a node the graph author placed (typically tools → steer → next round), via the script global host.drainSteer(). The graph executor itself never drains — pollInterrupt is not a delivery point.

Contract:

  • DrainSteer returns the messages queued so far and empties the queue. It is take-all and destructive: a message is never returned twice.
  • It MUST NOT block and MUST be safe for concurrent callers. An empty result (nil or zero length) means "nothing queued".
  • Draining is single-consumer by intent. A deployment that drains from two places concurrently gets an unspecified split of the queue.
  • DrainSteer does NOT mean "delivered". A message a node drained and then failed to use, or that a checkpoint rollback left outside the resumed board, is gone: core guarantees the position of delivery only up to the drain call. Producers that need to report non-delivery must treat it as best-effort (see session.Turn.PendingSteer for the turn-owned queue).
  • Hosts that own a steer queue implement this; embedders that wrap a Host must not synthesise one. The runtime installs its own turn-owned source on every turn and rejects a HostFactory product that already implements SteerSource.

func SteerFromHost added in v0.4.6

func SteerFromHost(h Host) (SteerSource, bool)

SteerFromHost returns h's steer source when h implements SteerSource, mirroring EventBusFromHost: nil interfaces, typed-nil hosts and typed-nil capabilities all count as unsupported, and built-in Host decorators are traversed without claiming SteerSource themselves.

type StreamDeltaPayload

type StreamDeltaPayload struct {
	// Type discriminates the payload variant. See StreamDeltaType
	// constants for the standard values.
	Type StreamDeltaType `json:"type"`

	// Payload carries the raw script-supplied value for
	// forward-compatible / custom event types (host.emit with an
	// unrecognized type). Standard types decode their fields directly
	// and leave this empty. Opaque to the stream protocol; consumers
	// decode it against the event type's own schema.
	Payload json.RawMessage `json:"payload,omitempty"`

	// Part carries one canonical output part. Required on "part".
	// Wire-encoded with the same type discriminator as
	// [message.Content] parts.
	Part message.Part `json:"part,omitempty"`

	// Speculative reports whether the delta belongs to a parallel branch
	// whose output has not yet been accepted into the parent board.
	Speculative bool `json:"speculative,omitempty"`

	// ForkID identifies the graph-runner parallel fork for speculative
	// branch deltas and parallel_branch_* control deltas.
	ForkID string `json:"fork_id,omitempty"`

	// BranchID identifies the branch within ForkID for speculative
	// branch deltas and parallel_branch_* control deltas.
	BranchID string `json:"branch_id,omitempty"`

	// Reason carries the rollback/abort reason for parallel_branch_cancel.
	Reason string `json:"reason,omitempty"`

	// FinishReason carries the normalized finish reason on "finish".
	// Values use the inference.FinishReason vocabulary.
	FinishReason string `json:"finish_reason,omitempty"`

	// RequestID and ResponseID ride the "finish" delta when the
	// provider exposes them.
	RequestID  string `json:"request_id,omitempty"`
	ResponseID string `json:"response_id,omitempty"`

	// ProviderOutputs carries the final observational outputs on
	// "provider_outputs", one envelope per output.
	ProviderOutputs []ProviderOutputEnvelope `json:"provider_outputs,omitempty"`
}

StreamDeltaPayload is the canonical decoded shape of a SubjectStreamDelta envelope's payload.

Engines MUST emit payloads that JSON-decode into this struct; the runtime constraint is checked by DecodeStreamDelta. Engines MAY add fields beyond what this struct lists — the JSON decoder is permissive on unknowns — but consumers SHOULD only rely on the fields documented here.

Per-Type field requirements:

Type                     Required         Recommended
-----------------------   ---------------  --------------------
part                      Part             —
finish                    FinishReason     RequestID, ResponseID
provider_outputs          ProviderOutputs  —
parallel_branch_accept    ForkID, BranchID —
parallel_branch_cancel    ForkID, BranchID Reason
(forward-compatible)      Payload          —

Speculative data deltas (part, finish, provider_outputs) additionally require both ForkID and BranchID. Non-speculative data deltas MUST carry neither field, avoiding a half-speculative state that consumers cannot interpret safely. Parallel-branch control deltas MUST NOT carry Part.

func DecodeStreamDelta

func DecodeStreamDelta(env event.Envelope) (StreamDeltaPayload, error)

DecodeStreamDelta extracts the payload of a stream-delta envelope. It returns an error when the envelope payload is empty or does not JSON-decode into StreamDeltaPayload. It does NOT verify the subject; callers may pre-filter with IsStreamDelta when iterating a mixed stream.

func (StreamDeltaPayload) MarshalJSON

func (p StreamDeltaPayload) MarshalJSON() ([]byte, error)

MarshalJSON encodes Part through message.MarshalPart so the wire form carries the canonical "type" discriminator.

func (*StreamDeltaPayload) UnmarshalJSON

func (p *StreamDeltaPayload) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes the "part" subtree through message.UnmarshalPart.

type StreamDeltaType

type StreamDeltaType string

StreamDeltaType enumerates the kinds of in-flight increments a stream envelope can carry. Engines MUST set StreamDeltaPayload.Type to one of these values; consumers SHOULD treat unknown values as forward- compatible additions and skip them.

const (
	// StreamDeltaPart carries one canonical [message.Part] — text,
	// image, audio, video, file, data, tool_call, tool_result, or
	// reasoning. Required field: Part.
	StreamDeltaPart StreamDeltaType = "part"

	// StreamDeltaFinish marks the terminal event of a generation's
	// delta sequence: the normalized finish reason and the
	// provider-issued request / response identifiers. Required field:
	// FinishReason; RequestID / ResponseID are optional.
	StreamDeltaFinish StreamDeltaType = "finish"

	// StreamDeltaProviderOutputs carries the final collection of
	// provider-owned observational outputs (citations, search-call
	// records, code-interpreter metadata) attached to a generation.
	// Required field: ProviderOutputs.
	StreamDeltaProviderOutputs StreamDeltaType = "provider_outputs"

	// StreamDeltaParallelBranchAccept marks a speculative parallel
	// branch's stream output as accepted by the graph runner.
	// Required fields: ForkID, BranchID.
	StreamDeltaParallelBranchAccept StreamDeltaType = "parallel_branch_accept"

	// StreamDeltaParallelBranchCancel marks a speculative parallel
	// branch's stream output as canceled/rolled back by the graph runner.
	// Required fields: ForkID, BranchID. Recommended: Reason.
	StreamDeltaParallelBranchCancel StreamDeltaType = "parallel_branch_cancel"
)

type StreamSink

type StreamSink interface {
	OnDelta(ctx context.Context, env event.Envelope, delta StreamDeltaPayload) error
}

StreamSink is the consumer-side counterpart of the EmitStream* helpers. A sink receives one decoded StreamDeltaPayload at a time along with its source envelope (for headers / trace ids / raw subject access) and forwards it to whatever transport the caller cares about.

Implementations:

  • MUST be safe for concurrent OnDelta calls;
  • SHOULD return errors only for unrecoverable failures;
  • MUST observe ctx.Done and return promptly.

type StreamSinkFunc

type StreamSinkFunc func(ctx context.Context, env event.Envelope, delta StreamDeltaPayload) error

StreamSinkFunc is a func adapter for StreamSink.

func (StreamSinkFunc) OnDelta

OnDelta implements StreamSink.

type UsageReporter

type UsageReporter interface {
	ReportUsage(ctx context.Context, usage inference.Usage) error
}

UsageReporter accepts incremental LLM token-usage reports an engine observes during a run. Each call adds delta usage; the host is responsible for accumulation, billing, and downstream telemetry.

Engines should call ReportUsage once per LLM invocation that returns usage metadata (typical: streaming nodes call it on completion with the per-call totals). Reports SHOULD be best-effort for *observability* failures — a slow exporter must not block forward progress.

Budget enforcement contract:

  • The host MAY return errdefs.BudgetExceeded (or any error classified by errdefs.IsBudgetExceeded) to signal that the accumulated usage has crossed a configured budget and the next LLM call would not be authorised.
  • Engines that observe such an error MUST stop performing further LLM-cost-incurring work in this run and return the error from Execute (typically wrapped). Continuing would defeat the budget.
  • Any other non-nil error is observability-only — engines SHOULD log/swallow and continue, matching the pre-budget contract.

Hosts without billing or budget enforcement return nil unconditionally (see NoopHost.ReportUsage).

The usage value is inference.Usage: token totals plus the Model / LatencyMs envelope, with cost carried by Billing.Cost (a Money with explicit currency and scale) when a pricing catalog is configured.

type UserPrompt

type UserPrompt struct {
	Parts    []message.Part
	Schema   []byte
	Source   string
	Metadata map[string]string
}

UserPrompt describes what the engine is asking the host to relay to the end user. It deliberately stays one level below "chat message":

  • Parts carries the multi-modal payload (text, image, audio, file, structured data) using message.Part — the same building block that message.Message content uses, minus the chat-specific Role.
  • Schema is an optional structured-input hint (JSON-schema-shaped bytes) for cases where the host wants to render a form or validate the response.
  • Source identifies the engine step that produced the prompt; useful for trace correlation and resume.
  • Metadata is free-form host-passed-through metadata.

Why []message.Part rather than message.Message: a Message also carries Role (system/user/assistant/tool), which is a chat-layer concept the engine has no business naming. Parts give us full multi-modality (image, audio, file, data) without tying the engine to chat semantics — the agent layer wraps Parts back into a Message with the right Role on its way out, and unwraps user-supplied Parts on the way in.

type UserPrompter

type UserPrompter interface {
	AskUser(ctx context.Context, prompt UserPrompt) (UserReply, error)
}

UserPrompter lets an engine ask the host to prompt the end user (chat input, voice DTMF, structured form, …) and block until the reply arrives.

Hosts that don't expose user interaction should return an errdefs.NotAvailable-classified error. Engines that get such an error from a step that strictly needs user input should propagate it so the host can decide whether to fail or fall back.

type UserReply

type UserReply struct {
	Parts    []message.Part
	Metadata map[string]string
}

UserReply is what the host returns to satisfy a UserPrompt. Like UserPrompt it uses []message.Part so the response can carry any modality the host's user interface produced — typed text, an uploaded image, recorded audio, a file attachment, structured form data, …

Directories

Path Synopsis
Package a2a adapts a remote A2A (Agent2Agent) agent into a FlowCraft agent.Engine.
Package a2a adapts a remote A2A (Agent2Agent) agent into a FlowCraft agent.Engine.
Package agenttest provides reusable contract-test machinery for the interfaces declared in core/agent — agent.Engine, agent.Host, agent.Observer, agent.Referee, agent.CheckpointStore, agent.Resumer, agent.StreamSink, agent.Preparer, agent.Committer, agent.CommitViewProvider, agent.CheckpointSuggester and agent.ScriptRuntime.
Package agenttest provides reusable contract-test machinery for the interfaces declared in core/agent — agent.Engine, agent.Host, agent.Observer, agent.Referee, agent.CheckpointStore, agent.Resumer, agent.StreamSink, agent.Preparer, agent.Committer, agent.CommitViewProvider, agent.CheckpointSuggester and agent.ScriptRuntime.
Package bindings exposes per-execution host APIs to script runtimes.
Package bindings exposes per-execution host APIs to script runtimes.
checkpoint
workspace
Package workspace provides an agent.CheckpointStore backed by a workspace.Workspace.
Package workspace provides an agent.CheckpointStore backed by a workspace.Workspace.
Package scriptrt owns the deployment resource protocol for script runtimes.
Package scriptrt owns the deployment resource protocol for script runtimes.
jsrt
Package jsrt provides a goja-based JavaScript implementation of agent.ScriptRuntime.
Package jsrt provides a goja-based JavaScript implementation of agent.ScriptRuntime.
luart
Package luart provides a pure-Go Lua 5.1 implementation of agent.ScriptRuntime using github.com/yuin/gopher-lua (no CGO).
Package luart provides a pure-Go Lua 5.1 implementation of agent.ScriptRuntime using github.com/yuin/gopher-lua (no CGO).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL