runtime

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 14, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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.

View Source
var ErrTierUnavailable = errors.New("sandbox tier unavailable")

ErrTierUnavailable is returned when a requested execution tier is not available.

View Source
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

func IsReplayDivergenceError(err error) bool

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

type Clock interface {
	// Now returns the current time.
	Now() time.Time
}

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

type ErrForkBeyondLog struct {
	At      uint64
	LastSeq uint64
}

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

func (NoopTelemetry) RecordSpend(event.LoopID, budget.Spend, string)

RecordSpend implements Telemetry. No-op.

func (NoopTelemetry) SpanEnd

SpanEnd implements Telemetry. No-op.

func (NoopTelemetry) SpanStart

SpanStart implements Telemetry. No-op; returns zero SpanID.

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 RealClock

type RealClock struct{}

RealClock implements Clock using system time.

func (RealClock) Now

func (RealClock) Now() time.Time

Now returns the current system time.

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

func (r *Runner[S, R]) Interrupt(id event.LoopID)

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

func (r *Runner[S, R]) Replay(ctx context.Context, id event.LoopID) (Trajectory[S], error)

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

func (r *Runner[S, R]) Resume(ctx context.Context, id event.LoopID) (outcome.Outcome[R], error)

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

func (r *Runner[S, R]) Start(ctx context.Context, input any) (outcome.Outcome[R], error)

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 SpanID

type SpanID [16]byte

SpanID is an opaque span identifier returned by SpanStart.

type SpanKind

type SpanKind string

SpanKind identifies the semantic kind of a telemetry span.

const (
	SpanKindRun       SpanKind = "run"
	SpanKindIteration SpanKind = "iteration"
	SpanKindAction    SpanKind = "action"
)

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 Tier

type Tier string

Tier represents a sandbox execution tier.

const (
	// TierInProc executes tools directly within the process (trusted).
	TierInProc Tier = "inproc"
	// TierWASM executes tools via WASM with wazero runtime (sandboxed).
	TierWASM Tier = "wasm"
	// TierContainer executes tools in container (deferred).
	TierContainer Tier = "container"
)

type TokenEstimator

type TokenEstimator func(req model.Request) int64

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

func (m *ToolMux) RegisterSandbox(tier Tier, s Sandbox)

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.

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.

Jump to

Keyboard shortcuts

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