Documentation
¶
Overview ¶
Package loop implements the ReAct (Reasoning + Acting) agent loop.
Index ¶
- func IngestRecorderFrom(ctx context.Context) func(source, content string)
- func PartialSummaryReason(final string) (string, bool)
- func WithIngestRecorder(ctx context.Context, fn func(source, content string)) context.Context
- type DeltaHandler
- type Engine
- func (e *Engine) BudgetSnapshot() budget.Snapshot
- func (e *Engine) EmitEvent(ev events.Event)
- func (e *Engine) RequestFinalization()
- func (e *Engine) Run(ctx context.Context, task string) (string, error)
- func (e *Engine) RunWithMessages(ctx context.Context, messages []llm.Message) (string, []llm.Message, error)
- func (e *Engine) SetApprover(a danger.Approver)
- func (e *Engine) SetBudgetHints(on bool)
- func (e *Engine) SetCompaction(enabled bool)
- func (e *Engine) SetDangerousConfig(cfg *danger.DangerousConfig)
- func (e *Engine) SetDeltaHandler(cb DeltaHandler)
- func (e *Engine) SetEpisodeContextFunc(ef EpisodeContextFunc)
- func (e *Engine) SetEventHandler(cb func(events.Event))
- func (e *Engine) SetEventsIncludeArgs(enabled bool)
- func (e *Engine) SetExtendedMemoryContextFunc(ef ExtendedMemoryContextFunc)
- func (e *Engine) SetInteractionMode(mode string)
- func (e *Engine) SetIterationCallback(cb IterationCallback)
- func (e *Engine) SetLimits(l budget.Limits, model string)
- func (e *Engine) SetMaxToolParallel(n int)
- func (e *Engine) SetMemoryPromptFunc(fn func() string)
- func (e *Engine) SetMessagesPersistCallback(cb MessagesPersistCallback)
- func (e *Engine) SetModel(model string)
- func (e *Engine) SetNarrator(n *narrate.Narrator)
- func (e *Engine) SetPlanStore(s *PlanStore)
- func (e *Engine) SetSideCallTimeout(d time.Duration)
- func (e *Engine) SetSignalHandler(cb SignalHandler)
- func (e *Engine) SetSkillLoader(sl SkillLoader)
- func (e *Engine) SetSkillVerbose(verbose bool)
- func (e *Engine) SetStream(on bool)
- func (e *Engine) SetThinking(thinking string)
- func (e *Engine) SetToolEventHandler(cb ToolEventHandler)
- func (e *Engine) SetUntrustedWrapper(fn func(source, content string) string)
- func (e *Engine) SetUserMessageHandler(fn UserMessageHandler)
- func (e *Engine) SideCallTimeout() time.Duration
- type EpisodeContextFunc
- type ExtendedMemoryContextFunc
- type IterationCallback
- type IterationInfo
- type MessagesPersistCallback
- type PlanChange
- type PlanState
- type PlanStep
- type PlanStore
- type PlanTool
- type SignalEvent
- type SignalHandler
- type SkillLoader
- type StepStatus
- type ToolEventHandler
- type UserMessageHandler
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IngestRecorderFrom ¶ added in v1.10.0
IngestRecorderFrom extracts the ingest recorder from ctx, if any.
func PartialSummaryReason ¶ added in v1.28.0
PartialSummaryReason classifies a final answer produced by one of the engine's budget-exhaustion paths. It returns (reason, true) when the text carries a partial-summary marker — "iteration_budget", "execution_budget", or "time_budget" — so callers (the sub-agent CLI's result contract) can set status/partial_reason without string-matching the markers themselves.
func WithIngestRecorder ¶ added in v1.10.0
WithIngestRecorder returns a context that carries fn as the active ingest recorder. Callers such as cmd/odek wrapUntrusted use IngestRecorderFrom to read it back. Using a context value removes the package-global recorder that previously caused cross-session races in the WebUI.
Types ¶
type DeltaHandler ¶ added in v1.25.0
DeltaHandler receives streamed LLM output fragments when streaming is enabled (see SetStream / docs/STREAMING.md). It is invoked synchronously from the SSE reader and must be non-blocking. Returning a non-nil error aborts generation; the loop then fails the turn with the wrapped *llm.StreamAbortedError instead of retrying.
type Engine ¶
type Engine struct {
// PromptCaching enables Anthropic prompt caching markers. When enabled
// and the LLM endpoint is Anthropic, the system prompt and first user
// message are annotated with cache_control markers, and the system
// prompt is moved to the dedicated "system" field. For non-Anthropic
// endpoints (OpenAI, DeepSeek) the markers are skipped entirely — those
// providers cache automatically or reject the Anthropic request shape.
PromptCaching bool
// MaxToolParallel controls how many tool calls run concurrently per
// iteration. 0 = use default (4). Models that support parallel tool
// calling (Claude 3.5+, GPT-4o, DeepSeek V4) can emit multiple tool
// calls in one response — this setting bounds concurrency so tools
// like read_file, search_files, and web_search run in parallel while
// avoiding resource exhaustion.
MaxToolParallel int
// Token accounting — accumulated across all iterations of the most recent run.
// Reset on each Run/RunWithMessages call and read by callers (e.g. WebUI).
TotalInputTokens int
TotalOutputTokens int
// Cache metrics accumulated across all iterations.
TotalCacheCreationTokens int // Anthropic: tokens written to cache
TotalCacheReadTokens int // Anthropic: tokens read from cache
TotalCachedTokens int // OpenAI: cached prompt tokens
TotalCacheReported bool // provider returned cache metrics at least once
// contains filtered or unexported fields
}
Engine runs the agent loop: observe → think → act → repeat.
func New ¶
func New(client *llm.Client, registry *tool.Registry, maxIterations int, systemMessage string, renderer *render.Renderer, maxContext int) *Engine
New creates a new loop Engine. maxContext is the model's maximum context window in tokens. Pass 0 for no limit enforcement.
func (*Engine) BudgetSnapshot ¶ added in v1.28.0
BudgetSnapshot implements budget.View: a point-in-time view of the run's remaining budget with the engine's cumulative token totals applied. Returns the zero Snapshot when no run is active or no limits are configured. Safe for tools to call: during a tool batch the loop goroutine is blocked, so there is no concurrent Checker access.
func (*Engine) EmitEvent ¶ added in v1.28.0
emitEvent fires a structured runtime event if a handler is configured, stamping the timestamp when the caller left it zero. Safe to call unconditionally. Run-level metadata (schema, run_id, session_id) is stamped centrally by the events.Emitter the handler is wired to. EmitEvent exposes the runtime event stream to holders of an emitter reference (view-pattern, like budget.View) — e.g. delegate_tasks surfacing child policy denials as subagent_denied events. Redaction and the non-blocking dispatch contract are inherited from emitEvent.
func (*Engine) RequestFinalization ¶ added in v1.28.0
func (e *Engine) RequestFinalization()
RequestFinalization asks the active run to conclude at the next iteration boundary: no new tool batches start and the engine produces the partial-progress summary prefixed with timeBudgetSummaryMarker instead of running to the iteration cap. Non-blocking and safe to call from any goroutine — typically a watcher on the caller's soft deadline. The flag is reset when the next run starts; a request arriving after the run finished is a no-op.
func (*Engine) RunWithMessages ¶
func (e *Engine) RunWithMessages(ctx context.Context, messages []llm.Message) (string, []llm.Message, error)
RunWithMessages executes the agent loop starting from a pre-built message history. The messages must include the system prompt (if any), all prior conversation turns, and the new user message as the last entry. Returns the final answer plus the full updated message history so callers can persist it (e.g. to a session file).
Use this for multi-turn conversations: load the session, append the new user message, call RunWithMessages, then save the returned messages.
func (*Engine) SetApprover ¶
SetApprover sets the approval gate for dangerous operations. When set and the LLM returns multiple tool calls in one iteration, a single batch approval prompt is shown. Individual tool-level approval is bypassed when the batch is approved (if the approver supports SetTrustAll).
func (*Engine) SetBudgetHints ¶ added in v1.28.0
SetBudgetHints enables budget-awareness telemetry for runs of this engine. Sub-agents enable it via the operator subagent config section; top-level runs default to off so interactive behaviour is unchanged.
func (*Engine) SetCompaction ¶ added in v1.15.8
SetCompaction enables or disables LLM-based rolling compaction of dropped context. When enabled, turn groups dropped by context trimming are summarized into a rolling digest system message instead of vanishing entirely. The digest is derived from (potentially untrusted) tool output, so it is wrapped with the engine's untrusted-content wrapper when set.
func (*Engine) SetDangerousConfig ¶
func (e *Engine) SetDangerousConfig(cfg *danger.DangerousConfig)
SetDangerousConfig provides the DangerousConfig for batch gate pre-classification. Without it, the batch gate cannot know which risk classes require approval and would skip pre-checking.
func (*Engine) SetDeltaHandler ¶ added in v1.25.0
func (e *Engine) SetDeltaHandler(cb DeltaHandler)
SetDeltaHandler sets the streamed-fragment callback (see DeltaHandler).
func (*Engine) SetEpisodeContextFunc ¶
func (e *Engine) SetEpisodeContextFunc(ef EpisodeContextFunc)
SetEpisodeContextFunc sets the optional per-turn episode search callback. When set, it is called once per new user message to search for relevant past session episodes. The returned context is injected as a system message before the LLM invocation.
func (*Engine) SetEventHandler ¶ added in v1.24.0
SetEventHandler sets the optional structured runtime event sink (schema odek.event/v1). The handler must be non-blocking — events fire inside the hot loop; odek.New wires it to a drop-on-full events.Emitter. Passing nil disables event emission.
func (*Engine) SetEventsIncludeArgs ¶ added in v1.27.0
SetEventsIncludeArgs opts tool_call_started events into carrying the raw (secret-redacted) tool-call arguments alongside the digest. Off by default: raw args can include sensitive task content, but incident review on an opt-in basis is strictly better than a stream that cannot answer "what actually ran?" once the session is gone.
func (*Engine) SetExtendedMemoryContextFunc ¶ added in v1.12.0
func (e *Engine) SetExtendedMemoryContextFunc(ef ExtendedMemoryContextFunc)
SetExtendedMemoryContextFunc sets the optional per-turn Extended Memory search callback. The returned context is injected as a system message after the legacy memory prompt block.
func (*Engine) SetInteractionMode ¶
SetInteractionMode sets how progress is surfaced. "off" suppresses all per-iteration render output except the final answer.
func (*Engine) SetIterationCallback ¶
func (e *Engine) SetIterationCallback(cb IterationCallback)
SetIterationCallback sets the iteration progress callback. If nil, no callback is fired.
func (*Engine) SetLimits ¶ added in v1.24.0
SetLimits configures hard execution budgets (odek-extension/v1): runtime, tool-call count, input/output token totals, and estimated cost. The zero value disables enforcement. Wired by odek.New from Config.Limits. The model ID is fixed per run, so per-model prices (Limits.ModelPrices) are resolved once here into the effective flat prices every cost check uses.
func (*Engine) SetMaxToolParallel ¶
SetMaxToolParallel sets the maximum concurrency for tool execution per iteration. 0 or negative = use default (4).
func (*Engine) SetMemoryPromptFunc ¶
SetMemoryPromptFunc sets the optional memory prompt callback. When set, it is called before each LLM invocation to get fresh memory content. This ensures the agent sees the latest facts even if it modifies memory during a session.
func (*Engine) SetMessagesPersistCallback ¶ added in v1.19.0
func (e *Engine) SetMessagesPersistCallback(cb MessagesPersistCallback)
SetMessagesPersistCallback sets the per-step message persistence callback. If nil, no callback is fired.
func (*Engine) SetModel ¶
SetModel updates the LLM model used by this engine at runtime. The model string must be a valid OpenAI-compatible model identifier.
func (*Engine) SetNarrator ¶
SetNarrator sets the optional narrator for engaging mode. When nil (the default), tools render in verbose mode via the Renderer.
func (*Engine) SetPlanStore ¶ added in v1.27.0
SetPlanStore wires the shared plan state (internal/loop/plan.go). The CLI layer creates one store and hands it to both the plan tool and the engine; nil (the zero behavior) disables planning end-to-end — no sync, no render, no protected-message logic, and no plan events.
Wiring the store here also registers the engine's event emitter as the store's change callback: the store knows when an effective mutation happened, the engine owns how that reaches the odek.event/v1 stream (same emitEvent path as iteration_completed). Replacing an already-wired store detaches the old one so no stale notification path survives.
func (*Engine) SetSideCallTimeout ¶ added in v1.20.0
SetSideCallTimeout sets the bound for the compaction digest and progress-summary side calls. 0 or negative restores the default (30s).
func (*Engine) SetSignalHandler ¶ added in v1.2.0
func (e *Engine) SetSignalHandler(cb SignalHandler)
SetSignalHandler sets the optional agent-loop signal callback. Passing nil disables signal emission.
func (*Engine) SetSkillLoader ¶
func (e *Engine) SetSkillLoader(sl SkillLoader)
SetSkillLoader sets the optional skill loader callback.
func (*Engine) SetSkillVerbose ¶
SetSkillVerbose controls whether skill loading shows full banners (true) or condensed markers (false, default). Condensed saves context window space.
func (*Engine) SetStream ¶ added in v1.25.0
SetStream enables SSE streaming of the main think step (docs/STREAMING.md). Requires a delta handler (SetDeltaHandler) to change anything user-visible; without one the transport still streams but nothing is displayed incrementally.
func (*Engine) SetThinking ¶ added in v1.0.0
SetThinking updates the thinking/reasoning mode used by this engine at runtime. Accepts the same values as Config.Thinking: "enabled", "disabled", "low", "medium", "high", or "" (provider default). Safe to call between RunWithMessages calls.
func (*Engine) SetToolEventHandler ¶
func (e *Engine) SetToolEventHandler(cb ToolEventHandler)
SetToolEventHandler sets the optional tool event callback for live streaming.
func (*Engine) SetUntrustedWrapper ¶ added in v1.8.0
SetUntrustedWrapper sets a function that wraps externally-sourced content (skill context, episode context) with a nonce'd boundary before injecting it into the model's system context. When nil, that content is injected directly.
func (*Engine) SetUserMessageHandler ¶ added in v1.12.0
func (e *Engine) SetUserMessageHandler(fn UserMessageHandler)
SetUserMessageHandler sets an optional callback invoked once per new user message. It is used by callers to trigger Extended Memory atom extraction.
func (*Engine) SideCallTimeout ¶ added in v1.20.0
SideCallTimeout returns the effective bound for the compaction digest and progress-summary side calls (default 30s).
type EpisodeContextFunc ¶
EpisodeContextFunc is an optional callback that the loop engine calls before each LLM invocation to discover relevant past session episodes. The callback receives the latest user input as a search query and returns formatted episode context to inject, or empty string if nothing matches.
type ExtendedMemoryContextFunc ¶ added in v1.12.0
ExtendedMemoryContextFunc is an optional callback that returns formatted Extended Memory context for the latest user input. It is injected as a system message after the legacy memory prompt block.
type IterationCallback ¶
type IterationCallback func(info IterationInfo)
IterationCallback is an optional callback invoked after each iteration of the agent loop. Used by Telegram/WebUI for progress reporting.
type IterationInfo ¶
type IterationInfo struct {
Turn int // current iteration (1-indexed)
MaxTurns int // max iterations configured
ToolNames []string // tools called this turn (duplicates possible)
InputTokens int // cumulative input tokens
OutputTokens int // cumulative output tokens
CacheCreationTokens int // cumulative cache creation tokens
CacheReadTokens int // cumulative cache read tokens
CachedTokens int // cumulative cached tokens (OpenAI)
CacheReported bool // provider returned cache metrics at least once
TotalLatency time.Duration // cumulative wall time
HasFinalAnswer bool // true when the agent reached a final answer
ReasoningContent string // LLM reasoning before tool calls (empty if none)
IsPreTool bool // true when fired BEFORE tool execution (shows reasoning + tools)
}
IterationInfo holds data about a single agent loop iteration, passed to the IterationCallback after each turn. Used for progress reporting.
type MessagesPersistCallback ¶ added in v1.19.0
MessagesPersistCallback is an optional callback invoked after each completed step of the agent loop (after a tool batch's result messages are appended, and after the final assistant message). It receives a freshly-allocated copy of the current message history so callers can persist per-turn progress; an interrupted run can then be resumed from the last completed step instead of losing the whole in-progress turn.
type PlanChange ¶ added in v1.27.0
type PlanChange struct {
Created bool // true when the mutation was a create verb (wholesale replace)
Steps int // total step count after the mutation
Done int
InProgress int
Blocked int
Pending int
Version int // store version after the mutation
}
PlanChange describes one effective plan mutation for the change notification path (see PlanStore.SetOnChange). It carries aggregate counts and the new version ONLY — never step titles or notes — so it can be mapped straight onto the minimality-constrained odek.event/v1 stream (plan_created / plan_updated).
type PlanState ¶ added in v1.27.0
PlanState is the authoritative plan. Version bumps on every mutation and is echoed in the rendered message so drift is correlatable.
func ExtractPlan ¶ added in v1.27.0
ExtractPlan parses the newest parseable plan message out of a message history. It is the shared read-only surface for `odek serve`'s GET /api/sessions/{id}/plan endpoint and the Telegram /plan_status command, so the parsing logic stays single-sourced with the engine's resume path: recognition requires role system + the "[Current plan:" prefix (isPlanMessage), each candidate goes through the same strict total parser (parsePlanState), corrupt messages are skipped fail-closed (a stale or mangled plan must never render as authoritative), and the newest parseable message wins. ok=false when no parseable plan exists.
Unlike syncPlanFromMessages this never mutates the input history and has no engine state to seed; it is safe to call on any transcript snapshot.
type PlanStep ¶ added in v1.27.0
type PlanStep struct {
ID string `json:"id"`
Title string `json:"title"`
Status StepStatus `json:"status"`
Note string `json:"note,omitempty"`
}
PlanStep is one unit of planned work. IDs are model-chosen short tokens (e.g. "s1"); they exist so updates can target steps without positional ambiguity when the list is reordered.
type PlanStore ¶ added in v1.27.0
type PlanStore struct {
// contains filtered or unexported fields
}
PlanStore holds the engine's plan behind a dedicated mutex: plan calls can arrive inside a parallel tool batch (max_tool_parallel defaults to 4), so every mutation must serialize. Caps come from resolved config values — never raw project config.
func NewPlanStore ¶ added in v1.27.0
NewPlanStore creates a store with the given resolved caps. Degenerate values fall back to the defaults above.
func (*PlanStore) Execute ¶ added in v1.27.0
Execute runs one plan tool call (the full argument envelope) and returns the model-facing result. Serialized internally; safe inside parallel batches. Every effective mutation fires the OnChange callback exactly once per call — never per-step within an atomic batch.
func (*PlanStore) Reset ¶ added in v1.27.0
func (s *PlanStore) Reset()
Reset clears the state (run start with no persisted plan).
func (*PlanStore) Restore ¶ added in v1.27.0
Restore replaces the state wholesale (restart-resume path). The caller owns validation — see parsePlanState.
func (*PlanStore) SetOnChange ¶ added in v1.27.0
func (s *PlanStore) SetOnChange(fn func(PlanChange))
SetOnChange registers an optional callback fired exactly once per effective mutation (create, or update/complete that bumped the version). Idempotent no-ops and the read-only get verb never fire it; Restore and Reset are resume-path bookkeeping, not model actions, and never fire it.
The engine registers its event emitter here at SetPlanStore time — the store knows WHEN a mutation happened, the engine owns HOW it reaches the odek.event/v1 stream. The callback is invoked while the store mutex is held so notification order always matches mutation (== version) order even when parallel tool batches race: fn must therefore be non-blocking and must not call back into the PlanStore.
type PlanTool ¶ added in v1.27.0
type PlanTool struct {
Store *PlanStore
}
PlanTool implements the built-in `plan` tool. It delegates everything to the shared PlanStore (the memory-tool pattern: the CLI layer creates one store and hands it to both this tool and the engine via SetPlanStore, so mutations are visible to the loop without any late-bound plumbing).
func NewPlanTool ¶ added in v1.27.0
NewPlanTool creates a PlanTool bound to the given store.
func (*PlanTool) Description ¶ added in v1.27.0
type SignalEvent ¶ added in v1.2.0
type SignalEvent struct {
// Type is the signal kind. One of:
// "context_trimmed" — prior message groups were dropped to fit the token
// budget (Count = groups dropped, Detail = "proactive"
// for the pre-call budget trim, "survival" for the
// post-error nuclear trim, or "margin_calibrated"
// when the safety margin tightened after the provider
// reported more input tokens than estimated)
// "tool_recovery" — a tool failed repeatedly, or the same successful
// call was repeated with identical arguments, and
// the engine injected a corrective hint so the model
// changes approach
// (Tool = failing/stalled tool, Detail = the
// correction or "repeated identical call (Nx)")
// "tool_running" — a single tool call is still executing after the
// heartbeat interval (Tool = tool name, Detail =
// human-readable elapsed, e.g. "running for 2m0s").
// Fires every interval until the call returns, so
// long-running tools no longer look like a hang.
// "budget_warning" — the run crossed 50/75/90% of its iteration or
// wall-clock budget and the engine injected a
// budget-awareness hint (Detail = threshold and
// usage, e.g. "threshold_75: 11/15 iterations")
Type string
Detail string // human-readable detail (mode, correction text, etc.)
Tool string // tool name for tool_recovery
Count int // groups dropped (context_trimmed)
Timestamp time.Time // when the signal fired (UTC)
}
SignalEvent represents an internal agent-loop signal that was previously invisible to the operator — moments where the engine silently intervened to keep the session alive or productive. Surfacing these closes observability gaps around context management and tool-failure recovery.
Not every field is set for every Type; the zero value means "not applicable".
type SignalHandler ¶ added in v1.2.0
type SignalHandler func(event SignalEvent)
SignalHandler receives agent-loop signal events. Implementations must be non-blocking — signals fire inside the hot loop.
type SkillLoader ¶
SkillLoader is an optional callback that the loop engine calls before each LLM invocation to discover contextually relevant skills. The callback receives the latest user input and returns additional system context (formatted skill content) to inject, or empty string if no skills match.
type StepStatus ¶ added in v1.27.0
type StepStatus string
StepStatus is the lifecycle state of one plan step.
const ( StepPending StepStatus = "pending" StepInProgress StepStatus = "in_progress" StepDone StepStatus = "done" StepBlocked StepStatus = "blocked" )
type ToolEventHandler ¶
ToolEventHandler is an optional callback invoked for each tool execution during the agent loop — fires before (tool_call) and after (tool_result) each tool invocation. Used by the WebUI for live streaming of tool events.
type UserMessageHandler ¶ added in v1.12.0
UserMessageHandler is an optional callback invoked once per new user message. It is used by callers (e.g. odek.New) to trigger Extended Memory atom extraction.