Documentation
¶
Overview ¶
Package runtime defines the application-layer ports for the loop kernel. These are interfaces consumed by the runner; adapters implement them. Following Go idiom, ports are defined consumer-side (here), not provider-side.
Package runtime implements the agent loop runner. The runner folds the event log into state, executes actions with the intent-before-effect durability protocol, and appends result events.
Package runtime defines the sandbox execution tiers and contract.
Package runtime — telemetry port. Telemetry is a runtime port (defined consumer-side). All adapters implement it. The default no-op implementation allocates nothing on the hot path.
Package runtime implements the agent loop runner. toolmux.go defines the ToolMux router for multiplexing multiple tool executors.
Index ¶
- Variables
- func Fork[S, R any](ctx context.Context, def loop.Definition[S, R], source []event.Envelope, ...) (outcome.Outcome[R], []event.Envelope, error)
- func IsReplayDivergenceError(err error) bool
- func Respond(ctx context.Context, store EventStore, id event.LoopID, ...) error
- func Shadow[S, R any](ctx context.Context, def loop.Definition[S, R], source []event.Envelope, ...) ([]event.Envelope, error)
- type ArtifactStore
- type ChildResult
- type ChildSpawner
- type Clock
- type CompactionPlan
- type ContextManager
- type ErrForkBeyondLog
- type EventStore
- type ForkSpec
- type ListFilter
- type LoopMeta
- type ModelProvider
- type NoopTelemetry
- type OffloadSpec
- type Overrides
- type PolicyMode
- type RealClock
- type ReplayDivergenceError
- type Runner
- func (r *Runner[S, R]) Interrupt(id event.LoopID)
- func (r *Runner[S, R]) Replay(ctx context.Context, id event.LoopID) (Trajectory[S], error)
- func (r *Runner[S, R]) Resume(ctx context.Context, id event.LoopID) (outcome.Outcome[R], error)
- func (r *Runner[S, R]) Start(ctx context.Context, input any) (outcome.Outcome[R], error)
- func (r *Runner[S, R]) StartWithParent(ctx context.Context, request []byte, parentID event.LoopID, depth int) (outcome.Outcome[R], event.LoopID, error)
- type RunnerOptions
- type Sandbox
- type SandboxResult
- type SandboxSpec
- type SpanAttrs
- type SpanID
- type SpanKind
- type StreamObserver
- type StreamReader
- type SummarySpec
- type Telemetry
- type Tier
- type TokenEstimator
- type ToolDescriptor
- type ToolExecutor
- type ToolMux
- type ToolRequest
- type ToolResult
- type Trajectory
Constants ¶
This section is empty.
Variables ¶
var ErrConflict = errors.New("event store: optimistic concurrency conflict")
ErrConflict is returned by EventStore.Append when the expectedLastSeq does not match the actual last sequence number. This prevents two concurrent runners from writing to the same loop simultaneously.
ErrTierUnavailable is returned when a requested execution tier is not available.
var ErrUnsupported = errors.New("runtime: unsupported action type")
ErrUnsupported is returned when the runner encounters an unsupported action type (e.g., Spawn in SP1).
Functions ¶
func Fork ¶
func Fork[S, R any]( ctx context.Context, def loop.Definition[S, R], source []event.Envelope, spec ForkSpec, opts RunnerOptions, ) (outcome.Outcome[R], []event.Envelope, error)
Fork replays source up to spec.At (inclusive), then continues live with spec.Overrides. The forked run gets a new LoopID; its first event is ForkedFrom{source, atSeq}. The source log is immutable — the source store is only read, never written.
opts configure the forked runner (store, clock, budget, etc.). If opts does not include a store, Fork returns an error.
func IsReplayDivergenceError ¶
IsReplayDivergenceError returns true if err is a *ReplayDivergenceError.
func Respond ¶
func Respond(ctx context.Context, store EventStore, id event.LoopID, response json.RawMessage) error
Respond appends a HumanResponded event to a loop that is awaiting human input. This is a package-level function (not a method) for use by the facade.
func Shadow ¶
func Shadow[S, R any]( ctx context.Context, def loop.Definition[S, R], source []event.Envelope, overrides Overrides, opts RunnerOptions, ) ([]event.Envelope, error)
Shadow replays a source trajectory, but sends each ModelCall LIVE against the override provider instead of replaying the recorded model response. Tool calls always use the recorded results — the tool executor is never invoked live. This allows comparing how a new provider/model would have responded to the same loop.
The shadow run gets a new LoopID and its own store (provided via opts). The source log is immutable.
Returns the shadow trajectory (all events) and any error.
Types ¶
type ArtifactStore ¶
type ArtifactStore interface {
// Put stores an artifact and returns its key.
Put(ctx context.Context, loopID event.LoopID, seq uint64, data []byte) (string, error)
// Get retrieves an artifact by key.
Get(ctx context.Context, key string) ([]byte, error)
}
ArtifactStore is the port for storing and retrieving large artifacts (blobs). Used by the ArtifactOffload compaction strategy.
type ChildResult ¶
type ChildResult struct {
LoopID event.LoopID
OutcomeKind string
RespJSON []byte
ErrDetail string
Spend budget.Spend
}
ChildResult is the result of a spawned child loop.
type ChildSpawner ¶
type ChildSpawner interface {
// SpawnChild executes a new child loop and returns its result.
// opts is the current parent runner options (shared store/provider/tools).
// parentID and depth come from the parent's LoopStarted event.
SpawnChild(
ctx context.Context,
opts RunnerOptions,
parentID event.LoopID,
depth int,
childBudget budget.Budget,
childGrant policy.Grant,
definitionName string,
request []byte,
) ChildResult
// ResumeChild resumes an existing unfinished child loop by ID.
// definitionName is used to look up the correct typed runner.
ResumeChild(
ctx context.Context,
opts RunnerOptions,
childID event.LoopID,
definitionName string,
) ChildResult
}
ChildSpawner is the port for spawning and resuming child loops. Implemented by loopkit.Registry (or any user-provided registry).
type Clock ¶
Clock is the time source for the runtime. In live mode, it returns system time. In replay mode, it returns event-log time.
type CompactionPlan ¶
type CompactionPlan struct {
// Strategy is the name of the strategy that produced this plan.
Strategy string `json:"strategy"`
// DropRanges is a list of [from, to] message index ranges to remove.
DropRanges [][2]int `json:"drop_ranges,omitempty"`
// Summaries is a list of summary insertions to inject.
Summaries []SummarySpec `json:"summaries,omitempty"`
// Offloads is a list of message indices to offload to the ArtifactStore.
Offloads []OffloadSpec `json:"offloads,omitempty"`
}
CompactionPlan describes a set of message-history transformations to apply before a ModelCall to keep the context within token limits. It is stored verbatim in the CompactionApplied event for deterministic replay.
type ContextManager ¶
type ContextManager interface {
// Plan decides whether and how to compact the message history.
// ledger tracks per-source token totals for the current loop run.
// msgs is the current message slice.
// Returns (planJSON, true) if compaction is needed, (nil, false) otherwise.
// planJSON is the serialised plan stored verbatim in CompactionApplied.PlanJSON.
Plan(ledger *ctxmgr.Ledger, msgs []model.Message) (planJSON []byte, needed bool)
// ApplyPlanJSON transforms the message slice according to a previously
// recorded plan (from CompactionApplied.PlanJSON). Called during replay.
ApplyPlanJSON(planJSON []byte, msgs []model.Message) []model.Message
// StrategyName returns the strategy identifier for CompactionApplied.Strategy.
StrategyName() string
}
ContextManager is the interface for context compaction strategies. The runner calls Plan before each ModelCall; if needed=true, it records a CompactionApplied event and transforms the message slice before sending. On replay, ApplyPlanJSON re-applies the stored plan without re-deciding.
type ErrForkBeyondLog ¶
ErrForkBeyondLog is returned when ForkSpec.At exceeds the source trajectory length.
func (*ErrForkBeyondLog) Error ¶
func (e *ErrForkBeyondLog) Error() string
type EventStore ¶
type EventStore interface {
// Append atomically appends evs to the loop's event log.
// expectedLastSeq must equal the current last sequence number of the loop
// (0 if no events have been written yet). Returns ErrConflict if the
// actual last seq differs — the caller must reload and retry.
// All events in evs must be pre-populated (LoopID, Seq, Time, Kind, Payload, Hash).
Append(ctx context.Context, id event.LoopID, expectedLastSeq uint64, evs []event.Envelope) error
// Load returns events for the given loop starting at fromSeq (inclusive).
// fromSeq=1 returns all events. The iterator yields events in seq order.
// An error in the iterator terminates iteration.
Load(ctx context.Context, id event.LoopID, fromSeq uint64) iter.Seq2[event.Envelope, error]
// SaveSnapshot persists a snapshot for later use by Resume.
SaveSnapshot(ctx context.Context, snap event.Snapshot) error
// LatestSnapshot returns the most recent snapshot for the given loop.
// Returns (snap, true, nil) if found, or (Snapshot{}, false, nil) if not.
LatestSnapshot(ctx context.Context, id event.LoopID) (event.Snapshot, bool, error)
// List returns summary metadata for loops matching the filter.
List(ctx context.Context, f ListFilter) ([]LoopMeta, error)
}
EventStore is the durable, append-only event log port. Every loop has its own sequence space (1-based, contiguous, no gaps).
type ForkSpec ¶
type ForkSpec struct {
// At is the sequence number at which to diverge. Events 1..At are replayed
// from the source log; everything after is executed live with Overrides applied.
At uint64
// Overrides replaces components for the live portion.
Overrides Overrides
}
ForkSpec controls where and how to fork a trajectory.
type ListFilter ¶
type ListFilter struct {
// Status filters by loop status. Empty string means all statuses.
Status string
// Limit caps the number of results (0 = no limit).
Limit int
// Offset skips this many results (for pagination).
Offset int
// ParentID filters by parent loop ID (SP5: recursive resume).
// Empty string means no parent filter.
ParentID string
}
ListFilter controls which loops are returned by EventStore.List.
type LoopMeta ¶
type LoopMeta struct {
// ID is the loop's unique identifier.
ID event.LoopID
// Name is the human-readable loop name (from LoopStarted).
Name string
// CreatedAt is when the loop was started.
CreatedAt time.Time
// LastSeq is the sequence number of the most recent event.
LastSeq uint64
// Status is the current loop status: "running", "completed", "interrupted", "failed".
Status string
// ParentID is the parent loop's ID (empty for root loops). SP5.
ParentID string
}
LoopMeta is summary metadata for a loop, returned by EventStore.List.
type ModelProvider ¶
type ModelProvider interface {
// Complete sends a request to the model and returns its complete response.
// The response includes content, stop reason, and usage information.
Complete(ctx context.Context, req model.Request) (model.Response, error)
// Stream sends a request to the model and returns a StreamReader for reading chunks.
// Each chunk represents an incremental update to the response.
// The caller must call Close() on the reader to free resources.
Stream(ctx context.Context, req model.Request) (StreamReader, error)
}
ModelProvider is the port for calling AI models via model.Request. Implementations handle provider-specific wire format translation and pricing calculation.
type NoopTelemetry ¶
type NoopTelemetry struct{}
NoopTelemetry is the default Telemetry implementation. Every method is a no-op and performs zero allocations.
func (NoopTelemetry) OnEvent ¶
func (NoopTelemetry) OnEvent(event.Envelope)
OnEvent implements Telemetry. No-op.
func (NoopTelemetry) RecordSpend ¶
RecordSpend implements Telemetry. No-op.
type OffloadSpec ¶
type OffloadSpec struct {
// MessageIndex is the index in the message slice to offload.
MessageIndex int `json:"message_index"`
// ArtifactKey is the key under which the content is stored.
ArtifactKey string `json:"artifact_key,omitempty"` // set after Put
}
OffloadSpec describes one message to offload to the ArtifactStore.
type Overrides ¶
type Overrides struct {
// Provider replaces the ModelProvider for the live portion of the fork.
Provider ModelProvider
// Model overrides the model name for all ModelCall actions.
Model string
// SystemPatch is prepended to all system prompts in ModelCall requests.
SystemPatch string
// Tools replaces the ToolExecutor for the live portion of the fork.
Tools ToolExecutor
}
Overrides allows replacing components of a live loop (provider, model, system prompt, tools). Zero/nil values mean "keep the original".
type PolicyMode ¶
type PolicyMode int
PolicyMode controls what happens when the policy engine denies a capability.
const ( // PolicyModeTerminate (default): a denied capability terminates the loop with PolicyDenied outcome. PolicyModeTerminate PolicyMode = iota // PolicyModeDenyAction: a denied capability records an error ToolResult; the loop continues. PolicyModeDenyAction )
type ReplayDivergenceError ¶
type ReplayDivergenceError struct {
// Seq is the sequence number of the first divergent event.
Seq uint64
// Want is the expected (stored) hash.
Want [32]byte
// Got is the recomputed hash.
Got [32]byte
}
ReplayDivergenceError is returned by strict replay when a recomputed hash does not match the stored hash. This indicates either data corruption or a non-deterministic transition.
func (*ReplayDivergenceError) Error ¶
func (e *ReplayDivergenceError) Error() string
type Runner ¶
type Runner[S, R any] struct { // contains filtered or unexported fields }
Runner executes a loop definition against a store. Generic over S (state) and R (result).
func NewRunner ¶
func NewRunner[S, R any](def loop.Definition[S, R], opts RunnerOptions) *Runner[S, R]
NewRunner creates a Runner with the given definition and options.
func (*Runner[S, R]) Interrupt ¶
Interrupt signals the runner to interrupt the given loop. The loop will emit an Interrupted event and return an Interrupted outcome.
func (*Runner[S, R]) Replay ¶
Replay strictly replays the event log for the given loop ID. It re-folds all events and recomputes chain hashes, returning ReplayDivergenceError if any hash doesn't match. In strict replay mode, no live calls are made (provider/tools are NOT invoked).
func (*Runner[S, R]) Resume ¶
Resume loads a loop from the store, folds all events, handles any dangling actions per their ReplaySemantics, and continues execution to completion. For multi-agent loops: first recursively resumes any unfinished children, then continues.
func (*Runner[S, R]) Start ¶
Start begins a new loop execution. Appends LoopStarted, then runs the main loop until a terminal outcome.
func (*Runner[S, R]) StartWithParent ¶
func (r *Runner[S, R]) StartWithParent(ctx context.Context, request []byte, parentID event.LoopID, depth int) (outcome.Outcome[R], event.LoopID, error)
StartWithParent starts a child loop with a known parentID and depth. The LoopStarted event will carry ParentID and Depth for multi-agent tracing. request is JSON-encoded input (may be nil). Returns the outcome and the child LoopID.
type RunnerOptions ¶
type RunnerOptions struct {
Store EventStore
Provider ModelProvider
Tools ToolExecutor
Clock Clock
SnapshotEvery int // take snapshot every N events (0 = disabled)
Seed int64
MaxIterations int // 0 = unlimited
PricingTable model.Table // pricing for model cost calculation; nil uses model.DefaultTable
StreamObserver StreamObserver // optional callback for streaming chunks; nil disables
// SP3: governance
Budget budget.Budget // zero = no budget enforcement
TokenEstimator TokenEstimator // nil = default heuristic (chars/4 + MaxTokens)
Policy *policy.Grant // nil = allow-all (back-compat)
PolicyMode PolicyMode // default: PolicyModeTerminate
Convergence *converge.Config // nil = no convergence monitoring
// SP5: multi-agent
Spawner ChildSpawner // nil = Spawn actions return ErrUnsupported
ArtifactStore ArtifactStore // nil = ArtifactOffload not available
// CtxMgr is the context manager strategy. nil = no compaction.
CtxMgr ContextManager
// SP6: telemetry — nil uses NoopTelemetry (zero hot-path alloc).
Telemetry Telemetry
}
RunnerOptions configures a Runner.
type Sandbox ¶
type Sandbox interface {
// Run executes the given sandbox spec and returns the result.
// If ctx is cancelled or exceeds its deadline, the sandbox must stop
// execution and return an error.
Run(ctx context.Context, spec SandboxSpec) (SandboxResult, error)
}
Sandbox is the port for executing tools in a sandboxed environment.
type SandboxResult ¶
type SandboxResult struct {
// Output is the tool's stdout as JSON.
Output []byte
// Stderr is the tool's stderr output.
Stderr []byte
}
SandboxResult is the output of a sandboxed execution.
type SandboxSpec ¶
type SandboxSpec struct {
// Module is the WASM binary (required for TierWASM).
Module []byte
// Fn is the in-process function (required for TierInProc).
Fn func(ctx context.Context, input []byte) ([]byte, error)
// Input is the JSON stdin for the tool.
Input []byte
// Caps are the capabilities granted to the sandbox.
// fs:read:<dir> grants read-only access to that directory.
Caps []policy.Capability
// MemoryLimitPages sets the WASM memory limit in 64KiB pages.
// 0 means use the default (typically 256 pages = 16 MiB).
MemoryLimitPages uint32
}
SandboxSpec defines the inputs and constraints for a sandboxed execution.
type SpanAttrs ¶
type SpanAttrs struct {
// ActionType is the action kind ("ModelCall", "ToolCall", "Finish", etc.).
ActionType string
// Tool is the tool name (only for ToolCall spans).
Tool string
// Model is the model identifier (only for ModelCall spans).
Model string
// InputTokens is the number of input tokens charged (GenAI semconv gen_ai.usage.input_tokens).
InputTokens int64
// OutputTokens is the number of output tokens charged.
OutputTokens int64
// CostUSD is the cost charged in US dollars.
CostUSD float64
// Outcome is the final outcome kind (for run-level spans).
Outcome string
// Iteration is the iteration number (for iteration spans).
Iteration int
}
SpanAttrs carries optional attributes for a span.
type StreamObserver ¶
type StreamObserver func(loopID event.LoopID, chunk event.StreamChunk)
StreamObserver is an optional callback that receives chunks from model streaming. It is invoked for each chunk received during streaming.
type StreamReader ¶
type StreamReader interface {
// Recv returns the next chunk from the stream.
// Returns io.EOF when the stream is complete.
Recv() (event.StreamChunk, error)
// Close closes the stream and frees resources.
Close() error
}
StreamReader reads chunks from a streamed model response. Recv blocks until a chunk is available or an error occurs. It returns io.EOF when the stream is exhausted.
type SummarySpec ¶
type SummarySpec struct {
// InsertAfter is the index after which to insert the summary message.
InsertAfter int `json:"insert_after"`
// Content is the summary text.
Content string `json:"content"`
}
SummarySpec describes one summary to inject into the context.
type Telemetry ¶
type Telemetry interface {
// OnEvent is called after each event is appended to the store.
// Must return quickly (no blocking I/O on the hot path).
OnEvent(ev event.Envelope)
// SpanStart records the beginning of a span. Returns an opaque SpanID.
// kind indicates the semantic level; attrs provides initial attributes.
SpanStart(kind SpanKind, loopID event.LoopID, attrs SpanAttrs) SpanID
// SpanEnd closes a previously started span.
// attrs may carry final attributes not known at SpanStart (e.g. token counts).
// elapsed is the wall-clock duration of the span.
SpanEnd(id SpanID, attrs SpanAttrs, elapsed time.Duration, err error)
// RecordSpend records a resource charge (counters and histograms).
RecordSpend(loopID event.LoopID, spend budget.Spend, actionType string)
}
Telemetry is the runtime port for observability. Implementations must be safe for concurrent use from multiple goroutines.
The default implementation is NoopTelemetry, which performs zero allocations on the hot path and is the out-of-the-box default.
type TokenEstimator ¶
TokenEstimator estimates the number of tokens for a model request before it executes. The default heuristic is: sum of all message content lengths / 4 + MaxTokens.
type ToolDescriptor ¶
type ToolDescriptor struct {
// Name is the tool's unique identifier.
Name string
// Description explains what the tool does (shown to the model).
Description string
// Schema is the JSON schema for the tool's arguments.
Schema []byte
// Semantics controls crash-resume behavior for this tool.
Semantics action.ReplaySemantics
// RequiredCapabilities is the list of capability tokens needed (SP3).
RequiredCapabilities []string
// Tier specifies the sandbox execution tier for this tool (zero value = TierInProc).
Tier Tier
}
ToolDescriptor describes a tool available for the loop to use.
type ToolExecutor ¶
type ToolExecutor interface {
// Execute runs the named tool with the given request.
Execute(ctx context.Context, call ToolRequest) (ToolResult, error)
// Describe returns metadata about all available tools.
Describe() []ToolDescriptor
}
ToolExecutor is the port for executing tools.
type ToolMux ¶
type ToolMux struct {
// contains filtered or unexported fields
}
ToolMux multiplexes tool execution across multiple ToolExecutor implementations. If multiple executors have a tool with the same name, NewToolMux returns an error. Optional sandbox tiers may be registered to route tools by their declared Tier.
func NewToolMux ¶
func NewToolMux(executors ...ToolExecutor) (*ToolMux, error)
NewToolMux creates a ToolMux from the given executors. Returns error if any tool names are duplicated across executors.
func (*ToolMux) Describe ¶
func (m *ToolMux) Describe() []ToolDescriptor
Describe returns all tools from all executors.
func (*ToolMux) Execute ¶
func (m *ToolMux) Execute(ctx context.Context, call ToolRequest) (ToolResult, error)
Execute runs the named tool using the appropriate executor. If the tool declares a tier (TierWASM or TierContainer), and a sandbox is registered for that tier, execution is routed through the sandbox. Otherwise, it routes to the tool's executor directly. Returns ToolResult with IsError=true if the tool is unknown or if the required tier is unavailable.
func (*ToolMux) RegisterSandbox ¶
RegisterSandbox registers a Sandbox implementation for a given tier. If a sandbox is already registered for this tier, it is replaced.
type ToolRequest ¶
type ToolRequest struct {
// Tool is the tool name.
Tool string
// Args is the tool arguments as canonical JSON.
Args []byte
// IdempotencyKey is a stable key for this specific call, used to deduplicate.
IdempotencyKey string
}
ToolRequest is passed to ToolExecutor.Execute.
type ToolResult ¶
type ToolResult struct {
// Result is the tool's output as JSON.
Result []byte
// IsError indicates whether the tool returned an error.
IsError bool
}
ToolResult is returned by ToolExecutor.Execute.
type Trajectory ¶
type Trajectory[S any] struct { // Events is the full ordered event log. Events []event.Envelope // FinalState is the state after all events have been folded. FinalState S // Outcome is the terminal outcome (nil if loop has not finished). Outcome outcome.Outcome[S] }
Trajectory is the result of a Replay operation. It contains all events and the final state reached.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Command crashchild is the test child process for the crash-proof integration test.
|
Command crashchild is the test child process for the crash-proof integration test. |