The runner module is the core agent execution engine. It owns the agent loop: call the decide function, process tool requests, enforce budgets and policy, record audit events, and return a RunResult.
- Every run has exactly one terminal audit event. Either
run_completed,run_failed, orrun_pausedis always recorded beforerun()returns. - Budget limits are enforced. If
max_iterationsis exceeded,run()returns with statusfailed:model_logic. If cost or tool-call budgets are exceeded,run()returns withfailed:budget_exceeded. - Cancellation is checked before each iteration. If
cancel_tokenis set,RunCancelledErroris raised and recorded asrun_failed. - Every tool call has a matching lifecycle audit event. A
tool_call_startedis always followed by exactly one of:tool_call_completed,tool_call_failed,tool_call_blocked, ortool_call_denied. - Observations are append-only within a run. The
context['observations']list only grows (or is compacted, never deleted). - The decide function receives updated context on every iteration. Cost, tokens, and compaction state from the previous iteration are merged into
contextbeforedecide()is called again. RunResultis always returned (never raises to caller). AllAgentHarnessErrorand bareExceptionsubclasses are caught, logged to audit, and converted to a failedRunResult.
- Tool execution atomicity — a tool that partially succeeds and then raises
ToolExecutionErrorwill have its partial effects visible to subsequent tool calls. - Context size — the compactor reduces observations but cannot guarantee the context fits within
max_context_tokens. - Plugin stability — plugin load failures are logged as warnings but do not abort the run.
| Invariant | Where enforced |
|---|---|
budget.max_iterations > 0 |
RunBudget.validate() called in __init__ |
cost_cents is monotonically non-decreasing during a run |
_assert_cost_budget called before and after decide() |
tool_calls counter equals len(initial_observations) + number of tool dispatches |
Initialized from len(initial_observations), incremented on every dispatch |
compaction_warning_threshold is clamped to [0.0, 1.0] |
__init__ line 90-91 |
max_context_tokens is at least 1 |
__init__ line 92 |
| Budget warning levels (50, 80, 90, 100%) fire at most once per run | _budget_warning_levels_emitted set, checked before emitting |
| Compaction warning fires at most once per run | _compaction_warning_emitted flag |
auto_mode_manager approval policy override is applied per-iteration, not globally |
Lines 387-388: self.approval_policy = auto_approve_policy inside loop |
┌──────────────────────────────────────────────────────┐
│ run() called │
│ audit: run_started │
└────────────────────┬─────────────────────────────────┘
│
┌──────────▼──────────┐
│ iteration_started │◄────────────────────┐
│ (audit recorded) │ │
└──────────┬──────────┘ │
│ │
┌────────────────▼──────────────────┐ │
│ cancel_token check │ │
│ cost budget check (pre-decide) │ │
└────────────────┬──────────────────┘ │
│ ok │
┌────────────────▼──────────────────┐ │
│ decide(context) │ │
└────┬──────────────────────┬────────┘ │
│ FinalAnswer │ ToolRequest │
│ │ │
┌──────────────▼──┐ ┌──────────────▼────────────────┐ │
│ audit: │ │ cost check (post-decide) │ │
│ run_completed │ │ file_policy.assert_allowed │ │
│ │ │ plan_validator.validate_write │ │
│ return RunResult │ │ auto_mode.validate_tool │ │
│ status=completed │ └───────────┬────────────────────┘ │
└──────────────────┘ │ ok / ToolPermissionError │
│ │
┌────────────▼──────────────────┐ │
│ approval_policy.assert_allowed│ │
└────────┬──────────────────────┘ │
│ raises ToolPermissionError │
┌────────────▼──────────────────────┐ │
│ can_request_approval? │ │
└────┬──────────────────────────┬───┘ │
yes │ │ no │
┌───────────────▼──┐ ┌─────────▼─────┐ │
│ approval_handler │ │ record_blocked │ │
│ call │ │ re-raise │ │
└──────┬───────────┘ └───────────────┘ │
│ True/False │
┌───────────▼──────────┐ False + no handler │
│ approved=True │──────────────────────────► return │
│ continue to execute │ pending │
└──────────────────────┘ approval │
│ │
┌──────────────▼────────────────────────────────────────────┐ │
│ audit: tool_call_started │ │
│ bind_parent_run_id / bind_tool_call_context │ │
│ registry.execute(tool_name, arguments) │ │
└──────────────────────┬────────────────────────────────────┘ │
│ ok / ToolExecutionError │
┌───────────▼──────────────────┐ │
│ append observation to context│ │
│ audit: tool_call_completed │ │
│ or tool_call_failed │ │
│ checkpoint_store.save │ │
│ compaction check │ │
└───────────┬──────────────────┘ │
│ │
└─────────────────────────────────────────┘
(next iteration)
Abnormal exits:
─────────────
AgentHarnessError → audit: run_failed → return RunResult(failed:category)
bare Exception → audit: run_failed → return RunResult(failed:system)
iterations == max → audit: run_failed → return RunResult(failed:model_logic)
cost_cents / max_cost_cents * 100 → warning_level
50% fired? → emit budget_warning(50)
80% fired? → emit budget_warning(80)
90% fired? → emit budget_warning(90) → if BudgetAction.PROMPT_CONFIRM → raise RunCancelledError
100% fired? → emit budget_warning(100) → if BudgetAction.SUGGEST_READ_ONLY → emit budget_read_only_suggested
Each level fires at most once per run (guarded by _budget_warning_levels_emitted).