agent

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jun 15, 2026 License: MIT Imports: 39 Imported by: 0

Documentation

Overview

Package agent owns the cornerstone runtime contract every later phase implements or consumes: the open Agent interface, the single-Run-scoped InvocationContext, the forward-compat Event/Actions/LLMResponse shape, the OTel-correct trace IDs, and the Budget tree that bounds a run.

The Agent interface is OPEN by design (no unexported seal): Phase 3 LlmAgent and Phase 9 swarm implement it directly. This diverges from google/adk-go, which seals its interface via an unexported internal() method so only its own constructors can implement it — adk's own docs note they are moving toward an open interface, which is the premise Aura locks in now (D-01).

Budget is the cornerstone resource-exhaustion control (a DoS-prevention mechanism, ASVS V11): a single shared *atomic.Int32 step counter bounds the WHOLE agent tree, a wallclock deadline bounds total time, and a two-phase dedup ring (budget_dedup.go) bounds repeated tool calls.

Pattern derivato da google/adk-go v1.4.0 agent/workflowagents/loopagent/agent.go (Apache 2.0). Adattato per Aura con SC#2 budget exhaustion + SC#3 child-inherits-remaining + SC#4 UUIDv7 OTel-compat.

The shared-atomic design (D-10) is what makes SC#3 true: a depth-3 fan-3 tree consumes ≤ max_steps total, NOT max_steps³ — it avoids the fresh-per-child explosion that nanobot-style designs fall into. Budget.Child shares the SAME counter by pointer and never forks it.

Two-tier loop-guard dedup (D-18/A2), split out of budget.go per RESEARCH #1 so neither file approaches the 600-LOC no-god-class cap (CLAUDE.md).

WHY two-tier and WHY result is VETO-only: the original (name,args,result)-in-hash design was FAIL-OPEN — any tool returning a volatile field (timestamp, page-token, request-id) never produces a repeating triple, so a runaway loop is never detected and slips past the guard. A2 reversed it: the primary fingerprint is sha256(name + canonical_json(args)) ONLY, checked in BeforeToolCall BEFORE the tool re-executes (so repeated side effects are blocked). A bounded result preview is recorded later by AfterToolResult and used ONLY as a progress VETO: if the args repeat but the latest result preview CHANGED, the next BeforeToolCall suppresses dedup and treats it as progress. Volatile-result tools therefore fail SAFE (look like progress) instead of fail-open.

CALLER-CANONICALIZES CONTRACT (B2): BeforeToolCall and AfterToolResult both accept PRE-CANONICALIZED argsCanonicalJSON []byte. The CALLER runs internal/canonicaljson.Marshal(args) before calling — these methods do NOT call canonicaljson internally. Plan 05 Task 2 (the LoopAgent caller) and this callee agree on this contract; keeping canonicalization in the caller lets the loop hash once per turn and avoids a hidden re-serialization.

LlmAgent is Aura's first real Agent implementation (D-01 commit 6): the budget-gated tool-dispatch run-loop that drives the openai_compat client, threads ToolResult into in-memory history, streams chunk/tool-call/tool-result/ final Events, and terminates via text_response with a content-stop fallback. It resolves the Phase-2 deferred SpanID minting (full-tree crypto/rand, tracing.go) and emits one llm.request span per LLM call.

The loop shape replicates agenttest.CountingAgent.Run (the canonical budget-gated iter.Seq2 emitter): budget gate BEFORE each call -> on a trip yield a terminal Event (never the error slot, D-04); the yield-after-false guard is mandatory.

Completion critic gate (amendment #54 / D-43): the spine that turns the prompt's "verify before reporting" from prose into an enforced contract. Before the loop accepts a VOLUNTARY termination (text_response or the content-stop fallback) on a turn that mutated host state, a cheap critic call judges the user's request against the OBSERVED tool results — not the agent's claims — and vetoes ONCE when the promised deliverable is not verifiably present. It reuses the maybeRecover counter discipline (D-08): a dedicated completionAttempts counter (max 1) keeps the veto bounded, the extra turn rides the normal budget gate, and a broken/empty/unparseable critic fails OPEN so a verifier outage can never wedge a turn. Concern-split out of llm_agent.go to keep that file under the no-god-class cap.

consume is the stream-drain half of the run loop, split out of llm_agent.go to keep that file under the no-god-class cap (D-07). It turns one provider stream into the (text, calls, finish, usage) the loop acts on, re-emitting chunk/ tool-call/reasoning Events as they arrive and honoring the iter.Seq2 yield-after-false contract (a consumer break drains-to-close, then reports stopped so Run never yields again).

Forced finalization (Req#2, the SPINE for plan 04's recovery/fallback work): when the run-loop trips an early-termination gate (budget max_steps/wallclock, or the dedup veto) it used to die on a prose-less terminalBudgetEvent — the ~1-in-6 empty answer. finalize() instead issues ONE tool-free synthesis turn (ToolChoice="none", full in-memory history + the D-03 nudge), drains it to a content string, and emits a NON-EMPTY finalEvent that still carries the trip reason in StateDelta for observability (landmine #10).

Concern-split out of llm_agent.go (D-07): that file is at its no-god-class headroom, and the recovery counter + Italian-stub fallback land here in plan 04.

Pause-DETECTION seam (D-A1-03 / AM-01). This file holds ONLY the half of the HITL pause primitive that lives in the agent: catching the tools.ErrAwaitingUserInput sentinel, applying intra-turn exclusivity, and emitting the pause as an Actions.AwaitingInput Event. The agent stays DB-free — pause PERSISTENCE + resume orchestration live in the Runner (04-05), which observes this Event and is the sole writer of aura.paused_states. Split out of llm_agent.go for separation of concern (AM-01), not size.

The pause is Event-only, NEVER the iter.Seq2 error slot (RESEARCH Pattern-3 anti-pattern; mirrors budget exhaustion at llm_agent.go terminalBudgetEvent).

Output-budget truncation handling, split out of llm_agent.go to keep that file under the no-god-class cap. When a tool-call turn finishes with finish_reason= "length" its arguments were cut mid-JSON by the output budget, so dispatching it yields "unexpected end of JSON input" and the model retries the same oversized call — the 203-turn truncation thrash observed live 2026-06-14. Tool turns get the full output budget (reasoning_policy floor) so this fires only on a genuinely oversized call; when it does, nudge once and finalize on a repeat so the loop can never thrash.

Package agent's OTel wiring (D-03..D-06): the real TracerProvider bootstrap, full-tree crypto/rand SpanID minting (resolving the Phase-2 deferral at agent.go:51-52), and the per-call llm.request span helper. This file REPLACES the otel_deps.go blank-import anchor — the v1.44.0 trace train is now used for real, so the anchor is deleted in the same commit (truth: go.mod keeps the four modules pinned because tracing.go imports them).

Index

Constants

View Source
const (
	ToolInvocationStart = "start"
	ToolInvocationEnd   = "end"
)

ToolInvocationStart and ToolInvocationEnd label tool invocation lifecycle events.

View Source
const SystemPrompt = `` /* 11491-byte string literal not displayed */

SystemPrompt is Aura's canonical system prompt — the operator-authored XML-tagged rewrite (2026-06-06, commit 6cf1895e) with the <skills> section aligned to amendment #51/#52 (the action=catalog/action=install routing those amendments deleted is gone; discovery+install is the always-on find-skills skill riding the host terminal). It is a package constant — never templated, never carrying a timestamp or any per-turn-mutating value — so it stays byte-identical across turns and preserves OpenRouter's implicit prompt-cache discount (Req#14; memory: reference_aura_cache_poisoning_sites). It explains MECHANISMS (the agentic loop, tool_search discovery, the skill capability-gap doctrine, the shell_exec full-terminal home, ask_user approvals, the Phase-15 <memory> doctrine — D-01 agent-decides writes, D-03 pull-on-demand recall) WITHOUT enumerating the volatile tool set — enumeration would cache-bust the prefix every time the tool set changes; concrete tool schemas ride in req.Tools OUTSIDE this prefix. Only the structural verbs (tool_search, text_response, skill, ask_user, shell_exec) are named. Authored in English with an explicit output-language directive (memory: feedback_all_prompts_in_english_only: never mix IT/EN in the prompt itself — drive output language via a directive). This Go constant is the authored source of truth; do not recreate stale prompt copies in docs.

Variables

View Source
var ErrBudgetExhausted = errors.New("agent budget exhausted")

ErrBudgetExhausted is the exported sentinel for callers that inspect agent termination OUTSIDE the Event stream (D-04). Inside a Run the canonical signal is an explicit Event (Actions.Escalate=true + StateDelta termination_reason); this sentinel exists only so Phase 3/9 consumers can do errors.Is(err, agent.ErrBudgetExhausted) when a budget limit surfaces through the error slot.

Functions

func AgentOwnsBudget

func AgentOwnsBudget(a Agent) bool

AgentOwnsBudget reports whether an agent has opted into owning its own budget gates. Workflow parents use this to keep the single-budget-owner contract for a composed agent tree.

func ExemptToolsFromEnv

func ExemptToolsFromEnv(extra ...string) map[string]struct{}

ExemptToolsFromEnv returns the AURA_LOOP_DEDUP_EXEMPT_TOOLS allowlist plus any extra tool names, as a fresh set (D-19). It lets a caller compose the operator's env exemptions with its own (e.g. the dry-run tool) and pass the result to NewBudget via BudgetOptions.ExemptTools — no process-global env mutation (WR-04).

func WithSwarmContext

func WithSwarmContext(ctx context.Context, budget *Budget, registry *tools.Registry, client llm.Client, llmCfg llm.Config, convID string) context.Context

WithSwarmContext returns a ctx carrying the parent deps the swarm_spawn runner adapter reads. The agent's runTool calls this before dispatching each tool so a swarm_spawn call can resolve the live parent budget/registry/client/config. It mirrors tools.WithToolCallContext.

Types

type Actions

type Actions struct {
	Escalate       bool            `json:"escalate,omitempty"`       // true → stop this branch / cancel siblings
	StateDelta     map[string]any  `json:"state_delta,omitempty"`    // termination_reason/limit_hit/steps_consumed, etc.
	ArtifactDelta  map[string]any  `json:"artifact_delta,omitempty"` // produced-artifact deltas (forward-compat)
	AwaitingInput  *AwaitingInput  `json:"awaiting_input,omitempty"` // HITL pause payload (Slice 1.5); nil unless ask_user fired
	ToolInvocation *ToolInvocation `json:"tool_invocation,omitempty"`
	// DiscardStreamed is the mid-stream-retry repudiation signal (B-12). When a
	// stream fails mid-way and the loop retries, the partial chunk Events the failed
	// attempt already streamed are stale; the agent emits ONE Event carrying this
	// flag BEFORE the retry's fresh chunks so a streaming consumer (the REPL renderer,
	// the AG-UI gateway) drops what it has shown so far and renders the retry over a
	// blank slate — instead of the user seeing partial+answer. It fires ONLY on a
	// retry, so live token-by-token streaming on the common no-retry path is
	// untouched (no buffering). Omitted on the wire unless set.
	DiscardStreamed bool `json:"discard_streamed,omitempty"`
}

Actions are the control signals an Event carries. Escalate is the canonical termination/cancellation signal (D-04: budget exhaustion is Event-only, never the error slot). StateDelta/ArtifactDelta accept nested map[string]any and map to the AG-UI STATE_DELTA stream in Phase 12. AwaitingInput is the HITL pause signal (D-A1-03): a sibling to Escalate, set by the pause-detection seam (llm_agent_pause.go) when ask_user fires — the pause travels as this Event, NEVER the iter.Seq2 error slot. It is a pointer so an unset pause omits the `awaiting_input` key on the wire (mirrors the MessageID *pointer omitempty trick), keeping decode(encode())==identity.

type Agent

type Agent interface {
	// Name is the stable identifier used as the Event Author for this agent's
	// runtime-emitted events and as the lookup key for FindAgent.
	Name() string
	// Description is the human/LLM-facing one-liner (Phase 3 surfaces it to the model).
	Description() string
	// Run executes one invocation and streams Events. The error slot of the
	// iter.Seq2 carries only REAL failures (LLM/tool errors); termination and
	// budget exhaustion are signalled by an explicit Event, never the error slot
	// (D-04). Agents that consume Budget internally should implement BudgetOwner so
	// workflow parents do not double-charge their emitted events.
	Run(InvocationContext) iter.Seq2[*Event, error]
	// SubAgents returns the direct children (nil for leaf agents).
	SubAgents() []Agent
	// FindAgent returns self if name matches, else recurses into SubAgents;
	// nil if not found (collapses adk's FindAgent/FindSubAgent split).
	FindAgent(name string) Agent
}

Agent is the cornerstone contract every later phase implements or consumes. Open by design (no unexported seal) — Phase 3 LlmAgent and Phase 9 swarm implement this directly. Pattern derivato da google/adk-go v1.4.0 agent/agent.go (Apache 2.0); the internal() seal is intentionally removed (D-01).

type AwaitingInput

type AwaitingInput struct {
	Question           string          `json:"question"`
	Options            []PauseOption   `json:"options,omitempty"`
	Kind               string          `json:"kind"`
	Priority           int             `json:"priority,omitempty"`
	ToolCallID         string          `json:"tool_call_id"`
	ResumeContext      json.RawMessage `json:"resume_context,omitempty"`
	OriginAgent        string          `json:"origin_agent,omitempty"`          // emitting agent name (swarm proxy forward-compat, D-A1-08)
	ProxiedFromChildID string          `json:"proxied_from_child_id,omitempty"` // child id when this pause relays a child's needs_user_input report (D-05); empty on a direct call
	ProxiedToolCallID  string          `json:"proxied_tool_call_id,omitempty"`  // child's originating tool_call id for the relay (D-05); empty on a direct call
}

AwaitingInput is the pause payload an ask_user Event carries (D-A1-03). It is the Event-side projection of tools.ErrAwaitingUserInput plus the dispatch- stamped ToolCallID and the originating-agent id for swarm forward-compat (D-A1-08: paused_states.proxied_* stay NULL for direct calls, populated by Phase 9 as the Event crosses the child→parent boundary). It carries no DB type — the askuser.Store reads these plain fields off the Event, never the other way round (the agent stays DB-free).

type Budget

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

Budget bounds one agent run. The steps counter is shared by pointer across the whole tree (D-10); deadlineWallclock and now are shared by value; the dedup ring is per-branch (forked by Child, D-09).

func NewBudget

func NewBudget(opts BudgetOptions) (*Budget, error)

NewBudget builds a Budget applying CLI > env > builtin-default precedence (D-06) in ONE place: each opts override wins over the AURA_LOOP_* env value, which wins over the builtin default. It is fail-fast on any SET-but-malformed env value — it never silently falls back to a default on a parse error (the opposite of internal/config's silent-absorb int helper). Unset env vars use the builtin defaults; only a SET-but-malformed value is an error. No process-global state is mutated.

func NewBudgetFromEnv

func NewBudgetFromEnv() (*Budget, error)

NewBudgetFromEnv builds a Budget from the AURA_LOOP_* environment with no overrides — the env-only callers' entry point. Fail-fast on any malformed value (D-06); see NewBudget for the precedence rules.

func (*Budget) AfterToolResult

func (b *Budget) AfterToolResult(name string, argsCanonicalJSON, resultPreview []byte)

AfterToolResult records the bounded result preview for the fingerprint as a PROGRESS VETO (D-18). The caller passes the same already-canonical args (B2). If the result preview for a repeated fingerprint CHANGED since last time, the veto is set (progress) so the next BeforeToolCall suppresses dedup; if it is unchanged, the stale marker stays so period-1/period-2 repeat can terminate. resultPreview is truncated to the budget's resultCap before hashing (A7).

func (*Budget) BeforeToolCall

func (b *Budget) BeforeToolCall(name string, argsCanonicalJSON []byte) (dedup bool, reason string)

BeforeToolCall is the PRE-EXECUTION dedup gate (D-18). The caller passes the already-canonical args bytes (B2). It returns (true, "dedup") when the same fingerprint has repeated to the window threshold (period-1) OR forms a period-2 ping-pong AND no progress veto applies — i.e. the loop should terminate before the side effect re-runs. Exempt tools (AURA_LOOP_DEDUP_EXEMPT_TOOLS, D-19) never dedup. The fingerprint is recorded so the next call can detect the repeat.

func (*Budget) BranchConsumed

func (b *Budget) BranchConsumed() int

BranchConsumed is the number of steps THIS branch has consumed (the per-branch counter ConsumeStep bumps) — the <budget> "used" count (Req#6, D-06). There is no MaxSteps() getter (landmine #11): the agent reads this counter directly rather than deriving used from a cap.

func (*Budget) Child

func (b *Budget) Child(fanout int) *Budget

Child forks a budget for a PARALLEL branch (D-09): it shares the SAME *atomic.Int32 step counter and the same wallclock + clock (so the hard total bound is preserved across the tree), but gets a DISTINCT dedup ring (no cross-branch false positives) and a PASSIVE per-branch soft cap (D-12). Sequential/Loop sub-agents instead use InvocationContext.WithSubAgent, which shares the ring for cross-iteration repeat detection.

IN-04 — the soft cap is a SPAWN-TIME SNAPSHOT and therefore TIMING-DEPENDENT BY DESIGN: it is computed off b.Remaining() at the instant Child is called. When a ParallelAgent spawns siblings in a loop, a later sibling reads an already-depleted pool and so gets a SMALLER advisory share than an earlier one. This is intentional and consistent with the non-terminal "passive advisory" framing (D-12) — the soft cap never bounds correctness, only fairness hints — so siblings are NOT guaranteed equal shares. A caller that wants equal sibling shares must snapshot Remaining() once before the fan-out loop and reuse it across every Child call.

func (*Budget) ConsumeStep

func (b *Budget) ConsumeStep() (ok bool, reason string)

ConsumeStep is the TOCTOU-safe (D-11) gate every agent step passes through. It checks the wallclock FIRST (D-13), then does an atomic decrement-then-check-then-restore so N concurrent goroutines can never over-spend the shared cap: the goroutines that overshoot below zero all add 1 back. Only HARD terminal reasons are returned ("max_steps" | "wallclock") — the per-branch soft cap is NON-terminal and surfaced via SoftCapExceeded (D-12).

func (*Budget) NodeTimeout

func (b *Budget) NodeTimeout() time.Duration

NodeTimeout is the optional per-node soft timeout (AURA_LOOP_NODE_TIMEOUT_SEC, D-13); zero means disabled.

func (*Budget) Now

func (b *Budget) Now() time.Time

Now returns the budget's clock value. It is the runtime's single per-turn time source so prompt hints and wallclock gates agree in tests and production.

func (*Budget) Remaining

func (b *Budget) Remaining() int

Remaining is the current shared step balance (never negative once restored).

func (*Budget) SetMaxSteps

func (b *Budget) SetMaxSteps(n int32)

SetMaxSteps overrides the shared step counter (CLI flag precedence, D-06). It resets the WHOLE tree's remaining budget; intended only at boot before any child is spawned.

func (*Budget) SoftCapExceeded

func (b *Budget) SoftCapExceeded() bool

SoftCapExceeded reports whether THIS branch has consumed at least its passive fair-share soft cap (D-12). It is a non-terminal scheduling/fairness signal: the hard total bound (steps) stays authoritative, the branch may still consume against the shared pool. A root budget (branchSoftCap==0) never reports true.

func (*Budget) WithDeadline

func (b *Budget) WithDeadline(parent context.Context) (context.Context, context.CancelFunc)

WithDeadline derives a context bounded by the budget's wallclock deadline so in-flight LLM/tool calls are cancelled end-to-end, not just blocked from new steps (D-13). The caller owns the returned CancelFunc.

type BudgetOptions

type BudgetOptions struct {
	MaxSteps        *int                // overrides AURA_LOOP_MAX_STEPS
	MaxWallclockSec *int                // overrides AURA_LOOP_MAX_WALLCLOCK_SEC
	DedupWindow     *int                // overrides AURA_LOOP_DEDUP_WINDOW
	ExemptTools     map[string]struct{} // overrides AURA_LOOP_DEDUP_EXEMPT_TOOLS allowlist
	// Now is the injectable clock (W8). When nil it defaults to time.Now. It is
	// the SINGLE time source for BOTH the wallclock deadline anchor (computed as
	// Now().Add(wallclock) at construction) AND the ConsumeStep deadline check, so
	// a caller can drive wallclock behavior end-to-end through the constructor
	// instead of building a Budget literally to bypass it (WR-03).
	Now func() time.Time
}

BudgetOptions carries explicit overrides for the CLI > env > default precedence (D-06) WITHOUT touching process-global environment. A nil pointer field means "unset → fall through to env then builtin default"; a non-nil field overrides both. ExemptTools, when non-nil, replaces the env allowlist entirely (the caller has already merged in any operator-set exemptions). This lets `aura agent dry-run` pass resolved flag values directly instead of round-tripping through os.Setenv (WR-04), which was not goroutine-safe and collided with t.Setenv in parallel tests.

type BudgetOwner

type BudgetOwner interface {
	OwnsBudget() bool
}

BudgetOwner is an optional Agent capability: agents that already enforce their own Budget gates expose it so workflow parents can remain observational instead of double-charging the same shared budget.

type Event

type Event struct {
	RequestID    uuid.UUID    `json:"request_id"`               // UUIDv7 TraceID/run_id, shared tree-wide
	SpanID       [8]byte      `json:"span_id"`                  // 8-byte OTel SpanID, per-node; wire = lower-hex
	ParentSpanID *[8]byte     `json:"parent_span_id,omitempty"` // nil at root; wire = lower-hex when set
	Author       string       `json:"author"`                   // workflow agent name, "user", or LLM agent name (D-14)
	Branch       string       `json:"branch,omitempty"`         // hierarchical label only (D-15)
	ThreadID     string       `json:"thread_id,omitempty"`      // AG-UI conversation thread (Phase 4 / Slice 1.8), forward-compat
	MessageID    uuid.UUID    `json:"message_id,omitempty"`     // AG-UI message correlation (UUIDv7); omitted on wire until set
	LLMResponse  *LLMResponse `json:"llm_response,omitempty"`   // nil when this Event is not an LLM turn
	Actions      Actions      `json:"actions"`                  // control signals (escalate, state/artifact deltas)
	Timestamp    time.Time    `json:"timestamp"`                // emit time, serialized RFC3339Nano UTC
}

Event is the single type every runtime- and LLM-emitted signal flows through. The shape is full / forward-compat so the Phase-12 AG-UI gateway is a fan-out adapter, not a refactor (D-17): MessageID/ThreadID/ToolCallID are present now even though nothing consumes them yet.

Trace IDs are OTel/W3C-correct widths (D-16): RequestID is a 16-byte UUIDv7 (TraceID/run_id), SpanID is 8 random bytes, ParentSpanID is nil at the root. Storing a 16-byte UUID in the SpanID slot would force lossy truncation when a future OTel slice maps these — 8 bytes makes that mapping drop-in.

WR-04 — SPAN MINTING IS DEFERRED, BY DESIGN, to the future OTel-integration slice (SPEC §"OTel dep transitive": Phase 2 ships the OTel-compatible SHAPE without the OTel dep; populating real per-node spans is out of Phase-2 scope). Phase 2 therefore leaves SpanID at its zero value [8]byte{} on every Event, which serializes as the constant "span_id":"0000000000000000". This all-zero value is the documented not-yet-minted sentinel, NOT a bug: the wire field exists now so the OTel slice fans out an existing field rather than retrofitting one. When that slice lands it mints a crypto/rand SpanID per child InvocationContext and chains ParentSpanID.

The struct stores SpanID/ParentSpanID as fixed-size byte arrays, but the wire form (see eventWire) is the OTel/W3C-idiomatic LOWER-HEX string, NOT the JSON number array a [N]byte array would default to. MessageID is value-typed here but omitted on the wire until set (eventWire uses *uuid.UUID so omitempty fires).

func (Event) MarshalJSON

func (e Event) MarshalJSON() ([]byte, error)

MarshalJSON is the SINGLE user-facing serialization path for an Event (W7): the Plan-07 dry-run prints Events through json.NewEncoder(...).SetEscapeHTML(false) honoring this method. canonicaljson is for hashing/fingerprinting only, never for Event output. HTML escaping is disabled and Timestamp is normalized to RFC3339Nano UTC so decode(encode(ev)) round-trips byte-identical (D-21).

func (*Event) SetAuthorIfEmpty

func (e *Event) SetAuthorIfEmpty(name string)

SetAuthorIfEmpty stamps the author on an LLM-emitted Event that has not set one (D-14). Aura's open interface has no base-struct embed hook, so each workflow agent sets Author explicitly; this helper covers events minted deeper down.

func (*Event) UnmarshalJSON

func (e *Event) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes through eventWire (UseNumber for any nested StateDelta numbers, RFC3339Nano timestamp) so decode(encode(ev)) is symmetric.

type InvocationContext

type InvocationContext struct {
	Ctx          context.Context // request-scoped cancellation/deadline (named, never embedded)
	Agent        Agent           // the agent currently executing (self-reference for workflow recursion)
	RequestID    uuid.UUID       // UUIDv7, 16 bytes — OTel/W3C TraceID + run_id shape (D-16), minted once at root
	SpanID       [8]byte         // 8-byte OTel/W3C SpanID shape (D-16), per-node; minting DEFERRED to the OTel slice (WR-04) — stays [8]byte{} in Phase 2
	ParentSpanID *[8]byte        // nil at root; the parent node's SpanID otherwise — chaining DEFERRED to the OTel slice (WR-04)
	Branch       string          // hierarchical LABEL only ("root.iter-2.worker-3"); hierarchy comes from span IDs (D-15)
	Budget       *Budget         // shared budget tree (same *atomic.Int32 across the whole tree, D-10)
}

InvocationContext is single-Run-scoped; never store on a long-lived struct, never cache, never share across invocations. It is passed by value down the agent tree; WithContext/WithSubAgent always return a COPY and never mutate the receiver (D-24). Ctx is a NAMED field, never embedded — embedding invites passing the InvocationContext where a context.Context is expected and storing it, which the single-Run-scope invariant forbids.

func (InvocationContext) WithContext

func (ic InvocationContext) WithContext(ctx context.Context) InvocationContext

WithContext returns a copy of ic with a replaced Ctx. The receiver is never mutated (D-24).

func (InvocationContext) WithSubAgent

func (ic InvocationContext) WithSubAgent(sub Agent) InvocationContext

WithSubAgent returns a copy of ic re-pointed at sub, keeping the SAME *Budget (same *atomic.Int32 step counter AND the same dedup ring) so a single logical reasoning thread sees its own cross-iteration repeats (D-09). The receiver is never mutated (D-24). Parallel branches use Budget.Child instead to fork a distinct dedup ring while still sharing the atomic counter.

type LLMResponse

type LLMResponse struct {
	Content      string         `json:"content,omitempty"`       // assistant text
	Reasoning    string         `json:"reasoning,omitempty"`     // stream-only CoT delta (amendment #57); never persisted to conversation_turns, additive D-17 forward-compat
	ToolCalls    []llm.ToolCall `json:"tool_calls,omitempty"`    // reuses llm.ToolCall (D-17); its ID is the AG-UI tool_call_id
	FinishReason string         `json:"finish_reason,omitempty"` // provider finish reason on the final turn
}

LLMResponse carries the model output for an Event that IS an LLM turn. It is referenced via a pointer on Event so non-LLM events omit it entirely.

type LlmAgent

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

LlmAgent drives the autonomous tool-dispatch loop. history is in-memory only this phase (D-26); messages[0] is the byte-stable system prompt (Req#14) and is never mutated. sessionID = Event.ThreadID (D-26) — the sidecar/spillover key.

func NewLlmAgent

func NewLlmAgent(cfg LlmAgentConfig) *LlmAgent

NewLlmAgent builds an LlmAgent. messages[0] is ALWAYS the byte-stable system prompt (D-08/D-09) followed by the supplied user turns; the agent owns history from here. Name/Description default when empty.

func (*LlmAgent) Description

func (a *LlmAgent) Description() string

Description is the human/LLM-facing one-liner.

func (*LlmAgent) FindAgent

func (a *LlmAgent) FindAgent(name string) Agent

FindAgent returns self when name matches, else nil.

func (*LlmAgent) Name

func (a *LlmAgent) Name() string

Name is the Event Author / FindAgent key.

func (*LlmAgent) OwnsBudget

func (*LlmAgent) OwnsBudget() bool

OwnsBudget tells workflow parents that LlmAgent already consumes the shared Budget at its own loop gates, so wrappers must not charge its emitted tool-call events a second time.

func (*LlmAgent) Run

Run drives the budget-gated tool-dispatch loop (Req#9/#10). Termination paths: text_response (D-13), content-stop fallback on finish_reason=stop with no tool call (D-13/D-16), a budget trip (terminal Event with reason, D-04), or a real infra failure (the iter.Seq2 error slot, D-15).

func (*LlmAgent) SubAgents

func (a *LlmAgent) SubAgents() []Agent

SubAgents returns nil — LlmAgent is a leaf.

type LlmAgentConfig

type LlmAgentConfig struct {
	Name        string
	Description string
	Client      llm.Client
	LLM         llm.Config
	Registry    *tools.Registry
	PreviewCap  int    // AURA_CONTEXT_PREVIEW_CAP_BYTES (config.ToolPreviewCap)
	RunDir      string // config.RunDir — sidecar root
	SessionID   string // Event.ThreadID; sidecar dir key (D-26)
	Workspace   string // the shell workspace path, rendered into the per-turn tail hint (#52/D-41); "" omits
	UserTurns   []llm.Message
	// Classifier is the SHARED long-lived reasoning-tier classifier (anchors built
	// once, reused across turns). Production injects this via the Runner so the
	// 18-seed anchor build + Neo4j example load is amortized, not paid per turn.
	Classifier *prompt.ReasoningClassifier
	// Embedder/ExampleStore are a convenience for tests/standalone construction:
	// when Classifier is nil and Embedder is set, NewLlmAgent builds a per-agent
	// classifier. Production leaves these unset and passes Classifier instead.
	Embedder     prompt.Embedder
	ExampleStore prompt.ExampleStore
	// Breaker is the SHARED process-lifetime circuit breaker (B-05). The Runner owns
	// ONE breaker and injects it into every per-turn agent so a provider outage trips
	// cross-turn protection (a fresh per-agent breaker reset each turn and never
	// opened). nil => NewLlmAgent mints a fresh per-agent breaker (tests/standalone).
	Breaker *llm.Breaker
}

LlmAgentConfig carries the LlmAgent constructor inputs.

type PauseOption

type PauseOption struct {
	Label string `json:"label"`
	Value string `json:"value"`
}

PauseOption mirrors a tools.Option on the wire. It is redeclared here (rather than importing tools.Option into the Event model) so internal/agent/event.go stays free of any tools dependency cycle; the pause seam copies the fields.

type SwarmContextValue

type SwarmContextValue struct {
	Budget   *Budget
	Registry *tools.Registry
	Client   llm.Client
	LLMCfg   llm.Config
	ConvID   string
}

SwarmContextValue carries the parent deps the internal/swarm RunnerAdapter needs to build a swarm RunConfig: the shared Budget tree, the parent tool Registry (the adapter derives the worker registry via Without(reg, "swarm_spawn")), the LLM Client + Config the workers run against, and the conversation id that keys each worker SessionID and the transcript dir. config.Config (RunDir, swarm caps) is NOT carried here — the adapter holds it as a construction-time field, so this package never imports internal/config.

func SwarmContext

func SwarmContext(ctx context.Context) (SwarmContextValue, bool)

SwarmContext reads the parent deps the runner adapter needs off the ctx. ok is false when the key is absent (a non-swarm dispatch path), letting the adapter return a model-readable error instead of panicking.

type ToolInvocation

type ToolInvocation struct {
	Event             string         `json:"event"`
	ToolCallID        string         `json:"tool_call_id"`
	ToolName          string         `json:"tool_name"`
	Arguments         string         `json:"arguments,omitempty"`
	ArgsBytes         int            `json:"args_bytes,omitempty"`
	BatchIndex        int            `json:"batch_index,omitempty"`
	BatchSize         int            `json:"batch_size,omitempty"`
	StartedAt         *time.Time     `json:"started_at,omitempty"`
	EndedAt           *time.Time     `json:"ended_at,omitempty"`
	DurationMS        int64          `json:"duration_ms,omitempty"`
	Status            string         `json:"status,omitempty"`
	Error             string         `json:"error,omitempty"`
	ResultPreview     string         `json:"result_preview,omitempty"`
	PreviewBytes      int            `json:"preview_bytes,omitempty"`
	ResultBytes       int            `json:"result_bytes,omitempty"`
	ResultTruncated   bool           `json:"result_truncated,omitempty"`
	ResultSidecarPath string         `json:"result_sidecar_path,omitempty"`
	ExitCode          *int           `json:"exit_code,omitempty"`
	Meta              map[string]any `json:"meta,omitempty"`
}

ToolInvocation is the typed audit payload emitted around real tool execution. It is intentionally runtime-only shape, not a DB type: the Runner projects it into the append-only tool invocation ledger.

type TracerProvider

type TracerProvider interface {
	// Shutdown flushes any batched spans and releases the exporter. The caller
	// defers it on REPL exit; a bounded ctx keeps a missing collector from hanging.
	Shutdown(ctx context.Context) error
}

TracerProvider is the package-edge handle the REPL (cmd/aura chat) and Phase-9 swarm hold to flush the span batch on exit (Req#13). It hides the otel SDK type so callers at the binary edge never import go.opentelemetry.io directly.

func NewTracerProvider

func NewTracerProvider(ctx context.Context, mode, endpoint string) (TracerProvider, error)

NewTracerProvider is the exported wiring the binary edge uses to install the real exporter from AURA_OTEL_EXPORTER (D-05/D-06) and obtain a Shutdown handle. It delegates to newTracerProvider (the package-internal builder the agent loop's spans resolve against via the global provider) and returns the SDK provider as the narrow TracerProvider interface.

Directories

Path Synopsis
Package agenttest holds the shared Agent mocks (D-07) that the workflow tests (Plan 05 Sequential/Loop, Plan 06 Parallel), the CLI dry-run (Plan 07), and future Phase 3/9 all reuse — one source of truth, zero inline mock duplication (CLAUDE.md "reusable code").
Package agenttest holds the shared Agent mocks (D-07) that the workflow tests (Plan 05 Sequential/Loop, Plan 06 Parallel), the CLI dry-run (Plan 07), and future Phase 3/9 all reuse — one source of truth, zero inline mock duplication (CLAUDE.md "reusable code").
Package mcptools bridges a generic MCP server's tools into Aura's agent tool registry: it lists the server's tools and adapts each to a tools.Tool whose Execute routes through the MCP client's tools/call.
Package mcptools bridges a generic MCP server's tools into Aura's agent tool registry: it lists the server's tools and adapts each to a tools.Tool whose Execute routes through the MCP client's tools/call.
Package prompt owns the single wire-Request assembly chokepoint (PromptBuilder) and the content fingerprint (PrefixHash) that the cache-invariant gate reads.
Package prompt owns the single wire-Request assembly chokepoint (PromptBuilder) and the content fingerprint (PrefixHash) that the cache-invariant gate reads.
Package tools defines the tool interface the agent loop dispatches against, the deferred-tool flag that keeps big specs out of the default LLM manifest, and the built-in `tool_search` hook the model uses to fetch deferred specs.
Package tools defines the tool interface the agent loop dispatches against, the deferred-tool flag that keeps big specs out of the default LLM manifest, and the built-in `tool_search` hook the model uses to fetch deferred specs.
Pattern derivato da google/adk-go v1.4.0 agent/workflowagents/loopagent/agent.go (Apache 2.0).
Pattern derivato da google/adk-go v1.4.0 agent/workflowagents/loopagent/agent.go (Apache 2.0).

Jump to

Keyboard shortcuts

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