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.
0 · Architecture map
1 · Module graph
2 · One turn
3 · Context budget
4 · Approval gate
5 · Live pane
6 · oh-my-pi
7 · Comparison
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.
Operator the human
app.repl interface
core.AgentLoop the loop
CompactHistory context budget
types.Provider the model
RequestApproval the gate
types.Tool 24 local + MCP
core.Session append-only JSONL
ui.LivePane 90ms 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 →
Diagrams are plain SVG — no runtime, no CDN, no fonts to fetch. They render the same in the browser, on GitHub, and in an offline checkout.
The Mermaid source for diagrams 1–5 is kept in ARCHITECTURE.md under a collapsed block.
A concise Chinese companion is available at ARCHITECTURE.zh-CN.md ;
6 and 7 were drawn directly from the upstream repository layout.