runtime

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

Documentation

Overview

Package runtime contains the shared invocation boundary for model-directed work.

Package runtime: terminal result delivery.

Every invocation that does not complete normally leaves through this file: a failure, a cancellation, a timeout, or a policy block. They share one delivery path so a waiter is released exactly once however the call ended, and differ only in the status they stamp.

Index

Constants

View Source
const MaxHookContextBytes = 8 << 10

MaxHookContextBytes bounds hook-supplied advisory text for one invocation.

It is a fixed constant rather than configuration: the bound exists so hook output can never be the reason a model-visible payload is unbounded, and a bound an operator can raise is not that. Over-budget context is TRUNCATED with a notice, not refused - unlike tool output, which the dispatcher destroys because an undeclared result cannot be bounded, hook context is advisory, and losing a tool's result because its formatter was chatty is the worse failure.

Variables

This section is empty.

Functions

func ContextWithCaller

func ContextWithCaller(ctx context.Context, caller Caller) context.Context

ContextWithCaller associates an invocation caller with ctx.

func ContextWithMailboxAccess

func ContextWithMailboxAccess(ctx context.Context, access MailboxAccess) context.Context

ContextWithMailboxAccess associates a MailboxAccess bundle with ctx.

func ContextWithMailboxDrain

func ContextWithMailboxDrain(ctx context.Context, drain MailboxDrainFunc) context.Context

ContextWithMailboxDrain associates a drain function with ctx. It is a thin wrapper over ContextWithMailboxAccess.

func ContextWithTaskIdentity

func ContextWithTaskIdentity(ctx context.Context, id TaskIdentity) context.Context

ContextWithTaskIdentity associates coordination identity with ctx.

func DeriveOutputCeiling

func DeriveOutputCeiling(r *tools.Registry, maxInputBytes int) int

DeriveOutputCeiling returns the runaway-tool output backstop for a dispatcher serving the given registry: the largest tool-declared result budget (tools.ResultBudgetTool) plus an input allowance plus slack, floored at 256KiB. maxInputBytes is the dispatcher's input cap; <= 0 means the Policy default (64KiB). It is added because tool results may echo request input verbatim (run_command's argv header), so an honest result can exceed its content budget by up to the input size.

This is the GLOBAL value: the dispatcher's Policy.MaxOutputBytes, which is a hard cap that no single tool's ceiling may exceed. The bound actually enforced on a given call is the per-tool ceiling from toolOutputCeiling, min'd against this - see Dispatcher.OutputCeiling.

Post-invoke policy (applyOutputCeiling):

  • size ≤ ceiling: pass through
  • ceiling < size ≤ ceiling×4: tail-truncate at a UTF-8 boundary with an honest kept/total notice (never destroy honest oversize)
  • size > ceiling×4: destroy as runaway

Deriving the ceiling from registry budgets keeps config-compliant tool results under the pass-through band so they are not truncated either.

func NewSessionID

func NewSessionID() string

NewSessionID returns an unguessable principal identifier for one session.

Unguessability is the whole point: the session ID is the principal that orchestration run access is scoped to (INV-AG-9). crypto/rand.Read never returns an error and always fills its buffer, crashing the program itself if the operating system's source fails, so there is no error path here - and a fallback to a weaker source would silently turn the principal into something enumerable, which is worse than not starting.

Types

type Caller

type Caller struct {
	SessionID string
	TurnID    string
	ParentID  string
	Depth     int
	// Role is populated by role-aware dispatch. It is empty until roles are in
	// use, but remains part of the principal so later role separation does not
	// require another boundary signature change.
	Role string
}

Caller identifies the agent invoking a handler. It travels in the context because tools receive no Request and must attribute and authorize their work.

func CallerFrom

func CallerFrom(ctx context.Context) (Caller, bool)

CallerFrom returns the caller associated with ctx.

type Dispatcher

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

func New

func New(policy Policy) *Dispatcher

func NewToolDispatcher

func NewToolDispatcher(r *tools.Registry, p Policy) (*Dispatcher, error)

func (*Dispatcher) Allow

func (d *Dispatcher) Allow(k Kind, name string)

func (*Dispatcher) Close

func (d *Dispatcher) Close()

Close releases retained invocation state at the end of a session. It wakes duplicate callers with a closed result. Active owners continue under their caller-owned contexts, but cannot restore released dispatcher state.

func (*Dispatcher) Has

func (d *Dispatcher) Has(k Kind, name string) bool

Has reports whether a handler is registered for a runtime kind and name. It is used to validate that model-visible capabilities are executable.

func (*Dispatcher) Invoke

func (d *Dispatcher) Invoke(ctx context.Context, req Request) (result Result)

func (*Dispatcher) OnClose

func (d *Dispatcher) OnClose(hook func())

OnClose registers a callback invoked once when Close releases dispatcher state. It is used by owners of dispatcher-keyed resources to unregister those resources without retaining sessions for the process lifetime.

func (*Dispatcher) OutputCeiling

func (d *Dispatcher) OutputCeiling(k Kind, name string) int

OutputCeiling returns the output bound the dispatcher actually enforces for one registered capability: the per-tool ceiling recorded at RegisterTool time, capped by Policy.MaxOutputBytes. Kinds with no declarable budget (Skill, Subagent) and handlers installed through the bare Register path get the policy value.

func (*Dispatcher) Policy

func (d *Dispatcher) Policy() Policy

Policy returns a shallow copy of the dispatcher's effective policy for a derived dispatcher. Allow is deliberately omitted: a derived dispatcher rebuilds its allow map from its own registered handlers.

func (*Dispatcher) Register

func (d *Dispatcher) Register(k Kind, name string, h Handler) error

Register installs a handler with no derivable result budget of its own: it keeps Policy.MaxOutputBytes as its output ceiling. Registry-backed tools go through RegisterTool, which additionally records a per-tool ceiling.

func (*Dispatcher) RegisterTool

func (d *Dispatcher) RegisterTool(r *tools.Registry, t tools.Tool) error

RegisterTool adds a registry-backed tool to an existing dispatcher. This is the supported path for tools that need the dispatcher during construction (for example delegation tools); it keeps the model-visible registry and the executable dispatcher in sync.

func (*Dispatcher) Validate

func (d *Dispatcher) Validate(req Request) error

Validate checks a request without reserving an invocation or calling a handler. It is used by resume preflight, where a routing failure must leave the durable run untouched.

type Event

type Event struct {
	Type     string
	Metadata Metadata
}

Event is one dispatched invocation's lifecycle observation for a Policy.Sink.

type Handler

type Handler interface {
	Invoke(context.Context, Request) (json.RawMessage, error)
}

Metadata, Event and the invocation record types live in dispatcher_types.go.

type HookResult

type HookResult struct {
	Context string
	Runs    []HookRun
}

HookResult is a reactive hook's answer: what the model is told, and what the operator is shown. They are separate fields because they are separate questions - a hook that ran silently has a run to report and nothing to say.

type HookRun

type HookRun struct {
	// Event is PreToolUse, PostToolUse or Stop.
	Event string
	// Program is the hook script's name, not its path: this reaches a screen,
	// and the absolute path runs through the operator's home directory.
	Program string
	// Tool is the tool this hook fired for.
	Tool string
	// Input is the tool input this handler saw, bounded and redacted before
	// it is set (internal/composition's hookRunsFor): it is retained in the
	// dispatcher's dedup completed map for the life of the turn, not just at
	// display time, so it must already be safe to hold and to show.
	//
	// A plain string, not json.RawMessage: HookRun is compared with != in
	// the ID-keyed dedup regression tests, and a slice-backed field would
	// make the struct non-comparable.
	Input string
	// Denied is true for the PreToolUse run that blocked the call.
	Denied bool
	// Output is what this hook said - advisory text, or the block reason.
	// Empty means it ran silently, which is normal and still worth showing.
	Output string
	// Warning is the operator diagnostic this run produced, if it misbehaved.
	Warning string
}

HookRun is one lifecycle hook execution, described for display.

runtime deliberately does not import internal/hooks, so this is a plain value the wiring layer fills in rather than a re-export.

type HookVerdict

type HookVerdict struct {
	// Denied blocks the invocation. The handler is never reached.
	Denied bool
	// Reason is why. It reaches the model - that is the point of a block.
	Reason string
	// Context is advisory text the gate produced even when it allowed. It
	// reaches the model merged with the reactive event's context.
	Context string
	// Runs is what executed, for the operator's view. A denial carries its own
	// run: the tool did not happen, and the reason came from a script.
	Runs []HookRun
}

HookVerdict is a PreToolUse gate's answer.

type Kind

type Kind string
const (
	Tool     Kind = "tool"
	Skill    Kind = "skill"
	Subagent Kind = "subagent"
)

type MailboxAccess

type MailboxAccess struct {
	Drain     MailboxDrainFunc
	Interrupt func() <-chan struct{}
	Pending   func() bool
	// PendingInterrupt reports whether an Interrupt-flagged steer is queued
	// (the strict gate for the loop watcher's signal branch; Pending is the
	// len-based gate for the watchdog branch).
	PendingInterrupt func() bool
}

MailboxAccess bundles the mailbox-related hooks a step-boundary consumer may need: draining pending parent→child messages, interrupting the current step, and asking whether messages are pending. All fields are optional; a bundle with every field nil is treated as absent by the readers.

func MailboxAccessFrom

func MailboxAccessFrom(ctx context.Context) (MailboxAccess, bool)

MailboxAccessFrom returns the MailboxAccess bundle on ctx, if any. A bare context, or one holding a nil bundle (every field nil), reports not-ok.

type MailboxDrainFunc

type MailboxDrainFunc func() []ParentMessage

MailboxDrainFunc drains pending parent→child messages at a step boundary.

func MailboxDrainFrom

func MailboxDrainFrom(ctx context.Context) (MailboxDrainFunc, bool)

MailboxDrainFrom returns the drain function on ctx, if any.

type Metadata

type Metadata struct {
	ID, ParentID, TurnID, Name, Kind, Status, Scope, InputHash, OutputHash string
	Duration                                                               time.Duration
	InputPreview, OutputPreview                                            string
}

Metadata is the audit record for one invocation.

InputPreview and OutputPreview are bounded previews of the payloads: at most 256 bytes each. They are redacted ONLY to the extent the workspace's configured redaction policy removes something; an unconfigured workspace gets raw content, so treat them as payload, not as sanitised text. They are empty unless a Policy.Sink is attached - with no sink there is no consumer, so the previews are not computed at all.

type ParentMessage

type ParentMessage struct {
	Kind      string // "steer", "answer", or "ask"
	Body      string
	MessageID string // correlation id (required for kind=ask answers)
}

ParentMessage is a parent→child envelope fragment for step-boundary inject.

type Policy

type Policy struct {
	MaxDepth, MaxRetries, MaxInputBytes, MaxOutputBytes int
	MaxBudget                                           int
	Allow                                               map[Kind]map[string]bool
	Sink                                                func(Event)
	// PreInvokeHook and PostInvokeHook are the optional lifecycle gates. They
	// live on Policy next to Sink, and that placement is load-bearing rather
	// than incidental: Policy is copied to derived dispatchers by
	// Dispatcher.Policy(), which clears only Allow, so the hooks propagate to
	// scoped subagent dispatchers. A PreToolUse gate a subagent escapes is not
	// a gate - subagents run the same tools against the same workspace.
	//
	// internal/runtime deliberately does not import internal/hooks: these are
	// plain func fields, so nil is no hooks, one nil compare, and today's
	// behaviour exactly.
	PreInvokeHook  func(context.Context, Request) HookVerdict
	PostInvokeHook func(context.Context, Request, Result) HookResult
}

type Request

type Request struct {
	ID, ParentID, TurnID, SessionID, Role, Name, Scope string
	// AgentName, AgentDigest, Skill, ProviderName and Model are immutable work metadata used by
	// agent-routing handlers. They are not policy scope or permission grants.
	AgentName, AgentDigest, Skill string
	ProviderName, Model           string
	Kind                          Kind
	Input                         json.RawMessage
	Timeout                       time.Duration
	Budget                        int
	WorkLimits                    WorkLimits
	DisableProviderReplay         bool
	Permission                    string
	Depth, Retry                  int
	// Step is the loop-stamped model step this invocation belongs to. 0 means
	// legacy turn-scoped dedup; Step > 0 scopes the per-turn dedup to that step,
	// so an identical call re-issued in a LATER step of the same turn re-runs
	// while a same-step re-issue still dedups.
	Step int
	// SkipDedup exempts this Tool invocation from the per-turn dedup and the
	// ID-keyed dedup state (completed map, active/waiters): it never reserves a
	// flight key, never joins a waiter, is never answered from a recorded
	// result, and never writes dedup state. Zero value keeps today's behavior.
	SkipDedup bool
	// OutputSchema: structured subagent output schema (tools/02); nil = free-text.
	OutputSchema map[string]any
}

type RequestValidator

type RequestValidator interface {
	ValidateRequest(Request) error
}

RequestValidator permits a handler to reject stale or unauthorized work before a coordinator mutates durable state for a retry or resume.

type Result

type Result struct {
	ID, Name string
	Kind     Kind
	Output   json.RawMessage
	Err      error
	Attempts int
	Metadata Metadata
	// HookContext is advisory text a PostToolUse hook produced for this
	// invocation. It travels in its own separately bounded field and is NEVER
	// spliced into Output: appending it there would write past the per-tool
	// ceiling check inside execute (INV-AG-25/26/27) and leave Metadata's
	// OutputHash and OutputPreview describing bytes the model never received.
	HookContext string
	// HookRuns records the lifecycle hooks that executed for this invocation,
	// for the OPERATOR's view. It is not the model's copy and not the audit
	// record: a hook that ran and said nothing appears here and nowhere else,
	// which is the whole reason it exists.
	HookRuns []HookRun
}

func (Result) IsDuplicate

func (r Result) IsDuplicate() bool

IsDuplicate reports whether this invocation was served from the dedup cache (same-step or ID-keyed re-delivery) rather than executing.

type TaskIdentity

type TaskIdentity struct {
	RunID  string
	TaskID string
	Agent  string
}

TaskIdentity is the coordination identity of a running subagent task (run + task + agent). It is distinct from Caller (session principal) and from the opaque dispatcher Request.ID. Tools that post messages need this to stamp ledger provenance without spoofing.

func TaskIdentityFrom

func TaskIdentityFrom(ctx context.Context) (TaskIdentity, bool)

TaskIdentityFrom returns the task identity on ctx, if any.

type WorkLimits

type WorkLimits struct {
	MaxTurns         int       `json:"max_turns,omitempty"`
	MaxPromptTokens  int       `json:"max_prompt_tokens,omitempty"`
	MaxOutputTokens  int       `json:"max_output_tokens,omitempty"`
	MaxOutputPerCall int       `json:"max_output_per_call,omitempty"`
	MaxToolCalls     int       `json:"max_tool_calls,omitempty"`
	DeadlineAt       time.Time `json:"deadline_at,omitempty"`
}

WorkLimits bounds cumulative work for one task. A zero numeric field is unlimited. A zero deadline is unset.

func LowestPositiveWorkLimits

func LowestPositiveWorkLimits(limits ...WorkLimits) WorkLimits

LowestPositiveWorkLimits returns the tightest positive limit for each field.

Jump to

Keyboard shortcuts

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