autonomous

package
v2.9.0 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Index

Constants

View Source
const (
	MetricAutonomousRuns = "core_agent.autonomous.runs"

	// AttrStopReason carries the StopReason constant that ended the
	// run: completed, max_turns_exceeded, max_tokens_exceeded,
	// max_cost_exceeded, wallclock_exceeded, context_cancelled,
	// retry_policy_aborted, deferred — or "error" when the run
	// failed before any reason was assigned.
	AttrStopReason = "stop_reason"

	// StopReasonErrorFallback is the AttrStopReason value for
	// error-return paths that never assigned a StopReason.
	StopReasonErrorFallback = "error"
)

Autonomous-run metrics (#338 Phase 3). One sync counter increment per completed Run, dimensioned by the ACTUAL StopReason string constants (plus "error" for runs that abort before a reason is assigned).

Scope note for dashboard builders: the daemon binary does not call autonomous.Run — its consumers are background subagent spawns (pkg/agent/background) and library embedders driving the loop directly. A daemon with no autonomous spawns legitimately shows zero here.

View Source
const DefaultReturnToolName = "return_result"

DefaultReturnToolName is the name WithReturnTool uses when ReturnToolConfig.Name is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AutonomousHandle deprecated

type AutonomousHandle = Handle

AutonomousHandle is the pre-#492 name for Handle.

Deprecated: use Handle.

type AutonomousOption deprecated

type AutonomousOption = Option

AutonomousOption is the pre-#492 name for Option.

Deprecated: use Option.

type AutonomousStatus

type AutonomousStatus int

AutonomousStatus describes the lifecycle state of a run started via Start. Read via Handle.Status; transitions are driven by Pause / Resume / Stop and by the run goroutine's terminal handoff.

const (
	// AutonomousRunning — goroutine is alive and not paused.
	AutonomousRunning AutonomousStatus = iota
	// AutonomousPaused — Pause was called; loop is blocked at the
	// next pre-turn checkpoint until Resume fires.
	AutonomousPaused
	// AutonomousStopped — Stop was called; goroutine has unwound or
	// is about to (the ctx cancel propagates through the current
	// turn's LLM/tool calls).
	AutonomousStopped
	// AutonomousCompleted — Run returned with
	// Reason==Completed.
	AutonomousCompleted
	// AutonomousFailed — Run returned with a non-Completed
	// terminal reason (budget exceeded, retry aborted, etc.) or a
	// Go error from the loop machinery.
	AutonomousFailed
)

func (AutonomousStatus) String

func (s AutonomousStatus) String() string

String renders the status for diagnostics and tool results.

type BuildFunc

type BuildFunc func(extraTools []tool.Tool) (*agent.Agent, error)

BuildFunc has the same shape Run expects: the driver hands it the extra tools it injected (today: just the done tool) and the consumer returns a configured *agent.Agent. The Agent's session.Service must be wired (durable or in-memory).

type Handle

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

Handle is the programmatic-control surface returned by Start. The autonomous loop runs in its own goroutine; methods on the handle are safe for concurrent callers.

Typical usage from a harness:

h, _ := autonomous.Start(ctx, build, "monitor cluster X",
    agent.WithMaxTurns(0), agent.WithMaxWallclock(time.Hour))
defer h.Stop()
// Inject new instructions as they arrive from outside:
h.Inject("priority changed: focus on Q4 review")
// Or pause briefly:
h.Pause(); ...; h.Resume()
// Block until terminal:
result, err := h.Wait()

func Start

func Start(ctx context.Context, build BuildFunc, goal string, opts ...Option) (*Handle, error)

Start launches a new autonomous run in a goroutine and returns a handle the caller uses to control / observe it. Otherwise identical surface to Run — same BuildFunc, same options. Wait() returns the same RunResult shape.

The goroutine context is derived from ctx with our own cancel function so Stop() can cancel independently of the caller's ctx. If the caller's ctx fires, the run is still cancelled (the derived ctx inherits cancellation).

func StartAutonomous deprecated

func StartAutonomous(ctx context.Context, build BuildFunc, goal string, opts ...Option) (*Handle, error)

StartAutonomous is the pre-#492 name for Start.

Deprecated: use Start.

func (*Handle) Done

func (h *Handle) Done() <-chan struct{}

Done returns the channel that closes when the autonomous goroutine exits. Useful when a caller wants to combine the wait with other selects (e.g. ctx + Done).

func (*Handle) Inject

func (h *Handle) Inject(message string) error

Inject queues a message on the underlying agent's inbox. The next turn drains the inbox and prepends an "[Inbox]" block to the prompt the model sees. Returns an error when called before the goroutine has constructed the agent (typically a fraction of a second after Start returns) or after the agent is inaccessible.

func (*Handle) InjectAs

func (h *Handle) InjectAs(message string, caller auth.Caller) error

InjectAs is Inject with a per-message originator identity (see Agent.InjectAs). Same lifecycle rules as Inject.

func (*Handle) Pause

func (h *Handle) Pause() error

Pause requests the loop to pause at the next per-turn checkpoint. The currently-running turn finishes normally; subsequent turns block until Resume fires or Stop / ctx cancellation tears the goroutine down.

Idempotent: calling Pause while already paused is a no-op. Returns an error only when called after the run has terminated.

Emits a synthetic "paused" event to the agent's eventlog (Author="<binary>/autonomous", CustomMetadata.kind="paused") for audit, when an eventlog is wired. No-op when not.

func (*Handle) Ready

func (h *Handle) Ready() <-chan struct{}

Ready returns a channel that closes once the underlying agent has been constructed (i.e. the wrappedBuild closure inside the autonomous loop has run and captured the agent). Inject and RequestWake fail with "agent not yet constructed" when called before this fires; out-of-band consumers (stdin readers, alert watchers) should wait on Ready before issuing those calls. May already be closed by the time Ready returns — the select-on-Ready pattern handles both cases naturally.

func (*Handle) RequestWake

func (h *Handle) RequestWake()

RequestWake fires the underlying agent's wake signal, interrupting any active scheduler sleep, and publishes a `wake` event to attached operators (#802). Pairs with Inject for "operator nudged the loop, wake now" semantics; Inject already fires the same signal internally (deliberately without the event — the inject has its own frame), so this is for the alert-arrival case, or any other signal that doesn't carry a message. This is the door a host wires a BackgroundAgentManager.Alerts() drain to; dev/uat/scheduled-monitor is the worked example. No-op when the agent hasn't been constructed yet.

func (*Handle) Resume

func (h *Handle) Resume() error

Resume unblocks the autonomous loop's BeforeTurn hook so the next turn can start. Idempotent: calling Resume while not paused is a no-op. Returns an error only when called after the run has terminated.

Emits a synthetic "resumed" event to the agent's eventlog for audit, when an eventlog is wired.

func (*Handle) Status

func (h *Handle) Status() AutonomousStatus

Status returns the current lifecycle state. Safe to call any time; the goroutine's terminal handoff is mutex-coordinated.

func (*Handle) Stop

func (h *Handle) Stop() error

Stop cancels the run's context. The currently-running LLM call returns context.Canceled; the loop exits; the goroutine cleans up. Idempotent: subsequent Stop calls are no-ops.

If the loop is paused when Stop is called, the ctx cancellation unblocks the BeforeTurn hook (which selects on both pauseCh and ctx.Done) so the goroutine can exit.

func (*Handle) Wait

func (h *Handle) Wait() (RunResult, error)

Wait blocks until the autonomous goroutine exits, then returns the same RunResult + error pair Run returns. Safe to call from multiple goroutines; the result + err are set under the mutex once before the done channel closes.

type Option

type Option func(*autoConfig)

func WithBeforeTurn

func WithBeforeTurn(cb func(ctx context.Context, turnNo int) error) Option

WithBeforeTurn installs a callback invoked at the top of each iteration of the autonomous loop, after budget checks and before the turn's runOneTurn call. The callback receives the upcoming turn number (1-based). Returning a non-nil error aborts the run with that error.

This is the seam Handle uses to implement Pause: the callback blocks while paused, returning when Resume fires or the run context is cancelled. Library callers can wire arbitrary gating logic (rate limits, external approvals, etc.) on top.

func WithContinuationPrompt

func WithContinuationPrompt(s string) Option

WithContinuationPrompt overrides the prompt sent on every turn after the first. Default: "continue". Real consumers often pass something more specific to their loop ("what's your next step?").

func WithDoneToolDescription

func WithDoneToolDescription(desc string) Option

WithDoneToolDescription overrides the description shown to the model for the internal done tool. Override when the default prose doesn't fit your task — for example to instruct the model to call done only after writing a summary.

func WithDoneToolName

func WithDoneToolName(name string) Option

WithDoneToolName overrides the function name of the internal done tool. Useful when "report_done" collides with an existing tool the consumer has registered. Default: "report_done".

func WithMaxCost

func WithMaxCost(usd float64) Option

WithMaxCost caps the cumulative dollar cost of the run. Requires a non-zero pricing source — either WithTracker(tracker, pricing) or the recorded UsageMetadata being priced via the same Pricing.

func WithMaxDefer

func WithMaxDefer(d time.Duration) Option

WithMaxDefer is a driver-level ceiling on how far in the future the scheduler can wait. Zero means no cap, matching the existing WithMaxTurns / WithMaxWallclock convention. Acts as an operator safety net: if a turn emits a schedule intent past this ceiling, the driver clamps the wake-time and logs a warning, then proceeds with the clamped value. The model-facing cap is configured via WithScheduleToolMaxDefer.

func WithMaxTokens

func WithMaxTokens(input, output int) Option

WithMaxTokens caps the cumulative input + output token totals for the run. A zero value for either disables that side of the cap.

func WithMaxTurns

func WithMaxTurns(n int) Option

WithMaxTurns caps the number of turns the loop will execute. Zero disables the cap (use with caution; pair with another budget). The default is 50.

func WithMaxWallclock

func WithMaxWallclock(d time.Duration) Option

WithMaxWallclock caps the wall-clock duration of the run, measured from Run entry. Checked between turns; a single rogue turn can still exceed this — pair with WithPerTurnTimeout to bound that.

func WithMeterProvider

func WithMeterProvider(mp metric.MeterProvider) Option

WithMeterProvider overrides the OTel MeterProvider backing the core_agent.autonomous.runs counter. Defaults to the global provider resolved when Run starts (noop when metrics are disabled). Primarily for tests injecting a ManualReader.

func WithPerTurnTimeout

func WithPerTurnTimeout(d time.Duration) Option

WithPerTurnTimeout wraps each turn's context with a timeout so a single hung turn cannot stall the whole run. Distinct from WithMaxWallclock, which bounds total time.

func WithPermissionsGate

func WithPermissionsGate(g *permissions.Gate) Option

WithPermissionsGate hands the driver a reference to the permissions gate the consumer wired into their tools. The driver only uses this for one purpose: a startup check that rejects ask-mode + no-prompter configurations that would deadlock on the first tool call. The gate is otherwise enforced by the tools themselves; passing it here does not change runtime gating behavior.

Pass this when your build function constructs gated tools and your permission mode might be ask. Omit it for ModeYolo / ModeAllow runs where deadlock isn't a risk.

func WithPricing

func WithPricing(p usage.Pricing) Option

WithPricing sets the Pricing used for cost rollup when a usage.Tracker is not supplied. Useful for headless runs that just want a final dollar number on RunResult.

func WithProgress

func WithProgress(cb func(turn int, ev *session.Event)) Option

WithProgress invokes cb for every session.Event observed during the run. The turn index is the 1-based count of completed turns at the time the event is emitted (always at least 1 inside a turn).

func WithRetryPolicy

func WithRetryPolicy(p RetryPolicy) Option

WithRetryPolicy installs a callback consulted whenever a turn returns an error. The callback receives the error and the 1-indexed attempt count and returns one of AbortRun, RetryTurn, or SkipTurn. Without a policy, the driver aborts on the first error.

func WithReturnTool added in v2.9.0

func WithReturnTool(rc ReturnToolConfig) Option

WithReturnTool switches the driver from the lifecycle-style done tool (`report_done(state, detail)`) to a result-style return tool (`return_result(result)`) plus optional aliases.

Opt-in: consumers that don't set it keep the lifecycle done tool unchanged, so WithDoneToolName / WithDoneToolDescription and every existing `report_done` prompt keep working exactly as before. The in-tree background subagent path sets it; nothing else does.

func WithScheduleToolDescription

func WithScheduleToolDescription(desc string) Option

WithScheduleToolDescription overrides the description shown to the model for the internal schedule tool. The default includes a cadence ladder, good-vs-bad next_prompt examples, and the state-persistence reminder; override when domain-specific guidance is needed (e.g. "always wake by the top of the hour"). Only takes effect when WithScheduler is also set.

func WithScheduleToolMaxDefer

func WithScheduleToolMaxDefer(d time.Duration) Option

WithScheduleToolMaxDefer sets the tool-level cap on how far the model may schedule a wake. Calls past the cap return a tool-result error to the model so it can adapt. Zero means no cap. Distinct from WithMaxDefer, which is the driver's silent safety net. Only takes effect when WithScheduler is also set.

func WithScheduleToolName

func WithScheduleToolName(name string) Option

WithScheduleToolName overrides the function name of the internal schedule tool. Useful when the default "schedule_next_turn" collides with a consumer-registered tool. Only takes effect when WithScheduler is also set.

func WithScheduler

func WithScheduler(s coretools.Scheduler) Option

WithScheduler installs a tools.Scheduler that's consulted between turns when the prior turn emitted a schedule intent via the schedule_next_turn tool. Loops without a scheduler don't get the tool registered at all, so the model can't emit intent the driver has no way to honor.

Bundled schedulers: tools.SleepScheduler() for long-lived daemons (sleeps the goroutine between turns), tools.ExitOnDeferScheduler() for orchestrator-managed deployments (exits with StopReasonDeferred + RunResult.NextWakeAt populated, Resume picks up at the wake-time). See docs/scheduled-monitoring-design.md.

func WithStopOnNaturalEnd added in v2.9.0

func WithStopOnNaturalEnd() Option

WithStopOnNaturalEnd makes the loop stop the first time a turn ends without the model asking for another tool, reporting StopReasonCompleted with the turn's text as the result (#730).

This is the termination rule for a BOUNDED delegation — a subagent handed one task, which is done when it stops working. The default (standing worker) is the opposite: a turn that produces only text is a status report, and the loop feeds it the continuation prompt and keeps going until a budget or an explicit done signal fires.

On its own it suppresses the done/return tool entirely: with one termination path there is nothing for the model to choose between, and no way to leave the loop running by forgetting to call something.

Pair it with WithReturnTool and the tool IS registered (#745): the two are not alternatives but a preference order. A model that calls the tool hands back a curated result and ends; a model that just stops still ends, with its last message as the result. Nothing can hang, because the natural end never stops being a termination path. Bounded-without-a-return-tool remains the right shape only where the model genuinely has no gesture available.

func WithTracker

func WithTracker(t *usage.Tracker, p usage.Pricing) Option

WithTracker hands the driver an existing usage.Tracker plus the Pricing to use for per-turn cost accounting. Each turn appends to the tracker; RunResult also rolls up totals independently so callers can read them without touching the tracker.

When omitted, RunResult still tracks tokens — but cost is zero unless a non-zero Pricing is supplied via WithPricing.

type ResumeBuildFunc

type ResumeBuildFunc func(extras []tool.Tool, sessionID string) (*agent.Agent, error)

ResumeBuildFunc is the agent constructor accepted by Resume. It mirrors Run's BuildFunc signature but adds the sessionID the new agent must adopt — implementations pass it to agent.WithSession so the constructed agent reuses the session being resumed.

type RetryDecision

type RetryDecision int

RetryDecision tells the driver what to do after a turn fails.

const (
	// AbortRun stops the run immediately and propagates the error.
	AbortRun RetryDecision = iota
	// RetryTurn re-runs the same prompt for another attempt.
	RetryTurn
	// SkipTurn moves on to the continuation prompt as if the failed
	// turn had completed normally without a done signal.
	SkipTurn
)

type RetryPolicy

type RetryPolicy func(turnErr error, attempt int) RetryDecision

RetryPolicy decides what Run does when a turn errors. The callback receives the error and the 1-indexed attempt count (the first failure is attempt=1, second is attempt=2, etc.).

type ReturnToolConfig added in v2.9.0

type ReturnToolConfig struct {
	// Name is the primary tool name. Empty means
	// DefaultReturnToolName.
	Name string

	// Aliases are additional tool names wired to the same signal.
	// Duplicates of Name, and of each other, are dropped. Empty
	// entries are dropped.
	Aliases []string

	// Description overrides the prose shown to the model on every
	// registered name. Empty falls back to a default that states the
	// return contract.
	Description string
}

ReturnToolConfig replaces the driver's lifecycle-style done tool with a result-style one: a single `result` argument that is the value handed back to the caller, plus any number of alias names that funnel to the same signal.

Why this exists (#728). The stock done tool is a lifecycle status emitter — `report_done` was introduced in docs/autonomous-plan.md as one of the "report_done / set_status lifecycle tools", and its payload field is called `detail` because pkg/tools.NewLifecycleTool documents it as "an optional short human-readable note". That framing is right for a status emission and wrong for a delegation's return value, and it produced exactly the failure it describes: content-free reports the delegating agent had to re-derive.

Aliases exist because a subagent's namespace accumulated three near-synonymous names — report_done, report_completed and (on the parent only) mark_task_done — of which one terminated the loop, one did not, and one was not registered at all. A model that reaches for any of them should succeed rather than have to guess which is real.

type RunResult

type RunResult struct {
	// Reason explains why the loop stopped.
	Reason StopReason
	// FinalText is the accumulated streaming text from the last
	// *substantive* turn — the last turn that both produced output and
	// used a tool. A turn that only produced text cannot displace it
	// (#731): FinalText is the fallback return value wherever
	// DoneDetail is absent, and under a re-drive loop the trailing
	// turns are the model narrating that it has nothing left to do, so
	// last-wins reliably returned the worst thing the run said.
	//
	// A run that never used a tool at all keeps last-wins, since for a
	// pure-reasoning loop the newest text really is the best one.
	FinalText string
	// Turns is the number of turns the driver actually executed
	// (including failed ones that were retried or skipped).
	Turns int
	// InputTokens / OutputTokens are summed from each turn's
	// UsageMetadata. Zero when no usage info was returned.
	InputTokens  int
	OutputTokens int
	// CostUSD is the cumulative dollar cost computed via the
	// configured Pricing. Zero when pricing is zero.
	CostUSD float64
	// Duration is the wall-clock time from Run entry to
	// loop exit.
	Duration time.Duration
	// DoneDetail is the result the model handed back through the done
	// or return tool. Under WithStopOnNaturalEnd it is ALSO set from a
	// turn that simply stopped calling tools, which is a different
	// thing wearing the same field — see Returned.
	DoneDetail string
	// Returned reports whether the run ended because the model invoked
	// the done/return tool, as opposed to any other path that reports
	// StopReasonCompleted.
	//
	// The distinction is the delegation's return contract (#710).
	// WithStopOnNaturalEnd makes "the model stopped asking for tools"
	// a termination path, and it reports StopReasonCompleted with the
	// turn's text as DoneDetail — so a subagent that trails off, or
	// ends by asking a question nobody is there to answer, is
	// indistinguishable by Reason alone from one that deliberately
	// handed back a curated result. In the live GKE run that filed
	// this, the "deliverable" was "Please let me know if you would
	// like me to continue", and the parent redid the whole
	// investigation itself.
	//
	// False therefore does not mean the text is worthless — for many
	// personas the prose IS the answer — it means nobody asserted that
	// it is. Consumers that care (pkg/agent/background renders it as
	// the "no_return" stop class) should treat it as "verify this
	// answers the goal", not as an error.
	Returned bool
	// NextWakeAt is set when Reason==StopReasonDeferred — the
	// scheduler returned ErrSchedulerDefer and the loop exited
	// cleanly with a wake-time persisted to the eventlog. Whatever
	// orchestrator wraps the process restarts at or after this time
	// and Resume picks up the deferred checkpoint.
	NextWakeAt time.Time
}

RunResult is the structured outcome of Run.

func Resume

func Resume(ctx context.Context, build ResumeBuildFunc, ref SessionRef, opts ...Option) (RunResult, error)

Resume reads the most recent checkpoint event from the session's event log, reconstructs RunResult totals, and continues the run from the next turn. The build function receives the resumed sessionID so the constructed agent rejoins the same session via agent.WithSession.

Behavior:

  • Acquires an exclusive SessionLock on (App, User, Session). A concurrent Resume on the same session returns ErrSessionLocked from eventlog.
  • If the session has no checkpoint events at all, the run starts from turn 0 with whatever event history the session already holds — "make this existing session autonomous from here" is a valid use case.
  • If the latest checkpoint has stop_reason set (terminal state), Resume returns that state immediately without constructing the agent or running any turns.
  • Otherwise, the loop continues with prompt = checkpoint.ContinuationPrompt; budgets carry forward.

func ResumeAutonomous deprecated

func ResumeAutonomous(ctx context.Context, build ResumeBuildFunc, ref SessionRef, opts ...Option) (RunResult, error)

ResumeAutonomous is the pre-#492 name for Resume.

Deprecated: use Resume.

func Run

func Run(ctx context.Context, build BuildFunc, goal string, opts ...Option) (RunResult, error)

Run drives a multi-turn loop against an Agent built by build, sending goal as the first prompt and a continuation prompt thereafter, until one of the stop conditions fires. Returns a RunResult describing why it stopped and the totals it accumulated, plus any error.

The driver constructs the agent via build, passing in an extra "done" tool the model calls to signal completion. The tool name is "report_done" by default and can be overridden with WithDoneToolName. Consumers compose the done tool with their own tool registry inside build (see examples/autonomous for the pattern).

The constructor pattern keeps the driver from mutating a caller-supplied Agent (which would race with concurrent runs) and keeps agent.New's surface free of "extra tools" plumbing that only matters here.

The build closure SHOULD set agent.WithMode(agent.ModeAutonomous) (#459) — the driver cannot inject it (drivers don't mutate caller-built agents), so an agent built without it runs with the interactive overlay and may ask questions nobody will answer. The driver logs a one-line warning when it detects that; consumers replacing the whole prompt via agent.WithInstruction can ignore it.

func RunAutonomous deprecated

func RunAutonomous(ctx context.Context, build BuildFunc, goal string, opts ...Option) (RunResult, error)

RunAutonomous is the pre-#492 name for Run.

Deprecated: use Run.

type SessionRef

type SessionRef struct {
	Handle    *eventlog.Handle
	AppName   string
	UserID    string
	SessionID string
}

SessionRef identifies the session Resume resumes from. Handle supplies both the eventlog.Stream (used to find the latest checkpoint) and the session.Service (used by the constructed agent for live event reads + writes).

type StopReason

type StopReason string

StopReason explains why Run returned.

const (
	// StopReasonCompleted means the run reached an ending of its own:
	// the model called the done tool, or — under WithStopOnNaturalEnd
	// — a turn ended without asking for another tool. RunResult.Returned
	// separates the two, and a delegating caller should read it: the
	// second form covers a subagent that trailed off or stopped to ask
	// a question (#710).
	StopReasonCompleted StopReason = "completed"
	// StopReasonMaxTurns means WithMaxTurns was hit.
	StopReasonMaxTurns StopReason = "max_turns_exceeded"
	// StopReasonMaxTokens means WithMaxTokens (input or output) was hit.
	StopReasonMaxTokens StopReason = "max_tokens_exceeded" //nolint:gosec // not a credential
	// StopReasonMaxCost means WithMaxCost was hit.
	StopReasonMaxCost StopReason = "max_cost_exceeded"
	// StopReasonWallclockExceeded means WithMaxWallclock was hit.
	StopReasonWallclockExceeded StopReason = "wallclock_exceeded"
	// StopReasonContextCancelled means the supplied context was
	// cancelled or its deadline expired.
	StopReasonContextCancelled StopReason = "context_cancelled"
	// StopReasonRetryAborted means the configured RetryPolicy
	// returned AbortRun for a turn error.
	StopReasonRetryAborted StopReason = "retry_policy_aborted"
	// StopReasonDeferred means the configured Scheduler returned
	// ErrSchedulerDefer in response to a schedule emission. The loop
	// exited cleanly with RunResult.NextWakeAt populated; whatever
	// orchestrator wraps the process restarts at or after the
	// wake-time and Resume picks up.
	StopReasonDeferred StopReason = "deferred"
)

Jump to

Keyboard shortcuts

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