解剖课那天,讲台上摆开四副骨架:一头蹲踞的狮子,一只振翅的隼,一尾游弋的鱼,还有一只没有脊骨、靠肌肉蠕动前行的软体动物。学生们起先只看见皮毛、翎羽、鳞片的差别,几乎要当成四门不同的课来听。
老先生却拿着同一支教鞭,在四副骨架上依次点过去,只问五个问题。第一问:心脏在哪儿,血怎么流一圈再回来——狮子的是扑通扑通的一颗肉泵,隼的也是,鱼的却要经过鳃才能换气,软体动物干脆没有闭合的血管网,靠一腔体液漫灌全身。第二问:它用什么抓东西——狮子用爪,隼用喙,鱼用不上抓取这回事,软体动物伸出的是一条会伸缩的假足。第三问:挡在食道口的那圈括约肌,什么时候松、什么时候绷——这一圈肌肉几乎每具身体里都能找到,只是绷紧的信号各不相同。第四问:肚子里那间发酵室,满了怎么办——有的现场清一遍,有的干脆搬一间偏房续着装。第五问,也是最难的:它怎么再生出一整只自己的分身——有的靠单开一支血脉喂养,有的靠现成的一整套配方直接照抄。
五个问题问下来,学生才恍然:狮子的爪子和隼的喙原来答的是同一件事——怎么抓取;鱼鳃翕张的节律和软体动物蠕动的波,答的也是同一件事——循环怎样周而复始。骨架尽管千差万别,答案落的坑却始终是那五个——连那只没有爪子的软体动物,食道口照样有一圈能松能绷的括约肌,器官的位置从不落空,只是换了个长法;唯独有一具身体,连"抓取"这个动作本身都没有,它用的是另一套完全不同的办法。
这堂解剖课要教的,从来不是四种动物,而是同一份解剖图,量在四具不同的身体上——五脏六腑照旧摆在那五处,骨架各有各的长法。
先猜揭晓前,先押一个猜测 · 你觉得这讲的是?
谜底 · The Concept
Overview — the organ matrix: loop, tools, permission gate, context, sub-agents, read across Claude Code, OpenCode, Gemini CLI, and Aider
Other Coding Agents · 他山之石
中文速览 · Quick read
Citations pinned at opencode@77429f5 · gemini-cli@b31b755 · aider@5dc9490 (cloned 2026-07-08).
The matrix
Five organs, four bodies. Every cell below is one real mechanism and one real citation — including the one cell that names an absent organ and points at what stands in its place.
| Organ | Claude Code | OpenCode | Gemini CLI | Aider |
|---|---|---|---|---|
| Loop | A literal while(true) inside the generator queryLoop; the loop continues on observed tool_use blocks (needsFollowUp), never on the model's self-reported stop_reason (claude-code/src/query.ts:307, claude-code/src/query.ts:553-558). |
runLoop is a literal while(true) that re-reads the session's entire message history from a SQL store on every iteration instead of holding it in memory; tool execution runs inside the AI-SDK stream (opencode/packages/opencode/src/session/prompt.ts:1088, opencode/packages/opencode/src/session/prompt.ts:1092-1094). |
Split across an outer driver while(true) that resubmits tool results as the next user message, and an inner recursive generator; an LLM-judged checkNextSpeaker can keep the loop going with no tool calls at all (gemini-cli/packages/cli/src/nonInteractiveCli.ts:310-324, gemini-cli/packages/core/src/core/client.ts:880-904). |
No model-driven tool loop exists: run_one() loops only while the harness's own reflected_message (failure feedback) got set, hard-capped at 3 reflections (aider/aider/coders/base_coder.py:924-944, aider/aider/coders/base_coder.py:100-101). |
| Tools | A typed contract — name + a Zod inputSchema + call; input is parsed against the schema and rejected before tool.call() ever runs (claude-code/src/Tool.ts:394, claude-code/src/services/tools/toolExecution.ts:615, claude-code/src/services/tools/toolExecution.ts:1207). |
A Tool.Def; Tool.define wraps every tool with argument validation (failures become a typed, model-facing "rewrite your input" error) and dispatch is outsourced entirely to the Vercel AI SDK's streamText (opencode/packages/opencode/src/tool/tool.ts:55-65, opencode/packages/opencode/src/tool/tool.ts:24-34, opencode/packages/opencode/src/session/llm.ts:280). |
A DeclarativeTool; build(params) validates raw args against a JSON schema and returns a frozen ToolInvocation, and dispatch runs through an explicit multi-status scheduler — (gemini-cli/packages/core/src/tools/tools.ts:683-694, gemini-cli/packages/core/src/scheduler/scheduler.ts:428-433). |
The "tool" is an edit format — a Coder subclass whose schema is prose in the system prompt mandating SEARCH/REPLACE syntax, parsed by regex and dispatched by string match; there is no tool_use block at all (aider/aider/coders/editblock_prompts.py:8-30, aider/aider/coders/editblock_coder.py:386-394, aider/aider/coders/base_coder.py:190-194). |
| Permission gate | A layered pipeline, hasPermissionsToUseToolInner: a deny rule is checked first and is absolute — honored even in bypass-permissions mode — an allowlist grants silent allow, and the fall-through default is ask; the interactive resolver itself pushes to a local confirm queue that races against an optional remote/bridge channel, not a purely local-only prompt (claude-code/src/utils/permissions/permissions.ts:1158, claude-code/src/utils/permissions/permissions.ts:1171, claude-code/src/utils/permissions/permissions.ts:1226, claude-code/src/utils/permissions/permissions.ts:1299, claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:92, claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:244). |
Flat rules {permission, pattern, action}, last-matching-rule-wins, defaulting to ask — but the check is pull-based and lives inside each tool (ctx.ask()), blocking on a server-side Deferred resolved over HTTP by whichever client happens to be attached (opencode/packages/opencode/src/permission/index.ts:28-38, opencode/packages/opencode/src/permission/index.ts:67-107, opencode/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts:16-38). |
A priority-sorted TOML policy engine — tiered rules (Admin>User>Workspace>Extension>Default) sorted once by priority, first match wins ALLOW/DENY/ASK_USER; YOLO is a priority-998 allow-all rule, not code, and an in-session "always allow" click lands at Workspace tier 3.950 — not User 4.950 (gemini-cli/packages/core/src/policy/policy-engine.ts:209-212, gemini-cli/packages/core/src/policy/policies/yolo.toml:50-56, gemini-cli/packages/core/src/policy/config.ts:728-731, gemini-cli/packages/core/src/policy/types.ts:370-377). | One primitive, InputOutput.confirm_ask() — a terminal Y/N with (A)ll/(S)kip all/persistent (D)on't ask again; files already in the chat are pre-authorized, anything else asks, shell commands require explicit_yes_required=True no rules engine — the safety net is that every applied edit auto-commits to git for /undo (aider/aider/io.py:807-925, aider/aider/coders/base_coder.py:2191-2240, aider/aider/coders/base_coder.py:2450-2463, aider/aider/coders/base_coder.py:2375-2395). |
| Context | A cost-graded layering: cheap microcompaction (a time-based content-clear, or a cache_edits surgical delete) runs first every turn; autocompaction — a full LLM summary near a 13K-token buffer under the window limit — is the fallback, with a circuit breaker after 3 consecutive failures (claude-code/src/query.ts:414-468, claude-code/src/services/compact/microCompact.ts:305-399, claude-code/src/services/compact/autoCompact.ts:62-76, claude-code/src/services/compact/autoCompact.ts:260-265). |
Token usage folds from provider step-finish events; on overflow, compaction is not summarized inline but queued as a synthetic user message that the next loop iteration runs as an ordinary turn, executed by a hidden, tool-denied compaction agent (opencode/packages/opencode/src/session/overflow.ts:22-34, opencode/packages/opencode/src/session/compaction.ts:513-536, opencode/packages/opencode/src/agent/agent.ts:219-233). |
ChatCompressionService.compress no-ops until history exceeds 50% of the model's token limit, then keeps the most recent ~30% and sends the rest to a summarizer that must produce a mergeable <state_snapshot> — an anchor so a second compaction folds the first one in rather than overwriting it; durable memory is GEMINI.md, discovered hierarchically upward (gemini-cli/packages/core/src/context/chatCompressionService.ts:271-285, gemini-cli/packages/core/src/context/chatCompressionService.ts:353-374, gemini-cli/packages/core/src/utils/memoryDiscovery.ts:458-470). |
Proactive, backgrounded, weak-model-first: once done_messages exceeds a per-model token cap, a background thread summarizes it (recursive split at an assistant-message boundary, verbatim tail kept); the PageRank repo map replaces on-demand read/grep rather than adding a compaction-adjacent organ (aider/aider/models.py:355-358, aider/aider/history.py:33-96, aider/aider/repomap.py:470-531). |
| Sub-agents | The Agent tool spawns a named specialist, or — when subagent_type is omitted with the fork experiment on — an implicit fork child that inherits the parent's full context and system prompt, isolated in its own git worktree and guarded against recursion (claude-code/src/tools/AgentTool/AgentTool.tsx:323, claude-code/src/tools/AgentTool/AgentTool.tsx:332, claude-code/src/utils/worktree.ts:102-138). |
The task tool creates a child session carrying a parentID — a first-class persisted session, resumable via task_id — and the recursion guard is a default-deny task permission rule inherited into the child's ruleset, not a code check (opencode/packages/opencode/src/tool/task.ts:142-158, opencode/packages/opencode/src/agent/subagent-permissions.ts:14-27). |
AgentTool (invoke_agent) resolves a named agent definition and runs it in LocalAgentExecutor, its own while(true) loop with a separate chat and a restricted tool list; termination is protocol-based — the child must call complete_task or it errors out (gemini-cli/packages/core/src/agents/agent-tool.ts:43-73, gemini-cli/packages/core/src/agents/local-executor.ts:674-688, gemini-cli/packages/core/src/agents/local-executor.ts:362-371). |
A different mechanism, cited as such: no spawned agent exists at all — architect→editor is a synchronous, in-process coder-to-coder handoff sharing the same io. The fresh context is not the summarize-on-format-switch machinery used elsewhere (that gate is explicitly disabled for this handoff) but a direct wipe of the new coder's message lists (aider/aider/coders/architect_coder.py:11-48, aider/aider/coders/architect_coder.py:32, aider/aider/coders/architect_coder.py:38-39). |
Same organs, different bodies
Read down any column and you get one agent's personality; read across any row and you get one organ's design space. Three patterns are worth naming.
Claude Code encodes allow/ask/deny as an ordered sequence of code checks — a
deny rule that nothing outranks, then an allowlist, then a default ask
(claude-code/src/utils/permissions/permissions.ts:1158,
claude-code/src/utils/permissions/permissions.ts:1171,
claude-code/src/utils/permissions/permissions.ts:1226,
claude-code/src/utils/permissions/permissions.ts:1299). OpenCode keeps the same
three verdicts but inverts where the check lives — inside the tool, which
volunteers its own evidence — and how it resolves: a server-side Deferred
answered over HTTP (opencode/packages/opencode/src/permission/index.ts:67-107,
opencode/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts:16-38).
Gemini CLI goes furthest and turns the entire gate into data: a
priority-sorted table of TOML rules where even "allow everything" (YOLO) is a
row, not an if (gemini-cli/packages/core/src/policy/policy-engine.ts:209-212,
gemini-cli/packages/core/src/policy/policies/yolo.toml:50-56) — and the exact
priority arithmetic matters: an ordinary "always allow" click during a session
lands at Workspace tier 3.950, and only a saved, untrusted-folder choice climbs
to User tier 4.950
(gemini-cli/packages/core/src/policy/config.ts:728-731,
gemini-cli/packages/core/src/policy/types.ts:370-377) — collapsing that
distinction to "it becomes a User rule" would misstate the engine. Aider has no
rules engine at all, just one confirm_ask() primitive scoped to
(question, file) pairs in memory, backstopped by git auto-commit rather than a
policy (aider/aider/io.py:807-925,
aider/aider/coders/base_coder.py:2375-2395).
One easy overclaim to resist here: it is tempting to frame OpenCode's network-crossing approval as the outlier against a Claude Code that "never leaves the process." That is not quite right. Claude Code's own interactive handler pushes each pending decision to a local confirm queue, but that queue races against an optional remote/bridge channel that can resolve the same request from elsewhere (claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:92, claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:244). The real distinction is architectural, not "network vs. no network": OpenCode's gate is always server-resolved because the whole harness is an HTTP server by construction, while Claude Code's is in-process by default with an optional remote path bolted on beside it — not a categorical absence of one.
Claude Code, OpenCode, and
Gemini CLI all spawn something recognizable as a child agent loop: a fresh
context, a restricted tool set, an asynchronous or protocol-gated return path
(claude-code/src/tools/AgentTool/AgentTool.tsx:323,
opencode/packages/opencode/src/tool/task.ts:142-158,
gemini-cli/packages/core/src/agents/local-executor.ts:674-688). Aider has no
such thing — there is no tool a model calls to spin one up. What Aider has
instead is a synchronous, user-gated handoff between two Coder instances in
the same process (aider/aider/coders/architect_coder.py:11-48). Per the
"no such organ" rule, the finding is not "Aider lacks sub-agents" full stop —
it is that the slot is filled by a different mechanism, and that mechanism's
"fresh context" is easy to misattribute: it is not the
summarize-on-format-switch machinery that handles ordinary edit-format
switches elsewhere in the codebase (that gate is explicitly turned off for
this handoff, summarize_from_coder = False
aider/aider/coders/architect_coder.py:32) but a direct wipe of
cur_messages/done_messages on the freshly built editor coder
(aider/aider/coders/architect_coder.py:38-39).
The loop and context organs show a spectrum from "the model drives" to "the harness drives." Claude Code and Gemini CLI both loop on the model's tool-calling behavior (claude-code/src/query.ts:553-558, gemini-cli/packages/core/src/core/client.ts:880-904); OpenCode's loop is driven by whatever is queued in its database, including compaction turns it queues for itself (opencode/packages/opencode/src/session/compaction.ts:513-536); Aider's loop is driven entirely by the harness's own success/failure judgment of a single parsed reply (aider/aider/coders/base_coder.py:924-944). Context management mirrors this: Claude Code and OpenCode both compact reactively, at the threshold; Aider compacts proactively, in the background, ahead of need (aider/aider/models.py:355-358, aider/aider/history.py:33-96) — and replaces an entire organ (on-demand reading) with an always-present repo map instead of compacting around it (aider/aider/repomap.py:470-531).
对照 how to read the columns
Each column was built the same way the individual deep-dive pages were: read
the pinned source directly, cite file:line, and let the shipped page — not
the pre-review analyst brief — stand as the corrected record where the two
disagree. Three corrections from that adversarial-review process are load-
bearing for this matrix specifically, and are folded into the cells above
rather than left as background trivia:
- Aider's sub-agent cell is deliberately marked "a different mechanism, cited as such" rather than left to imply Aider has no organ there at all — the organ exists, it is just a synchronous in-process handoff, and its fresh context comes from an explicit wipe, not the summarizer (aider/aider/coders/architect_coder.py:38-39).
- Gemini CLI's permission cell states the Workspace-tier arithmetic (3.950, not User's 4.950) precisely, because the informal version of this claim ("always allow" becomes a user-level rule) is the more common — and wrong — shorthand (gemini-cli/packages/core/src/policy/config.ts:728-731).
- The OpenCode-vs-Claude-Code permission contrast avoids the absolute claim that Claude Code's approvals never cross a network; its own queue races an optional remote channel too (claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:92, claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:244).
Every citation in this matrix resolves against the pinned SHAs recorded for
this cluster — opencode@77429f5, gemini-cli@b31b755, aider@5dc9490
(all cloned 2026-07-08) — and against whatever claude-code checkout this
knowledge base's other pages already cite. Never re-clone or git pull any of
the three comparison repos; a newer commit would silently invalidate every
line number on this page.
Where to go next
The three sibling pages go one agent deep each:
- /stories/compare/2/ — OpenCode: the harness as an HTTP server, and a pull-based, network-resolved permission gate.
- /stories/compare/3/ — Gemini CLI: the permission gate as a priority-sorted TOML policy engine.
- /stories/compare/4/ — Aider: the "tool" is an edit
format, not a
tool_useloop.
To see each organ built from nothing, in order — loop, tools, permission gate, context, sub-agents:
- /stories/scratch/2/ — the loop.
- /stories/scratch/3/ — tools.
- /stories/scratch/4/ — the permission gate.
- /stories/scratch/5/ — context.
- /stories/scratch/6/ — sub-agents.
To read the same five organs dissected in the real Claude Code source, at full depth:
- /stories/loop/1/ — the turn loop.
- /stories/tools/1/ — tool anatomy.
- /stories/tools/2/ — the permission gate.
- /stories/context/1/ — compaction.
- /stories/multiagent/1/ — sub-agent spawning.
flowchart TB
subgraph SKEL["Shared organ skeleton — present in every harness body"]
direction TB
LOOP["Loop
call model, detect calls,
run them, repeat or stop"]
TOOLS["Tools
name + schema +
validate + execute"]
GATE["Permission gate
allow / ask / deny
before a side effect"]
CTX["Context
compact or summarize
when the window fills"]
SUB["Sub-agents
spawn a child with
its own history"]
end
LOOP -->|"proposes a call"| TOOLS
TOOLS -->|"before it runs"| GATE
GATE -->|"result feeds back"| LOOP
LOOP -->|"window nears full"| CTX
CTX -->|"trimmed history"| LOOP
LOOP -->|"delegates a subtask"| SUB
SUB -->|"result feeds back"| LOOP
CC["Claude Code"] -.realizes.-> SKEL
OC["OpenCode"] -.realizes.-> SKEL
GC["Gemini CLI"] -.realizes.-> SKEL
AI["Aider"] -.realizes.-> SKEL 读法:五个器官——loop、tools、
permission gate、context、sub-agents——是
每一具身体都长着的共同骨架:模型经循环提出一次调用,先过工具的定义与派发,
再过权限门才落到副作用;循环把窗口塞满时找上下文收拾,把子任务甩给一个子代理。
Claude Code、OpenCode、Gemini CLI、Aider 四具身体各自「实现」这同一副骨架,
而不是各自发明一套全新的器官(详见下方矩阵每一格的引用)。
五个器官,四具身体。点一个格子,看同一处器官在这具身体里长成什么样——
每一格背后都是一条真实的 repo/path:line 引用,包括那一格
标着"不同机制"的格子。
点上面矩阵里任意一格,这里会显示那具身体在这处器官上的真实机制,以及它的引用行号。
试试同一行——比如「权限门」——横着点过 4 格:同一个器官,四种截然不同的长法。
Citations pinned at opencode@77429f5 · gemini-cli@b31b755 · aider@5dc9490 (cloned 2026-07-08).
The matrix
Five organs, four bodies. Every cell below is one real mechanism and one real citation — including the one cell that names an absent organ and points at what stands in its place.
| Organ | Claude Code | OpenCode | Gemini CLI | Aider |
|---|---|---|---|---|
| Loop | A literal while(true) inside the generator queryLoop; the loop continues on observed tool_use blocks (needsFollowUp), never on the model's self-reported stop_reason (claude-code/src/query.ts:307, claude-code/src/query.ts:553-558). |
runLoop is a literal while(true) that re-reads the session's entire message history from a SQL store on every iteration instead of holding it in memory; tool execution runs inside the AI-SDK stream (opencode/packages/opencode/src/session/prompt.ts:1088, opencode/packages/opencode/src/session/prompt.ts:1092-1094). |
Split across an outer driver while(true) that resubmits tool results as the next user message, and an inner recursive generator; an LLM-judged checkNextSpeaker can keep the loop going with no tool calls at all (gemini-cli/packages/cli/src/nonInteractiveCli.ts:310-324, gemini-cli/packages/core/src/core/client.ts:880-904). |
No model-driven tool loop exists: run_one() loops only while the harness's own reflected_message (failure feedback) got set, hard-capped at 3 reflections (aider/aider/coders/base_coder.py:924-944, aider/aider/coders/base_coder.py:100-101). |
| Tools | A typed contract — name + a Zod inputSchema + call; input is parsed against the schema and rejected before tool.call() ever runs (claude-code/src/Tool.ts:394, claude-code/src/services/tools/toolExecution.ts:615, claude-code/src/services/tools/toolExecution.ts:1207). |
A Tool.Def; Tool.define wraps every tool with argument validation (failures become a typed, model-facing "rewrite your input" error) and dispatch is outsourced entirely to the Vercel AI SDK's streamText (opencode/packages/opencode/src/tool/tool.ts:55-65, opencode/packages/opencode/src/tool/tool.ts:24-34, opencode/packages/opencode/src/session/llm.ts:280). |
A DeclarativeTool; build(params) validates raw args against a JSON schema and returns a frozen ToolInvocation, and dispatch runs through an explicit multi-status scheduler — (gemini-cli/packages/core/src/tools/tools.ts:683-694, gemini-cli/packages/core/src/scheduler/scheduler.ts:428-433). |
The "tool" is an edit format — a Coder subclass whose schema is prose in the system prompt mandating SEARCH/REPLACE syntax, parsed by regex and dispatched by string match; there is no tool_use block at all (aider/aider/coders/editblock_prompts.py:8-30, aider/aider/coders/editblock_coder.py:386-394, aider/aider/coders/base_coder.py:190-194). |
| Permission gate | A layered pipeline, hasPermissionsToUseToolInner: a deny rule is checked first and is absolute — honored even in bypass-permissions mode — an allowlist grants silent allow, and the fall-through default is ask; the interactive resolver itself pushes to a local confirm queue that races against an optional remote/bridge channel, not a purely local-only prompt (claude-code/src/utils/permissions/permissions.ts:1158, claude-code/src/utils/permissions/permissions.ts:1171, claude-code/src/utils/permissions/permissions.ts:1226, claude-code/src/utils/permissions/permissions.ts:1299, claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:92, claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:244). |
Flat rules {permission, pattern, action}, last-matching-rule-wins, defaulting to ask — but the check is pull-based and lives inside each tool (ctx.ask()), blocking on a server-side Deferred resolved over HTTP by whichever client happens to be attached (opencode/packages/opencode/src/permission/index.ts:28-38, opencode/packages/opencode/src/permission/index.ts:67-107, opencode/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts:16-38). |
A priority-sorted TOML policy engine — tiered rules (Admin>User>Workspace>Extension>Default) sorted once by priority, first match wins ALLOW/DENY/ASK_USER; YOLO is a priority-998 allow-all rule, not code, and an in-session "always allow" click lands at Workspace tier 3.950 — not User 4.950 (gemini-cli/packages/core/src/policy/policy-engine.ts:209-212, gemini-cli/packages/core/src/policy/policies/yolo.toml:50-56, gemini-cli/packages/core/src/policy/config.ts:728-731, gemini-cli/packages/core/src/policy/types.ts:370-377). | One primitive, InputOutput.confirm_ask() — a terminal Y/N with (A)ll/(S)kip all/persistent (D)on't ask again; files already in the chat are pre-authorized, anything else asks, shell commands require explicit_yes_required=True no rules engine — the safety net is that every applied edit auto-commits to git for /undo (aider/aider/io.py:807-925, aider/aider/coders/base_coder.py:2191-2240, aider/aider/coders/base_coder.py:2450-2463, aider/aider/coders/base_coder.py:2375-2395). |
| Context | A cost-graded layering: cheap microcompaction (a time-based content-clear, or a cache_edits surgical delete) runs first every turn; autocompaction — a full LLM summary near a 13K-token buffer under the window limit — is the fallback, with a circuit breaker after 3 consecutive failures (claude-code/src/query.ts:414-468, claude-code/src/services/compact/microCompact.ts:305-399, claude-code/src/services/compact/autoCompact.ts:62-76, claude-code/src/services/compact/autoCompact.ts:260-265). |
Token usage folds from provider step-finish events; on overflow, compaction is not summarized inline but queued as a synthetic user message that the next loop iteration runs as an ordinary turn, executed by a hidden, tool-denied compaction agent (opencode/packages/opencode/src/session/overflow.ts:22-34, opencode/packages/opencode/src/session/compaction.ts:513-536, opencode/packages/opencode/src/agent/agent.ts:219-233). |
ChatCompressionService.compress no-ops until history exceeds 50% of the model's token limit, then keeps the most recent ~30% and sends the rest to a summarizer that must produce a mergeable <state_snapshot> — an anchor so a second compaction folds the first one in rather than overwriting it; durable memory is GEMINI.md, discovered hierarchically upward (gemini-cli/packages/core/src/context/chatCompressionService.ts:271-285, gemini-cli/packages/core/src/context/chatCompressionService.ts:353-374, gemini-cli/packages/core/src/utils/memoryDiscovery.ts:458-470). |
Proactive, backgrounded, weak-model-first: once done_messages exceeds a per-model token cap, a background thread summarizes it (recursive split at an assistant-message boundary, verbatim tail kept); the PageRank repo map replaces on-demand read/grep rather than adding a compaction-adjacent organ (aider/aider/models.py:355-358, aider/aider/history.py:33-96, aider/aider/repomap.py:470-531). |
| Sub-agents | The Agent tool spawns a named specialist, or — when subagent_type is omitted with the fork experiment on — an implicit fork child that inherits the parent's full context and system prompt, isolated in its own git worktree and guarded against recursion (claude-code/src/tools/AgentTool/AgentTool.tsx:323, claude-code/src/tools/AgentTool/AgentTool.tsx:332, claude-code/src/utils/worktree.ts:102-138). |
The task tool creates a child session carrying a parentID — a first-class persisted session, resumable via task_id — and the recursion guard is a default-deny task permission rule inherited into the child's ruleset, not a code check (opencode/packages/opencode/src/tool/task.ts:142-158, opencode/packages/opencode/src/agent/subagent-permissions.ts:14-27). |
AgentTool (invoke_agent) resolves a named agent definition and runs it in LocalAgentExecutor, its own while(true) loop with a separate chat and a restricted tool list; termination is protocol-based — the child must call complete_task or it errors out (gemini-cli/packages/core/src/agents/agent-tool.ts:43-73, gemini-cli/packages/core/src/agents/local-executor.ts:674-688, gemini-cli/packages/core/src/agents/local-executor.ts:362-371). |
A different mechanism, cited as such: no spawned agent exists at all — architect→editor is a synchronous, in-process coder-to-coder handoff sharing the same io. The fresh context is not the summarize-on-format-switch machinery used elsewhere (that gate is explicitly disabled for this handoff) but a direct wipe of the new coder's message lists (aider/aider/coders/architect_coder.py:11-48, aider/aider/coders/architect_coder.py:32, aider/aider/coders/architect_coder.py:38-39). |
Same organs, different bodies
Read down any column and you get one agent's personality; read across any row and you get one organ's design space. Three patterns are worth naming.
The permission gate is the organ with the most divergent realizations.
Claude Code encodes allow/ask/deny as an ordered sequence of code checks — a
deny rule that nothing outranks, then an allowlist, then a default ask
(claude-code/src/utils/permissions/permissions.ts:1158,
claude-code/src/utils/permissions/permissions.ts:1171,
claude-code/src/utils/permissions/permissions.ts:1226,
claude-code/src/utils/permissions/permissions.ts:1299). OpenCode keeps the same
three verdicts but inverts where the check lives — inside the tool, which
volunteers its own evidence — and how it resolves: a server-side Deferred
answered over HTTP (opencode/packages/opencode/src/permission/index.ts:67-107,
opencode/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts:16-38).
Gemini CLI goes furthest and turns the entire gate into data: a
priority-sorted table of TOML rules where even "allow everything" (YOLO) is a
row, not an if (gemini-cli/packages/core/src/policy/policy-engine.ts:209-212,
gemini-cli/packages/core/src/policy/policies/yolo.toml:50-56) — and the exact
priority arithmetic matters: an ordinary "always allow" click during a session
lands at Workspace tier 3.950, and only a saved, untrusted-folder choice climbs
to User tier 4.950
(gemini-cli/packages/core/src/policy/config.ts:728-731,
gemini-cli/packages/core/src/policy/types.ts:370-377) — collapsing that
distinction to "it becomes a User rule" would misstate the engine. Aider has no
rules engine at all, just one confirm_ask() primitive scoped to
(question, file) pairs in memory, backstopped by git auto-commit rather than a
policy (aider/aider/io.py:807-925,
aider/aider/coders/base_coder.py:2375-2395).
One easy overclaim to resist here: it is tempting to frame OpenCode's network-crossing approval as the outlier against a Claude Code that "never leaves the process." That is not quite right. Claude Code's own interactive handler pushes each pending decision to a local confirm queue, but that queue races against an optional remote/bridge channel that can resolve the same request from elsewhere (claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:92, claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:244). The real distinction is architectural, not "network vs. no network": OpenCode's gate is always server-resolved because the whole harness is an HTTP server by construction, while Claude Code's is in-process by default with an optional remote path bolted on beside it — not a categorical absence of one.
Sub-agents is the organ with a genuine absence. Claude Code, OpenCode, and
Gemini CLI all spawn something recognizable as a child agent loop: a fresh
context, a restricted tool set, an asynchronous or protocol-gated return path
(claude-code/src/tools/AgentTool/AgentTool.tsx:323,
opencode/packages/opencode/src/tool/task.ts:142-158,
gemini-cli/packages/core/src/agents/local-executor.ts:674-688). Aider has no
such thing — there is no tool a model calls to spin one up. What Aider has
instead is a synchronous, user-gated handoff between two Coder instances in
the same process (aider/aider/coders/architect_coder.py:11-48). Per the
"no such organ" rule, the finding is not "Aider lacks sub-agents" full stop —
it is that the slot is filled by a different mechanism, and that mechanism's
"fresh context" is easy to misattribute: it is not the
summarize-on-format-switch machinery that handles ordinary edit-format
switches elsewhere in the codebase (that gate is explicitly turned off for
this handoff, summarize_from_coder = False
aider/aider/coders/architect_coder.py:32) but a direct wipe of
cur_messages/done_messages on the freshly built editor coder
(aider/aider/coders/architect_coder.py:38-39).
The loop and context organs show a spectrum from "the model drives" to "the harness drives." Claude Code and Gemini CLI both loop on the model's tool-calling behavior (claude-code/src/query.ts:553-558, gemini-cli/packages/core/src/core/client.ts:880-904); OpenCode's loop is driven by whatever is queued in its database, including compaction turns it queues for itself (opencode/packages/opencode/src/session/compaction.ts:513-536); Aider's loop is driven entirely by the harness's own success/failure judgment of a single parsed reply (aider/aider/coders/base_coder.py:924-944). Context management mirrors this: Claude Code and OpenCode both compact reactively, at the threshold; Aider compacts proactively, in the background, ahead of need (aider/aider/models.py:355-358, aider/aider/history.py:33-96) — and replaces an entire organ (on-demand reading) with an always-present repo map instead of compacting around it (aider/aider/repomap.py:470-531).
对照 how to read the columns
Each column was built the same way the individual deep-dive pages were: read
the pinned source directly, cite file:line, and let the shipped page — not
the pre-review analyst brief — stand as the corrected record where the two
disagree. Three corrections from that adversarial-review process are load-
bearing for this matrix specifically, and are folded into the cells above
rather than left as background trivia:
- Aider's sub-agent cell is deliberately marked "a different mechanism, cited as such" rather than left to imply Aider has no organ there at all — the organ exists, it is just a synchronous in-process handoff, and its fresh context comes from an explicit wipe, not the summarizer (aider/aider/coders/architect_coder.py:38-39).
- Gemini CLI's permission cell states the Workspace-tier arithmetic (3.950, not User's 4.950) precisely, because the informal version of this claim ("always allow" becomes a user-level rule) is the more common — and wrong — shorthand (gemini-cli/packages/core/src/policy/config.ts:728-731).
- The OpenCode-vs-Claude-Code permission contrast avoids the absolute claim that Claude Code's approvals never cross a network; its own queue races an optional remote channel too (claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:92, claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:244).
Every citation in this matrix resolves against the pinned SHAs recorded for
this cluster — opencode@77429f5, gemini-cli@b31b755, aider@5dc9490
(all cloned 2026-07-08) — and against whatever claude-code checkout this
knowledge base's other pages already cite. Never re-clone or git pull any of
the three comparison repos; a newer commit would silently invalidate every
line number on this page.
Where to go next
The three sibling pages go one agent deep each:
- /stories/compare/2/ — OpenCode: the harness as an HTTP server, and a pull-based, network-resolved permission gate.
- /stories/compare/3/ — Gemini CLI: the permission gate as a priority-sorted TOML policy engine.
- /stories/compare/4/ — Aider: the "tool" is an edit
format, not a
tool_useloop.
To see each organ built from nothing, in order — loop, tools, permission gate, context, sub-agents:
- /stories/scratch/2/ — the loop.
- /stories/scratch/3/ — tools.
- /stories/scratch/4/ — the permission gate.
- /stories/scratch/5/ — context.
- /stories/scratch/6/ — sub-agents.
To read the same five organs dissected in the real Claude Code source, at full depth:
- /stories/loop/1/ — the turn loop.
- /stories/tools/1/ — tool anatomy.
- /stories/tools/2/ — the permission gate.
- /stories/context/1/ — compaction.
- /stories/multiagent/1/ — sub-agent spawning.
来源 · Source citations
- [1]
claude-code/src/query.ts:307 - [2]
claude-code/src/query.ts:553-558 - [3]
claude-code/src/Tool.ts:394 - [4]
claude-code/src/services/tools/toolExecution.ts:615 - [5]
claude-code/src/services/tools/toolExecution.ts:1207 - [6]
claude-code/src/utils/permissions/permissions.ts:1158 - [7]
claude-code/src/utils/permissions/permissions.ts:1171 - [8]
claude-code/src/utils/permissions/permissions.ts:1226 - [9]
claude-code/src/utils/permissions/permissions.ts:1299 - [10]
claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:92 - [11]
claude-code/src/hooks/toolPermission/handlers/interactiveHandler.ts:244 - [12]
claude-code/src/query.ts:414-468 - [13]
claude-code/src/services/compact/microCompact.ts:305-399 - [14]
claude-code/src/services/compact/autoCompact.ts:62-76 - [15]
claude-code/src/services/compact/autoCompact.ts:260-265 - [16]
claude-code/src/tools/AgentTool/AgentTool.tsx:323 - [17]
claude-code/src/tools/AgentTool/AgentTool.tsx:332 - [18]
claude-code/src/utils/worktree.ts:102-138 - [19]
opencode/packages/opencode/src/session/prompt.ts:1088 - [20]
opencode/packages/opencode/src/session/prompt.ts:1092-1094 - [21]
opencode/packages/opencode/src/tool/tool.ts:55-65 - [22]
opencode/packages/opencode/src/tool/tool.ts:24-34 - [23]
opencode/packages/opencode/src/session/llm.ts:280 - [24]
opencode/packages/opencode/src/permission/index.ts:28-38 - [25]
opencode/packages/opencode/src/permission/index.ts:67-107 - [26]
opencode/packages/opencode/src/server/routes/instance/httpapi/handlers/permission.ts:16-38 - [27]
opencode/packages/opencode/src/session/overflow.ts:22-34 - [28]
opencode/packages/opencode/src/session/compaction.ts:513-536 - [29]
opencode/packages/opencode/src/agent/agent.ts:219-233 - [30]
opencode/packages/opencode/src/tool/task.ts:142-158 - [31]
opencode/packages/opencode/src/agent/subagent-permissions.ts:14-27 - [32]
gemini-cli/packages/cli/src/nonInteractiveCli.ts:310-324 - [33]
gemini-cli/packages/core/src/core/client.ts:880-904 - [34]
gemini-cli/packages/core/src/tools/tools.ts:683-694 - [35]
gemini-cli/packages/core/src/scheduler/scheduler.ts:428-433 - [36]
gemini-cli/packages/core/src/policy/policy-engine.ts:209-212 - [37]
gemini-cli/packages/core/src/policy/policies/yolo.toml:50-56 - [38]
gemini-cli/packages/core/src/policy/config.ts:728-731 - [39]
gemini-cli/packages/core/src/policy/types.ts:370-377 - [40]
gemini-cli/packages/core/src/context/chatCompressionService.ts:271-285 - [41]
gemini-cli/packages/core/src/context/chatCompressionService.ts:353-374 - [42]
gemini-cli/packages/core/src/utils/memoryDiscovery.ts:458-470 - [43]
gemini-cli/packages/core/src/agents/agent-tool.ts:43-73 - [44]
gemini-cli/packages/core/src/agents/local-executor.ts:674-688 - [45]
gemini-cli/packages/core/src/agents/local-executor.ts:362-371 - [46]
aider/aider/coders/base_coder.py:924-944 - [47]
aider/aider/coders/base_coder.py:100-101 - [48]
aider/aider/coders/editblock_prompts.py:8-30 - [49]
aider/aider/coders/editblock_coder.py:386-394 - [50]
aider/aider/coders/base_coder.py:190-194 - [51]
aider/aider/io.py:807-925 - [52]
aider/aider/coders/base_coder.py:2191-2240 - [53]
aider/aider/coders/base_coder.py:2450-2463 - [54]
aider/aider/coders/base_coder.py:2375-2395 - [55]
aider/aider/models.py:355-358 - [56]
aider/aider/history.py:33-96 - [57]
aider/aider/repomap.py:470-531 - [58]
aider/aider/coders/architect_coder.py:11-48 - [59]
aider/aider/coders/architect_coder.py:32 - [60]
aider/aider/coders/architect_coder.py:38-39