Agent Harness完整架构和核心模块开发指导手册 - 参考生产级Agent源码:Claude Code,OpenCode,OpenClaw和Hermess
概览
Agent Harness Dev Book(Agent Harness开发指导手册) —— 参考业内生产级Agent源码:Claude Code,OpenCode,OpenClaw和Hermes 如果有用,麻烦点个小星星,并且分享给大家下,多谢 这个定义是从技术实现角度出发,指出实现一个Agent,它需要基于大模型,需要有规划的能力,能够思考接下来做的事,需要有记忆,包括长期记忆和短期记忆,需要能使用工具,这些能力的集合体叫做Agent。这一公式的价值在于**。 1. 2022年10月提出Agent理论基石ReAct:由普林斯顿大学和谷歌研究院团队合作提出ReAct论文《ReAct: Synergizing Reasoning and Acting in Language Models》,核心是通过"Thought(思考)→Action(行动)→Observation(观察)"的闭环流程让大模型具备边推理边交互的能力。 2. 2023年6月提出Agent定义(四要素公式):此公式由由 OpenAI 应用研究负责人 在2023年6月的知名博客文章《LLM Powered Autonomous Agents》中系统阐述。 在 AI 工程领域, 被借用并定义为:**其目的是:让具有非确定性、概率性特征的大模型,能够在生产环境中表现为一个确定、可靠、可控的“工人”。 Harness 在英语中的原意是 “马具”——即套在马身上用于控制方向、承受重负、连接马车的那套皮革与金属装置。 Harness概念提出时间线: 1. 2025年11月概念首发: 在其工程博客《Effective harnesses for long-running agents》中首次将Claude Agent SDK描述为“一个强大的、通用的 Agent Harness”。 3.
README
If you find this useful, please give it a star and share it with others — thank you!
English | 中文
📕 PDF version (viewable online on GitHub): Agent Harness Explained (English) | Agent Harness详解 (中文)
2026-09 changelog (based on 20 engineering articles + deep source-code reading of 7 projects):
| # | Added / updated | Highlights |
|---|---|---|
| 1 | DSH (DeepSeek Harness) source-code supplement (folded into §1/§2/§3/§4/§5/§6) | Cordis plugin kernel, event-sourcing log, six-state TurnEndReason, two-level compaction + Spill, three-stage progressive Skills, SubAgent 6 providers, Code Mode run_code, guard pipeline (repeat-tool-reminder + timeout-policy at source level) |
| 2 | Codex (Rust) source-code supplement (folded into §1/§3/§4/§5/§6) | Thread/Turn/Item three layers, independent Approval + Sandbox layers, two-stage memory pipeline (stage-1 extraction → stage-2 consolidation + git baseline + subagents), 90%-threshold dual-scope compaction |
| 3 | Pi (minimalism) source-code supplement (folded into §1/§2/§4) | 4 tools + <1000-token system prompt, 25+ hook points, cut-point compaction algorithm, no permission system |
| 4 | Master comparison table of the six projects at the end of §1 | language / architecture / loop / tool count / system prompt / compaction / memory / self-evolution / permissions / sandbox / multi-agent / distinctives |
| 5 | §4 three major context constraints | Lost in the Middle (U-shaped curve: ~75% at beginning/end, significant mid-position drop), Context Rot, attention budget O(n²) + engineering countermeasures + core formulas |
| 6 | §3 four multi-agent collaboration patterns | Workflow/Supervisor/Hierarchical/Swarm decision matrix + two communication paradigms + five-project comparison + misuse signals |
| 7 | §7 Agent self-evolution (new chapter) | Lilian Weng’s “Harness Engineering for Self-Improvement” framework: RSI starts from the harness + progressive chain + ACE/MCE + Self-Harness/DGM + Skill/memory/RL paths + six-project comparison + seven RSI bottlenecks |
| 8 | §8 Loop Engineering | Inner/Outer loop conceptual hierarchy + cybernetic three-role mapping + three loop outcomes + phased delegation of authority L1/L2/L3 |
| 9 | §2 deterministic harness | high-risk scenario four essentials (orchestration / audit trail / short-circuit / gateway) + graded confidence L1/L2/L3 |
| 10 | §4/§5 OpenCode compaction and memory | source-level: trigger paths / threshold calculation / 8-segment summary template / FTS5 BM25 memory architecture / comparison with Claude Code |
| 11 | Structure reorganization | project-specific chapters folded into per-chapter “project comparison” sections; OpenCode content merged into §4/§5; deterministic harness merged into §2; new §7 self-evolution chapter |
Table of Contents:
- Agent definitions
- How to Build an Agent (Harness)
- 1. Agent Harness architecture
- Four-layer architecture
- Core modules & technologies of the production-grade Harness architecture
- Claude Code architecture diagram
- DSH Cordis meta-framework: five core concepts (plugin-based microkernel)
- What DSH does not have (equally important)
- Codex architecture: the Thread / Turn / Item three layers
- Pi design philosophy: minimalism
- Master comparison table of the six projects
- 2. Agent Loop (ReAct/TAOR)
- The real Agent Loop implementation
- The five-layer compaction pipeline
- Error recovery chain
- Termination conditions (10 kinds)
- Key mechanisms at a glance
- Agent Loop
- Loop state machine
- Parallel (async) tool call execution
- Streaming: act while generating
- Exit conditions: when to terminate the loop
- Retry and backoff + interruption and recovery
- Agent Loop pseudocode
- DSH event sourcing log: the “Model-visible ⟺ logged” hard invariant
- DSH Agent Loop: ReactLoopAgent + the six-state TurnEndReason
- Pi Agent Loop: flat two-layer loop + 25+ TypeScript hook points
- Deterministic harness: the four-piece toolkit for high-stakes scenarios
- 3. Multi-Agent (multi-agent orchestration)
- Claude Code built-in agents and adversarial verification (source-level)
- Core points of the engineering implementation
- Engineering implementation of task delegation
- Agent Communication Mechanism
- Parallel and Serial Orchestration
- State Tracking and Recovery
- Error Handling and Fallback Strategies
- DSH SubAgent: First-Class Seam + Cross-Product Interoperability
- The Four Multi-Agent Collaboration Patterns (Workflow / Supervisor / Hierarchical / Swarm)
- 4. Context System
- Structured Assembly of the System Prompt
- Dynamic Context Injection
- Context Compaction
- Claude Code’s Five-Layer Compaction System (Source-Level)
- Engineering Design of the Compaction Summary Prompt (
compact/prompt.ts) - Folding
- Session Pruning
- LLM Intelligent Summarization (Compaction)
- Hermes Threshold Computation and Anti-Thrash Breaker (source-level)
- Compaction Techniques Comparison Summary
- Cache Optimization: Reducing Repeated Token Consumption
- DSH Compaction: Two-Tier Plus Spill, a Third Path
- Spill Source Details (
spill/types.tsall types +cordis.patch.yml:352) - Codex Compaction Mechanism: 90% Threshold + Dual Scope
- Pi compaction: the cut point algorithm
- OpenCode compaction implementation (source-level)
- The three major constraints of Context and engineering countermeasures
- 5. Memory System
- Claude Code memdir implementation (source-level)
- Introduction
- Memory lifecycle tiers: short-term, mid-term, long-term
- How production-grade agents implement memory engineering
- Automatic Learning and Evolution
- Memory persistence
- Codex two-stage memory pipeline (the heaviest self-evolution implementation)
- OpenCode memory system architecture (source-level)
- 6. Tool Integration System (Tool System)
- 7. Agent self-evolution: from Skill to model weights (Harness Engineering for Self-Improvement)
- 7.1 The Lilian Weng framework: RSI starts from the Harness layer
- 7.2 The progressive chain: optimization targets go deeper step by step
- 7.3 Two tiers: non-parametric and parametric
- 7.4 Layer 1: Context Engineering self-evolution (ACE / MCE)
- 7.5 Layer 2: Workflow Design (AI Scientist / ADAS / AFlow)
- 7.6 Layer 3: Self-Improving Harness and evolutionary search (Self-Harness / DGM)
- 7.7 Non-parametric: dynamic Skill consolidation
- 7.8 Memory evolution: from static storage to self-managing memory
- 7.9 Parametric: the RL training loop
- 7.10 Self-evolution comparison of the six projects
- 7.11 Boundaries and risks: the seven bottlenecks of RSI
- 8. Loop Engineering
- 1. Agent Harness architecture
Agent definitions
1. Classic definition - four-element formula (2023~2025)
**Agent = Model (LLM) +Planning +Memory +Tool (tool use)**
This definition takes a technical implementation perspective: to build an Agent, it must be based on an LLM, have planning capability — the ability to think about what to do next — have memory, including long-term and short-term memory, and be able to use tools; the combination of these capabilities is called an Agent. The value of this formula is that it **was the first to systematically break down the capability boundary of an Agent**.
- October 2022: ReAct, the theoretical cornerstone of Agents, was proposed. A joint Princeton University and Google Research team published the ReAct paper ReAct: Synergizing Reasoning and Acting in Language Models; its core is the closed-loop flow of “Thought → Action → Observation” that gives LLMs the ability to interact while reasoning.
- June 2023: the Agent definition (four-element formula) was proposed. This formula was systematically articulated by OpenAI’s head of applied research Lilian Weng in the well-known June 2023 blog post LLM Powered Autonomous Agents.
2. Latest definition - Harness Engineering (2026~)
Latest formula
**Agent = Model (LLM) + Harness (infrastructure) **
In AI engineering, **Agent Harness** has been borrowed and defined as: **a complete set of deterministic, defensive engineering infrastructure wrapped around the large language model core.** Its purpose: to let a non-deterministic, probabilistic LLM behave as a deterministic, reliable, controllable "worker" in production environments.
The word “harness” originally means “horse gear” in English — the leather-and-metal rigging fitted onto a horse to control direction, bear heavy loads, and connect it to a cart.
Harness concept timeline:
November 2025, concept debut: Anthropic, in its engineering blog Effective harnesses for long-running agents, first described the Claude Agent SDK as “a powerful, general-purpose Agent Harness”.
February 2026, concept validation: OpenAI published the technical blog Harness engineering: leveraging Codex in an agent-first world, revealing the experiment in which its internal team, codenamed “Codex”, had AI Agents autonomously generate over 1 million lines of production-grade code in 5 months with only 3-7 engineers, providing strong practical validation for Harness Engineering,
Core Harness formula
Production-grade Agent=model potential−model entropy increase+Harness constraints
- The LLM is essentially “entropy-increasing”: it generates based on probability, its outputs are uncertain, and a tiny Prompt change on the input can cause huge behavioral drift.
- The Harness is essentially “entropy-reducing”: it uses deterministic code logic to fence in uncertain model outputs.
Relationship between the four-element formula and Harness
The four-element formula and Harness are not mutually exclusive, opposing concepts; they are two key cognitive leaps taking the AI Agent from **theoretical conception** to **engineering practice** — an implementation journey from design blueprint to engineering infrastructure. The Agent narrative has fundamentally shifted: from **pursuing individual capability** to **building system reliability**.
Harness Engineering
Harness Engineering is an engineering discipline focused on building "deterministic constraint boundaries" around "non-deterministic AI models". Its core goal is to turn Agent errors from "accidents" into "fixable system bugs".
February 2026, concept coined: HashiCorp co-founder Mitchell Hashimoto formally proposed and named the practice of “Harness Engineering” in his blog post My AI Adoption Journey. Its core: every time an Agent makes a mistake, engineer it into a permanent system fix so it never repeats the same mistake" - “Prompts are suggestions. Harness code is law.”.
| Concept | Core focus | Problem solved | Time range | Metaphor |
|---|---|---|---|---|
| Prompt Engineering | How to make the model understand your intent | Quality of a single output | 2022-2024 | Techniques for shouting at a horse |
| Context Engineering | How to give the model the right knowledge boundary | What information the model sees | 2025 | Showing the horse a map |
| Harness Engineering | How to keep the Agent reliable, sustained, and under control | Reliability of multi-step, long-horizon tasks | 2026- | Build a highway with guardrails and speed-limit signs |
How to Build an Agent (Harness)
Harness design should follow the principle of "start with the simplest solution and add complexity only when necessary".
The four projects compared differ fundamentally and are positioned differently: Claude Code and OpenCode are positioned as intelligent coding Agents, while OpenClaw and Hermes are positioned as general-purpose task-execution, personal-assistant-style Agents.
**Why are Claude Code, OpenCode, OpenClaw, and Hermes put together for comparison here?**
- They are all complete instances of the Agent Harness concept, each implementing the four-layer Agent Harness architecture below in its own way.
- They each embody a different stage of the Agent’s evolution from “tool” to “partner”; comparing them reveals the causal relationship between architecture and product form.
- They form a complete decision tree for technology selection, providing a reference matrix for building a custom Harness; many of their modules can be borrowed or ported.
1. Agent Harness architecture
Four-layer architecture
🖱️ Interactive version (supports zoom/search/path tracing/dark mode)
Taking the backend architecture diagram (v2.1.88) of Claude Code — currently the most mature and powerful AI Agent product — as an example, the Agent architecture can be divided into four layers:
-
Reasoning & Orchestration Layer: acts as the “brain and scheduling hub”; its core consists of two major modules, Agent Loop and multi-agent orchestration, responsible for the Agent’s core decision logic and task flow control.
- Main loop (Agent Loop): receives parsed user instructions from the support layer and starts the TAOR closed loop.
- Plan-execution decoupling: complex tasks first enter Plan Mode (e.g., generating a TodoWrite task list); after user approval they switch to Execute Mode.
- Multi-agent collaboration: when subtasks need isolation (e.g., exploring a codebase), they are delegated to a SubAgent via the
Tasktool; the SubAgent has its own context and tool set, and the main Agent continues processing once results return. - State control: monitors exit conditions (max turns, Token budget, user interruption, etc.) and triggers Checkpoint state persistence when necessary.
-
Context & Memory Layer: manages the inputs the model needs and supports the Agent’s continuous evolution; it contains the Context System and Memory System, managing the model’s input context (System Prompt, project conventions, conversation history) and the storage and evolution of short-term/long-term memory.
- Context assembly: before each reasoning round, this layer dynamically builds the System Prompt from multiple sources:
- Static parts: role definition, output conventions (placed at the front to leverage the LLM cache mechanism).
- Dynamic parts: project-level context (CLAUDE.md), current session summary, TodoWrite list, tool output summaries.
- Context compaction: if Token usage exceeds the threshold (e.g., 92%), folding or LLM intelligent summarization is automatically triggered to compact early conversation.
- Memory persistence:
- Short-term: current session messages are written in real time to SQLite and similar data stores.
- Long-term: user preferences and project conventions are analyzed in the background by AutoMemory and written to files such as MEMORY.md for later sessions to load.
- Interaction with the reasoning layer: on each loop, the reasoning layer fetches the assembled context from this layer and writes back new observations (Observe); this layer decides whether to store the raw log or save it compacted.
- Context assembly: before each reasoning round, this layer dynamically builds the System Prompt from multiple sources:
-
Tool & Secure Execution Layer: encapsulates external tool calls and builds multi-level security barriers; its core contains the Tool System and Security modules. It encapsulates external tool calls (including MCP, Skill, and SubAgent as tools) and provides multi-level security protection and sandbox isolation.
- Tool call interception: every tool request issued by the reasoning layer first enters the security system.
- Permission verification: judged based on allow/ask/deny rules, Bash AST risk classification, and an independent Auto Mode background classifier (Claude Sonnet 4.6).
- Sandbox isolation: high-risk operations (e.g., file modifications) are redirected to a Git Worktree temporary copy or a system-level sandbox (Daytona/Seatbelt).
- Actual execution: once cleared, a controlled Runtime invokes the concrete tool (MCP, Skill, and SubAgent all act as tools).
- Result return: tool output is truncated if it exceeds the Token budget; the full content is persisted locally, and only a summary or partial content is returned to the reasoning layer.
-
Support & Infrastructure Layer: provides the underlying infrastructure for stable Agent operation, covering the Session Manager, the communication system, and the Config System. It provides underlying infrastructure support, including session management, frontend-backend communication, config loading, model gateway, and observability.
- Session management: assigns a unique Session ID to the interaction, records the full conversation history, and supports later Fork/Revert.
- Config and model gateway: loads multi-level config (command line > project > user > organization) and calls LLMs through a unified Provider.
- Communication and observability: receives user requests via HTTP/SSE while enabling full-link Trace, recording Token consumption and latency for every Span.
The four layers (Reasoning & Orchestration, Context & Memory, Tool & Secure Execution, Support & Infrastructure) are not independently stacked modules but a tightly collaborating pipeline built around the “perceive → decide → act → feedback” closed loop. It is precisely because the four layers each do their own job and cooperate closely that an Agent can, like a human: remember the past (memory layer), think about the present (reasoning layer), act safely (tool layer), and keep working in a stable environment (support layer), ultimately completing the conversion from natural language to reliable behavior.
Core modules & technologies of the production-grade Harness architecture
Claude Code architecture diagram
Open-sourced in August 2026, 20K+ Stars within 3 hours. Core design “Everything is a Plugin”—even the Agent Loop itself is a plugin. Built on the embedded Cordis meta-framework (derived from the Koishi QQ bot framework, 4 years in production, 4000+ community plugins). Formally proven by the 88-page paper “A Programming Paradigm for Spatiotemporal Composability” from Peking University + DeepSeek.
DSH Cordis meta-framework: five core concepts (plugin-based microkernel)
| Concept | Description | Key mechanism |
|---|---|---|
| Plugin | Three forms: function/object/class; everything is a plugin | Registration is a side effect (ctx.effect()), unloading auto-rolls back |
| Context | Tree-derived child contexts | Each plugin owns an independent Context, derived parent to child |
| Fiber | Six-phase lifecycle state machine | PENDING → LOADING → ACTIVE → FAILED / UNLOADING → DISPOSED |
| inject | Declarative reactive dependencies | Service disappears → dependents auto-unload; restored → auto-reload |
| effect | Reversible side-effect primitive | All context changes reduce to the single ctx.effect primitive |
Five event dispatch modes: emit / parallel / serial / bail / waterfall (Koa-style middleware chain; the DSH tool pipeline pre-execute → execute → post-execute is a waterfall chain).
Paper core: effect (what is done to the environment) and coeffect (what is required from the environment) are lifted from compile-time static analysis to runtime mechanisms; the independence (permutability) theorem supports hot-unloading a single plugin; two meta-theorems: Progress (no deadlock, guaranteed termination) + Confluence (configuration reconciliation is order-independent).
Compared with VS Code: 87 of the Top100 extensions contain executable code, but a single extension cannot be unloaded at runtime—the host must restart; only 7 extensions use
extensionDependencies, and its type is any.
What DSH does not have (equally important)
❌ No vector store / no embeddings / no long-term memory consolidation / no RL / no automatic skill generation / no planned reflection
Bet: structural composability + log reconstructability, not model self-learning.
Open-sourced by OpenAI, implemented in Rust (codex-rs). OpenBench 2026.7: same model, 7 harnesses, 42 tasks — Codex completed 31 (73.8%), but with a median 94.6s per task and 117,107 tokens on a single task, both the heaviest tier — the price of a safety boundary.
Codex architecture: the Thread / Turn / Item three layers
- Thread Manager:
HashMap>maintains all sessions - Thread = one complete session (corresponds to OpenCode’s Session)
- Turn = one user interaction (including multi-step tool calls)
- Item = atomic unit such as message/tool call/reasoning
- Subagent = a child Thread spawned by the Thread Manager (forked from the parent Rollout snapshot)
Codex key differentiators
- Rust implementation: best performance, memory safety, compile-time guarantees
- Approval and sandbox (Sandbox Policy) designed as two independent layers
- Thread-level fork: subagents fork from the parent Rollout snapshot
- Heaviest but safest: spending 3x more tokens in exchange for a deterministic safety boundary
Developed by Mario Zechner (badlogic). The core is just four tools: read / write / edit / bash, with a system prompt < 1000 tokens. The website’s “What we did not build” is longer than the feature list.
Pi design philosophy: minimalism
Modern models have already been trained on massive tool call patterns; the harness just needs to step aside.
- Minimalist four tools + ultra-short system prompt = lowest token overhead
- Composio benchmark: DeepSeek V4 Flash running 30 tasks, Pi pass rate 66.7%, median cost $0.012 (lowest of the four frameworks)
- Databricks million-line codebase benchmark: about 3x less context per turn, over 2x cost difference with the same model
- Cost: no permission prompts; security responsibility is handed back to the user (you must attach your own container/sandbox)
Pi key differentiators
- Most minimalist: four tools + ultra-short prompt = lowest token overhead and cost
- Most decomposable: 25+ hook points let third parties do almost anything
- No permission system: security responsibility is handed back to the user by design
- Tree-shaped sessions: supports branch derivation; the same session can trial different approaches in parallel
Master comparison table of the six projects
| Dimension | Claude Code | OpenCode | DSH | Codex | Pi | Hermes |
|---|---|---|---|---|---|---|
| Language | TypeScript/Bun | TypeScript/Bun | TypeScript/Bun | Rust | TypeScript | Python |
| Architecture style | Modular services | Event-driven services | Plugin-based microkernel | Thread/Turn/Item | Minimalist loop | Self-evolving closed loop |
| Agent Loop | while(true) six-step pipeline | State machine + Runner | ReactLoopAgent (replaceable plugin) | Turn loop within Thread | Flat while + 25 hooks | TAOR + state machine |
| Tool count | 43+ | 16-18 | 40+ (including plugin extensions) | Depends on config | 4 (minimalist) | 40+ |
| System prompt | Dynamically assembled (static + dynamic sections) | Template per-provider | Assembly service (replaceable) | Minimal checkpoint | <1000 tokens | Frozen snapshot injection |
| Compaction trigger | Window−13K | count≥usable | pressure + overflow dual trigger | Window×90% (dual scope) | Window−16,384 | Input budget×50% |
| Compaction strategy | Four-way (micro/session/legacy/partial) | Folding + summary | Two-level + Spill | 90% threshold + dual timing | Cut point algorithm | Four-phase + anti-jitter circuit breaker |
| Memory storage | File directory + MEMORY.md index | MEMORY.md + FTS5 | No long-term memory | SQLite + git-ified workspace | None | MEMORY.md + FTS5 CJK |
| Memory retrieval | Full injection + Sonnet picks 5 | FTS5 BM25 | None | developer instruction | None | Startup freeze + FTS5 |
| Self-evolution | autoDream scheduled consolidation | None | None (bets on composability) | Two-phase pipeline + consolidation subagent | None | curator + RL dual path |
| Permission model | 6 levels + AST + classifier | Three-layer Ruleset | Guard pipeline + monotonic ToolGuard | Approval + Sandbox independent | None | Write approval gate, three states |
| Sandbox | bubblewrap | — | bwrap/Landlock/Seatbelt | Built-in sandbox policy | None | — |
| Multi-agent | 6 built-in Agents + Fork | SubAgent + Inbox | SubAgent 6 providers + Teams | Thread Manager fork | None built-in | Subagents: recursion forbidden |
| Unique traits | Most complete details, most mature ecosystem | Works out of the box, production-proven | Everything is a plugin, replaceable loop | Rust safety, heaviest self-evolution | Minimalist, lowest cost | The only self-evolving one (Skill+RL) |
2. Agent Loop (ReAct/TAOR)
Based on a line-by-line reading of the claude-code
src/query.tssource (1729 lines).
The real Agent Loop implementation
🖱️ Interactive version (supports zoom/path tracing/demo mode). The general shape of the main loop: assemble context → streaming inference → parse → loop guards → parallel execution → feed results back → continue the loop or terminate; on overflow, compact and resend.
The Claude Code Agent Loop is an AsyncGenerator (query.ts:219); its core is a while(true) loop carrying 10 mutable state fields that persist across iterations.
flowchart TD
START["query() entry"] --> LOOP[while true main loop]
LOOP --> SUB1["① Skill Prefetch (non-blocking, parallel)"]
SUB1 --> SUB2["② applyToolResultBudgettool result size budget"]
SUB2 --> SUB3["③ snipCompact (HISTORY_SNIP)"]
SUB3 --> SUB4["④ microcompactclear old results by tool_use_id"]
SUB4 --> SUB5["⑤ contextCollapseread-time projection"]
SUB5 --> SUB6["⑥ autocompactfork subagent to generate summary"]
SUB6 --> SUB7["⑦ Blocking Limit check"]
SUB7 --> LLM["⑧ callModel() streaming call"]
LLM --> STREAM["⑨ StreamingToolExecutortools run in parallel as the model streams"]
STREAM --> RESP["⑩ Response handling (withheld error recovery)"]
RESP --> TOOLS["⑪⑫ Tool collection + Abort handling"]
TOOLS --> HOOKS["⑬⑯ Post-Sampling + Stop Hooks"]
HOOKS --> RECOVERY{"⑭⑮ Errors to recover?"}
RECOVERY -- 413 --> DRAIN["collapse drain → reactive compact"]
RECOVERY -- max-tokens --> ESCALATE["8K→64K escalation → Resume directly"]
RECOVERY -- no error --> BUDGET["⑰ Token Budget"]
BUDGET -- continue --> TOOL_EXEC["⑱ Tool Execution (streaming or batch)"]
BUDGET -- done --> DONE["✅ return completed"]
TOOL_EXEC --> ATTACH["⑲ Attachments (file changes/memory/skill consumption)"]
ATTACH --> REFRESH["⑳⑲㉑ Tool Refresh / Task Summary"]
REFRESH --> MAXCHECK{"㉒ maxTurns?"}
MAXCHECK -- no --> LOOP
MAXCHECK -- yes --> STOP["return max_turns"]
style LOOP fill:#003A70,color:#fff
style LLM fill:#1565C0,color:#fff
style DONE fill:#2E7D32,color:#fff
style RECOVERY fill:#C8102E,color:#fff
The five-layer compaction pipeline
In execution order, each layer is independent and stackable. The first four layers do not call the LLM; only the fifth forks a subagent:
flowchart LR
A["① ToolResultBudgetalways ontruncate by message budget"] -->
B["② snipCompactHISTORY_SNIPhistory pruning"] -->
C["③ microcompactCACHED_MICROCOMPACTclears 8 kinds of tool results"] -->
D["④ contextCollapseCONTEXT_COLLAPSEread-time projection replay"] -->
E["⑤ autocompactfork subagentgenerates 9-section summary"]
style A fill:#E3F2FD
style B fill:#E3F2FD
style C fill:#E3F2FD
style D fill:#E3F2FD
style E fill:#003A70,color:#fff
The first four layers do not call the LLM (zero cost); if compaction gets below the threshold, the fifth layer’s autocompact automatically becomes a no-op.
microcompact whitelist (microCompact.ts:41-50; only the results of 8 tool types get cleared):
Read / Bash / Grep / Glob / WebSearch / WebFetch / Edit / Write
Error recovery chain
When the model returns an error, it is not yielded immediately but withheld; recovery is attempted before deciding:
flowchart TD
ERR["Model returns error (withheld)"] --> TYPE{"Error type?"}
TYPE -- 413 prompt-too-long --> PTL
subgraph PTL ["413 recovery chain"]
P1["First: collapse drain(cheap, keeps fine-grained context)"] --> P2{"Drained but still 413?"}
P2 -- yes --> P3["Next: reactive compact(full summary)"]
P2 -- no --> RETRY["continue retry"]
P3 --> P4{"Recovery succeeded?"}
P4 -- yes --> RETRY
P4 -- no --> SURFACE["surface error⛔ stop hooks forbidden(prevents death spiral)"]
end
TYPE -- max-output-tokens --> MOT
subgraph MOT ["max-tokens recovery chain"]
M1["First: retry with 8K→64K escalation"] --> M2{"64K also hits the cap?"}
M2 -- yes --> M3["Next: multi-turn recoveryinject Resume directly"]
M3 --> M4{"Recovery count < 3?"}
M4 -- yes --> RETRY2["continue"]
M4 -- no --> SURFACE2["yield error"]
end
TYPE -- media-size --> MED["reactive compact strip-retryhasAttemptedReactiveCompact prevents spiraling"]
style SURFACE fill:#C8102E,color:#fff
style SURFACE2 fill:#C8102E,color:#fff
style RETRY fill:#2E7D32,color:#fff
style RETRY2 fill:#2E7D32,color:#fff
Key design: after a failed 413 recovery, running stop hooks is forbidden — error → hook blocking → retry → error forms a death spiral that burns thousands of API calls.
Termination conditions (10 kinds)
| Termination reason | Condition | Code location |
|---|---|---|
completed |
No tool_use and no stop hook blocking | :1357 |
aborted_streaming |
Ctrl+C during streaming output | :1051 |
aborted_tools |
Ctrl+C during tool execution | :1515 |
max_turns |
Limit reached | :1711 |
hook_stopped / stop_hook_prevented |
Hook blocked | :1520/:1279 |
blocking_limit |
Hard block when autoCompact is off | :646 |
model_error |
Model error | :996 |
image_error |
Image error | :977 |
prompt_too_long |
413 recovery exhausted | :1175 |
Key mechanisms at a glance
| Mechanism | Description |
|---|---|
| StreamingToolExecutor | Executes completed tool_use in parallel while the model streams; on fallback, discard()+rebuild prevents orphans |
| Task Budget across compaction | remaining -= preCompactContext; sent to the server so it keeps tracking consumption after compaction |
| Fallback cleanup | yield tombstones → clear 4 arrays → discard executor → strip thinking signatures → switch models |
| Memory Prefetch | startRelevantMemoryPrefetch runs in parallel with the model stream; on consumption, filters files already read in readFileState |
| Skill Prefetch | 97% of calls found no new content — changed to non-blocking parallel, retried each iteration until consumed |
Agent Loop
The Agent Loop (agent loop) is the “heart” and core power engine inside the Agent Harness (agent harness system).
// Agent Loop pseudocode
while (done === false) {
const response = await callLLM(messages); // model inference
if (response.toolCalls.length > 0) { // model wants to call tools
const results = await executeTools(response.toolCalls);
messages.push(...results); // inject results into context
} else { // model returns plain text, task done
done = true;
return response;
}
}
Agent Loop flowchart:
graph TD
A[user input] --> B["🏗️ Support & Infrastructure layer(session management, event bus, end-to-end tracing)"]
subgraph "🔁 Agent Loop core"
direction LR
B --> C["🧠 Context & Memory layer(assemble System Prompt, load memory, compaction)"]
C --> D{"⚙️ Reasoning & Orchestration layer(TAOR loop)"}
D -- call tools --> E["🛡️ Tool & Safe Execution layer(permission checks, sandboxed execution)"]
E --> F["🧠 Context & Memory layer(inject tool results, persist)"]
F --> D
end
D -- final response --> G[user]
Loop state machine
Although the Agent Loop is essentially an infinite loop with termination conditions, real production environments cannot simply use a bare while(true); the common industry practice is to introduce a state machine to handle each loop step and manage complex transitions.
// Typical state definitions for the Agent Loop state machine
type LoopState =
| 'INIT' // initial, about to assemble context
| 'THINKING' // calling the LLM
| 'PARSING' // parsing LLM output (extract tool_calls or text)
| 'EXECUTING' // executing tools (possibly several in parallel)
| 'OBSERVING' // collect tool results, inject into context
| 'REFLECTING' // (optional) let the LLM reflect on results
| 'COMPRESSING' // trigger context compaction
| 'TERMINATED'; // done
Loop state machine flowchart:
graph TD
INIT([INIT]) -->|call LLM| THINKING
THINKING -->|stream response ends| PARSING
PARSING -->|has tool_calls| EXECUTING
PARSING -->|no tool_calls and final answer| TERMINATED([TERMINATED])
EXECUTING -->|all tools finished| OBSERVING
OBSERVING -->|if tokens exceed threshold| COMPRESSING
OBSERVING -->|continue normally| THINKING
COMPRESSING -->|resume after compaction| THINKING
Implementation notes: the state machine uses an enum + switch, combined with event-driven (Event Bus) decoupling of the phases.
Industry evolution trend: the Agent Loop is being challenged by new paradigms such as SGH (Structured Graph Harness):
The April 2026 paper “From Agent Loops to Structured Graphs” points out three structural flaws in the current mainstream Agent Loop paradigm: implicit dependencies between steps, unbounded recovery loops, and mutable execution history that makes debugging difficult. The paper proposes the SGH (Structured Graph Harness) framework, which extracts control flow from implicit context into an explicit static DAG; its core promises include: execution plans are immutable within a version, planning, execution, and recovery are separated into three layers, and recovery follows a strict escalation protocol.
Parallel (async) tool call execution
The model may return multiple tool_calls at once (e.g., reading three files simultaneously). The engineering implementation must support parallelism while respecting rate limits and dependencies (if tool B depends on A’s output, they cannot run in parallel).
Parallelism strategies:
- Parallel without dependencies: all tool calls are issued at once (Promise.all).
- Topological sort for dependencies: parse the dependencies declared by tools (via the
depends_onfield), build a DAG, and parallelize layer by layer. - Rate-limited concurrency: cap the number of simultaneous executions (e.g., at most 5) to avoid resource exhaustion.
Caveats:
- Side-effect conflicts: two tools writing to the same file simultaneously can race → file locks or serialization are needed.
- Error handling: one tool failing must not affect the others.
- Timeout control: each async tool should have its own timeout to avoid waiting forever.
// Tool parallel execution pseudocode
async function executeToolCalls(toolCalls: ToolCall[]): Promise {
const results: ToolResult[] = [];
const pending = toolCalls.map(tc => executeOne(tc));
// wait for all to finish, but catch each failure independently
const settled = await Promise.allSettled(pending);
for (const s of settled) {
if (s.status === 'fulfilled') results.push(s.value);
else results.push({ error: s.reason });
}
return results;
}
Async tool execution means: non-blocking: executing tools does not block other parts of the Agent Loop (such as receiving user interrupts or handling streaming output); concurrent: multiple tool calls can proceed simultaneously instead of waiting serially one after another; Promise/Future-based: tool execution returns an awaitable handle, and the main loop can continue other work;
Streaming: act while generating
Streaming can significantly improve user experience. The Agent Loop needs to support the hybrid streaming + tool call mode.
Technical challenge: the model may return tool_calls mid-way through streaming text (via special SSE events).
Engineering solutions:
- Double buffer: one buffer accumulates text tokens, the other accumulates tool_call deltas
- Incremental parsing: on each chunk received, try to parse whether it contains a complete tool_call
- Early termination: once the first tool_call’s complete arguments are detected, immediately stop stream reception and enter the execution phase
- Streaming text output: if there are no tool_calls, push text to the frontend in real time
// Agent Loop streaming processing pseudocode
let fullResponse = '';
let toolCalls: ToolCall[] = [];
for await (const chunk of stream) {
fullResponse += chunk.choices[0]?.delta?.content || '';
// accumulate tool_call deltas
if (chunk.choices[0]?.delta?.tool_calls) {
mergeToolCallDelta(toolCalls, chunk.choices[0].delta.tool_calls);
}
// optional: push text to the UI in real time
emit('text-delta', chunk.choices[0]?.delta?.content);
}
// once the stream ends, parse the complete toolCalls
Exit conditions: when to terminate the loop
Since the Agent Loop is essentially an infinite loop with exit conditions, and each conversation round has a clear beginning and end, explicit termination conditions must be defined to prevent dead loops or resource exhaustion.
Typical termination conditions are as follows:
| Condition | Detection logic | Handling |
|---|---|---|
| Model returns no tool_calls | Parse the LLM response; the tool_calls array is empty or of length 0 |
Terminate normally, return the final content |
| Max rounds exceeded | round >= max_rounds (usually 15~30) |
Force termination, indicate the task is incomplete |
| Token budget exhausted | total_tokens_used >= budget_limit (e.g., 200k) |
Terminate, save the current state for recovery |
| User interrupt | SIGINT received or a frontend cancel signal | Stop the current LLM request, terminate the loop |
| Safety refusal | The model outputs a safety interception flag (e.g., a refusal field) |
Terminate immediately, return the refusal reason |
| Consecutive tool call failures | The same tool fails more than max_retries times in a row |
Terminate to avoid a dead loop |
| Loop detection | A repeated tool_calls sequence is detected (e.g., calling the same tool repeatedly with no progress) | Terminate, indicate a possible logic error |
Retry and backoff + interruption and recovery
Retry and backoff
- Exponential backoff + jitter: 1s, 2s, 4s… capped at 30s, with a random delay to avoid thundering herds
- Retryable errors: 5xx, timeouts, network jitter; 4xx (auth/params) are not retried
- Implementation notes: each retry uses the same message history, without appending error messages
Interruption and recovery
- Interruption points:
THINKING:AbortControllercancels the LLM requestEXECUTING: tools supportAbortSignal; stop on timeout/cancelOBSERVING: usually not interrupted
- Recovery mechanism:
- Serialize
round,messages, andpending tool callsto the database - Load from the latest checkpoint and resume the loop
- Partially completed tool calls require idempotent design (transactions/state checks)
- Serialize
Agent Loop pseudocode
// Agent Loop pseudocode
class AgentLoop {
private messages: Message[] = [];
private round = 0;
private aborted = false;
async run(userInput: string): Promise {
this.messages.push({ role: 'user', content: userInput });
while (!this.shouldExit()) {
this.round++;
// 1. THINKING
const response = await this.callLLMWithRetry(this.messages);
// 2. if no tool_calls, done
if (!response.tool_calls?.length) {
return response.content;
}
// 3. execute tools in parallel
const results = await this.executeToolCalls(response.tool_calls);
// 4. inject results into messages
this.messages.push({
role: 'assistant',
tool_calls: response.tool_calls
});
for (const res of results) {
this.messages.push({
role: 'tool',
tool_call_id: res.id,
content: res.output
});
}
// 5. optional: compact context (if tokens exceed threshold)
await this.maybeCompress();
}
throw new Error('Loop terminated by guard condition');
}
private shouldExit(): boolean {
if (this.aborted) return true;
if (this.round > 25) return true;
if (this.totalTokens > 200_000) return true;
return false;
}
}
DSH event sourcing log: the “Model-visible ⟺ logged” hard invariant
Based on a source read of
packages/core/session/src/surface.ts(460 lines).
- An event log that is append-only, lossless JSON, with contiguous seq, backed by an in-memory store (
ctx.sessions) - Model history is projected from the log by
deriveMessages() - Invariant: anything that reaches a model request must be reconstructible from the log, enforced by runtime invariant assertions
Two SurfaceOp states (surface.ts:49-67):
// Type predicates
export function isAppendSurfaceEvent(e: SessionEvent): e is SurfaceEvent & { surfaceOp: 'append' }
export function isReplacementSurfaceEvent(e): e is SurfaceEvent & { surfaceOp: { op: 'replace' } }
Key design (surface.ts:41-47, original text):
“The model-visible surface deliberately shadows replaced ranges, so it is the wrong source for a human transcript — a landed replacement would erase durable source material; replacement copies stay model-only.”
| Operation | Human-visible | Model-visible | Purpose |
|---|---|---|---|
append |
✓ | ✓ | Normal append |
replace |
✗ | ✓ | Model-override ranges (e.g. plan-mode rewrites) |
Why model-visible is separated from human-visible: replace only affects the model view, human replay still sees the original source — the model gets to rewrite (cache hit) without losing historical traceability.
Dual persistence backends (swappable seam):
- Default
session-persistence-jsonl(JSONL + zstd compression) session-persistence-sqlite: node:sqliteDatabaseSync,SCHEMA_VERSION = 17, WAL journal- Cross-session retrieval: SQLite FTS5 full-text search (off by default), with 5 model-side read-only tools such as
session_search
DSH Agent Loop: ReactLoopAgent + the six-state TurnEndReason
Based on a line-by-line source read of
packages/core/agent-loop/src/agent.ts(515 lines).
- Hierarchy: driver loop (
kick) → turn → step (a step = one model request + the tool calls it makes) - ReactLoopAgent is registered via
ctx.agents.setFactory()— the loop itself is swappable - State machine:
idle/maintenance/running(with abort/turn/step/wakeRequested)
flowchart TD
KICK["kick()"] --> W{"while (await turn())"}
W -->|turn returns true| W
W -->|false| DONE["driver boundary"]
subgraph TURN["inside turn()"]
TS["session.append('turn/start')"] --> INNER{"while(true) step loop"}
INNER --> PRE["preStep(target, position)"]
PRE --> REJ{"decision.kind"}
REJ -->|reject| BLOCKED["turnEnds = blocked"]
REJ -->|enter| SS["session.append('step/start')"]
SS --> UMSG["append user/message × N"]
UMSG --> STEP["step()"]
STEP --> SE["session.append('step/end')"]
SE --> STICKY{"turnEnds is max-tokens?"}
STICKY -->|no| SET["turnEnds = stepEnd"]
STICKY -->|yes| KEEP["keep max-tokens (sticky)"]
SET --> EMPTY{"turnEnds && inbox.nextStep empty?"}
KEEP --> EMPTY
EMPTY -->|yes| TSTOP["dispatch.serial('agent/turn-stopping')"]
TSTOP --> BREAK["break"]
EMPTY -->|no| NEXTSTEP["target = 'next-step'"]
NEXTSTEP --> INNER
end
TURN --> TE["session.append('turn/end', reason)"]
style KICK fill:#003A70,color:#fff
style STICKY fill:#C8102E,color:#fff
style TSTOP fill:#1565C0,color:#fff
preStep’s waterfall-style decision (agent.ts:225-243):
const decision = await this.dispatch.waterfall(
'agent/pre-step',
{ messages: claimed, ...position, signal },
() => Promise.resolve({ kind: 'enter', messages }),
)
// decision.kind === 'reject' → close the turn without spending a model call
Source comment on max-tokens stickiness (agent.ts:285-290):
// max-tokens is sticky: once any step hits the ceiling, later steps
// that complete normally must not downgrade the turn outcome.
if (turnEnds === null || turnEnds.kind !== 'max-tokens') turnEnds = stepEnd
Structured errors (agent.ts:302-315):
// Every failure is structured: LlmError keeps the facts, anything else flattens to
// errorChain text + UNKNOWN code
turnEnds = signal.aborted
? { kind: 'aborted', reason: signal.reason as AgentCancelCause }
: { kind: 'error', error: error instanceof LlmError
? error.failure
: { message: errorChain(error), code: 'UNKNOWN' } }
The six TurnEndReason states:
| State | Meaning |
|---|---|
completed |
Normal completion without tool calls |
aborted |
Cancellation (cause: user / parent / hook / disposed) |
blocked |
agent/pre-step reject |
error |
Structured failure |
max-tokens |
Sticky: once any step hits the ceiling, it is not downgraded by later normal steps |
interrupted |
Orphan turn left by a crash (never emitted by the loop) |
The inbox’s two queues:
next-turn:followup()wakes the loop and opens a new turnnext-step:steer()(inserts into the current turn) andinject()(no wake-up, rides along into the next request) — “inject context without interrupting current work”
Tool-call conclusion aggregation: DSH uses OR aggregation (any single tool with concludesTurn=true produces a soft conclusion), while Pi uses AND aggregation (all tools must set terminate:true before the turn terminates).
Parallel tools: execution may complete out of order, but commits must follow model call order (the slots mechanism), guaranteeing replay/log determinism.
Pi Agent Loop: flat two-layer loop + 25+ TypeScript hook points
Based on a line-by-line source read of
packages/agent/src/agent-loop.ts(796 lines).
flowchart TD
START["runAgentLoop()"] --> INI["runLoop()"]
INI --> STEER["read steering messages(typed by the user while waiting)"]
STEER --> OUTER{"outer while(true)"}
OUTER --> INNER{"inner whilehasMoreToolCalls || pendingMessages"}
INNER --> LLM["call LLM streaming"]
LLM --> TOOLS["execute tool batch"]
TOOLS --> TERM["hasMoreToolCalls = !executedToolBatch.terminate"]
TERM --> HOOK{"shouldStopAfterTurn?()"}
HOOK -->|no| STEER2["re-read steering messages"]
STEER2 --> INNER
HOOK -->|yes| BREAK["break out of inner loop"]
BREAK --> FOLLOW["read follow-up messages"]
FOLLOW --> OUTER
style OUTER fill:#003A70,color:#fff
style INNER fill:#1565C0,color:#fff
style HOOK fill:#C8102E,color:#fff
Division of responsibilities between the two loop layers:
| Layer | Condition | Responsibility |
|---|---|---|
Outer while(true) |
— | Consumes the follow-up message queue |
Inner while(hasMoreToolCalls || pendingMessages) |
Tool calls exist, or there is pending steering | Consumes tool calls + steering messages |
Steering vs follow-up:
// read steering before the inner loop starts (user may have typed while waiting)
let pendingMessages = (await config.getSteeringMessages?.()) || []
// re-read after each tool execution round in the inner loop
pendingMessages = (await config.getSteeringMessages?.()) || []
// outer loop reads follow-up
const followUpMessages = (await config.getFollowUpMessages?.()) || []
AND aggregation of terminate (agent-loop.ts:583, personally verified):
function shouldTerminateToolBatch(finalizedCalls): boolean {
return finalizedCalls.length > 0
&& finalizedCalls.every((finalized) => finalized.result.terminate === true)
}
Compared with DSH: Pi uses AND aggregation (all tools must set
terminate: truebefore the turn terminates); DSH uses OR aggregation (any single tool withconcludesTurn=trueproduces a soft conclusion). Pi is more conservative — the turn only ends when every tool agrees to end.
Hook point inventory (getSteeringMessages / getFollowUpMessages / shouldStopAfterTurn are the three core callbacks):
| Hook | Purpose |
|---|---|
getSteeringMessages() |
Retrieve messages inserted mid-run (without interrupting current work) |
getFollowUpMessages() |
Retrieve follow-up messages after a turn ends |
shouldStopAfterTurn() |
Decide whether to stop after a turn ends |
session_before_compact |
Can cancel or replace the entire compaction result |
input / before_agent_start / tool_call / turn_end / agent_end |
25+ fine-grained control points |
- Tree-structured sessions: supports branch derivation for parallel experiments
- Full event stream (
agent_start/turn_start/message_update/tool_execution_*) pushed viaEventStream
Deterministic harness: the four-piece toolkit for high-stakes scenarios
Core tension: the LLM’s probabilistic output vs conclusions that must be deterministic.
The four pieces:
| Design | Description | Effect |
|---|---|---|
| Phased orchestration | 9-stage pipeline + goto labels | Deterministic path |
| End-to-end tracing | FlowTracer | Zero overhead when debug is off |
| Early short-circuit | IsFinal declarative list | 2-3 minutes → 20-40 seconds |
| Unified gateway | MD5 signature + Redis 100h idempotency | Retriable without duplication |
Tiered confidence: L1 model emits directly / L2 model suggests + human signs off / L3 SOP only, automatic execution forbidden
3. Multi-Agent (multi-agent orchestration)
The core of multi-agent orchestration is breaking a complex task into multiple subtasks and assigning them to multiple agents to complete collaboratively.
Claude Code built-in agents and adversarial verification (source-level)
Based on a line-by-line source read of
src/tools/AgentTool/.
flowchart TD
subgraph BUILTIN["6 built-in Agents (builtInAgents.ts)"]
GP["General-Purposetools: '*'"]
SL["Statusline SetupRead + Edit only"]
EX["Explore (read-only)Haiku · omitClaudeMd"]
PL["Plan (read-only)inherits parent model · four-step method"]
CG["Code GuideHaiku looks up official docs"]
VF["★ Verificationred/blue adversarial"]
end
subgraph GATE["Feature Gate control"]
F1["BUILTIN_EXPLORE_PLAN_AGENTS"]
F2["VERIFICATION_AGENT+ tengu_hive_evidence"]
F3["COORDINATOR_MODE→ getCoordinatorAgents()"]
end
MAIN["Main Agent"] -->|Task tool| BUILTIN
GATE -.controls.-> BUILTIN
style VF fill:#C8102E,color:#fff
style MAIN fill:#003A70,color:#fff
Verification Agent: red-vs-blue adversarial design (verificationAgent.ts)
The opening of the system prompt is the core philosophy:
“Your job is not to confirm the implementation works — it’s to try to break it.”
Two explicitly documented failure modes:
| Failure mode | Manifestation |
|---|---|
| Verification Avoidance | Finding reasons not to run the check — reading code, narrating “what I would test”, writing PASS, moving on |
| Seduced by the first 80% | Inclined to pass something with a pretty UI or green tests, missing that half the buttons don’t respond, state is lost after refresh, or the backend crashes on bad input |
Key technical constraints:
// Strictly forbidden to modify the project
=== CRITICAL: DO NOT MODIFY THE PROJECT ===
- Do not create/modify/delete any file in the project directory
- Do not install dependencies
- No git write operations
- You may write temporary test scripts to /tmp, but clean up after use
// Anti-faking: the caller may re-run your commands to spot-check
"The caller may spot-check your commands by re-running them —
if a PASS step has no command output, or output that doesn't match
re-execution, your report gets rejected."
Adaptive strategy by change type (10 types):
| Change type | Verification strategy |
|---|---|
| Frontend | Start the dev server → check for browser automation tools (mcp__claude-in-chrome__* / mcp__playwright__*) and actually use them → curl sub-resources (the HTML may return 200 while every referenced asset is broken) → run frontend tests |
| Backend/API | Start the server → curl endpoints → validate the response shape (not just status codes) → test error handling → boundary cases |
| CLI/scripts | Run with representative inputs → verify stdout/stderr/exit code → boundary inputs (empty/malformed/edge) → verify --help |
| Infrastructure/config | Syntax validation → dry-run (terraform plan / kubectl --dry-run / nginx -t) → check that env/secrets are actually referenced, not just defined |
| Bug fixes | Reproduce the original bug → verify the fix → regression tests → check side effects on related functionality |
| Mobile | clean build → install on an emulator → dump the accessibility tree → find elements by label → tap → re-dump to verify |
| Database migrations | up → verify the schema matches intent → down (reversibility) → test against existing data, not an empty database |
| Refactoring | the existing test suite must pass unchanged → diff the public API surface → spot-check behavioral consistency |
Recognizing self-rationalization (this passage is brilliant):
You will feel the urge to skip checks. These are the exact excuses
you reach for — recognize them and do the opposite:
- "The code looks correct based on my reading" — reading code is not verification. Run it.
- "The implementer's tests already pass" — the implementer is also an LLM. Verify independently.
- "This is probably fine" — probably is not verified. Run it.
- "I don't have a browser" — did you actually check mcp__claude-in-chrome__*?
- "This would take too long" — that is not your call.
If you catch yourself writing an explanation instead of a command, stop. Run the command.
One adversarial probe is mandatory before issuing PASS (BEFORE ISSUING PASS):
Your report must include at least one adversarial probe you ran
(concurrency, boundary, idempotency, orphan op, or similar) and its result
— even if the result was "handled correctly".
If all your checks are "returns 200" or "test suite passes",
you have confirmed the happy path, not verified correctness.
Adversarial probe types:
- Concurrency: parallel requests to a create-if-not-exists path → duplicate sessions? Lost writes?
- Boundary values: 0, -1, empty string, overlong strings, unicode, MAX_INT
- Idempotency: run the same mutating request twice → duplicate creation? Error? A correct no-op?
- Orphan operations: delete/reference a nonexistent ID
Reverse checks before issuing FAIL (avoid false positives):
- Whether it is already handled elsewhere (upstream validation / downstream error recovery)
- Whether it is intentional (documented in CLAUDE.md / comments / commit message)
- Whether it is non-actionable (a real limitation that breaks an external contract and cannot be fixed) → record it as an observation, not a FAIL
Design essence: the very existence of this agent is an engineering fix for the “self-assessment” flaw — an LLM evaluating its own work always “confidently praises” it, so the verifier is split out and given a set of metacognitive instructions for recognizing its own avoidance tendencies.
Runtime design of the Task tool (AgentTool.tsx, 1398 lines)
Input schema (baseInputSchema + multi-agent extensions):
{
description: string // 3-5 word task description
prompt: string // task content
subagent_type?: string // agent type
model?: 'sonnet'|'opus'|'haiku' // model override (takes precedence over agent definition)
run_in_background?: boolean // run asynchronously, notify on completion
// multi-agent extensions
name?: string // name; addressable via SendMessage({to: name})
team_name?: string // team name
mode?: permissionMode // permission mode (e.g. "plan" requires plan approval)
isolation?: 'worktree'|'remote' // isolation mode
cwd?: string // working directory override
}
Six trigger conditions for sync → async (AgentTool.tsx:567):
const shouldRunAsync = (
run_in_background === true || // ① explicit request
selectedAgent.background === true || // ② declared in agent definition
isCoordinator || // ③ coordinator mode
forceAsync || // ④ Fork subagent experiment
assistantForceAsync || // ⑤ KAIROS assistant mode
(proactiveModule?.isProactiveActive() ?? false) // ⑥ proactive mode
) && !isBackgroundTasksDisabled
Why assistant mode forces everything async (source comment): a synchronous subagent keeps the main loop’s turn open until it finishes — the daemon’s
inputQueuebacks up; when the first overdue cron catch-up spawns, it turns into N serial subagent turns blocking all user input.
Why the fork experiment forces everything async: to unify the `` interaction model — not just fork spawns; all spawns go async.
Abort semantics of background agents (AgentTool.tsx:694-696):
// Do not link the parent abort controller — background agents should survive
// when the user cancels the main thread with ESC. They are killed explicitly via chat:killAgents.
Cleanup strategy for worktree isolation (AgentTool.tsx:644-685):
flowchart TD
A["Agent finished"] --> B{"hook-based worktree?"}
B -- yes --> C["always keep(cannot detect VCS changes)"]
B -- no --> D{"has changes?"}
D -- yes --> E["keep worktreereturn path to parent"]
D -- no --> F["delete worktree + clear metadata(prevents resume pointing at deleted dir)"]
style C fill:#FFF3E0
style E fill:#E3F2FD
style F fill:#E8F5E9
Prompt cache protection on the fork path (AgentTool.tsx:610-633):
// Fork path: pass the parent system prompt AND the parent's exact tool array (cache-identical prefix).
// workerTools is rebuilt under permissionMode 'bubble'; its tool-def serialization differs from the parent's,
// which would break the cache at the first differing tool.
override: isForkPath ? { systemPrompt: forkParentSystemPrompt } : ...,
availableTools: isForkPath ? toolUseContext.options.tools : workerTools,
useExactTools: true, // inherits parent thinkingConfig and isNonInteractiveSession
Core trade-off: the worker needs its own tool pool (independent permissions), but a fork needs a cache-identical prefix (saves tokens). When the two conflict, the fork path chooses to inherit the parent’s exact tools and gives up independent permissions — because the money saved by cache hits matters more than permission isolation.
Name → agentId routing registration (AgentTool.tsx:700-712):
// register after registerAsyncAgent, so a failed spawn doesn't leave a stale entry
if (name) {
rootSetAppState(prev => {
const next = new Map(prev.agentNameRegistry)
next.set(name, asAgentId(asyncAgentId))
return { ...prev, agentNameRegistry: next }
})
}
Worktree path-transition notice (AgentTool.tsx:595-601):
// Fork + worktree: inject a notice telling the child to switch paths and re-read possibly stale files.
// Appended after the fork directive, as the latest guidance the child sees.
if (isForkPath && worktreeInfo) {
promptMessages.push(createUserMessage({
content: buildWorktreeNotice(getCwd(), worktreeInfo.worktreePath)
}))
}
Core points of the engineering implementation
- Wrap the subagent as a standard tool (
task); the main agent has no special logic - Independent context and tool set, avoiding pollution and permission overreach
- Communication mechanism: parent-child uses synchronous request-response; peers use an event bus or a file mailbox
- Parallel/serial control:
Promise.allfan-out + DAG workflows - State persistence: checkpoint each subtask’s state, supporting interruption and resume
- Timeout and error handling: an independent timeout per subagent; on failure the main agent can re-plan
The multi-agent collaboration patterns are listed below; production environments most commonly use task delegation, which is easy to implement and controllable:
| Pattern | Architecture | Applicable scenarios | Engineering complexity |
|---|---|---|---|
| Task delegation | Main agent → subagent → result returned | Task decomposes naturally, subtasks isolated | Low |
| Peer mesh | Point-to-point communication between agents, no center | Multi-role debate, distributed information gathering | High |
| Swarm | Dynamic roles, signal-triggered | Flexible exploration, emergent collaboration | High |
Engineering implementation of task delegation
Wrapping the subagent as a standard tool (Task Tool)
The main agent needs no special logic; just wrap “launch a subagent” into an ordinary tool, invoked no differently from reading or writing files.
// Agent Tool pseudocode for task delegation
// Tool definition (JSON Schema)
const taskTool = {
name: 'task',
description: 'Delegate a subtask to a specialized SubAgent',
parameters: {
type: 'object',
properties: {
subagent_type: { type: 'string', enum: ['code-explorer', 'test-generator'] },
prompt: { type: 'string' },
context: { type: 'object' } // optional, context passed to the subagent
}
}
};
// Tool executor
async function executeTask(args: TaskArgs): Promise {
const subAgent = await createSubAgent(args.subagent_type);
subAgent.setInitialContext(args.context);
const result = await subAgent.run(args.prompt);
return { output: result.finalAnswer };
}
Independent Lifecycle of a SubAgent
Each SubAgent has **its own session context, tool set, token budget, and exit conditions**. Since a SubAgent's execution flow is identical to the main Agent's (including the full lifecycle of the Agent Loop, context assembly, tool calls, compaction, and exit checks), production systems typically design SubAgents as **independent tasks that run asynchronously and non-blockingly**:
-
When the main Agent invokes a SubAgent, it issues an async wait through the
Tasktool (await task.execute()); the current main loop suspends, with a Promise underneath waiting for the SubAgent to finish; -
Supports concurrent delegation: the main Agent can launch multiple SubAgents at once (e.g.
Promise.all), running multiple isolated subtasks in parallel and aggregating results once all of them finish; -
SubAgent execution can be interrupted: the main Agent’s
AbortSignalpropagates to all running SubAgents so they can stop in time and clean up resources; -
Each SubAgent has independent timeout control (e.g. 60 seconds); on timeout it terminates automatically and returns partial results or an error summary to the main Agent.
Context Isolation
To avoid polluting the main Agent's context, a SubAgent **returns only a summary**; full logs are stored separately.
Agent Communication Mechanism
Parent-Child Communication: Request-Response
The parent Agent dispatches a task, waits for the child Agent to finish, and gets the results back. This is the simplest pattern and requires no extra communication infrastructure.
Peer-to-Peer Communication: Message Bus
When point-to-point collaboration between Agents is needed, introduce an in-memory event bus or a file-based mailbox.
// Agent communication - in-memory event bus pseudocode
class EventBus {
private subscribers = new Map void>>();
publish(topic: string, data: any) {
this.subscribers.get(topic)?.forEach(cb => cb(data));
}
subscribe(topic: string, callback: (data: any) => void) {
if (!this.subscribers.has(topic)) this.subscribers.set(topic, new Set());
this.subscribers.get(topic)!.add(callback);
}
}
// Agent A sends a message
eventBus.publish('agentB.inbox', { from: 'A', content: 'need data' });
// Agent B subscribes to its own mailbox
eventBus.subscribe('agentB.inbox', (msg) => {
// handle the message, possibly triggering Agent B's proactive behavior
});
// Agent communication - file-based mailbox pseudocode
// Sender
const inboxPath = `~/.agent/inboxes/${targetAgentId}`;
await fs.writeFile(`${inboxPath}/${uuid()}.json`, JSON.stringify(message));
// Receiver polling
setInterval(async () => {
const files = await fs.readdir(inboxPath);
for (const file of files) {
const msg = await fs.readJSON(`${inboxPath}/${file}`);
await handleMessage(msg);
await fs.remove(`${inboxPath}/${file}`);
}
}, 1000);
Parallel and Serial Orchestration
- Fan-out / Fan-in (parallel delegation)
The main Agent launches multiple SubAgents at once and waits for all of them to complete.
- DAG workflow (serial + parallel hybrid)
Complex tasks can be modeled as a directed acyclic graph and executed in topological order.
// Agent orchestration - DAG workflow pseudocode
interface WorkflowNode {
id: string;
subagent_type: string;
prompt: string;
depends_on: string[]; // IDs of the nodes it depends on
}
async function executeWorkflow(nodes: WorkflowNode[]) {
const results = new Map();
const completed = new Set();
const remaining = [...nodes];
while (remaining.length) {
const ready = remaining.filter(node =>
node.depends_on.every(dep => completed.has(dep))
);
await Promise.all(ready.map(async node => {
const result = await executeTask({
subagent_type: node.subagent_type,
prompt: node.prompt,
context: { dependencies: Array.from(results.entries()) }
});
results.set(node.id, result);
completed.add(node.id);
}));
// remove executed nodes
remaining.splice(remaining.indexOf(...ready), ready.length);
}
return results;
}
State Tracking and Recovery
- SubAgent lifecycle states:
A SubAgent generally has the following lifecycle states:
type SubAgentStatus =
| 'PENDING' // created, not started
| 'RUNNING' // executing
| 'WAITING' // waiting for an external message (peer collaboration)
| 'COMPLETED' // completed successfully
| 'FAILED' // failed
| 'TIMEOUT'; // timed out
SubAgent state transitions:
stateDiagram-v2
[*] --> PENDING: create subagent
PENDING --> RUNNING: start execution
RUNNING --> WAITING: needs external message (peer collaboration)
WAITING --> RUNNING: message received
RUNNING --> COMPLETED: finished normally
RUNNING --> FAILED: execution error
RUNNING --> TIMEOUT: execution timed out
WAITING --> TIMEOUT: wait timed out
WAITING --> FAILED: collaboration failed
COMPLETED --> [*]
FAILED --> [*]
TIMEOUT --> [*]
-
Checkpoint persistence:
Multi-Agent orchestration usually runs long-running tasks (minutes to hours) that may be interrupted by service restarts, network outages, or manual user pauses. Checkpoints ensure:
-
After an interruption, execution can resume from the latest state instead of starting over
-
Support for human-in-the-loop: the user can pause, inspect intermediate results, and continue
-
Support for debugging and auditing: replay the orchestration decision process
-
| Concern | Recommended practice |
|---|---|
| Checkpoint frequency | Persist on every state change (sync or async) |
| Storage format | JSON + SQLite, supporting event sourcing |
| Recovery validation | After recovery, verify dependency integrity and handle dangling SubAgents |
- SubAgent timeout and forced termination:
Each SubAgent should have independent timeout control.
-
A child Agent may run forever due to model infinite loops, hung tool calls, or deadlocks
-
Prevents a single subtask from exhausting the orchestration’s resources (tokens, time, money)
-
Complies with service level agreements (e.g. each subtask must finish within 60 seconds)
Error Handling and Fallback Strategies
| Failure type | Handling strategy |
|---|---|
| SubAgent internal error | Return an error summary to the main Agent, which can decide to retry or skip |
| SubAgent timeout | Terminate the child Agent and return partial results (if any) or mark it failed |
| Dependency node failure | Configurable: stop the whole workflow / skip downstream nodes / continue with defaults |
| Resource exhaustion (tokens) | Save a checkpoint and ask the user to expand the budget or simplify the task |
DSH SubAgent: First-Class Seam + Cross-Product Interoperability
Based on a source-level reading of
packages/subagent/subagent/src/index.ts(515 lines).
- 6 providers:
spawn(default) /fork(inherits the parent’s log prefix) /acp/claude-code/codex/dsh-sdk— treating competitor CLIs as subagent backends - Persistent continuable subagents:
startContinuable()+ cold recovery (restored from a persisted session) - Experimental Agent Teams: persistent roster / task board / mailbox / logs, with default limits of 8 members / 256 tasks
Fail-loud contract for provider registration (index.ts:385-400):
registerProvider(provider: SubagentProvider): () => void {
if (this.providers.has(provider.name)) {
throw new SubagentError(
`a subagent provider named "${provider.name}" is already registered`,
'DUPLICATE_PROVIDER'
)
}
// ...returns a disposer for effect teardown
}
Mandatory requirement for the Continuable capability (index.ts:456):
if (!provider.contributesContinuableChildren) {
throw new SubagentError(
`subagent provider "${provider.name}" does not support continuable children`,
'NOT_SUPPORTED'
)
}
Design essence:
startContinuablecreates persistent continuable subagents (the parent can send the next message at any moment, and the subagent cold-recovers from the persisted session). This contrasts with fork’s one-shot mode — one is lightweight (fork runs once and ends), the other is long-term collaboration (continuable runs for hours or days).
The Four Multi-Agent Collaboration Patterns (Workflow / Supervisor / Hierarchical / Swarm)
🖱️ Interactive version. An orchestration panorama of three subagent lifetimes (independent / fork / continuable) × two communication paradigms (structured reporting / async Inbox).
Three Bottlenecks of a Single Agent
- Too many tools degrade selection (20 APIs → rising error rate)
- Cross-contamination of context (intermediate results of one task interfere with another)
- One prompt serving contradictory roles (divergence vs rigor)
The Four Collaboration Patterns
| Pattern | Architecture | Use cases | Autonomy | Controllability |
|---|---|---|---|---|
| Workflow | Predefined paths in code | Report ETL, fixed approvals | Low | High |
| Supervisor | Central Agent splits dynamically | Deep research, dynamic routing | Medium | Medium-high |
| Hierarchical | Multi-level Agent tree | Large-scale collaboration + failure isolation | Medium-high | Medium |
| Swarm | Handoff relays between Agents | Customer service with shifting intents | High | Low |
Key to selection: if you pick Swarm but the underlying layer is “call–return”, Swarm is only a paper pattern.
Multi-Agent Comparison Across the Five Projects
| Project | SubAgent | Communication | Context isolation |
|---|---|---|---|
| Claude Code | Task tool + Team swarms | Parent-child structured + SendMessage | Independent messages + worktree |
| OpenCode | Actor tool + Inbox | Parent-child Future + Inbox | Independent sessions |
| DSH | 6 providers (including competitor adapters) | subagent + send_message + report | cordis Scope tree |
| Codex | Thread Manager fork | Message bus + agent-graph | Shared FS but independent sessions |
| Pi | None | — | — |
How the four patterns actually land in the five projects (source-anchored) — the mapping between textbook patterns and production implementations:
| Pattern | Implementation | Source evidence | Deviation from the textbook definition |
|---|---|---|---|
| Workflow | No native implementation in any of the five | — | Production Harnesses all bet on dynamism; fixed paths are left to outer scripts/CI |
| Supervisor | Claude Code Task tool | AgentTool.tsx: the subagent runs to completion in its own context window, and only a single final report returns to the parent as the tool result |
One-way reporting — the parent never sees the sub-process, only the conclusion |
| Hierarchical | Codex Thread Manager | HashMap>; threads can fork further (see §1 Codex architecture) |
The tree holds, but peer threads have no direct communication |
| Swarm | Claude Code SendMessage | SendMessageTool point-to-point messages + EnterWorktreeTool isolation; orchestration responsibility rests entirely on the model |
The framework provides gestures, not control flow |
| Supervisor × lifetime dual mode | DSH subagent | fork (one-shot) vs startContinuable (persistent continuable; see the DSH SubAgent section of this chapter) |
Two lifetime modes in one system rather than either-or |
Three implementation details worth calling out:
- The fork path is a controlled exception to “context isolation” (the fork branch in
AgentTool.tsx): underisForkPaththe subagent copies the parent context and keeps running rather than starting from scratch (fork-child detection at :332, and :496-497 even inheritsforkParentSystemPrompt) — suited to “expand my current line of thought” rather than “complete a task independently”. - QA nodes use the shortest possible lifetime (
verificationAgent.ts): the adversarial-verification subagent is capped atmaxTurns: 1— the check must complete within a single turn, and a rejected tool call means task failure (on Sonnet 4.6, 2.79% of calls fall back to another model for a retry). The “review layer” in hierarchical mode needs no long lifetime, only independence. - OpenCode makes subagents first-class citizens (
opencode/tool/actor.ts, 1017 lines): an Actor registry + Inbox table (a fork-specific DB table) decouples sending from receiving — the parent continues its own loop right after sending; results arrive asynchronously in the Inbox, and the actor registry manages waiting and wakeup. This is a controllable variant of Swarm: point-to-point messages exist, but go through persistent mediation rather than a direct connection.
4. Context System
The context system is the Agent's "working memory", responsible for dynamically assembling the context before each LLM call and maximizing information density within the token budget.
Structured Assembly of the System Prompt
Why Structure Is Needed: Static and Dynamic Zones
LLM context windows are limited, and some model providers support **prompt caching**, where rarely changing prefix content is cached and reused to reduce latency and cost. Therefore the System Prompt must be split into a **static zone** and a **dynamic zone**:
- Static zone: role definition, output format, tool schemas, general rules — rarely changes, cacheable
- Dynamic zone: current time, user info, temporary instructions, environment variables — may differ per request, placed after the cached zone
How the Cache Works
The Anthropic API lets you add a `cache_control` field to a text block in a message's `content` array. The API caches that block and all preceding blocks. If a subsequent request has an **identical prefix**, it hits the cache, is not billed for input tokens, and responds faster.
[static block 1] (cache_control) → static block 2 → static block 3 → dynamic block
↑
caching starts here (including earlier blocks)
OpenAI's prompt caching works slightly differently: it automatically caches the request prefix with no explicit marking. The principle is the same, though: **static content first, dynamic content last**.
Therefore the **static part must be contiguous and come first**, with the dynamic part appended after it.
Engineering Implementation Notes
- The static part (role definition, output conventions, tool schemas) barely changes and can be cached server-side by the LLM provider
- The dynamic part (time, environment variables, real-time user info) is appended at the tail each time to avoid breaking the cached prefix
Dynamic Context Injection
Dynamic context means fetching information from external sources (file system, database, runtime state) before each LLM call and injecting it into the context.
Project-Level Context (CLAUDE.md / AGENTS.md)
At startup, the Agent reads config files from the project root and injects project conventions, tech stack, common commands, and so on.
Purpose:
-
Lets the Agent know the project structure, coding conventions, common scripts, dependencies, and more
-
Avoids re-explaining project background in every conversation
-
Can be version-controlled with the project and shared across the team
Project context usually goes in the System Prompt’s dynamic zone (because it can change, though rarely; alternatively it can go at the tail of the static zone).
Conversation History Injection
Conversation history is the Agent's **short-term memory**; each message carries a role, content, tool call information, and metadata.
Tool Output Truncation and Persistence
Tool output can be huge (e.g. reading an entire file) and cannot go into the context in full. **Strategy: truncate to the token limit and save the complete content to external storage.**
// Tool output truncation and persistence pseudocode
class ToolOutputManager {
private readonly MAX_TOOL_OUTPUT_TOKENS = 25000; // Claude Code default
async processToolOutput(rawOutput: string): Promise {
const tokenCount = await this.countTokens(rawOutput);
if (tokenCount <= this.MAX_TOOL_OUTPUT_TOKENS) {
return rawOutput;
}
// truncate
const truncated = await this.truncateToTokens(rawOutput, this.MAX_TOOL_OUTPUT_TOKENS);
const fullOutputId = uuid();
// persist the full content
await this.storage.save(`tool_outputs/${fullOutputId}`, rawOutput);
// return truncated content + reference
return `${truncated}\n\n[Output truncated. Full content saved to ${fullOutputId}]`;
}
// truncate precisely by tokens (using tiktoken or a similar library)
private async truncateToTokens(text: string, limit: number): Promise {
const encoder = await getEncoder('cl100k_base');
const tokens = encoder.encode(text);
if (tokens.length <= limit) return text;
const truncatedTokens = tokens.slice(0, limit);
const truncated = encoder.decode(truncatedTokens);
return truncated + '…';
}
}
Context Compaction
🖱️ Interactive version. The generic compaction decision pipeline: upfront threshold check (thresholds differ across the six projects) → pre-compaction → summary generation (choosing cut points) → context reassembly → resend after compaction; provider overflow errors take the after-the-fact remediation branch.
The context window is one of the most precious resources in an Agent system. When conversation history accumulates close to the window limit, compaction becomes necessary. Mainstream compaction techniques fall into three kinds: **folding**, **pruning**, and **LLM-powered summarization (compaction)**. Each has trade-offs, and they are usually combined.
Claude Code’s Five-Layer Compaction System (Source-Level)
Based on a line-by-line reading of the
src/query.ts+src/services/compact/sources. This is what production actually looks like — not a choice of three, but five progressive layers + a reactive fallback.
flowchart TD
MSG["context accumulation"] --> L1
subgraph PROACTIVE["proactive compaction (before each LLM call)"]
L1["① applyToolResultBudgettool result size budget (always on)"]
L2["② snipCompacthistory pruning (HISTORY_SNIP)"]
L3["③ microcompactclears 8 kinds of tool results (CACHED_MICROCOMPACT)"]
L4["④ contextCollapseread-time projection (CONTEXT_COLLAPSE)"]
L5["⑤ autocompactfork subagent to generate 9-section summary"]
end
subgraph REACTIVE["reactive fallback (after API errors)"]
R1["collapse drain(cheap, keeps fine-grained)"]
R2["reactiveCompact(full summary)"]
R3["surface errorstop hooks forbidden"]
end
L1 --> L2 --> L3 --> L4 --> L5
L5 -->|below threshold| OK["continue LLM call"]
L5 -->|still over limit| API["call API"]
API -->|413| R1 -->|still 413| R2 -->|recovery failed| R3
style L1 fill:#E3F2FD
style L2 fill:#E3F2FD
style L3 fill:#E3F2FD
style L4 fill:#E3F2FD
style L5 fill:#003A70,color:#fff
style R3 fill:#C8102E,color:#fff
Key design points:
| Layer | Technical details | Source location |
|---|---|---|
| ① ToolResultBudget | Message-level budget on tool result sizes, running before microcompact (cached MC only operates by tool_use_id and never inspects content, so the two don’t conflict) | query.ts:379 |
| ② snipCompact | Returns snipTokensFreed to pass to autocompact, because tokenCountWithEstimation cannot see the amount freed by snips |
query.ts:403 |
| ③ microcompact | Whitelist mechanism that clears the results of only 8 tools | microCompact.ts:41-50 |
| ④ contextCollapse | Read-time projection via commit log replay. Runs before autocompact — if collapse has already pushed the context below the threshold, autocompact becomes a no-op, preserving fine-grained context instead of a single summary | query.ts:441 |
| ⑤ autocompact | The fork subagent reuses the main session’s prompt cache (experiments show a 98% miss rate without cache sharing, at a cost of ~38B tok/day) | compact.ts:435 |
The microcompact whitelist (microCompact.ts:41-50):
const COMPACTABLE_TOOLS = new Set([
FILE_READ_TOOL_NAME, // Read
...SHELL_TOOL_NAMES, // Bash
GREP_TOOL_NAME, // Grep
GLOB_TOOL_NAME, // Glob
WEB_SEARCH_TOOL_NAME, // WebSearch
WEB_FETCH_TOOL_NAME, // WebFetch
FILE_EDIT_TOOL_NAME, // Edit
FILE_WRITE_TOOL_NAME, // Write
])
Only the historical results of these 8 tools are cleared in place as
[Old tool result content cleared]. Images are uniformly estimated at 2000 tokens.
Engineering Design of the Compaction Summary Prompt (compact/prompt.ts)
Anti-tool-call preamble (NO_TOOLS_PREAMBLE) — must be placed at the very front:
CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- Tool calls will be REJECTED and will waste your only turn — you will fail the task.
Why at the very front: the cache-sharing fork path inherits the parent session’s full tool set (required for cache-key matching), and on Sonnet 4.6+ adaptive-thinking models the model sometimes ignores the weaker trailing instructions and attempts tool calls. With
maxTurns: 1, a rejected tool call means no text output at all → falling into streaming fallback (2.79% on 4.6 vs 0.01% on 4.5).
The 9-section summary template:
| # | Section | Content |
|---|---|---|
| 1 | Primary Request and Intent | All explicit user requests and intents |
| 2 | Key Technical Concepts | Key technical concepts and frameworks |
| 3 | Files and Code Sections | Specific files and code segments, with full code snippets |
| 4 | Errors and fixes | Errors encountered and how they were fixed (especially user feedback) |
| 5 | Problem Solving | Problems solved and investigations in progress |
| 6 | All user messages | All user messages (not tool results) |
| 7 | Pending Tasks | Outstanding tasks |
| 8 | Current Work | The specific work in progress immediately before the summary request |
| 9 | Optional Next Step | Next step (must align directly with the user’s most recent request) |
**`` + `
> `` is the **draft scratch area**, stripped and discarded by `formatCompactSummary()` once the summary is written — it only improves summary quality and has no informational value itself.
**Three prompt variants**:
| Variant | Applies to | Characteristics |
|---|---|---|
| `BASE_COMPACT_PROMPT` | Full compaction | Scope is "the conversation" |
| `PARTIAL_COMPACT_PROMPT` | Partial compaction (from) | Scope is "recent messages" |
| `PARTIAL_COMPACT_UP_TO_PROMPT` | Partial compaction (up_to) | Summary placed **before** the retained messages, includes a "Context for Continuing Work" section |
**Resume prompt** (`getCompactUserSummaryMessage`) — the continuation instruction when `suppressFollowUpQuestions` is set:
Continue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, do not preface with “I’ll continue” or similar. Pick up the last task as if the break never happened.
> **The subtlety**: it explicitly forbids pleasantries like "acknowledge the summary" and "I'll continue" — preventing the model from treating the resume as the start of a new conversation.
**Post-compaction state restoration** (`compact.ts:517-585`):
```mermaid
flowchart LR
C["Compaction complete"] --> F["Restore up to 5recently read files(total budget 50K, 5K per file)"]
F --> S["Restore invoked skills(total 25K, truncated at 5K per skill)"]
S --> P["Re-inject plan mode instructions"]
P --> A["Re-inject agent list"]
A --> M["Re-inject MCP instructions"]
M --> D["Re-inject deferred tools delta"]
style C fill:#003A70,color:#fff
Key point:
sentSkillNamesis deliberately not reset — re-injecting the full skill_listing (~4K tokens) after compaction is pure cache_creation with low marginal benefit; the model still holds the SkillTool schema, and theinvoked_skillsattachment preserves the content of skills already used.
Folding
The essence of folding is **reversible compaction**: a stretch of completed history messages is packed into a single summary, while the raw data remains fully preserved in external storage. When the model needs to inspect details (e.g., the user asks "how did you make those changes a few turns ago"), it can "expand" to restore them and re-inject them into context.
-
Core characteristics:
It does not modify the original message array; it only dynamically substitutes a summary view before the LLM request, leaving the session history itself unchanged.
-
Folding strategies:
-
Fold by turns: fold once every 10 conversation turns
-
Fold by token threshold: when accumulated tokens exceed a preset value (e.g., 50k), fold away the earliest portion
-
Fold by time: conversation older than 30 minutes is folded automatically
-
-
Claude Code’s “folded view”:
Claude Code’s folding is a lightweight pre-request preprocessing, not a persisted compaction mechanism.
Its implementation:
-
Does not modify the original message array: session history stays intact on disk
-
Only affects the request view: when issuing a request to the LLM, it dynamically builds a “summary view”
-
Decision basis: detects the task phase and folds the intermediate process of “clearly completed tasks” into conclusive descriptions
-
Typical output: “Issue located and tests passed”, hiding the detailed tool call transcripts and intermediate outputs
The cost of this mechanism is extremely low (no LLM calls), but the compaction ratio is limited — it only folds the “clearly completed” parts and does not intervene in tasks still in progress.
-
-
OpenCode’s “placeholder folding”:
OpenCode itself has no built-in “folding” concept, but the
@tarquinen/opencode-dcpplugin implements similar semantics.Core mechanism:
-
Session history is never modified: the original message array stays unchanged
-
Pre-request placeholder substitution: content to be folded is replaced with a
[Pruned: ...]placeholder -
Prompt cache impact: placeholder substitution breaks the cache prefix, but the token savings and reduced context poisoning are usually worth more
The folding operation is decided autonomously by the model: the
compresstool is exposed to the model, which can decide on its own when to fold what based on task completion status, rather than a static trigger.Advantage: the model holds the initiative and can fold at task completion milestones; it supports nested compaction (when a new compaction overwrites an old one, the old summary is nested rather than overwritten), so information gradually refines rather than dilutes across multiple compactions.
-
| Dimension | Claude Code | OpenCode (DCP plugin) |
|---|---|---|
| Trigger | Automatic system judgment (based on task completion detection) | Model actively calls the compress tool |
| Cost | Extremely low (no LLM calls) | Medium (requires an LLM to generate the summary) |
| Compaction ratio | Low (only hides intermediate process, keeps conclusions) | Medium (replaced with technical summaries) |
| Raw data retention | Fully preserved, expandable | Fully preserved, via placeholder substitution |
| Cache impact | None | Breaks the cache prefix (but token savings are usually worth more) |
Session Pruning
Pruning is a form of **irreversible compaction**: redundant, outdated, or invalid messages are deleted outright, freeing context space. Unlike folding, pruned messages **cannot be recovered**.
-
Core idea:
Not all history messages are of equal value. Certain messages (e.g., duplicate tool call results, outdated intermediate states, user confirmation messages) can be safely deleted without affecting comprehension of the subsequent conversation.
Then there is pruning. When actually performing pruning, Hermes adopts a strategy similar to OpenClaw’s: “keep head and tail, summarize the middle”:
1. Head protection: keep system instructions, the initial task definition, and other key guiding information.
2. Tail protection: keep the most recent few conversation turns to ensure the coherence of short-term memory.
3. Middle compaction: prune the verbose tool call processes and reasoning steps in the middle, and use an LLM to generate a refined summary (Summary) to replace the original details.
- Message types eligible for pruning:
| Message type | Pruning reason | Example |
|---|---|---|
| Duplicate tool output | The same tool was called multiple times; earlier output has already been superseded by later calls | Two consecutive read_file calls reading the same file |
| Outdated system prompt | The prompt contains time-sensitive information (e.g., “today is Monday”) that is no longer valid | One-time instructions |
| User confirmation messages | The user only replies “OK”, “continue”; no new information | Contains no decisions or new context |
| Tool call errors | The error has been handled and is no longer needed downstream | File not found, then switched to another path |
| Intermediate computation results | The final result has been derived; intermediate values are useless | Temporary variables in iterative computation |
-
Claude Code’s pruning implementation:
Claude Code’s pruning is embodied at two levels: tool result budgeting (Tool Result Budget) and microcompaction (Microcompact).
-
Tool result budgeting:
This is the cheapest layer in the five-tier compaction chain:
- Budget-limits each tool’s output immediately after execution (e.g., truncation, key information extraction)
- Uses disk persistence + string substitution, requiring no LLM calls at all
- Truncates thousands of lines of Bash command output, keeping the head and tail
- MCP tool output defaults to a 25,000-token cap; content over the cap is stored externally and only a summary enters the context
-
Microcompact:
Focuses on clearing already-processed tool results, with two trigger modes:
-
Time trigger: after the session idles for more than 60 minutes, only the latest 5 tool results are kept (“cold session”)
-
Cache trigger: leverages the Anthropic API’s prompt caching, dynamically evicting old results via parameters
Microcompact costs almost nothing because it makes no LLM calls — only content clearing or
cache_editsoperations.
- Session memory compaction (trySessionMemoryCompaction)
-
Trigger condition: feature enabled + memory files (MEMORY.md etc.) exist and are non-empty
-
Compaction method: compresses context using pre-stored memory files instead of generating summaries in real time
-
Cost: medium (no LLM calls, but requires reading and formatting memory files)
-
Priority: tried first when compaction is needed; only if the context still exceeds the threshold after compaction does it fall through to traditional compaction
In terms of engineering, Claude Code maintains memory files such as
MEMORY.mdunder the.claude/directory; session memory compaction achieves compaction by reading these pre-generated memory files, avoiding the cost of real-time LLM calls.
LLM Intelligent Summarization (Compaction)
Intelligent summarization is **lossy compaction**: an LLM is invoked to convert a stretch of conversation history into a refined natural-language summary, and this single summary message replaces the original multiple messages. Unlike folding, summarization is irreversible; the original messages are no longer kept (unless stored separately for audit).
-
Core idea:
The LLM itself is good at information extraction and compression. Through carefully designed prompts, the LLM can be made to extract the key information from a conversation (decisions, facts, open questions, code changes, etc.) and discard repetitive or trivial details.
Core trade-off: high compaction ratio vs. information loss. Research shows summarization can reach a 99.3% compression rate (OpenAI opaque compaction), but multi-session information retention is only 37%, with about 2/3 of facts distorted or lost. The most damaging losses: precise details such as file paths, error messages, and line numbers are easily rewritten into vague descriptions by summaries.
-
Claude Code’s LLM summarization implementation:
Claude Code’s LLM summarization is embodied at two levels: context collapse (Context Collapse) and auto-compaction (Auto-Compact).
-
Context collapse
Located at the fourth level of the five-tier compaction chain (after microcompact, before auto-compact), this operation modifies the message array, meaning the original information is replaced — a form of lossy compaction:
-
Trigger condition: tokens still exceed the threshold after microcompact
-
Implementation: importance-scores the conversation and invokes the LLM to generate structured summaries of the low-priority portions
-
Characteristics: more aggressive than microcompact, more fine-grained than a full summary (only summarizes the “low-value” parts)
-
In Claude Code’s LLM summary truncation, the difference between context collapse and its “folded view”: the “folded view” is a lightweight, zero-cost logical view that only affects presentation, while “context collapse” is a costly, history-modifying technical step in the compaction pipeline used to free context.
Dimension “Folded view” “Context collapse” Essence Logical view Physical compaction Calls the LLM No Yes Implementation Modifies the pre-request view Modifies the underlying message array Reversible Yes No Cost Extremely low High They correspond exactly to the two different kinds of “folding” in the Agent Harness architecture: one saves tokens in the tool and safe-execution layer through view processing, while the other actively compresses context in the reasoning and orchestration layer by invoking the model. They are engineering strategies of the Claude Code system deployed at different stages under different pressures.
-
Auto-compact (Auto-Compact)
-
Trigger threshold: context usage reaches 95% (~190K/200K tokens)
-
Available space: the 200K window must deduct the system prompt (~20K), MCP tool schemas (0.9K-51K), CLAUDE.md (0.3K-2K), MEMORY.md (~200 lines), and the output reservation buffer (~10K); actually usable is about 114K tokens
-
Three-phase flow: ① tool output clearing (the largest consumer, 60-80%); ② structured summary generation (7,000-12,000 characters); ③ CLAUDE.md re-injected from disk
-
Summary structure: contains sections such as “Completed analyses”, “Modified files”, “Key decisions”, “Pending tasks”
-
-
Manual compaction (/compact command)
Users can proactively trigger it at task completion milestones, avoiding being interrupted mid-task by automatic compaction:
-
Execution flow: clear old tool outputs → structured summary generation → session continues from the compacted state
-
Key advantage: the user can attach compaction instructions (e.g., “/compact preserve all file paths, test results, and the current debugging hypothesis”)
-
CLAUDE.md survives: all CLAUDE.md files are reloaded from disk after compaction, ensuring key instructions are not lost
-
-
OpenCode’s LLM summarization implementation:
OpenCode’s built-in auto-compaction mechanism:
-
Available context computation: model context window - output reservation (32K) - safety buffer (20K)
-
Compaction trigger: if the token count exceeds the usable cap, a
CompactionPartmarker is inserted into the user message, queuing summarization -
Post-compaction handling: after the summary is generated, user messages are replayed to maintain the conversation flow
-
Hermes Threshold Computation and Anti-Thrash Breaker (source-level)
Based on source reading of
hermes-agent/agent/context_compressor.py(5,077 lines).
Threshold computation formula (_compute_threshold_tokens):
effective_window = context_length - (max_tokens or 0) # subtract output reserve
pct_value = int(effective_window * threshold_percent) # default 50%
floored = max(pct_value, MINIMUM_CONTEXT_LENGTH) # floor protection
# Key: the floor must not eat the output reserve; bound by the 85% cap
trigger_cap = int(effective_window * 0.85)
if floored > pct_value and floored > trigger_cap:
floored = max(pct_value, trigger_cap)
# Percent >= window is unreachable -> trigger at 85% instead
if floored >= effective_window:
return max(1, min(trigger_cap, effective_window - 1))
Why these two layers of protection are needed (verbatim source comments):
| Bug | Scenario | Problem |
|---|---|---|
| #14690 | 64K local model | max(0.5*64000, 64000) == 64000 → threshold equals the entire window → auto-compaction never triggers, because the provider rejects before usage reaches 100% |
| #43547 | Custom provider with max_tokens=65536 | The input budget is far smaller than the raw window; a full-window threshold makes the session hit a provider 400 before compaction triggers |
The source comments specifically mention ollama: it silently trims over-window prompts and never throws an overflow backstop, leaving the session stuck — hence the 85% cap backstop is a must.
Anti-thrash breaker (_tripped):
def _tripped(self) -> bool:
"""Anti-thrash breaker: two consecutive ineffective compactions or two consecutive fallback summaries."""
return (self._ineffective_compression_count >= 2
or self._fallback_compression_streak >= 2)
Four blocking reasons (_compression_block_reason):
| Reason | Meaning |
|---|---|
cooldown: |
Cooldown after the last summary failure |
structural_backoff: |
Backoff for a structural no-op (nothing compressible) |
ineffective |
The breaker has already tripped |
Design essence: Hermes has the deepest defenses among the four projects — it assumes compaction will fail and prepares an independent state machine for each failure mode (cooldown/backoff/breaker). It also distinguishes a “structural no-op” (nothing compressible, should not count as failure) from a “real failure”, avoiding a mis-tripped breaker.
Compaction Techniques Comparison Summary
| Technique | Core principle | Reversibility | LLM cost | Typical use case |
|---|---|---|---|---|
| Folding | Summary replacement, raw data retained | Reversible | Low | Folding the intermediate process of completed tasks |
| Pruning | Deleting redundant/outdated content | Irreversible | Zero | Duplicate tool calls, overwritten writes, error cleanup |
| LLM summarization | LLM generates a structured summary | Irreversible | High | Final fallback compaction when tokens are tight |
Cache Optimization: Reducing Repeated Token Consumption
In large-model agent systems, every API call involves transferring large amounts of repeated content, especially the system prompt, tool definitions, project configuration files, etc. Cache optimization aims to reduce these repeated costs, lowering latency and expense.
System Prompt Cache
Why caching is needed:
The system prompt usually contains large amounts of static content, such as role definitions, output format requirements, general conventions, and the tool list & schemas. These barely change across requests yet consume large numbers of input tokens.
Cache principle:
For example, the Anthropic API supports **Prompt Caching**, allowing specific text blocks in a message's `content` array to be marked with `cache_control`. The API caches that block and all preceding text blocks. If a subsequent request's **prefix is exactly identical** (byte-level match), the cache is hit.
OpenAI's Prompt Caching is relatively simplified: **automatic caching** — no explicit markers needed; the API automatically caches the request prefix (including the system message and the beginning of messages), but the prefix must match exactly (identical string).
Caveats:
- Cache hit rate depends on prefix stability: any modification to the static portion (including spaces and newlines) invalidates the cache. Keep formatting consistent when using template strings.
- Push dynamic content to the end: place timestamps, random IDs, ad-hoc user instructions, etc. after all static content.
- Monitor cache hit rate: obtain cache hit information from API response headers (e.g.,
anthropic-ai-cache-hit) to optimize the static-portion design. - Multi-turn reuse in long sessions: the same system prompt can reuse the cache across multiple LLM calls within one session; the benefits are significant.
Tool Schema Cache
Why caching is needed:
Tool definitions (JSON Schema) are usually large — each tool includes a name, description, and parameter structure; multiple tools combine into an array that may total 5K-50K tokens, and every request must serialize the full tool list into a string and send it.
Although this content usually sits in the system prompt, every request re-serializes it (converting tool objects into JSON strings), which itself has CPU overhead. **Caching avoids repeated serialization**.
Optimizing the serialization format:
Beyond caching, the serialization format can be optimized:
- Minified JSON: remove unnecessary spaces and newlines (use
JSON.stringify(obj)rather than the formatted version) - Use YAML: for some APIs (e.g., Anthropic), YAML may be more compact than JSON
- Compress tool descriptions: trim verbose
descriptionfields, keeping key information
Synergy with Prompt Caching:
Tool schemas usually sit in the static region of the system prompt, and are therefore cached server-side by the LLM service together with the system prompt. Client caching solves **avoiding repeated serialization and network-transfer preparation**, while server-side caching solves **avoiding repeated computation and billing**; the two work in synergy.
Project-Level Context Cache
Why caching is needed:
On startup, the agent needs to read configuration files at the project root (e.g., `CLAUDE.md`, `AGENTS.md`). These files are relatively stable (low change frequency), can be large (thousands to tens of thousands of characters), and may be read multiple times within a single session (e.g., reloaded after every compaction); frequent disk I/O slows down responses, especially in hot-reload scenarios (taking effect immediately when a file changes). Hence an in-memory cache is needed.
Relationship with system prompt caching:
Project-level context is usually injected as the dynamic part of the system prompt (since it may update as files change). For files that change extremely rarely (e.g., `CLAUDE.md`), they can be placed at the tail of the static region; for files the user edits frequently, the dynamic region is more appropriate.
// Project-level context cache: hot-reload cache pseudocode based on file watching
import chokidar from 'chokidar';
class HotReloadProjectCache {
private cache: Map = new Map();
private watchers: Map = new Map();
private callbacks: Array void> = [];
async get(projectRoot: string): Promise {
if (this.cache.has(projectRoot)) {
return this.cache.get(projectRoot)!;
}
const content = await this.load(projectRoot);
this.cache.set(projectRoot, content);
this.startWatching(projectRoot);
return content;
}
private startWatching(projectRoot: string) {
if (this.watchers.has(projectRoot)) return;
const watcher = chokidar.watch(path.join(projectRoot, 'CLAUDE.md'));
watcher.on('change', async () => {
const newContent = await this.load(projectRoot);
this.cache.set(projectRoot, newContent);
// Notify all subscribers
this.callbacks.forEach(cb => cb(newContent));
});
this.watchers.set(projectRoot, watcher);
}
onChange(callback: (newContent: string) => void) {
this.callbacks.push(callback);
}
private async load(projectRoot: string): Promise {
// Same as above
}
}
Best Practices
- Always put static content (role, format, general tools) at the very front of the system prompt and enable cache markers.
- Put dynamic content (time, ad-hoc user instructions) after all static content to avoid breaking the prefix.
- Use client-side caching for tool schemas to avoid repeated serialization, while relying on server-side caching to reduce transfer.
- Use TTL in-memory caching + hot reload for project configuration files, balancing freshness and performance.
- Monitor cache hit rates; evaluate optimization effectiveness through logs or a metrics system.
DSH Compaction: Two-Tier Plus Spill, a Third Path
Based on line-by-line source reading of
packages/compaction/compaction-basic/src/index.ts+summarizer.ts.
Two-tier compaction:
- Prune tool results first:
compaction-tool-result-pruner(thresholdChars: 8192, headChars: 4096, tailChars: 1024) - Session compaction:
BasicCompactionEngine, dual triggers, with the summary reusing the session’s own system prompt/tools to avoid blowing the provider KV cache
Source implementation of the dual triggers (index.ts:133-212):
// Trigger 1: pressure - hooked at agent/pre-step, between-step checks token pressure
const result = await this.compactIfNeeded(agent, 'pressure', signal)
if (result !== null) logResult(result, 'step pressure')
// Trigger 2: context-overflow - hooked at agent/request-error
if (failure.code !== CONTEXT_WINDOW_EXCEEDED_CODE || signal.aborted) return next()
result = await this.compactIfNeeded(agent, 'context-overflow', signal)
// After compaction succeeds, return retry to resend the request
return { kind: 'retry' }
Key design (verbatim source comment):
context-overflow compaction failed after durable surface progress: ...; retrying from the replacement surface— even if compaction fails, as long as there is durable surface progress, it retries from the replacement surface without discarding anything.
Replaceable summarizer (index.ts:96-99):
// Dependency-light compaction backend using ctx.tokenMeter for pressure
// summarize() is the sole subclass customization hook
Spill mechanism (the third path): when a plain-text tool result exceeds maxInlineBytes (default 50000), the full text is stored externally and the model sees only a head/tail preview + locator. The read tool is skipped to prevent a read→spill→re-read infinite loop.
Spill Source Details (spill/types.ts all types + cordis.patch.yml:352)
// Opaque handle (branded type) - backend may be an FS path, URI, or DB key
export type SpillLocator = Branded
// save-time namespace
export interface SpillOwner { sessionId: SessionId }
// Tool + call ID - purely descriptive, not used as ACL
export interface SpillSource { toolName: string; callId: CallId; label: string }
// Save request
export interface SaveTextSpill { owner; source; suggestedName; content }
// Returned ref
export interface SpillRef { locator: SpillLocator; bytes: number; retrievalHint: string }
Key design (verbatim module docstring):
suggestedNameis a hint; the backend sanitizes it into a safe path segment and never parses it as a pathSpillLocatoris an opaque handle; consumers render it withretrievalHint- A
forksession inherits existing locators (no copying / no re-owning); spills produced after the fork use the child sessionId
Inline threshold (cordis.patch.yml:352): maxInlineBytes: 50000 (default 50KB)
Codex Compaction Mechanism: 90% Threshold + Dual Scope
Based on source reading of
codex-rs/protocol/src/openai_models.rs+core/src/session/context_window.rs(91 lines, full text) +core/src/compact.rs(799 lines).
Trigger threshold (openai_models.rs:486-497, personally verified):
pub fn auto_compact_token_limit(&self) -> Option {
let context_limit = self.resolved_context_window()
.map(|context_window| (context_window * 9) / 10); // default 90%
let config_limit = self.auto_compact_token_limit;
if let Some(context_limit) = context_limit {
return Some(config_limit.map_or(context_limit, |limit|
std::cmp::min(limit, context_limit))); // config override takes min
}
config_limit
}
Source implementation of the dual scopes (context_window.rs, 91 lines in total):
match turn_context.config.model_auto_compact_token_limit_scope {
AutoCompactTokenLimitScope::Total => (
active_context_tokens, // full context
turn_context.model_info.auto_compact_token_limit(),
None,
),
AutoCompactTokenLimitScope::BodyAfterPrefix => {
let window = sess.auto_compact_window_snapshot().await;
let baseline = window.prefill_input_tokens.unwrap_or(active_context_tokens);
let scope_limit = turn_context.config.model_auto_compact_token_limit
.or_else(|| turn_context.model_info.auto_compact_token_limit());
(
active_context_tokens.saturating_sub(baseline), // count only what the prefix added
scope_limit,
window.prefill_input_tokens,
)
}
}
Significance of
BodyAfterPrefix: only tokens added after the initial prefix are counted—because the initial context (system prompt, instructions, tool definitions) is fixed and should not consume the auto-compact budget.
Triple trigger check (end of context_window.rs):
// 1. Buffered auto-compact limit reached
// 2. Or the model's full context window reached (hard cap, independent of auto-compact scope)
let token_limit_reached = buffered_auto_compact_limit
.is_some_and(|limit| auto_compact_scope_tokens >= limit)
|| full_context_window_limit_reached; // active_context_tokens >= full limit
Conditional reservation of the fallback buffer (source comment verbatim):
// Only reserve the fallback buffer when there is a fallback prompt to use it.
let auto_compact_fallback_buffer_tokens = turn_context.config.token_budget
.as_ref()
.map_or(0, TokenBudgetConfig::fallback_buffer_tokens);
Budget-packing algorithm for rebuilding history (compact.rs:644-690, personally verified):
// Pack backward from the newest user message, budget 20K
let mut remaining = max_tokens; // COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000
for message in user_messages.iter().rev() { // reverse iteration (newest first)
if remaining == 0 { break }
let tokens = approx_token_count(&message.message);
if tokens <= remaining {
selected_messages.push(message.clone());
remaining = remaining.saturating_sub(tokens);
} else {
// If it does not fit, truncate to the remaining budget, then stop
let truncated = truncate_text(&message.message, TruncationPolicy::Tokens(remaining));
selected_messages.push(CompactedUserMessage { message: truncated, ... });
break;
}
}
selected_messages.reverse(); // restore chronological order
Post-compaction history structure:
New history = [re-injected initial context] + [recent real user messages (budget 20K)] + [summary]
Injection placement distinction:
| Timing | Strategy | Reason |
|---|---|---|
| mid-turn (within a turn) | BeforeLastUserMessage |
the model is trained to treat the summary as the last item of history |
| pre-turn / manual | DoNotInject |
fully re-injected on the next turn |
Pi compaction: the cut point algorithm
Based on a source reading of
packages/coding-agent/src/core/compaction/compaction.ts(~850 lines).
Trigger and configuration (DEFAULT_COMPACTION_SETTINGS):
export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {
enabled: true,
reserveTokens: 16384, // reserve (for summary output)
keepRecentTokens: 20000, // number of recent tokens kept
}
// Trigger condition
export function shouldCompact(contextTokens, contextWindow, settings): boolean {
return contextTokens > contextWindow - settings.reserveTokens
}
Core constraint of the cut point algorithm (isCutPointMessage):
function isCutPointMessage(message: AgentMessage): boolean {
switch (message.role) {
case "user":
case "assistant":
case "bashExecution":
case "custom":
case "branchSummary":
case "compactionSummary":
return true
case "toolResult":
return false // ★ Key: never cut at a tool result
}
}
Why never cut at a toolResult: keeps the
tool_use/tool_resultpairing intact—cutting in the middle makes the API error out on an orphaned tool_use or an unclaimed tool_result.
flowchart LR
MSG["Full session"] --> SCAN["Accumulate tokens backward from newest"]
SCAN --> CHK{"Accumulated >= keepRecentTokens(20K)?"}
CHK -- No --> SCAN
CHK -- Yes --> FIX["Adjust back to the nearestvalid cut point(skip toolResult)"]
FIX --> CUT["Cut here"]
CUT --> OLD["Before cut point -> summary"]
CUT --> NEW["After cut point -> kept verbatim"]
style FIX fill:#C8102E,color:#fff
Summary format (6 sections):
| # | Section |
|---|---|
| 1 | ## Goal |
| 2 | ## Constraints & Preferences |
| 3 | ## Progress (Done / In Progress / Blocked, three states) |
| 4 | ## Key Decisions (decision + rationale) |
| 5 | ## Next Steps (ordered) |
| 6 | ## Critical Context |
Accumulation of file operations across compactions (extractFileOperations):
// Inherited from the previous compaction's details (if generated by pi)
if (prevCompactionIndex >= 0) {
const prevCompaction = entries[prevCompactionIndex] as CompactionEntry
if (!prevCompaction.fromHook && prevCompaction.details) {
for (const f of details.readFiles) fileOps.read.add(f)
for (const f of details.modifiedFiles) fileOps.edited.add(f)
}
}
// Then extract from this round's tool calls
for (const msg of messages) extractFileOpsFromMessage(msg, fileOps)
Design intent: file operations are maintained cumulatively across multiple compactions—so even after N compactions, the agent still knows which files it has read/modified. The file inventory is injected into the summary.
Iterative merging: when a previousSummary already exists, merge incrementally via UPDATE_SUMMARIZATION_PROMPT (keep all old information, move In Progress items into Done, update Next Steps) instead of rewriting.
Storage of the compaction output: appended to the session’s CompactionEntry; when rebuilding context, old messages are not sent to the LLM.
No standalone memory system—pi’s “memory” is just the loading of context files such as AGENTS.md / CLAUDE.md (resource-loader.ts, collected from the global location plus all ancestor directories of cwd, in AGENTS.override.md → AGENTS.md → CLAUDE.md order).
OpenCode compaction implementation (source-level)
Compaction trigger paths
- Preventive: triggered at finish-step as soon as
count ≥ usable(processor.ts:617-634) - Reactive: provider overflow error →
ContextOverflowError→ compact and resend (processor.ts:762)
Threshold calculation (overflow.ts:8-25)
usable =
context not configured (=0) -> 0 -> never triggers
input configured -> input - reserved <- main branch
input not configured -> context - maxOutput <- fallback branch
reserved defaults to min(20K, maxOutputTokens)
maxOutputTokens = min(limit.output, 32K)
Example (context=128K, input=118K, reserved=36K): usable = 82K
In one sentence: input sets the ceiling, reserved sets the trigger lead.
New context after compaction
New context = [summary 2K] + [recent conversation verbatim ≤32K] + [new messages] ~= 34K starting point (verbatim = min(usable×50%, 32K): half of an 82K budget would be 41K, capped at 32K; Bocom raised the cap from 8K to 32K and the share from 25% to 50%)
- 8-section Markdown summary template (Current Focus / Goal / Constraints & Preferences / Progress(Done/In Progress/Blocked) / Key Decisions / Next Steps / Critical Context / Relevant Files)
- Recent conversation budget
usable × 50%, capped at 32K (OpenCode raised it from 25% to 50%, compaction.ts:192-201) - Lite mode
tailTurns=0(summary only); non-Lite returns 2
Comparison with Claude Code compaction
| Dimension | OpenCode | Claude Code |
|---|---|---|
| Trigger | input - reserved |
effective window - 13K |
| Summary template | 8 sections + Forward Progress + memory YAML | 9-section analysis+summary |
| Tail budget | usable × 50% |
last 5 files + skill 25K |
| Lite difference | tailTurns=0 |
no equivalent |
| Memory extraction | inline during compaction (saves LLM) | separate subagent |
| Fallback | splitTurn + MAX_AUTO_COMPACT | PTL retried 3 times |
The three major constraints of Context and engineering countermeasures
Sources: Anthropic “Effective Context Engineering for AI Agents” (2025.9), Stanford “Lost in the Middle” (2023), Philipp Schmid (2025.6)
Three physical constraints
Constraint 1: Lost in the Middle
Phenomenon: LLMs recall information in the middle of the context significantly worse than at the beginning or end.
- Stanford experiment: give the LLM 10-20 documents, only one of which contains the correct answer, and vary its position
- U-shaped curve: accuracy is about 75% when the answer is at the beginning or end; mid-position placement drops significantly — GPT-3.5-Turbo loses more than 20 percentage points, worst case falling below the closed-book baseline (the deeper the position and the more documents, the steeper the drop)
- Open-book (documents provided but the answer in the middle) even falls below the closed-book (no documents) baseline of 56.1%
- Stably reproduced across multiple models such as claude-1.3, gpt-3.5-turbo, mpt-30b-instruct, and longchat-13b
Root cause: positional bias in Transformer attention + far fewer long sequences than short ones in training data.
Engineering countermeasure: place key information at the beginning and end (primacy effect + recency effect); avoid burying key rules in the middle of tool call records.
Constraint 2: Context Rot
Phenomenon: the more tokens in the context, the more the model’s ability to accurately recall context information decays linearly.
Typical manifestation: a code refactoring agent sets a camelCase convention in round 1 and follows it strictly through rounds 1-10; by round 40, after tens of thousands of tokens have accumulated in the context, the convention’s signal is diluted and the agent starts generating snake_case—it is not “forgetting”, the signal is being diluted.
Engineering countermeasures: periodic compaction, externalized memory, truncating tool return values, injecting only the minimal set of fields required for decisions.
Constraint 3: Attention Budget
Phenomenon: self-attention has O(n²) complexity—the longer the context, the higher the triple cost of compute, latency, and expense.
| Token count | Compute | Meaning |
|---|---|---|
| 1K | 1× | an ordinary prompt |
| 10K | 100× | about 5,000 Chinese characters |
| 128K | 16,384× | near the GPT-4 Turbo limit |
| 200K | 40,000× | near the Claude limit |
Core principle: attention is a scarce resource. Adding irrelevant content is not a neutral operation—it actively damages the signal strength of the effective information.
Summary
| Constraint | Countermeasure |
|---|---|
| Lost in the Middle | place key information at the beginning and end |
| Context Rot | periodic compaction, trim history |
| Attention Budget | control total length, load on demand |
Core formula: optimal context = minimal token count × maximal signal-to-noise ratio
5. Memory System
The Memory System in an Agent Harness is one of the most core pieces of infrastructure in the whole of AI agent engineering. It defines how an agent stores, retrieves, and applies knowledge across time, and it is also the most essential difference between a Harness and a simple "model + tool" loop.
Claude Code memdir implementation (source-level)
Based on a line-by-line reading of the
src/memdir/directory source.
flowchart TD
subgraph STORAGE["Storage layer ~/.claude/projects/<slug>/memory/"]
EP["MEMORY.md index≤200 lines / ≤25KB"]
TOPIC["Topic files *.mdYAML frontmatter"]
end
subgraph INJECT["Injection layer (each turn's system prompt)"]
LOAD["loadMemoryPrompt()inject full MEMORY.md"]
TRUNC["truncateEntrypointContent()truncate over limit + warn"]
end
subgraph RECALL["Recall layer (on-demand retrieval)"]
SCAN["scanMemoryFiles()scan all topic files to build manifest"]
SIDE["Sonnet sideQuerymax 256 tokens structured output"]
SEL["Select up to 5 relevant files"]
FILT["Filter criteria:(1) already shown recently(2) tool docs currently in use"]
end
subgraph WRITE["Write layer"]
AUTO["Stop hook at turn endfork agent for extraction"]
DREAM["autoDream background consolidationgated by 24h + 5 sessions"]
end
EP --> LOAD --> TRUNC
TOPIC --> SCAN --> SIDE --> SEL --> FILT
FILT --> INJECT
AUTO --> TOPIC
DREAM --> TOPIC
DREAM --> EP
style EP fill:#003A70,color:#fff
style SEL fill:#1565C0,color:#fff
style DREAM fill:#C8102E,color:#fff
Four memory types (memoryTypes.ts)
| Type | Purpose |
|---|---|
| User | user preferences, habits |
| Feedback | user corrective feedback on agent behavior |
| Project | project conventions, architecture decisions |
| Reference | external reference material |
Entrypoint file hard limits (memdir.ts:34-38)
export const ENTRYPOINT_NAME = 'MEMORY.md'
export const MAX_ENTRYPOINT_LINES = 200 // ~125 chars/line
export const MAX_ENTRYPOINT_BYTES = 25_000 // p100 observation: 197KB squeezed into 200 lines
Truncation strategy (truncateEntrypointContent): truncate by lines first (natural boundary) → then by bytes at the last newline (to avoid splitting a line in half) → append a warning stating which limit was hit.
Recall mechanism (findRelevantMemories.ts)
// Use Sonnet for semantic selection, max 256 tokens, JSON schema structured output
const result = await sideQuery({
model: getDefaultSonnetModel(),
system: SELECT_MEMORIES_SYSTEM_PROMPT, // "up to 5, only if certain"
max_tokens: 256,
output_format: { type: 'json_schema', schema: {...} },
})
Key design: alreadySurfaced filters out files already shown in recent turns, so the 5 slots go to new candidates; recentTools filters out documentation of tools currently in use (avoiding false-positive keyword matches).
Background consolidation (autoDream.ts)
flowchart LR
A["Time gate≥24h since last run"] --> B["Session gate≥5 new transcripts"]
B --> C["Lockcross-process, prevents concurrency"]
C --> D["fork subagentrun /dream prompt"]
D --> E["Consolidate memory directorymerge duplicates/remove contradictions/normalize timestamps"]
style A fill:#E3F2FD
style B fill:#E3F2FD
style C fill:#E3F2FD
style D fill:#003A70,color:#fff
Claude Code vs OpenCode memory systems compared: Claude Code uses a Sonnet side query to pick 5 files (LLM semantic selection); OpenCode uses FTS5 BM25 full-text search (no LLM calls). The former is precise but costs one API call per recall; the latter is zero-cost but depends on keyword match quality (OpenCode added a CJK tokenizer for this).
Introduction
The memory system’s role
An Agent Harness is an operating system, and the Memory System is its file and memory management subsystem:
- Working memory (RAM): the context window, storing the current session’s conversation history.
- Long-term memory (Disk): persistent storage across sessions, ensuring the agent still remembers the past after a restart.
- Memory manager (OS Kernel): moves data between the two (loading/compaction) and decides which memories are valid.
Relationship with the Context System
In the Agent Harness architecture, the **Memory System** and the **Context System** are two core subsystems that collaborate closely yet have sharply divided responsibilities. A vivid analogy:
| Aspect | Memory System | Context System |
|---|---|---|
| Core question | how does the agent remember what it should remember across time? | how does the agent see what it should see right now? |
| Typical challenges | knowledge distillation, conflict resolution, forgetting/eviction | token budget, information pruning, latency control |
| Design goals | long-term coherence, personalization, continuous evolution | high quality, low cost, low latency for a single task |
flowchart LR
subgraph Harness [Agent Harness core]
direction TB
CS[Context System]
MS[Memory System]
end
User[User input] --> CS
CS -- 1.Retrieval request --> MS
MS -- 2.Return relevant memories --> CS
CS -- 3.Assemble full context --> LLM[LLM inference]
LLM -- 4.Generate response or call tools --> CS
CS -- 5.Output response --> User
CS -- 6.Conversation trace and results --> MS
MS -- 7.Distill and archive --> MS
Memory lifecycle tiers: short-term, mid-term, long-term
In agent systems, memory is divided by lifecycle and storage location into **short-term memory (working memory)**, **mid-term memory (project memory)**, and **long-term memory (global memory)**. The three tiers work together so the agent can handle the current conversation, reuse knowledge across sessions, and keep evolving.
| Tier | Stored content | Lifecycle | Typical capacity | Storage location | Example |
|---|---|---|---|---|---|
| Short-term memory | the current session’s message history, tool call results, intermediate state | clearable when the session ends (but usually persisted for recovery) | bounded by the LLM context window (usually 200K tokens) | in-memory + SQLite/files | the user’s just-spoken “refactor utils.js” |
| Mid-term memory | a specific project’s conventions, tech stack, common commands, user preferences for that project | across sessions, bound to the project, evolving with it | thousands of lines of text (e.g. CLAUDE.md can reach several hundred lines) | files under the project directory (CLAUDE.md, MEMORY.md, .claude/) |
“this project uses React 18 + Vite” |
| Long-term memory | user global preferences, cross-project general patterns, personal habits | across projects, permanent (unless manually deleted) | thousands of facts (e.g. the MEMORY.md index) | the user’s home directory (~/.claude/global-memory/) |
“the user likes detailed comments” |
Short-term memory: in-session message history
Short-term memory is the agent's **workbench**, storing all interactions of the current session. Its engineering implementation has already been covered in detail in "Context System".
Mid-term memory: project-level memory
Mid-term memory is bound to a **specific project** and persists across sessions. It contains project conventions, common commands, architecture decisions, user preferences for that project, and so on.
Storage forms
Storage forms:
| Platform | File/directory | Purpose |
|---|---|---|
| Claude Code | CLAUDE.md (project root) |
project instructions, tech stack, common scripts |
| Claude Code | MEMORY.md (project directory) |
project-specific memory index (200-line limit) |
| Claude Code | .claude/projects//memory/ |
automatically learned project-level facts (standalone .md files) |
| OpenCode | AGENTS.md |
project instructions (standard) |
| OpenCode | opencode-memory plugin |
project-level memory SQLite table |
Loading strategies:
-
Layered loading: Claude Code recursively loads all
CLAUDE.mdfiles from the root directory down to the current directory, merges them, and injects them into the System Prompt. -
Index-based loading:
MEMORY.mdcontains only index lines (≤150 characters); at startup the agent loads only the index, and fetches specific files via theReadtool when needed.
Hot reload and cache:
When project files change, the agent should sense it and update its memory. Implementation:
- Use
chokidarto watchCLAUDE.mdchanges and clear the cache. - Reload before the next request.
Long-term memory: global user preferences
Long-term memory spans projects and sessions, storing the user’s global preferences, general habits, and personal knowledge. For example: “the user is a backend engineer who likes TypeScript and hates writing documentation”.
Storage locations:
| Platform | Path | Format |
|---|---|---|
| Claude Code | ~/.claude/global-memory/ |
Markdown files |
| Claude Code | ~/.claude/memory.json |
JSON (some versions) |
| OpenCode | ~/.config/opencode/memory.db |
SQLite |
| Generic | ~/.agent/memory/ |
text files |
How production-grade agents implement memory engineering
-
OpenCode chose the purest “local-first, human-in-control” route: it does not learn proactively, and memory is defined entirely manually by the user via instruction files such as
AGENTS.md. -
Claude Code represents “semi-automated, deeply integrated”: it has a refined built-in memory system, but it is confined to its own ecosystem.
-
OpenClaw pioneered the “separation of data and index”, using Markdown files as the “memory source” and a vector database to build the “retrieval index”, balancing human readability with machine retrieval efficiency.
-
Hermes achieved “closed-loop learning”: it not only remembers, but can also automatically distill reusable “skills” from experience, forming a self-reinforcing positive loop.
| Project | Design Philosophy | Core Architecture Layers |
|---|---|---|
| OpenCode | Local-first, human in control | Runtime state (memory/disk/archive) + data model (Session/Message/Part) + instruction files |
| Claude Code | Semi-automated, deeply integrated | Manual memory (CLAUDE.md) + auto memory (Auto Memory) + session memory (Compact) + background consolidation (Auto Dream) |
| OpenClaw | Data and index separated | Foundation layer (Markdown source) + index layer (SQLite+vector) + runtime layer (context management) |
| Hermes | Closed-loop learning, self-evolution | Prompt memory (Prompt) + session archive (Session) + skill memory (Skills) + modeling layer (Optional) |
OpenCode Memory
OpenCode's memory system is "session persistence" rather than "learning". It ensures history can be restored after the app restarts, but information from one session does not influence the next.
OpenCode creates a new session through the `Session.createNext()` function, which **loads no data from any previous session**. Its "memory" therefore relies entirely on instruction files manually edited by the user, such as `AGENTS.md`, `CLAUDE.md`, `CONTEXT.md`.
So OpenCode simply persists memory without learning from history — which is also determined by its nature as a coding agent.
flowchart TD
A[OpenCode core] --> B[Session management modulesrc/session]
B --> C[In-memory active zonesession-manager.ts]
B --> D[On-disk persistence zonemessage-v2.ts]
B --> E[Historical archive zonecompaction.ts]
D --> F[SQLite database]
F --> G["(Session table)"]
F --> H["(Message table)"]
F --> I["(Part table)"]
C --> J[Fast accesscurrent session]
D --> K[Persistent storagerecover after restart]
E --> L[Compacted storagesaves space]
Claude Code Memory: Semi-Automatic Memory
Claude Code's memory system is the most ingeniously engineered part of its Harness, achieving semi-automatic knowledge management through a **four-layer architecture**.
This process is driven by four layers:
- Layer 1: Manual Memory (
CLAUDE.md): manually maintained by the user, supporting four scopes: enterprise, user, project, and local. It is inserted as a specialusermessage and does not enjoy cache optimization. - Layer 2: Auto Memory: the Agent autonomously decides to write cross-session knowledge, stored as Markdown files and divided into user, feedback, project, and reference categories. The system manages memory through a
MEMORY.mdindex file, but there is a hard 200-line read limit. - Layer 3: Session Memory: counters context window limits through three compaction strategies:
autoCompact,snipCompact, andcontextCollapse. - Layer 4: Background Consolidation (Auto Dream): a background process that runs when the user is idle, responsible for organizing, cleaning up, and consolidating memory.
flowchart TD
A[Session start] --> B[Load manual memoryCLAUDE.md]
B --> C[Load auto memory indexMEMORY.md]
C --> D[Inject into System Prompt]
D --> E[User interacts with Agent]
E --> F{Context nearly full?}
F -- Yes --> G[Trigger session compactionautoCompact/snipCompact]
G --> H[Compact and updatesession memory]
H --> E
E --> I[Agent decides to write memory]
I --> J[Generate Markdown notesstore by category]
J --> K[Update MEMORY.md index]
K --> L[Memory files directory~/.claude/projects/.../memory/]
subgraph BG [Background task]
M[Auto Dream process] --> N[Runs when idle]
N --> O[Consolidate and clean up memory files]
end
O --> L
OpenClaw Memory Practice: A Memory System with Files as the “Source” and Vectors as the “Index”
OpenClaw adopts a three-tier memory architecture (short-term logs, near-term sessions, long-term knowledge); its core innovation is completely separating the memory’s “source data” from its “retrieval index”.
- The memory “source”: human-readable Markdown files
OpenClaw stores memory’s “source data” in plain-text Markdown files:
MEMORY.md: stores long-term, persistent facts and preferences.memory/YYYY-MM-DD.md: daily logs serving as short-term memory.sessions/directory: stores complete near-term session archives.USER.md&SOUL.md: store user identity and Agent persona settings.
- The memory “index”: SQLite + vector database
OpenClaw maintains an SQLite database as an efficient index layer.
filesandchunkstables: record file metadata and chunked text, stored with deduplication.chunks_ftsvirtual table: uses FTS5 for full-text search.chunks_vecvirtual table: uses sqlite-vec for vector search.- Graceful fallback strategy: if the vector extension is not loaded, the system automatically falls back to brute-force computation in JavaScript.
flowchart TD
A[AI Agent] -- 1. Read/write memories --> B[Markdown source filesMEMORY.md / Daily Logs]
B -- 2. As source data --> C[Memory/workspace]
subgraph INDEX [Background indexing service]
D[Filesystem watcher] -- 3. Detect changes --> E[SQLite + sqlite-vec]
E -- 4. Create index --> F[Full-text index FTS5]
E -- 5. Create index --> G[Vector index vec0]
end
H[Agent retrieves memories] -- 6. Run search --> I[Retrieval API]
I -- 7. Full-text query --> F
I -- 8. Vector query --> G
I -- 9. Return paths/snippets --> B
Hermes: A Self-Evolving Closed-Loop Learning System
The Hermes Agent's design philosophy is "an AI Agent that evolves itself"; its memory system is a **closed-loop learning mechanism** that can actively learn, accumulate knowledge, and iterate on itself.
Based on source-level reading of
tools/memory_tool.py(397 lines) +tools/session_search_tool.py.
Frozen snapshot mechanism (original text from the memory_tool.py module docstring):
"""Memory Tool - persistent curated memory (MEMORY.md = agent notes, USER.md = user
profile). Both enter the system prompt as a FROZEN snapshot at session start;
mid-session writes hit disk but never change the prompt (prefix cache intact).
Single `memory` tool: add/replace/remove or a batch `operations` list."""
| Design | Description |
|---|---|
| Frozen snapshot | MEMORY.md + USER.md enter the system prompt as a frozen snapshot at session start |
| Mid-session writes | Hit disk but do not change the prompt — keeps the prefix cache intact |
| Single tool | The memory tool: add / replace / remove, or a batch operations list |
| Hard character limits | memory_char_limit = 2200 (MEMORY.md), user_char_limit = 1375 (USER.md) |
| Write approval gate | write_approval has three states: allow / blocked / staged (ops requiring approval are staged pending review) |
| Background review | Unattended review forks are only allowed to add; replace/remove are always staged |
Why the frozen snapshot is key (compared with OpenCode): OpenCode / OpenAI’s approach is to “compact the index section of MEMORY.md” to save tokens; Hermes uses hard character limits + a frozen prefix — writes to disk do not affect the prompt, so the cache always hits. Both point to the same goal: memory injection must not break the prompt cache.
Source-level correspondence of the four memory layers:
| Layer | Implementation | Characteristics |
|---|---|---|
| Prompt memory | MEMORY.md (2200 characters) + USER.md (1375 characters) |
Loaded at session start, frozen snapshot |
| Session archive | SQLite + session_search_tool.py (784 lines) |
FTS5 BM25 retrieval, CJK tokenizer |
| Skill memory | curator.py (1100 lines) |
Idle-triggered (7-day interval), archives only, never deletes |
| Optional modeling layer | External plugins (Mem0 / Honcho / Supermemory) | duck-typed provider |
Its core is driven by four layers:
- Prompt memory: composed of two small files,
MEMORY.md(long-term facts) andUSER.md(user profile), loaded at session start. - Session archive: uses an SQLite database to store the complete conversation history; the Agent can actively retrieve it via the
session_searchtool. - Skill memory: Hermes’ most differentiated capability. After completing a complex task, the Agent automatically generates skill documentation, settling it as reusable “procedural memory”.
- Optional modeling layer: performs deeper structuring of memory to support more complex reasoning.
flowchart TD
A[User interacts with Agent] --> B{Task execution}
B -- Success --> C[Auto-trigger memory extraction]
B -- Context nearly full --> D[Pre-compaction memory flush]
C --> E[Call LLM to analyze conversation]
D --> E
E --> F[Extract key facts and procedures]
F --> G[Write into layered memory]
G --> H[Prompt memoryMEMORY.md / USER.md]
G --> I[Session archiveSQLite + FTS5]
G --> J[Skill memoryMarkdown skill docs]
G --> K[Optional modeling layer]
H --> L[Auto-loaded next session]
I --> M[Retrieved via session_search]
J --> N[Consolidated into reusable skills]
N --> O[Skill library grows]
O -- Positive feedback loop --> B
# Hermes "pre-compaction memory flush" closed-loop learning pseudocode
class HermesMemoryManager:
async def handle_context_threshold(self, session):
"""Handle the case when context reaches the threshold"""
# 1. Before compaction, first call LLM to extract key memories
extracted = await self.extract_memory(session.get_conversation_history())
# 2. Decide whether each item is a fact or a skill
for item in extracted:
if item.type == "skill":
await self.create_skill_document(item)
else:
await self.update_memory_files(item)
# 3. Update session archive
await self.archive_session(session)
# 4. Finally perform context compaction
await self.compact_context(session)
Summary of Engineering Practice Principles
- Layered design is the foundation: whether OpenCode’s runtime layering or the cognitive layering of Claude Code/OpenClaw/Hermes, all follow the principle of “separation of concerns”.
- Data ownership is key: the open, self-hosted model represented by OpenCode and OpenClaw ensures your memory data is not locked in and remains maintainable over the long term.
- Retrieval intelligence determines the ceiling: Claude Code’s 200-line index and keyword matching are its bottleneck. Semantic vector search (e.g. OpenClaw’s
sqlite-vec) or full-text search (e.g. Hermes’s FTS5) is the industry’s standard answer to the “memory recall” problem. - Memory lifecycle requires active maintenance: a robust “memory metabolism” mechanism (e.g. Claude Code’s
Auto Dreamandcompact, OpenClaw’s “pre-compaction memory flush”) is key to building a long-term reliable memory system. - Closed-loop learning is the future: Hermes’s “automatic skill generation” represents the direction in which Agent memory systems evolve from “passive storage” toward “active learning”, enabling Agents to abstract reusable methodologies from experience, just like humans.
Automatic Learning and Evolution
Status and Role
The automatic learning and evolution of memory is the core driving force behind the Agent Harness transforming from a "**capable machine**" into a "**growing companion**". The automatic learning and evolution of memory essentially solves one core question: **how can an AI Agent autonomously "grow" through interaction, becoming more attuned to you and smarter with use?**
In the Harness, automatic learning and evolution of memory plays multiple key roles, driving a qualitative leap for the Agent.
- Endowing “experience”: through learning, the Agent can record successful workflows and failure lessons, providing experiential guidance for the future and escaping “goldfish memory”.
- Achieving “personalization”: through long-term interaction, the Agent gradually learns the user’s preferences and habits, evolving from a “generic tool” into a “dedicated companion”.
- Improving “efficiency”: through automated skill accumulation and intelligent memory retrieval, the Agent can greatly reduce repetitive work and improve execution efficiency on complex tasks.
- Breaking “context limits”: through memory compaction and consolidation, the Agent can effectively manage context, solve the “context rot” problem, and achieve effective interaction of unlimited length.
- Achieving “autonomous growth”: advanced systems such as Hermes have achieved closed-loop learning; the Agent can autonomously distill knowledge from every task and continuously strengthen its own capabilities.
Theoretical Foundations
- Closed-Loop Learning Cycle: after task completion, the Agent automatically reviews, distills knowledge, and consolidates it, forming a positive flywheel of “getting smarter with use”.
- Hierarchical Memory Consolidation: drawing on human memory models, the Agent’s memory moves from short-lived working memory to episodic memory, and then to durable semantic and procedural memory, achieving effective information management and compaction.
- Autonomous Skill Evolution: the advanced form. When a workflow is validated as efficient and general, the Agent can automatically distill and encapsulate it into a reusable “Skill” and keep optimizing it.
The Relationship Among Automatic Learning, Memory Freshness, Updating, and Forgetting
The three are not independent functional modules but a "trinity" mechanism forming the memory life cycle. Together they constitute a **proactive metabolism mechanism**, making the memory system no longer a static storage warehouse but a living, continuously evolving cognitive organ.
- Automatic learning handles memory “growth” — extracting new knowledge from interactions and generating new skills.
- Memory freshness handles memory “health” — ensuring every retained memory is accurate, valid, and unpolluted. It has three dimensions: accuracy freshness (via validation and conflict detection), timeliness freshness (via time-decay algorithms), and relevance freshness (via access-frequency weighting).
- Updating and forgetting handle memory “metabolism” — overwriting old knowledge with new, and proactively retiring low-value or outdated information. Without updating, the Agent ossifies; without forgetting, the Agent bloats and produces “hallucinations”.
Claude Code: Auto Memory & Auto Dream
Automatic Learning: Auto Memory
Claude Code's automatic learning is implemented through the **Auto Memory** mechanism. During a session, the Agent actively judges which information is worth keeping and automatically generates Markdown notes in four categories: **user preferences, feedback, project background, and reference pointers**.
- Index mechanism: all auto memories are indexed through the
MEMORY.mdfile. This index file is designed to stay within 200 lines, each line being a short title (≤150 characters) pointing to a detailed memory file. - Limitations: the auto memory index is hard-limited to 200 lines, and retrieval relies on exact keyword matching. Once memory entries exceed this limit or the query wording differs, the relevant memory cannot be found.
flowchart TD
subgraph AutoMemory [Claude Code auto memory flow]
A[User interacts with Agent] --> B{Agent decidesworth remembering?}
B -- Yes --> C[Generate Markdown note]
C --> D[Categorize:user/feedback/project/reference]
D --> E[Write into memory/ directory]
E --> F[Update MEMORY.md index]
F --> G[Index file limits: ≤200 lines≤150 chars per line]
end
Memory Freshness: Auto Dream
Claude Code's freshness mechanism is mainly implemented through the **Auto Dream** background process. Auto Dream is a cleanup program that runs when the user is idle, responsible for consolidating stale memories and cleaning up redundant information.
- Accuracy freshness: when Auto Dream consolidates memories, the LLM is explicitly asked to “eliminate contradictions and remove redundancy”; when old and new conflict, the latest information prevails with overwriting updates.
- Timeliness freshness: achieved indirectly through the consolidation process — stale content is replaced by new content during merging, naturally “scraped away”.
Auto Dream workflow:
flowchart TD
A[Session end/user idle] --> B{Auto Dream gate checks}
B --> C[Environment pre-checksisGateOpen]
C -->|Pass| D["Time gate>= 24h since last?"]
C -->|Fail| E[End]
D -->|Yes| F["Scan throttle>= 10min since last scan?"]
D -->|No| E
F -->|Yes| G["Session gate>= 5 new sessions?"]
F -->|No| E
G -->|Yes| H[File lockacquire distributed lock]
G -->|No| E
H -->|Success| I[Start forked subagentrun /dream skill]
H -->|Fail| E
I --> J[Iterate session history logsdistill, compact, consolidate memories]
J --> K[Update MEMORY.md and otherlong-term memory files in memory/]
K --> L[Done]
// Pseudocode of autoDream's core gating logic in Claude Code v2.1.88
// Source: src/services/autoDream/autoDream.ts
async function checkAndRunAutoDream(): Promise {
// 1. Environment checks
if (getKairosActive() || getIsRemoteMode() || !isAutoMemoryEnabled()) {
return;
}
// 2. Time gate: more than 24 hours since last consolidation
const lastConsolidatedTime = await readLastConsolidatedAt();
const hoursSince = (Date.now() - lastConsolidatedTime) / (1000 * 60 * 60);
if (hoursSince < 24) return;
// 3. Scan throttle: prevent frequent triggering within a short window
const lastScanTime = await readLastScanAt();
if (Date.now() - lastScanTime < 10 * 60 * 1000) return;
// 4. Session gate: at least 5 new sessions accumulated
const newSessionCount = await countSessionsSince(lastConsolidatedTime);
if (newSessionCount < 5) return;
// 5. Distributed lock: ensure only one dream process runs at a time
const lockAcquired = await acquireConsolidationLock();
if (!lockAcquired) return;
try {
// Start an independent subagent to run the /dream task
const result = await runForkedAgent("/dream", { querySource: "auto_dream" });
if (result.success) {
// Update the last-consolidated timestamp
await updateLastConsolidatedAt();
}
} finally {
releaseConsolidationLock();
}
}
Updating and Forgetting
-
Updating: implemented through Auto Dream’s consolidating updates. Old and new memories are merged into one more accurate memory during consolidation, with new content overwriting old files.
-
Forgetting: indirect elimination — stale/contradictory memories are replaced by new content during merging; there is no explicit deletion mechanism. The index’s hard 200-line limit is also a form of enforced forgetting.
Claude Code implements a form of silent, batch-wise memory metabolism through the
Auto Dreambackground process. It does not explicitly “delete” memories; instead, it achieves forgetting in disguise through consolidation.
OpenClaw
OpenClaw's memory evolution leans toward a **systematic "cognitive" process**: through biomimetic algorithms, it systematically converts short-term interactions into long-term cognition.
Automatic Learning: Dreaming
OpenClaw's automatic learning is implemented through the **Dreaming** background memory consolidation system. This is a memory-organizing mechanism that replicates human sleep logic down to the last detail, dividing the Agent's running state into three coordinated phases:
- Light Sleep phase: scans recent conversations, deduplicates, and generates a candidate list.
- Deep Sleep phase: through a weighted scoring mechanism (relevance 30%, frequency 24%, diversity 15%, timeliness 15%, consolidation 10%, etc.), filters high-value information into long-term memory files.
- REM phase: looks for associations between pieces of information, building logical patterns and reflective summaries to improve decision-making.
Hook logic: OpenClaw shifts from “tools logic” (look things up when needed) to “hooks logic” (automatic processing at key points); memory saving and updating can happen automatically in the background without the Agent actively invoking anything, greatly improving efficiency.
The three-phase Dreaming process:
flowchart TD
A[Auto-triggered daily at 3amor manual /dreaming on] --> B
subgraph B [Phase 1: Light Sleep]
B1[Read recent daily memory filesand recall records]
B2[Jaccard similarity dedup]
B3[Stage candidate memories in short-term storagerecord signals, do not write MEMORY.md]
end
B --> C
subgraph C [Phase 2: REM Sleep]
C1[Analyze short-term signals from the past 7 days]
C2[Extract high-frequency themes and association patterns]
C3[Generate reflective summaryrecord signals, do not write MEMORY.md]
end
C --> D
subgraph D [Phase 3: Deep Sleep]
D1[Collect all candidate memories]
D2[Rank with the six-dimension weighted scoring model]
D3[Apply Light and REM phase signal boosts]
D4[Hard gate filters:score≥0.8, recalls≥3, sources≥3 queries]
D5[Write qualifying memories into MEMORY.md]
end
Memory Freshness: Six-Dimension Weighted Scoring Model
OpenClaw's freshness mechanism is embedded in the Deep Sleep phase's **six-dimension weighted scoring model**, a precise "memory quality inspection" system:
| Scoring Dimension | Weight | Core Function |
|---|---|---|
| Relevance | 30% | Measures the average quality of the information when retrieved; the most core freshness indicator |
| Frequency | 24% | Counts the short-term signals an entry has accumulated; high frequency means high value |
| Query diversity | 15% | How many different contexts have touched the entry; the higher the diversity, the more solid it is |
| Timeliness | 15% | Freshness decay score; yesterday’s instruction matters more than one from half a year ago |
| Consolidation | 10% | The stability of recurring across multiple days |
| Conceptual richness | 6% | The semantic density of the information |
Before formally writing, the system also **re-reads the latest content from the daily log source files**, ensuring it does not wrongly consolidate old information that the user has already edited or deleted into long-term memory.
Updating and Forgetting
- Updating: progressive promotion updates — new information must first be marked as a “short-term signal” in the Light Sleep phase, and only after passing the Deep Sleep phase’s high-threshold filtering can it be formally written into
MEMORY.md. - Forgetting: explicit threshold elimination — information with a six-dimension score below 0.8, fewer than 3 recalls, or fewer than 3 source queries is directly discarded, achieving active forgetting.
Hermes
Hermes represents the most thorough evolutionary path. It treats "self-evolution" as a **core architectural requirement**, achieving autonomous evolution from memory to skill through complete closed-loop learning.
The main reason the Hermes Agent can achieve "self-evolution" is its reliance on two paths: one is the daily **automatic Skill Generation**, which is fast, lightweight, and takes effect immediately; the other is the manually triggerable **RL training (Reinforcement Learning)**, which changes the model's own capabilities more deeply and fundamentally. Together, these two paths form the Hermes Agent's "internal-external" dual-drive "**self-evolution loop**".
Hermes's Self-Evo mainly solves the problems of "**instant error correction**" and "*
推荐工具
换一个关键词,或者移除筛选条件。
安装
npx skillfish add holny/agent-harness-develop-book