← back to 0xAF-Re 中文架构图 Architecture doc

Architecture diagrams

Start with the design map, then use the focused views for module layering, one full turn, the context budget, the approval gate, the live pane, the harness it borrowed from, and where the two diverge.

00

Architecture map

A Cocoon AI-style overview of the product design: terminal surface, agent loop, workflow modes, provider seats, policy gate, and local evidence layer.

Open exportable HTML →

01

Module dependency graph

Which package may import which. Dependencies flow downward only; types and util import nothing, which is what keeps the graph acyclic and every package independently testable.
Module dependency graph: cmd/0xaf into app, then the runtime, presentation, support and leaf layers Module dependency graph 0xAF-Re · internal/ package layering · arrows point from dependant to dependency · no cycles CLI RUNTIME PRESENTATION SUPPORT LEAVES cmd/0xaf entrypoint app args · REPL · commands · editor core loop · session · compaction shell escape providers 5 adapters + stream normalizer tools registry · runner · spill 24 local tools mcp stdio JSON-RPC client borrowed tools security danger patterns + approval gate plan task-list tracker model-reported progress ui theme · pane · HUD · flow · trace config routes · providers auth CLI login probe assets go:embed prompt/skills skills SKILL.md workflows knowledge local corpus index types shared contracts, no imports util width math, no imports every runtime + support pkg core · tools · plan interface runtime security plan support leaf / entrypoint direct import aggregated edges Dependencies flow downward only. types and util import nothing, which is what keeps the graph acyclic and the packages independently testable.

open the SVG →

02

One turn, end to end

Prompt to reply: compaction, the provider call, every tool call passing the approval gate, and the three ways a turn can end. Every path writes to the session before it writes to the screen.
Sequence diagram of a single agent turn across nine participants One turn, end to end prompt → compaction → provider → tool calls behind the approval gate → session append → reply. Solid = call, dashed = return. Operatorthe human app.replinterface core.AgentLoopthe loop CompactHistorycontext budget types.Providerthe model RequestApprovalthe gate types.Tool24 local + MCP core.Sessionappend-only JSONL ui.LivePane90ms frames ① prompt line ② NewLivePane + SetPlan ③ Run(prompt, ctx, onEvent) ④ AppendMessage user TURN 1 ⑤ CompactHistory(history, budget) ⑥ view + dropped / elided counts ⑦ LoopEvent compaction, wire send ⑧ Complete(system, view, tools, ctx) ⑨ OnProgress: thinking / text / tool / usage / plan ⑩ progress events — plus a plan event when the list changed ⑪ text + toolCalls + usage ⑫ AppendMessage assistant ⑬ wire recv, reply LOOP — EACH TOOL CALL ⑭ tool_start ⑮ tier(Risk) × approvalMode ALT — NEEDS A HUMAN ⑯ Pause ⑰ draw the request block, read one key — y / a / n / d ⑱ decision, then Resume the pane ALT — ALLOWED / REFUSED ⑲ Execute(args, ToolContext{ctx}) → ToolResult ⑳ refused or failed — the error itself becomes the tool result ㉑ AppendMessage toolResult ㉒ tool_end HOW THE TURN ENDS — THREE WAYS ^C — cancel ctx, synthesise a result for every unrun call, write the interrupt marker, return RunResult{Interrupted:true} with err = nil more tool calls issued — next turn re-compacts, now with the results included reply had no tool calls — RunResult{Turns:n}, the loop is done ㉓ Stop the pane ㉔ reply header + markdown reply + usage footer Every path writes to the session before it writes to the screen. A refused tool and a crashed tool take the same route back into the transcript, which is why an interrupted run stays resumable: the loop never leaves a tool call without a matching result.

open the SVG →

03

The context budget

Two passes and a floor. Tool bodies are elided before whole exchanges are dropped, because a tool call stripped of its result would leave the transcript unresumable.
Flowchart of CompactHistory: elide tool results, drop exchanges, last-exchange floor, compaction marker The context budget CompactHistory — two passes and a floor. The disk record stays whole; only the view sent upstream is trimmed. CompactHistory(messages, budget) called once per turn, before the provider sees anything HistoryTokens within budget? return the list unchanged elided = 0 · dropped = 0 protectedFrom = len − keepRecent keepRecent defaults to 8 — a preference, not a floor PASS 1 — ELIDE OLD TOOL RESULTS every message before protectedFrom with role toolResult and text over 400 chars body replaced by a one-line note "older TOOL result elided, N chars, first line …" the assistant call and its arguments survive PASS 2 — DROP WHOLE EXCHANGES hardFloor = index of the last user message still over budget? kept tail + the marker emit cursor still before protectedFrom? cursor = nextBoundary one message + all of its tool results cursor still before hardFloor? eat into keep-recent a preference, not a floor LAST-EXCHANGE FLOOR stop over budget rather than delete the turn being answered anything dropped? prepend one CompactionMarker user message count dropped · last 6 prompts · tool names used plus "full transcript is on disk" the model is told what it can no longer see kept messages only no marker needed CompactionResult Messages · TokensBefore · TokensAfter ElidedToolResults · DroppedMessages yes no no yes yes no yes no yes no Order matters: eliding tool bodies is tried before dropping exchanges, because a tool call stripped of its result would leave the transcript unresumable. The last-exchange floor is the one place the budget is allowed to lose — deleting the turn currently being answered is worse than going over.

open the SVG →

04

Security: the approval gate

Tier × mode, with per-tool overrides that outrank both. With no confirm callback attached the gate denies rather than assumes — and a denial is recorded as a tool result, so the turn continues.
Flowchart of RequestApproval: overrides, concerns, auto-approval, the interactive prompt, and the denial sink Security: the approval gate tier × mode, with per-tool overrides that outrank both. Every denial becomes a tool result the model reads — the turn never dies on a refusal. RequestApproval tool · tier · summary · concerns mode = policy.ApprovalMode, default safe override = the per-tool entry in policy.Approvals the override is consulted first and wins over the mode override is deny? concerns non-empty AND mode is not yolo? override is allow, OR AutoApproves for this mode and tier? run no prompt tc.Confirm is nil? nobody attached DeniedError — non-interactive with concerns: "blocked by policy …" without: "needs approval, mode=X, not interactive" pause the pane, draw the request block read exactly one key the live frame timer stops so the prompt cannot be overdrawn decision y allow once a allow-always — remembered for this tool n deny once · Enter and EOF mean the same d deny-always — remembered for this tool DeniedError every refusal path lands here tool.Execute ToolResult back to the loop recorded as an error tool result the loop continues — the model reads the refusal and re-plans a blocked command is an answer, not a crash yes no yes no yes no yes no y · a n · d Two things make this safe to run unattended: with no confirm callback attached the gate denies rather than assumes, and a denial is never fatal — it is written into the transcript as a tool result so the model can choose a different approach on the next turn.

open the SVG →

05

The live pane

One frame every 90ms, redrawn in place. The flow diagram is all-or-nothing; the HUD sheds detail in a fixed priority order down to a floor of two cells — the clock and the phase label.
Flowchart of the live pane render path with the five-step HUD shedding order The live pane One frame every 90ms, redrawn in place. Everything below the budget is negotiable — the pane sheds detail rather than wrap or overflow. frame timer every 90 ms LivePane.render clearLocked walks back exactly p.drawn lines no guessing, no clear-screen — only what was written is erased bodyLocked width = paneWidth · budget = TerminalRows − heightMargin 2 ComposePane(now, width, budget, flow, hud) RenderFlow yields the raw diagram rows 5 rows — or none below 46 columns, or when idle raw rows fit in budget − hudFloorRows 6? DIAGRAM KEPT row 0 — you, then ctx, then the provider row 1 — carried payloads rows 2–4 — plan badge in the left gutter rows 3–4 — tool return path packet animation rides these rows DIAGRAM DROPPED WHOLE all of it or none of it a half-drawn diagram is worse than no diagram hud.MaxRows = budget − the diagram rows kept the HUD gets whatever is left over RenderHud sheds until it fits width under MinBoxWidth 20 OR MaxRows under 4? ONE UNBOXED STATUS LINE spinner · route · phase · elapsed the irreducible minimum THE FULL BOX top chips · status row with route · progress and cost optional plan note · plan rows beside telemetry cells reasoning tail · bottom edge diagram rows + HUD rows total within budget · every line within width print · p.drawn = the line count just written which is what the next frame will walk back yes no yes no SHEDDING ORDER each step re-renders the box and re-measures applied in order, stopping as soon as it fits 1 reasoning tail thinkWindow shrinks 3 → 0 the model's visible thinking is the first thing to go — it is the most interesting and the least load-bearing nothing structural is lost 2 plan note dropped entirely the note is commentary; the list itself survives longer 3 task list collapses collapseAfter 8 → 1 the finished head folds into "N done" the tail folds into "N more" the in-flight item is always the one kept 4 telemetry cells shed by priority limit 8 → telemetryFloor 2 token counters go first then the output sparkline the floor of 2 preserves the clock and the phase label — "is it alive, and what is it doing" outranks every number 5 last resort keep the head rows, re-append the closing edge the box always closes — a torn frame is never printed Two invariants hold at every step total rows ≤ budget every line ≤ width, measured CJK-aware so a 40-column terminal and a 200-column one both get a frame that fits Why shedding, not scrolling The pane is redrawn in place, so it cannot be allowed to grow: one extra line and clearLocked walks back the wrong distance, leaving debris on screen for the rest of the session. Decorative layers must never fail a run — the pane is allowed to lose detail, never to corrupt the terminal. The diagram is all-or-nothing; the HUD degrades gracefully. That asymmetry is deliberate — a partial flow diagram misleads about what the agent is doing, while a HUD with fewer cells still tells the truth about the cells it kept.

open the SVG →

06

oh-my-pi, the harness it came from

The project this one borrowed its skeleton from. GitHub labels it TypeScript; it is a Bazel-built polyglot monorepo — 16 TypeScript packages, 9 Rust crates and a resident Python. Read bottom-up: shell, grep, diff, traversal and AST were pushed down into Rust because tool noise, not model quality, was the bottleneck.
Layered architecture of oh-my-pi: entry, agent core, capability layers, the Rust substrate, bridges, extensibility, and Bazel underneath oh-my-pi — the harness, as built can1357/oh-my-pi · a Bazel-built polyglot monorepo: 16 TypeScript packages, 9 Rust crates, a resident Python. GitHub labels it "TypeScript" — the substrate is not. ENTRY packages/tui terminal interface coding-agent/src/cli argument surface · launch · modes slash-commands · commands operator verbs AGENT CORE — packages/agent · coding-agent/src agent loop event-driven async · live session export · eval hindsight task · subagents typed, schema-validated results the parent reads modes · plan-mode goals · advisor auto-thinking capability secrets permission surface registry · tools catalog · discovery one tool surface CAPABILITY LAYERS EDIT SAFETY packages/hashline hash-anchored edits — the anchor is verified before the write lands src/edit · src/commit typescript-edit-benchmark edit accuracy is measured, not assumed CONTEXT & MEMORY packages/snapcompact compaction as its own package mnemopi · memories memory-backend autolearn · autoresearch memory persists across sessions EXEC & TOOLS src/exec · src/subprocess src/web · src/exa browser and search src/ssh · src/irc src/stt · src/tts speech in and out LANGUAGE SERVICES src/lsp renames go through the server: workspace/willRenameFiles src/dap · src/debug a real debugger, not print statements src/markit · src/tiny RUST SUBSTRATE — crates/ · 9 crates, where the performance-critical work moved pi-shell its own shell — vendored brush (Rust bash) + minimizer/ per-toolchain output filters: cargo · git · go · jvm · npm pi-uu-grep pi-uu-diff coreutils rewritten on uutils, so grep and diff behave identically on every platform pi-uutils-ctx shared execution context for the rewritten utils pi-walker the file walker pi-ast per-language AST parsing (src/language/) structure-aware reads, not line ranges pi-iso isolation — the sandbox boundary for what the agent is allowed to touch pi-natives fonts · syntaxes native helpers the TS side loads through packages/natives BRIDGES — how other runtimes reach back in python/ — omp-rpc · robomp a resident Python that can call back into the agent's own tools packages/wire the transport under it all — the loopback bridge src/jsonrpc · src/internal-urls in-process addressing for everything the agent exposes src/mcp someone else's tools, on the same registry EXTENSIBILITY — and it dogfoods its own skills .omp/skills semantic-compression · system-prompts tool-prompt-optimization src/extensibility hooks · custom tools · SDK (examples/ ships all three) swarm-extension · collab many agents, and collab-web to watch them metaharness · stats · eval the harness measures itself BAZEL toolchains · platforms · triples · variants — one build graph across Rust, TypeScript and Python Three languages in one repo is a build problem before it is an architecture problem; Bazel is the answer they picked, and it shapes everything above it. Read bottom-up and the thesis is legible: the parts that must be fast and identical everywhere — shell, grep, diff, file walking, AST — were pushed down into Rust, while orchestration, memory and the interface stayed in TypeScript, and Python is kept resident because that is what people actually write scripts in. Its output minimizer has per-toolchain filters with test fixtures — cargo, git, go, jvm, npm — which tells you what the real bottleneck was: not model quality, but tool noise.

open the SVG → · can1357/oh-my-pi →

07

Where the two diverge, and why

Seven axes, side by side. oh-my-pi optimizes for changing code; 0xAF-Re optimizes for reading binaries — and almost every difference follows from that one sentence. The last panel records exactly what crossed over and what was left behind on purpose.
Side-by-side comparison of oh-my-pi and 0xAF-Re across engineering shape, shell and tool output, writing to disk, context and memory, model orchestration, understanding the target, and extensibility Where the two diverge, and why oh-my-pi optimizes for changing code. 0xAF-Re optimizes for reading binaries. Almost every difference below follows from that one sentence. oh-my-pi can1357/oh-my-pi · a coding agent 0xAF-Re this project · a reverse engineering agent vs engineering shape Bazel monorepo, three languages 16 TypeScript packages · 9 Rust crates · resident Python 123 modules under coding-agent/src alone Breadth is the point: it is a platform, and the build graph is load-bearing. One Go module, one binary 64 files · 18,610 lines · 1 external dependency 6.7 MB · ~6.7 ms cold start · prompts and skills embedded Narrowness is the point: lab boxes rarely have a runtime, and you scp one file. shell & tool output Ships its own shell — and its own coreutils pi-shell wraps vendored brush (bash in Rust) pi-uu-grep / pi-uu-diff rewrite the utils on uutils minimizer/ has per-toolchain filters with fixtures — cargo, git, go, jvm, npm. Uses the host shell, and budgets what comes back shell-escape analysis on the way in, output budget on the way out oversized output spills to an artifact — head + tail + path `objdump -d` output is noise the same way every time; no per-toolchain filter earns its keep. writing to disk Hash-anchored edits, and they benchmark them packages/hashline — the anchor is verified before the write typescript-edit-benchmark measures whether it actually works Editing is the product, so the edit path gets a safety net and a scoreboard. Writes are off until you ask no anchoring, no edit benchmark — deliberately the approval gate is the whole story: tier × mode, per-tool overrides The subject is read-only. Building an edit-safety net for solve notes does not pay for itself. context & memory Compaction is a package; memory is a subsystem snapcompact · mnemopi · memories · memory-backend autolearn · autoresearch · hindsight Knowledge accumulates across sessions — a codebase is a long-running relationship. One function, two passes, and a floor CompactHistory: elide tool bodies, then drop whole exchanges last-exchange floor — go over budget before deleting the live turn Memory is the knowledge base instead, and it must cite entry ids or the UI calls it out. model orchestration One model, fanned out to subagents the task tool splits work; results come back schema-validated swarm-extension scales the same idea wider Parallelism is the lever: many workers, one mind. Two vendors, two seats, swappable mid-run planner (codex, --sandbox read-only) · executor (claude) /planner /executor /agent /effort — no restart Vendor diversity is the lever: on a refusal, ask a different company. understanding the target The language server already knows src/lsp — renames via workspace/willRenameFiles src/dap — a real debugger; pi-ast — per-language parsing Source has ground truth available. Ask the tooling, do not ask the model to guess. There is no language server for a stripped binary 24 tools: triage · entropy · carve · symbols · mitigations · APK · Frida reverse_toolkit fronts radare2 · JADX · Ghidra · gdb · YARA · unidbg Ground truth is recovered, not queried — which is why every tool is also a slash command. extending it Hooks, custom tools, an SDK, a swarm src/extensibility with worked examples for all three .omp/skills — it dogfoods its own skill format metaharness · stats · eval: the harness measures itself. A markdown file, and MCP for the rest skills/<name>/SKILL.md — on-disk copies beat the embedded ones any stdio MCP server joins the same registry and the same gate No SDK to learn. The extension surface is a file you can write in a text editor. What actually crossed over Taken: the single event-driven loop · output budgets as a first-class concern · structured results instead of parsed prose · one tool registry · MCP · tiered approval Left behind: hash-anchored edits · the LSP/DAP layer · cross-language workers · a custom shell and coreutils · cross-session memory · the Bazel monorepo Every item on the second line is excellent engineering that solves a problem this project does not have. The honest summary: oh-my-pi is a platform and 0xAF-Re is a tool. Its Rust substrate exists because a coding agent runs cargo and npm a thousand times a day and their output is the bottleneck. A reverse agent runs strings once and then stares at the result — so the effort went into visibility, approval, and not lying about sources.

open the SVG →