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

agentrt

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 25, 2026 License: MIT Imports: 15 Imported by: 6

README

agent-runtime

ci

A small Go runtime for executing tool-using agents under deterministic control.

The model loop is not the point. Any SDK gives you that. This runtime exists for the controls around the loop:

  • the agent asks, the runtime decides: every tool request is schema-validated and policy-evaluated before it executes, and the agent has no influence over policy;
  • explicit, persisted state: runs, steps, and an append-only event log in SQLite;
  • hard limits enforced by the driver, not by asking the model to be careful;
  • deterministic testability: the whole loop runs against a scripted agent.

The first consumer is repo-steward, which supplies every requirement for the public API. Abstractions are added when it demonstrates the need, not before.

Status

v0.2: the runtime that repo-steward and casework run on, with the packages both rebuilt now shipped here and an MCP adapter that puts any server's tools behind the policy (docs/roadmap.md items 1 and 2). The nested modules are tagged separately with a directory prefix: providers/ollama/v0.1.0, providers/anthropic/v0.1.0, providers/openai/v0.1.0, mcp/v0.1.0. docs/architecture.md describes the loop, approvals, interruption, and persistence; docs/status.md lists every control with the test that verifies it; docs/decisions records why it is built this way. The wiki covers the same material at length for people using, extending, or evaluating the runtime. The API is pre-1.0 and changes when its consumer needs it to.

Quick start

Requires Go 1.27 or later.

The demo the project leads with is examples/mcp: a local model driving the stock MCP filesystem server, where reads are allowed and a write stops the run until an operator approves that exact call. It needs Node, for npx @modelcontextprotocol/server-filesystem, an Ollama server holding qwen3:30b-a3b, and a workspace, because the example and the MCP adapter are nested modules:

go work init . ./providers/ollama ./providers/anthropic ./providers/openai ./mcp ./examples/live ./examples/mcp
go run ./examples/mcp pin      # hash the server's tools, print the hints it claims
go run ./examples/mcp run      # load the operator's rules, run to the first write
go run ./cmd/agentrt -db <path> approve <run>
go run ./examples/mcp run -resume <run>
go run ./cmd/agentrt -db <path> events <run>

pin prints one line per tool with the annotations the server asserts, which classify nothing and are only there for the operator to disagree with. examples/mcp/rules.json is the operator's answer: four read tools, write_file as a local mutation with the server's own destructive hint overruled in writing, and the other nine tools the filesystem server offers left out, so run reports them unclassified and the model never sees them. The run then pauses at write_file with the path, the line count, and a preview of the content, and prints the two commands above with the run id and the database filled in. The trail ends with the read that was allowed, the write that waited, the operator who granted it, and the call that executed after the grant.

The no-network path is examples/scripted, which needs neither a model nor Node:

go test -race ./...
go run ./examples/scripted

It runs a scripted agent against three in-memory tools, including one malformed request the runtime rejects and one destructive request the policy denies, then prints the persisted event trace.

Shape of the API

store, _ := agentrt.OpenStore("runs.db")
driver, _ := agentrt.NewDriver(agentrt.Config{
    Store:  store,
    Agent:  myAgent,               // Decide(ctx, StepInput) (Decision, error)
    Policy: agentrt.DefaultPolicy(), // read/local allow, remote approval, destructive deny
    Tools:  []agentrt.Tool{...},   // Spec() with JSON schema, side effect, timeout
})
run, _ := driver.Start(ctx, "goal", agentrt.DefaultLimits())

What a consumer no longer has to write

Both existing consumers rebuilt the same provider plumbing, renderer, trace, and operator surface. These packages are in the core module and add no dependency beyond the standard library:

  • providers: transport and HTTP classification for a provider adapter (a timeout returned bare, retryable statuses transient), a bounded body read, tool-use id synthesis, a price lookup that refuses an unpriced paid model, and dollar parsing for budgets.
  • render: a run's recorded steps to model messages, with synthesized tool-use ids, a recent-results window, a content cap, and hooks for the opening message, the assistant turn, and the observation; plus render.Decide, which maps a response to a decision and turns a truncated or tool-less reply into a deliberately invalid decision the runtime records and the next render nudges.
  • trace: aligned and JSON Lines observers.
  • view: run, step, and approval read models, and the rule for choosing the approval an operator means.
go run ./cmd/agentrt -db runs.db runs
go run ./cmd/agentrt -db runs.db show <run>       # steps and the waiting approval
go run ./cmd/agentrt -db runs.db events <run>     # the audit log, -json for JSONL
go run ./cmd/agentrt -db runs.db approve <run> -by joey -note "ok"

cmd/agentrt reads -db or AGENTRT_DB, prints aligned text or -json, and exits 0, 1, or 2. It has no resume: resuming executes the approved request and continues the loop, which needs the consumer's agent, tools, and policy.

A run ends COMPLETED, FAILED with a terminal reason, or pauses in WAITING_FOR_APPROVAL on a hash-bound approval that Approve and Resume continue. A Model given to the driver is wrapped in an accounting caller that agents receive in StepInput: it records every attempt, enforces call, token, and cost limits before dispatch, and retries transient failures. The replay package provides scripted, recording, and replaying models so tests never call a provider.

Provider adapters

Each adapter is a nested module with its own go.mod, so a consumer imports only the provider it uses and a provider SDK never reaches the core:

  • providers/ollama: a local Ollama server over its native chat API, with no dependency beyond the standard library.
  • providers/openai: any OpenAI-compatible chat completions endpoint, which covers most local servers, with no dependency beyond the standard library.
  • providers/anthropic: the Claude Messages API through the official SDK, with the raw assistant turn round trip that keeps signed thinking blocks alive across a continuation, an opt-in strict schema subset, and a dated price table.
go get github.com/joeylking/agent-runtime/providers/ollama

All three implement agentrt.Model the same way: a Config carrying the model id, New(Config) (*Model, error), no streaming, no retries because the accounting caller retries and records every attempt, the provider's raw bytes on every response, and a stop reason mapped to tool_use, end_turn, max_tokens, or refusal. A reply that was cut off or refused comes back with no tool uses, so a partial call can never execute, and with its usage, so the attempt is still charged.

Name() is <provider>:<model> unless the config overrides it, so one price table and one recording directory can hold several providers. anthropic.Prices() prices the paid models; ollama.Free(...) and openai.Free(...) record local ones as free, which is what providers.PriceFor needs to tell a free model from an unpriced one.

No test calls a provider: the Anthropic adapter is exercised against an httptest fake, and each local adapter has one round trip behind the live build tag that skips unless a server answers. Working on the nested modules needs a workspace, which is what CI builds:

go work init . ./providers/ollama ./providers/anthropic ./providers/openai ./mcp ./examples/live ./examples/mcp

MCP servers behind the policy

mcp is a nested module that exposes an MCP server's tools as agentrt.Tools, so they pass the same validation, policy, limits, and audit log as any other tool. The MCP specification says a client must never make decisions from tool annotations, because an untrusted server supplies them, which is the premise of the design:

  • a tool with no operator Rule naming its side-effect class is not registered, and the load report says so;
  • Pin records each tool's name, description, and input schema in a manifest the operator reviews and stores; Load refuses any tool whose live schema or description no longer hashes to the pin, because a description is model input and a changed one is an injection vector;
  • annotations are recorded and cross-checked. They can only contradict an operator's classification, never relax it, and a contradiction refuses the tool unless the rule allows the mismatch, which the report records;
  • an operator may override a description, deny a parameter, and fix a parameter's value; a denied or fixed parameter is removed from the schema the model sees;
  • the model sees <server>_<tool> unless the rule renames it, so two servers cannot collide;
  • isError becomes a tool error the agent must work around, content is capped, and a server asking for more input is refused.
manifest, _ := mcp.Pin(ctx, server)            // review and store this
tools, report, _ := mcp.Load(ctx, server, manifest, rules)
defer report.Connection.Close()

ADR 4 records why the class is the operator's and why a hint may only refuse. examples/mcp is the whole thing running: the quick start above is its four commands.

A live run that stops for an operator

examples/live is the shortest complete consumer: the tools from examples/scripted plus a terminal one to finish on, an Agent that is render.Messages and render.Decide around the model, a stderr trace, and a policy that sends the first change to the counter to an operator. It prints the approve command with the run id filled in.

go run ./examples/live                              # pauses; prints the two below
go run ./cmd/agentrt -db <path> approve <run>
go run ./examples/live -db <path> -resume <run>

It runs ollama:qwen3:30b-a3b by default; -model openai:<id> with -base-url http://127.0.0.1:11434/v1 runs the same agent through the OpenAI-compatible adapter.

Documentation

Overview

Package agentrt is a small runtime for executing tool-using agents under deterministic control. The runtime owns the step loop, argument validation, policy evaluation, limits, persistence, and the audit event log. The agent owns only the decision of what to do next.

Milestone 0A implements one scripted run: decide, validate, evaluate policy, execute, observe, persist, repeat, and stop on a step limit. Approvals, resumption, model accounting, loop detection, and recovery are documented in docs/status.md and are not part of this package yet.

Index

Constants

View Source
const (
	EventRunCreated        = "run.created"
	EventRunStarted        = "run.started"
	EventRunFinished       = "run.finished"
	EventStepStarted       = "step.started"
	EventStepDecided       = "step.decided"
	EventStepPolicy        = "step.policy"
	EventStepToolStarted   = "step.tool_started"
	EventStepToolFinished  = "step.tool_finished"
	EventStepFailed        = "step.failed"
	EventApprovalRequested = "approval.requested"
	EventLimitExceeded     = "limit.exceeded"
	EventLoopDetected      = "loop.detected"
	EventApprovalDecided   = "approval.decided"
	EventRunResumed        = "run.resumed"
	EventRunInterrupted    = "run.interrupted"
	EventStepInterrupted   = "step.interrupted"
	EventModelDispatched   = "model.dispatched"
	EventModelCompleted    = "model.completed"
	EventModelFailed       = "model.failed"
)

Event types written by the driver.

Variables

View Source
var ErrApprovalHash = errors.New("agentrt: approval hash does not match its recorded request")

ErrApprovalHash means a stored approval no longer matches its own fields.

View Source
var ErrNotFound = errors.New("agentrt: not found")

ErrNotFound is returned when a run does not exist.

Functions

func Approve

func Approve(ctx context.Context, store *Store, obs Observer, runID, approvalID, by, note string) error

Approve records an approval decision without a driver, for command-line front ends that decide approvals in a process that will not resume the run. The hash is recomputed from the stored fields first.

func Cancel added in v0.1.1

func Cancel(ctx context.Context, store *Store, obs Observer, runID, by, note string) error

Cancel is Driver.Cancel without a driver, for command-line tools that decide on runs they did not start.

func DecodePresentation added in v0.2.0

func DecodePresentation[T any](a Approval) (T, error)

DecodePresentation unmarshals an approval's presentation into T, for a front end that renders it or a consumer that rechecks it before resuming.

func EstimateInputTokens

func EstimateInputTokens(req ModelRequest) int

EstimateInputTokens is the conservative input estimate the limits use: one token per three bytes of the serialized request. Consumers can use it to budget before a run.

func Reject

func Reject(ctx context.Context, store *Store, obs Observer, runID, approvalID, by, note string) error

Reject records a rejection without a driver and ends the run as CANCELLED.

Types

type Agent

type Agent interface {
	Decide(ctx context.Context, in StepInput) (Decision, error)
}

Agent decides what happens next.

type Approval

type Approval struct {
	ID           string          `json:"id"`
	RunID        string          `json:"run_id"`
	StepID       string          `json:"step_id"`
	Kind         string          `json:"kind"`
	Capability   json.RawMessage `json:"capability"`
	Presentation json.RawMessage `json:"presentation"`
	Request      ToolRequest     `json:"request"`
	Hash         string          `json:"hash"`
	Status       ApprovalStatus  `json:"status"`
	CreatedAt    time.Time       `json:"created_at"`
	ExpiresAt    time.Time       `json:"expires_at,omitempty"`
	DecidedAt    time.Time       `json:"decided_at,omitempty"`
	DecidedBy    string          `json:"decided_by,omitempty"`
	Note         string          `json:"note,omitempty"`
}

Approval is a durable, hash-bound request for a human decision. Its hash covers the kind, capability, presentation, and the exact tool request, so a changed request cannot inherit an old approval. On resume the runtime executes the recorded request and nothing else.

type ApprovalStatus

type ApprovalStatus string

ApprovalStatus is the state of an approval.

const (
	ApprovalPending  ApprovalStatus = "pending"
	ApprovalApproved ApprovalStatus = "approved"
	ApprovalRejected ApprovalStatus = "rejected"
	ApprovalExpired  ApprovalStatus = "expired"
)

type Config

type Config struct {
	Store    *Store
	Agent    Agent
	Policy   Policy
	Tools    []Tool
	Observer Observer
	// Model, when set, is wrapped in a ModelCaller that agents receive in
	// StepInput. Scripted agents leave it nil.
	Model *ModelConfig
	// Reconcile, when set, runs before a run found mid-step is continued by
	// Resume. Consumers reconcile their own journaled operations and say
	// whether the run continues, completed, waits, or is in conflict.
	Reconcile func(ctx context.Context, view RunView) (Reconciliation, error)
	// Now and NewID may be overridden by tests.
	Now   func() time.Time
	NewID func() string
}

Config configures a Driver.

type ContentBlock

type ContentBlock struct {
	Type string `json:"type"`
	// Text applies to text blocks.
	Text string `json:"text,omitempty"`
	// ToolUseID, Name, and Input apply to tool_use blocks; ToolUseID also
	// links a tool_result block to the call it answers.
	ToolUseID string          `json:"tool_use_id,omitempty"`
	Name      string          `json:"name,omitempty"`
	Input     json.RawMessage `json:"input,omitempty"`
	// Content and IsError apply to tool_result blocks.
	Content string `json:"content,omitempty"`
	IsError bool   `json:"is_error,omitempty"`
}

ContentBlock is one part of a message. Types are text, tool_use, and tool_result, matching native tool-use APIs closely enough that a provider adapter is a mapping, not a redesign.

type Decision

type Decision struct {
	Kind DecisionKind `json:"kind"`
	// Tool and Args apply to tool_call.
	Tool string          `json:"tool,omitempty"`
	Args json.RawMessage `json:"args,omitempty"`
	// Reason is the agent's stated rationale, recorded as data.
	Reason string `json:"reason,omitempty"`
	// Result applies to complete.
	Result json.RawMessage `json:"result,omitempty"`
	// Message applies to fail.
	Message string `json:"message,omitempty"`
}

Decision is the agent's output for one step. It is recorded verbatim before it is validated, so the audit log shows what the agent asked for even when the request was invalid.

type DecisionKind

type DecisionKind string

DecisionKind classifies what the agent asked for.

const (
	DecideToolCall DecisionKind = "tool_call"
	DecideComplete DecisionKind = "complete"
	DecideFail     DecisionKind = "fail"
	// KindTruncated and KindNoToolCall are what a model agent records when
	// the model asked for nothing executable: a reply cut off by the output
	// cap, and a reply with no tool use at all. They are deliberately not
	// valid decisions, so the driver records an invalid_decision
	// observation, the step counts against the consecutive failure limit,
	// and the next render nudges the model from the recorded kind. See
	// render.Decide.
	KindTruncated  DecisionKind = "truncated"
	KindNoToolCall DecisionKind = "no_tool_call"
)

type Driver

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

Driver runs the step loop. It is safe to reuse for many runs but a single run is executed by one call at a time.

func NewDriver

func NewDriver(cfg Config) (*Driver, error)

NewDriver validates the configuration and compiles every tool schema.

func (*Driver) Approve

func (d *Driver) Approve(ctx context.Context, runID, approvalID, by, note string) error

Approve marks a pending approval approved after recomputing its hash from the stored fields. The run stays WAITING until Resume.

func (*Driver) Cancel added in v0.1.1

func (d *Driver) Cancel(ctx context.Context, runID, by, note string) error

Cancel ends a run that is not terminal as CANCELLED with reason operator_cancelled. It is for a run the operator no longer wants: one waiting for an approval, one already approved but not yet resumed, or one left interrupted. The caller must ensure no process is executing the run; the runtime does not lock. A step still in flight is failed with an observation naming the cancellation, and pending approvals are left as they are.

func (*Driver) Reject

func (d *Driver) Reject(ctx context.Context, runID, approvalID, by, note string) error

Reject marks a pending approval rejected and ends the run as CANCELLED.

func (*Driver) Resume

func (d *Driver) Resume(ctx context.Context, runID string) (Run, error)

Resume continues a run. A WAITING run needs an approved approval for its awaiting step: policy is re-evaluated, and the recorded request is executed only if policy allows it or asks for exactly the approval that was granted. A RUNNING run with in-flight work was interrupted: the step is failed with an interrupted observation, the consumer's reconciliation runs, and the loop continues.

func (*Driver) Start

func (d *Driver) Start(ctx context.Context, goal string, limits Limits) (Run, error)

Start creates a run and executes it until it reaches a terminal status or pauses for approval. The returned run reflects the persisted state.

func (*Driver) StartWithID

func (d *Driver) StartWithID(ctx context.Context, id, goal string, limits Limits) (Run, error)

StartWithID is Start with a caller-chosen run id, so a consumer can key its own tables by the same identifier. The id must be unique.

type ErrAbortRun

type ErrAbortRun struct {
	Detail string
}

ErrAbortRun may be returned by a tool to end the run as FAILED with reason tool_abort. It is for conditions that make continuing pointless, such as a frozen proposal found invalid at publication.

func (ErrAbortRun) Error

func (e ErrAbortRun) Error() string

type ErrLimit

type ErrLimit struct {
	Reason TerminalReason
	Detail string
}

ErrLimit is returned by a ModelCaller when a run limit would be exceeded.

func (ErrLimit) Error

func (e ErrLimit) Error() string

type ErrModelUnavailable

type ErrModelUnavailable struct {
	Attempts int
	Last     error
}

ErrModelUnavailable is returned when every attempt failed.

func (ErrModelUnavailable) Error

func (e ErrModelUnavailable) Error() string

func (ErrModelUnavailable) Unwrap

func (e ErrModelUnavailable) Unwrap() error

type Event

type Event struct {
	Seq     int64
	RunID   string
	StepID  string
	At      time.Time
	Type    string
	Payload json.RawMessage
}

Event is one row of the append-only audit log.

type Limits

type Limits struct {
	// MaxSteps is the maximum number of steps a run may start.
	MaxSteps int `json:"max_steps"`
	// MaxConsecutiveToolFailures ends the run when this many consecutive steps
	// produced a failure observation (tool error, policy denial, or an invalid
	// decision).
	MaxConsecutiveToolFailures int `json:"max_consecutive_tool_failures"`
	// LoopThreshold ends the run when the same tool call with the same
	// arguments has produced the same observation this many times in a row.
	// Repeating a call whose result changed is progress and is allowed.
	LoopThreshold int `json:"loop_threshold"`
	// MaxModelCalls counts every attempt, including retries and ambiguous
	// ones. Zero means unlimited.
	MaxModelCalls int `json:"max_model_calls,omitempty"`
	// MaxOutputTokensPerCall caps each request's output. Zero leaves the
	// agent's value.
	MaxOutputTokensPerCall int `json:"max_output_tokens_per_call,omitempty"`
	// MaxTotalTokens and MaxEstimatedCost are estimated limits: they are
	// checked before each call against totals plus a conservative estimate
	// for the call. Zero means unlimited.
	MaxTotalTokens   int    `json:"max_total_tokens,omitempty"`
	MaxEstimatedCost Micros `json:"max_estimated_cost_micros,omitempty"`
	// MaxActiveTime bounds the sum of step durations while RUNNING;
	// approval waits are excluded. MaxElapsedTime is an absolute deadline
	// from creation, including waits. ApprovalTTL expires a pending
	// approval and cancels the run when it is next touched. Zero disables
	// each.
	MaxActiveTime  time.Duration `json:"max_active_time,omitempty"`
	MaxElapsedTime time.Duration `json:"max_elapsed_time,omitempty"`
	ApprovalTTL    time.Duration `json:"approval_ttl,omitempty"`
}

Limits are the caps the driver enforces before each step. Only the limits implemented in this milestone are present.

func DefaultLimits

func DefaultLimits() Limits

DefaultLimits returns conservative defaults.

type Message

type Message struct {
	Role    string         `json:"role"` // user or assistant
	Content []ContentBlock `json:"content"`
}

Message is one turn of the conversation the agent renders for the model.

type Micros

type Micros int64

Micros is money in millionths of a currency unit.

type Model

type Model interface {
	// Name identifies the model for accounting and pricing.
	Name() string
	Generate(ctx context.Context, req ModelRequest) (ModelResponse, error)
}

Model generates responses. Implementations are provider adapters; the runtime wraps every Model in a ModelCaller and agents only see that.

type ModelCall

type ModelCall struct {
	ID           string
	RunID        string
	StepID       string
	Attempt      int
	Status       ModelCallStatus
	Model        string
	Usage        Usage
	Cost         Micros
	Latency      time.Duration
	Error        string
	DispatchedAt time.Time
	CompletedAt  time.Time
}

ModelCall is one persisted attempt.

type ModelCallStatus

type ModelCallStatus string

ModelCallStatus is the outcome of one attempt.

const (
	CallDispatched ModelCallStatus = "dispatched"
	CallOK         ModelCallStatus = "ok"
	CallError      ModelCallStatus = "error"
	// CallAmbiguous means the request was sent and no answer came back, so
	// the provider may or may not have charged for it. It counts as a call
	// and is charged conservatively.
	CallAmbiguous ModelCallStatus = "ambiguous"
)

type ModelCaller

type ModelCaller interface {
	Generate(ctx context.Context, req ModelRequest) (ModelResponse, error)
}

ModelCaller is the agent's only handle to the model. It enforces limits before every call, records every attempt, and applies prices.

type ModelConfig

type ModelConfig struct {
	Model       Model
	Prices      PriceTable
	CallTimeout time.Duration // per attempt; default 2 minutes
	MaxRetries  int           // transient retries per Generate; default 2
	Backoff     time.Duration // base backoff; default 1 second
}

ModelConfig configures the wrapper.

type ModelRequest

type ModelRequest struct {
	System          string     `json:"system,omitempty"`
	Messages        []Message  `json:"messages"`
	Tools           []ToolSpec `json:"tools,omitempty"`
	MaxOutputTokens int        `json:"max_output_tokens,omitempty"`
}

ModelRequest is one generation request.

type ModelResponse

type ModelResponse struct {
	Text       string          `json:"text,omitempty"`
	ToolUses   []ToolUse       `json:"tool_uses,omitempty"`
	StopReason string          `json:"stop_reason,omitempty"`
	Usage      Usage           `json:"usage"`
	Raw        json.RawMessage `json:"raw,omitempty"`
}

ModelResponse is one generation result.

type Observation

type Observation struct {
	Kind    ObservationKind `json:"kind"`
	Content json.RawMessage `json:"content,omitempty"`
	Summary string          `json:"summary,omitempty"`
	// ContentHash is the SHA-256 of the canonical JSON content. It is recorded
	// now so that later no-progress detection can key on it.
	ContentHash string `json:"content_hash,omitempty"`
}

Observation is what a step produced for the agent to consider next.

func (Observation) Failure

func (o Observation) Failure() bool

Failure reports whether the observation counts toward the consecutive failure limit.

type ObservationKind

type ObservationKind string

ObservationKind classifies what the agent sees after a step.

const (
	ObserveToolResult      ObservationKind = "tool_result"
	ObserveToolError       ObservationKind = "tool_error"
	ObservePolicyDenied    ObservationKind = "policy_denied"
	ObserveInvalidDecision ObservationKind = "invalid_decision"
	ObserveInterrupted     ObservationKind = "interrupted"
)

type Observer

type Observer func(Event)

Observer receives every event after it has been committed.

type Policy

type Policy interface {
	Evaluate(ctx context.Context, req ToolRequest, view RunView) (PolicyDecision, error)
}

Policy decides whether a validated tool request may execute. It is evaluated by the runtime and the agent has no influence over it.

type PolicyDecision

type PolicyDecision struct {
	Outcome PolicyOutcome `json:"outcome"`
	Reason  string        `json:"reason,omitempty"`
	// Kind, Capability, and Presentation apply to require_approval. In this
	// milestone the run pauses and records them; the approval record and
	// resumption arrive with durable approvals.
	Kind         string          `json:"kind,omitempty"`
	Capability   json.RawMessage `json:"capability,omitempty"`
	Presentation json.RawMessage `json:"presentation,omitempty"`
}

PolicyDecision is the policy's answer for one request.

func NeedApproval added in v0.2.0

func NeedApproval(kind, reason string, capability, presentation any) (PolicyDecision, error)

NeedApproval builds a require_approval decision, marshalling the capability an operator would be granting and the presentation they are shown. Both enter the approval's hash, so a changed presentation cannot inherit an old approval. It is named for what a policy does rather than for the RequireApproval outcome it carries.

type PolicyFunc

type PolicyFunc func(ctx context.Context, req ToolRequest, view RunView) (PolicyDecision, error)

PolicyFunc adapts a function to the Policy interface.

func (PolicyFunc) Evaluate

func (f PolicyFunc) Evaluate(ctx context.Context, req ToolRequest, view RunView) (PolicyDecision, error)

Evaluate implements Policy.

type PolicyOutcome

type PolicyOutcome string

PolicyOutcome is what the policy decided about a tool request.

const (
	Allow           PolicyOutcome = "allow"
	RequireApproval PolicyOutcome = "require_approval"
	Deny            PolicyOutcome = "deny"
	Abort           PolicyOutcome = "abort"
)

type Price

type Price struct {
	InputPerMTok       Micros `json:"input_per_mtok"`
	OutputPerMTok      Micros `json:"output_per_mtok"`
	CachedInputPerMTok Micros `json:"cached_input_per_mtok"`
}

Price is a model's price per million tokens.

type PriceTable

type PriceTable map[string]Price

PriceTable maps model names to prices. A model absent from the table costs nothing, which is the right answer for local models and the wrong one for paid ones, so consumers pass a table for every paid model.

type ReconcileOutcome

type ReconcileOutcome string

ReconcileOutcome is what a consumer's reconciliation concluded about a run found mid-step.

const (
	// ReconcileContinue means the loop may proceed with the next decision.
	ReconcileContinue ReconcileOutcome = "continue"
	// ReconcileCompleted means the interrupted operation had already
	// succeeded; the run completes with Result and no further decision.
	ReconcileCompleted ReconcileOutcome = "completed"
	// ReconcileWaiting means the run must wait for an approval again.
	ReconcileWaiting ReconcileOutcome = "waiting"
	// ReconcileConflict means external state contradicts the record; the
	// run fails with reconcile_conflict and Detail.
	ReconcileConflict ReconcileOutcome = "conflict"
)

type Reconciliation

type Reconciliation struct {
	Outcome ReconcileOutcome
	Result  json.RawMessage
	Detail  string
}

Reconciliation is the structured result of a consumer's reconciliation.

type Run

type Run struct {
	ID           string
	Goal         string
	Status       RunStatus
	Reason       TerminalReason
	ReasonDetail string
	Limits       Limits
	// StepCount is the number of steps started, including the current one.
	StepCount  int
	CreatedAt  time.Time
	StartedAt  time.Time
	FinishedAt time.Time
	// Result is the payload of a Complete decision, if any.
	Result json.RawMessage
	// Model accounting totals across every attempt.
	ModelCalls    int
	Usage         Usage
	EstimatedCost Micros
	// ActiveTime is the sum of finished step durations.
	ActiveTime time.Duration
}

Run is the persisted state of one execution.

type RunStatus

type RunStatus string

RunStatus is the lifecycle state of a run.

const (
	StatusCreated            RunStatus = "CREATED"
	StatusRunning            RunStatus = "RUNNING"
	StatusWaitingForApproval RunStatus = "WAITING_FOR_APPROVAL"
	// StatusInterrupted marks a run found RUNNING with in-flight work by a
	// process that did not start it; Resume reconciles and continues it.
	StatusInterrupted RunStatus = "INTERRUPTED"
	StatusCompleted   RunStatus = "COMPLETED"
	StatusFailed      RunStatus = "FAILED"
	StatusCancelled   RunStatus = "CANCELLED"
)

func (RunStatus) Terminal

func (s RunStatus) Terminal() bool

Terminal reports whether no further steps can occur for a run in this status.

type RunView

type RunView struct {
	Run       Run
	Steps     []Step
	Approvals []Approval
}

RunView is the read-only view a policy may consult.

type SideEffect

type SideEffect string

SideEffect classifies what a tool may do. Policy is evaluated on it.

const (
	ReadOnly       SideEffect = "read_only"
	LocalMutation  SideEffect = "local_mutation"
	RemoteMutation SideEffect = "remote_mutation"
	Destructive    SideEffect = "destructive"
)

type SideEffectPolicy

type SideEffectPolicy map[SideEffect]PolicyOutcome

SideEffectPolicy maps each side-effect class to an outcome. It is the runtime's only built-in policy; consumers wrap or replace it.

func DefaultPolicy

func DefaultPolicy() SideEffectPolicy

DefaultPolicy allows reads and local mutations, requires approval for remote mutations, and denies destructive tools.

func (SideEffectPolicy) Evaluate

Evaluate implements Policy. A side-effect class with no mapping is denied.

type Step

type Step struct {
	ID          string
	RunID       string
	Index       int
	Status      StepStatus
	Decision    *Decision
	Policy      *PolicyDecision
	Observation *Observation
	StartedAt   time.Time
	FinishedAt  time.Time
}

Step is one iteration of the loop: a decision and what it caused.

type StepInput

type StepInput struct {
	Run       Run
	Steps     []Step
	Approvals []Approval
	Tools     []ToolSpec
	// Model is the agent's only handle to the model. It is nil when the
	// driver was configured without one, as for scripted agents.
	Model ModelCaller
}

StepInput is everything the agent receives when asked to decide. Steps are all prior steps in order; the agent is stateless between calls.

type StepStatus

type StepStatus string

StepStatus is the state of one step.

const (
	StepDeciding         StepStatus = "deciding"
	StepAwaitingApproval StepStatus = "awaiting_approval"
	StepExecuting        StepStatus = "executing"
	StepDone             StepStatus = "done"
	StepFailed           StepStatus = "failed"
	StepInterrupted      StepStatus = "interrupted"
)

type Store

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

Store persists runs, steps, and events in SQLite. It is the single implementation; there is no store interface until a second one is needed.

A Store opened on ":memory:" lives for the life of the connection and is meant for unit tests. A file-backed store uses WAL mode and survives process restarts.

func OpenStore

func OpenStore(path string) (*Store, error)

OpenStore opens or creates the database at path and applies migrations.

func (*Store) Close

func (s *Store) Close() error

Close releases the database.

func (*Store) CreateRun

func (s *Store) CreateRun(ctx context.Context, r Run) error

CreateRun inserts a new run. The caller is responsible for the run.created event; the driver writes it in the same transaction.

func (*Store) DB

func (s *Store) DB() *sql.DB

DB exposes the underlying database for consumers that keep their own tables in the same file and for tests. The runtime's tables are its own.

func (*Store) GetApproval

func (s *Store) GetApproval(ctx context.Context, runID, id string) (Approval, error)

GetApproval loads one approval.

func (*Store) GetRun

func (s *Store) GetRun(ctx context.Context, id string) (Run, error)

GetRun loads a run by id.

func (*Store) ListApprovals

func (s *Store) ListApprovals(ctx context.Context, runID string) ([]Approval, error)

ListApprovals returns a run's approvals in creation order.

func (*Store) ListEvents

func (s *Store) ListEvents(ctx context.Context, runID string) ([]Event, error)

ListEvents returns the events of a run in sequence order.

func (*Store) ListModelCalls

func (s *Store) ListModelCalls(ctx context.Context, runID string) ([]ModelCall, error)

ListModelCalls returns a run's model call attempts in dispatch order.

func (*Store) ListRuns

func (s *Store) ListRuns(ctx context.Context) ([]Run, error)

ListRuns returns every run, newest first.

func (*Store) ListSteps

func (s *Store) ListSteps(ctx context.Context, runID string) ([]Step, error)

ListSteps returns the steps of a run in index order.

type TerminalReason

type TerminalReason string

TerminalReason explains why a run reached a terminal status. Reasons are generic to the runtime; consumers record their own outcome codes separately.

const (
	ReasonGoalCompleted        TerminalReason = "goal_completed"
	ReasonGoalFailed           TerminalReason = "goal_failed"
	ReasonLimitSteps           TerminalReason = "limit_steps"
	ReasonLimitModelCalls      TerminalReason = "limit_model_calls"
	ReasonLimitTokens          TerminalReason = "limit_tokens"
	ReasonLimitCost            TerminalReason = "limit_cost"
	ReasonLimitActiveTime      TerminalReason = "limit_active_time"
	ReasonLimitElapsedTime     TerminalReason = "limit_elapsed_time"
	ReasonApprovalExpired      TerminalReason = "approval_expired"
	ReasonModelUnavailable     TerminalReason = "model_unavailable"
	ReasonRepeatedToolFailures TerminalReason = "repeated_tool_failures"
	ReasonLoopDetected         TerminalReason = "loop_detected"
	ReasonPolicyAbort          TerminalReason = "policy_abort"
	ReasonToolAbort            TerminalReason = "tool_abort"
	ReasonApprovalRejected     TerminalReason = "approval_rejected"
	ReasonOperatorCancelled    TerminalReason = "operator_cancelled"
	ReasonReconcileConflict    TerminalReason = "reconcile_conflict"
	ReasonAgentError           TerminalReason = "agent_error"
	ReasonInternalError        TerminalReason = "internal_error"
)

type Tool

type Tool interface {
	Spec() ToolSpec
	Call(ctx context.Context, call ToolCall) (ToolResult, error)
}

Tool is a capability the agent may request. The runtime validates arguments against Spec().InputSchema and evaluates policy before Call is invoked.

type ToolCall

type ToolCall struct {
	RunID  string
	StepID string
	Args   json.RawMessage
}

ToolCall carries the driver-supplied execution identity into a tool.

type ToolRequest

type ToolRequest struct {
	RunID  string          `json:"run_id"`
	StepID string          `json:"step_id"`
	Spec   ToolSpec        `json:"spec"`
	Args   json.RawMessage `json:"args"`
}

ToolRequest is a schema-validated request handed to the policy.

type ToolResult

type ToolResult struct {
	Content json.RawMessage
	Summary string
}

ToolResult is what a tool returns on success.

type ToolSpec

type ToolSpec struct {
	Name        string          `json:"name"`
	Description string          `json:"description"`
	InputSchema json.RawMessage `json:"input_schema"`
	SideEffect  SideEffect      `json:"side_effect"`
	// Timeout bounds a single call. It is required.
	Timeout time.Duration `json:"timeout"`
	// Terminal tools complete the run when they succeed; their result
	// becomes the run result and no further decision is requested.
	Terminal bool `json:"terminal,omitempty"`
}

ToolSpec describes a tool to the runtime and to the agent.

type ToolUse

type ToolUse struct {
	ID   string          `json:"id"`
	Name string          `json:"name"`
	Args json.RawMessage `json:"args"`
}

ToolUse is a tool call the model asked for.

type TransientError

type TransientError struct {
	Err error
}

TransientError marks a provider failure worth retrying, such as a rate limit or a 5xx response.

func (TransientError) Error

func (e TransientError) Error() string

func (TransientError) Unwrap

func (e TransientError) Unwrap() error

type Usage

type Usage struct {
	InputTokens       int `json:"input_tokens"`
	OutputTokens      int `json:"output_tokens"`
	CachedInputTokens int `json:"cached_input_tokens,omitempty"`
}

Usage is what the provider reported.

Directories

Path Synopsis
bench module
cmd
agentrt command
Command agentrt is the operator surface over a runs database: list runs, show one, print its event log, and approve, reject, or cancel.
Command agentrt is the operator surface over a runs database: list runs, show one, print its event log, and approve, reject, or cancel.
examples
scripted command
Command scripted runs one deterministic agent run against in-memory tools and prints the persisted event trace.
Command scripted runs one deterministic agent run against in-memory tools and prints the persisted event trace.
live module
mcp module
mcp module
Package providers holds the dependency-free helpers every provider adapter shares: transport and HTTP error classification, a bounded body read, tool-use id synthesis, and price table helpers.
Package providers holds the dependency-free helpers every provider adapter shares: transport and HTTP error classification, a bounded body read, tool-use id synthesis, and price table helpers.
anthropic module
ollama module
openai module
Package render is the generic half of a model-driven agent: it turns a run's recorded steps into a message list and a model response into a decision.
Package render is the generic half of a model-driven agent: it turns a run's recorded steps into a message list and a model response into a decision.
Package replay provides Model implementations that need no provider: a scripted model for unit tests, and a recorder and replayer pair so that a real model's responses can be captured once and served deterministically afterwards.
Package replay provides Model implementations that need no provider: a scripted model for unit tests, and a recorder and replayer pair so that a real model's responses can be captured once and served deterministically afterwards.
Package scripted provides an Agent that replays a fixed list of decisions.
Package scripted provides an Agent that replays a fixed list of decisions.
Package trace provides the two observers an operator wants while a run executes: aligned lines for a terminal and JSON Lines for a collector.
Package trace provides the two observers an operator wants while a run executes: aligned lines for a terminal and JSON Lines for a collector.
Package view builds read models over a store for operator surfaces: what runs exist, what a run did, and which approval is waiting.
Package view builds read models over a store for operator surfaces: what runs exist, what a run did, and which approval is waiting.

Jump to

Keyboard shortcuts

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