Documentation
¶
Overview ¶
Package mission turns a Goal into real work: it drives a goal as a tool-using conversation with a language model, through the provider-agnostic llm port.
It supplies the two ports the goal reconciler runs on. Executor is a goal.StepExecutor: each step advances the conversation by exactly one model turn (call the model, run any tools it asked for, append the results), so progress is checkpointed at turn granularity and a crashed step resumes from the persisted message history rather than restarting the conversation. Convergence is the matching goal.StopEvaluator: the mission is met once the model ends its turn with a final answer. The reconciler's step budget bounds how many turns a goal may spend, so a conversation that never settles stalls instead of looping forever.
Nothing here knows which model is behind the llm.Model port: the same loop runs against a hosted API client, an agent-CLI subprocess, or a local model. Tests run it against a scripted fake (llm/llmtest).
Index ¶
- Constants
- func ContinueConversation(status goal.Status, text string, images ...llm.Image) (goal.Status, error)
- type ApprovalDecision
- type ApprovalPrompter
- type ApprovalRequest
- type ChildResult
- type Convergence
- type Event
- type EventKind
- type Executor
- type Fanout
- type GenerationEnvelope
- type GenerationRecorder
- type Option
- func PlanOptions(plan harness.Plan) []Option
- func WithAdmitter(a dispatch.Admitter) Option
- func WithApproval(g *approval.Gate) Option
- func WithApprovalClock(c clock.Clock) Option
- func WithApprovalGrace(d time.Duration) Option
- func WithApprovalPrompter(prompter ApprovalPrompter, signer approval.Signer, host string) Option
- func WithBrakes(h *brakes.Hook) Option
- func WithBudget(h *budget.Hook) Option
- func WithCompactionBudget(tokens int) Option
- func WithEventSink(s dispatch.EventSink) Option
- func WithFanout(f Fanout) Option
- func WithGenerationRecorder(r GenerationRecorder) Option
- func WithGrant(g capability.Grant) Option
- func WithMaxTokens(n int) Option
- func WithObserver(r Reporter) Option
- func WithSampling(s *llm.Sampling) Option
- func WithSandbox(sb sandbox.Sandbox) Option
- func WithSimplifiedSchemas() Option
- func WithSystem(system string) Option
- func WithTools(tools ...Tool) Option
- func WithVerifyPasses(n int) Option
- type Reporter
- type ResultSummarizer
- type SubGoal
- type Tool
- type TrustedWork
Constants ¶
const ActionModelGenerate = "model.generate"
ActionModelGenerate is the dispatch action name a model call runs under, so the model call is admitted, traced, metered, and recorded on the spine like any tool call. It is a normal action, not implicitly allowed: a least-privilege grant must list it for the agent to call the model, which keeps the grant the complete record of what a run may do. A run that should not call the model omits it.
const ActionSpawn = "spawn"
ActionSpawn is the dispatch action a fan-out runs under, so spawning a sub-goal is admitted against the run's grant, metered, and recorded on the spine like any other action. A run whose grant does not list it cannot fan out, which keeps delegation a least-privilege decision.
Variables ¶
This section is empty.
Functions ¶
func ContinueConversation ¶
func ContinueConversation(status goal.Status, text string, images ...llm.Image) (goal.Status, error)
ContinueConversation reopens a converged goal for another user turn: it appends text as a new user message onto the recorded conversation and clears the done flag, so re-driving the goal advances the same exchange instead of stopping on the prior turn's convergence. The returned status must be persisted onto the goal and the goal re-enqueued (runtime.Resume) for the turn to run.
This is the mechanism behind a multi-turn session: each user line after the first continues one durable goal, so the model is handed the whole history and the run stays addressable, replayable, and auditable by a single id. The phase is reset off its settled value so the reconciler re-evaluates rather than no-op-skipping a converged goal, and the step counter is cleared so the new turn runs with a fresh step budget rather than inheriting the prior turn's spend.
Types ¶
type ApprovalDecision ¶
ApprovalDecision is a human's answer to an ApprovalRequest. Allow admits the action; Scope, when set, narrows the grant to an exact glob or target (bound into the minted approval, so the authorization does not over-grant); Feedback is a short reason fed back to the run on a denial so the model can adapt.
type ApprovalPrompter ¶
type ApprovalPrompter interface {
Prompt(ctx context.Context, req ApprovalRequest) (ApprovalDecision, error)
}
ApprovalPrompter resolves an approval request interactively (a TUI prompt) or by policy (a headless auto-decider). It is called on the run goroutine and blocks until the decision is made, so an implementation must observe ctx: a context deadline is the grace-period expiry, and the requester treats a context error or a returned error as a fail-closed decline.
type ApprovalRequest ¶
ApprovalRequest describes a privileged action the waist paused for a human decision: the action's name, the scope it would run in, and the host the run is on. Grace is how long the requester will wait before the request auto-declines, so an interactive prompter can show a countdown. It carries no action arguments, matching what the dispatch waist authorizes by (action identity, not payload).
type ChildResult ¶
ChildResult is the outcome of a spawned child, folded back into the parent's conversation as the result of its spawn call.
type Convergence ¶
type Convergence struct{}
Convergence is the goal.StopEvaluator paired with Executor: a mission has converged once its conversation reached a final turn. It reads the same checkpoint the executor writes, so the model's own decision to stop is the convergence signal.
func (Convergence) Met ¶
Met reports whether the conversation has finished, returning the model's final text as the reason. It decodes only the outcome fields, not the whole message history: convergence is checked on every reconcile tick, so folding the entire (growing) transcript back into memory each time just to read two fields is waste.
type Event ¶
type Event struct {
Kind EventKind
// Goal is the id of the goal this event belongs to: the run's root goal for a
// single conversation, or a delegated child's own id under fan-out. It attributes
// each event to its originating goal so events from concurrent children on one
// shared stream stay distinguishable. Empty is treated as the root.
Goal string
// Child is the id of a spawned child goal (EventChildSpawned only), the other end
// of the parent-to-child edge whose parent is Goal.
Child string
// Turn is the 1-based index of the model turn this event belongs to.
Turn int
// Text is the assistant's text (EventAssistantText).
Text string
// Tool is the tool's name (EventToolCall, EventToolResult).
Tool string
// ToolUseID correlates a call with its result across the two events.
ToolUseID string
// Input is the tool's JSON arguments (EventToolCall).
Input json.RawMessage
// Result is the tool's output text (EventToolResult).
Result string
// IsError reports that the tool call failed (EventToolResult).
IsError bool
// StopReason is why the turn ended (EventTurnCompleted): the llm.StopReason.
StopReason string
// Usage is the token cost the model reported for the turn (EventTurnCompleted),
// including the cache read/write split, so a caller can show live spend and
// cache effectiveness without re-deriving it.
Usage llm.Usage
}
Event is one observable moment in a mission's conversation: a turn boundary, the model's text, a tool call, or a tool result. It is reported live as the loop runs so a caller can render progress without polling. Fields are populated by Kind; the rest are zero.
type EventKind ¶
type EventKind string
EventKind classifies a conversational event a mission reports as it runs.
const ( // EventTurnStarted marks the beginning of one model turn, before the model is // called. EventTurnStarted EventKind = "turn.started" // EventAssistantText carries the natural-language text the model produced this // turn (empty turns that only call tools produce none). EventAssistantText EventKind = "assistant.text" // EventToolCall is the model asking to invoke a tool, with its arguments. It is // reported before the tool runs. EventToolCall EventKind = "tool.call" // EventToolResult is the outcome of a tool call, reported after it runs. EventToolResult EventKind = "tool.result" // EventTurnCompleted marks the end of a turn, carrying why the model stopped. EventTurnCompleted EventKind = "turn.completed" // EventChildSpawned marks a fan-out delegation: the goal named by Goal spawned the // child named by Child to run a sub-objective (carried in Text). It records the // parent-to-child edge on the stream, so a live tree of a fan-out can be built from // the record rather than a separate store. EventChildSpawned EventKind = "child.spawned" // EventChildCompleted marks a spawned child folding back into its parent: the child // named by Child finished and its answer (in Result, with IsError set when it // failed) was folded into the parent named by Goal. It is the closing half of the // edge EventChildSpawned opened, so a tree can flip the child from running to done. EventChildCompleted EventKind = "child.completed" )
type Executor ¶
type Executor struct {
// contains filtered or unexported fields
}
Executor drives a goal as a conversation with a model. It implements goal.StepExecutor.
func NewExecutor ¶
NewExecutor builds a mission executor over the given model and options. Tool calls run through a dispatch waist so governance, event recording, and tracing are applied once at the chokepoint rather than scattered across the loop.
func (*Executor) Execute ¶
Execute advances the goal's conversation by one model turn and returns the updated conversation as the checkpoint. A turn that calls tools runs them and appends their results so the next step continues; a turn that ends naturally marks the conversation done, which Convergence then observes.
type Fanout ¶
type Fanout interface {
// Spawn creates a child goal owned by parent and returns its id.
Spawn(ctx context.Context, parent resource.Resource, sub SubGoal) (id string, err error)
// Poll reports the children's outcomes and whether all of them have finished, so the parent
// knows when to fold. While any child is still running, allDone is false.
Poll(ctx context.Context, ids []string) (results []ChildResult, allDone bool, err error)
}
Fanout spawns child goals and reports their outcomes, so a parent goal can run sub-goals concurrently and fold their results into its own conversation. The implementation attaches each child to the parent (ownership and cascade teardown), narrows the grant to the requested actions, and shares the parent's budget, so a fan-out can neither escalate authority nor exceed the run's budget. A nil Fanout disables fan-out: the spawn tool is not offered and a goal runs as a single conversation, which is the n=1 case of the same mechanism.
type GenerationEnvelope ¶
type GenerationEnvelope struct {
// Pinned is true when the run fixed its sampling, rather than taking the server's defaults.
// A free-running generation is recorded as not pinned, so its non-reproducibility is itself
// an honest part of the history.
Pinned bool
// Deterministic is true when the decoding is in the guaranteed-reproducible regime (greedy),
// so a replay against the same weights must produce identical output.
Deterministic bool
// Seed, Temperature, and TopP are the pinned decoding parameters, normalized, or zero when
// the run is free-running.
Seed int64
Temperature float64
TopP float64
}
GenerationEnvelope is the reproducibility identity of a model call: the decoding parameters that, together with the model's weights, determine its output. It is recorded for every generation so the parameters that make a run reproducible are part of its durable history, and a recorded run can later be replayed and checked. The model and weights identity is recorded by the serving layer that knows it; this envelope carries the decoding half, and the two compose into a full, reproducible description of a generation.
type GenerationRecorder ¶
type GenerationRecorder interface {
RecordGeneration(ctx context.Context, env GenerationEnvelope)
}
GenerationRecorder records the decoding envelope of each model call onto a run's durable history, so the parameters that make a run reproducible are not lost. It is a narrow port, kept separate from the dispatch waist (which governs an action's metadata, not a model call's typed request) so the envelope is a domain event in its own right rather than waist payload. The default is a no-op; a host wires it to the event spine.
type Option ¶
type Option func(*Executor)
Option configures an Executor.
func PlanOptions ¶
PlanOptions translates a capability-driven scaffolding plan into the executor options that apply it: a tightened context budget for a model with a narrow window, simplified tool schemas for a weaker instruction-follower, and self-check passes for a less reliable model. The plan's tool-call constraint is applied to the model client at construction, not here, so it is not in this set. The zero plan yields no options, leaving a strong model on the lean path. Callers append the result after their own defaults, so a present plan field overrides a default and an absent one leaves it in place.
func WithAdmitter ¶
WithAdmitter sets the governance gate every tool call is admitted through (capability, budget, approval). The default is the capability admitter, which is permissive until a grant is bound (see WithGrant), so standalone behaviour is unchanged until a policy is supplied; a later WithAdmitter overrides it.
func WithApproval ¶
WithApproval wires a cryptographic approval gate into the waist, so a privileged action the gate's policy lists is refused unless a sufficient quorum of valid signed approvals is presented on the run's context. It composes above the capability grant: the grant decides an action is allowed in principle, the gate requires a fresh authorization for the specific privileged instance. Without it no approval is required, which keeps the standalone agent zero-config. A nil gate is ignored.
func WithApprovalClock ¶
WithApprovalClock sets the clock the executor stamps a minted approval's validity window from, so a deterministic run (and a test) controls the window. The default is clock.System. It should match the verifier's clock.
func WithApprovalGrace ¶
WithApprovalGrace overrides how long the waist waits on a human before a paused action auto-declines (default defaultApprovalGrace). A non-positive duration is ignored.
func WithApprovalPrompter ¶
func WithApprovalPrompter(prompter ApprovalPrompter, signer approval.Signer, host string) Option
WithApprovalPrompter enables interactive resolution of a paused privileged action: when the waist returns NeedsApproval, the executor asks prompter for a decision and, on allow, mints a single-use approval signed by signer for host, presents it, and retries the action once. Without this option (or with a nil prompter or signer) a NeedsApproval rejection surfaces to the model unchanged, so the standalone agent is zero-config. It composes with WithApproval, which installs the gate that pauses the action in the first place.
func WithBrakes ¶
WithBrakes wires a safety brake into the waist so the run is halted from outside the model loop when its observed behaviour trips a breaker or the kill-switch is engaged. The brake hook observes every action the run dispatches and refuses further work once halted; the caller keeps the hook so it can engage the kill-switch (h.Switch()) out of band. The run id is bound on the step context so the brake tracks behaviour per run, the same identity the conversation cache and budget key on. Without it no brake is applied, which keeps the standalone agent zero-config. A nil hook is ignored.
func WithBudget ¶
WithBudget wires a run's spend ceiling into the waist so every model and tool call is charged against the run's pool and refused once the ceiling is reached. The run id (its budget pool) is bound on the step context, so the budget tracks the right run; a fan-out shares one pool, since every descendant inherits the root's pool, so a single ceiling bounds the whole graph rather than a budget per goal. Without it no budget is applied, which keeps the standalone agent zero-config, and a run whose pool has no budget resource is unlimited. A nil hook is ignored.
func WithCompactionBudget ¶
WithCompactionBudget sets the input-token budget above which the oldest middle turns are elided from the transcript sent to the model (the objective and the recent tail are always kept). Zero, the default, disables compaction, so an embedder that wants the full transcript every turn is unaffected. The elision is a view over the durable checkpoint, never an overwrite, so nothing is lost. Set this to roughly half the model's context window so a long session stays well clear of the limit.
func WithEventSink ¶
WithEventSink records every tool call's lifecycle on the event spine (for audit and replay). The default discards, so standalone behaviour is unchanged.
func WithFanout ¶
WithFanout enables fan-out: the model is offered a spawn tool, and a call to it creates a child goal through f. The default is nil, which leaves a goal as a single conversation.
func WithGenerationRecorder ¶
func WithGenerationRecorder(r GenerationRecorder) Option
WithGenerationRecorder records the decoding envelope of every model call, so a run's reproducibility parameters become part of its durable history. The default discards them; a host wires this to the event spine.
func WithGrant ¶
func WithGrant(g capability.Grant) Option
WithGrant binds a default capability grant for goals that do not carry their own (see goal.Spec.Grant), so each tool call is admitted only if the grant permits its action. Without a grant the default capability admitter is permissive, so the agent runs unconstrained; with one the posture is default-deny. The grant is carried on the step's context, so it also reaches the sandbox layer below the waist. A goal that carries its own grant is governed by that grant instead, so a single executor can drive goals of differing authority.
func WithMaxTokens ¶
WithMaxTokens caps the output length requested of the model per turn.
func WithObserver ¶
WithObserver streams the mission's conversational events (turns, the model's text, tool calls and results) to r as the loop runs. The default is a no-op, so standalone behaviour is unchanged until an observer is supplied. It is the attachment point the session/stream front door wires a live event stream onto.
func WithSampling ¶
WithSampling pins the decoding parameters for every model call, so a run can be made reproducible. The default is nil, which leaves each call free-running on the server's defaults; setting it sends a fixed seed and sampler on every turn.
func WithSandbox ¶
WithSandbox wires the run's sandbox into the waist so every action is gated on containment sufficiency, alongside the capability grant: a work kind whose trust needs stronger isolation than the sandbox provides is refused before it runs, rather than downgraded. Without it the containment gate is absent and only the grant governs, which keeps the zero-config default permissive.
func WithSimplifiedSchemas ¶
func WithSimplifiedSchemas() Option
WithSimplifiedSchemas trims each tool definition before it is offered to the model: prose descriptions are shortened and per-field documentation and examples are dropped from the input schema, while the callable surface (every property and which are required) is left intact. A weaker, instruction-following-limited model is given a smaller surface to reason over without changing what it can call. The default leaves the full schemas in place.
func WithSystem ¶
WithSystem sets the standing system instructions framing every turn.
func WithTools ¶
WithTools registers the tools the model may call. Later registrations of the same name win, so a caller can override a default tool.
func WithVerifyPasses ¶
WithVerifyPasses adds up to n self-check passes before a turn is allowed to converge. When the model signals it is finished, it is instead asked to re-examine its work against the objective and fix anything incomplete; only after the passes are spent (or the model keeps the conversation going of its own accord) does the run conclude. Zero, the default, trusts the model's first claim of completion, which suits a reliable model; a weaker model gets the extra scrutiny. Each pass is a normal turn, so it stays bounded by the reconciler's step budget and survives a crash-resume like any other.
type Reporter ¶
Reporter receives a mission's conversational events as they happen, so a caller can stream live progress (turns, the model's text, tool calls and their results). It is additive observability layered beside the dispatch event spine: the default is a no-op and the loop's behaviour never depends on it. Report runs on the worker's step goroutine and must not block; a slow consumer should hand off to its own queue.
type ResultSummarizer ¶
type ResultSummarizer interface {
SummarizeResult(input json.RawMessage, result string) string
}
ResultSummarizer is an optional capability a Tool implements to describe a large result in one line. It is used when an older result is elided from the model's context to save tokens, so the model still sees what the call did without carrying its full output. A tool that does not implement it falls back to a generic size summary. The summary must be a pure function of the call so pruning stays deterministic and replayable, with no model round-trip.
type SubGoal ¶
type SubGoal struct {
Objective string `json:"objective"`
Actions []string `json:"actions,omitempty"`
// Agent, when set, names an Agent archetype to run the child as: its system prompt and
// capabilities configure the child (intersected with the parent's authority), instead of the
// ad-hoc Actions list. Empty runs an ad-hoc child from Actions.
Agent string `json:"agent,omitempty"`
}
SubGoal is a child goal a parent asks to run as part of a fan-out: an objective and either the actions the child may take or a named Agent to run it as. The actions (or the named Agent's capabilities) narrow the parent's authority, since a delegated run can never exceed the authority of the run that spawned it.
type Tool ¶
type Tool interface {
Def() llm.Tool
Invoke(ctx context.Context, input json.RawMessage) (string, error)
}
Tool is an executable capability the model may call during a mission. Def is the declaration handed to the model (name, description, argument schema); Invoke runs the call and returns its result as text. An Invoke error is not fatal: the loop reports it back to the model as a failed tool result so it can adapt, the same way a real tool failure would surface.
type TrustedWork ¶
TrustedWork is the optional interface a Tool implements to declare how far its work is trusted, which sets the containment the waist requires before the tool runs. A tool that does not implement it is the agent's own trusted code (it runs at any tier); a tool that executes model-authored content, such as a shell command, declares a lower trust so the waist refuses it on a host that cannot contain it.