chat_agent.py is the primary entry point for all agent execution. It assembles and wires together the LLM engine, tool registry, skill loader, memory catalog, budget, approval policy, and audit logger, then delegates to AgentRunner to execute an iterative reasoning loop.
What the module guarantees:
- Task execution completeness — The agent will attempt to complete
taskor explicitly terminate with a status (completed,budget_exceeded,suspended,error). It will never return without aRunResult. - Cost tracking — The
_cost_centskey in context is incremented on every LLM call.RunResult.cost_centsreflects the actual sum across all iterations. - Audit trail — Every tool call and LLM decision is emitted to the
AuditLoggerbefore and after execution. The heartbeat (if enabled) emits periodic liveness events. - Memory auto-curation — On a
completedrun, a summary entry is written toMemoryCatalogunless an identicalauto-curatedentry exists in the last 50 entries. - Budget enforcement —
ModelDecisionEngine.decide()callsbudget.check_cost_preflight()before every LLM request. If the budget is exceeded, the call is aborted before sending tokens. - Skill loading audit — A
skill_loadevent is always recorded at the start of_run_chat_agent_impl, whether skills were found or not. - Heartbeat lifecycle — If
heartbeat_seconds > 0, theHeartbeatthread is started beforerunner.run()and unconditionally stopped in thefinallyblock. - Plan contract injection — If
context_extracontains aplan_contractdict, it is reconstructed into aPlanContractobject and set asrunner._plan_contractbefore execution begins. - Subagent registration — If
enable_subagent=Trueanddepth < max_subagent_depth, subagent tools are registered on every call to_run_chat_agent_impl.
What the method guarantees:
- Always returns a
Decision— On parse failure aftermax_parse_retriesretries, it falls back to_plain_text_answer_fallback()or returns aFinalAnswerwith a sentinel error payload. - Retry loop correctness — When JSON parse fails and retries remain, the invalid response and a repair request are appended to the message history before the next LLM call.
- Cost accumulation —
context['_cost_cents'],context['_input_tokens'], andcontext['_output_tokens']are accumulated (not replaced) on each LLM response. - Stream filtering — When
stream=Trueandstream_text_only=True, the rawon_chunkcallback is wrapped withDecisionContentStreamerto strip JSON scaffolding.
| Invariant | Location |
|---|---|
config.root is always an absolute resolved Path |
ChatAgentConfig.from_root() line 107 |
skill_source_profile='custom' without skill_search_dirs raises ValueError |
_run_chat_agent_impl() line 472 |
heartbeat is always stopped in finally, even if runner raises |
_run_chat_agent_impl() lines 621–623 |
_auto_curate_memory() is called only on result.status == 'completed' |
line 673 |
context['_cost_cents'] is always added to, never overwritten |
ModelDecisionEngine.decide() lines 238–239 |
adapter is always non-None when _run_chat_agent_impl is called (created by caller if not passed) |
Enforced by run_chat_agent() signature |
Registry is marked _registry_fresh = registry is None to prevent duplicate tool registration |
line 438 |
┌──────────────────────────────────────────────────────┐
│ run_chat_agent() / _run_chat_agent_impl() │
│ │
│ ① Build tool_registry (fresh or passed) │
│ ② Register optional tools (code analysis, subagent, │
│ git, browser, MCP trust) │
│ ③ Load project instructions + memories │
│ ④ Load skills (eager / index_only mode) │
│ ⑤ Build RunBudget and ModelDecisionEngine │
│ ⑥ Build ApprovalPolicy (with approval store) │
│ ⑦ Build AgentRunner │
│ ⑧ Start Heartbeat (if enabled) │
│ ⑨ runner.run() ──────────────────────┐ │
│ │ │
│ ┌─────────────────────────────────▼──┐ │
│ │ AgentRunner iteration loop │ │
│ │ Per iteration: │ │
│ │ a. engine.decide(context) │ │
│ │ ├── assemble prompt │ │
│ │ ├── LLM call (with retries) │ │
│ │ └── return Decision │ │
│ │ b. Execute tool calls │ │
│ │ c. Check budget/stop conditions │ │
│ └───────────────────────────────────┘ │
│ │
│ ⑩ Stop Heartbeat (finally) │
│ ⑪ _auto_curate_memory() (if completed) │
│ ⑫ Return RunResult │
└──────────────────────────────────────────────────────┘
[LLM call]
│
▼
[parse_model_decision()]
│
├── success ──► return Decision
│
└── ToolValidationError
│
├── attempt < max_parse_retries ──► append repair prompt ──► [LLM call]
│
└── attempt == max_parse_retries
│
├── _plain_text_answer_fallback() returns FinalAnswer ──► return it
│
└── return FinalAnswer(sentinel error payload)
result.status == 'completed'?
NO ──► skip
YES ──► build summary
│
├── summary empty? ──► skip
│
└── duplicate in last-50 entries? ──► skip
│
NO ──► MemoryCatalog.add(summary, tags=('auto-curated', 'run-summary'))