Architecture
The package map — how types, ai, core, tools, mcp, tui, protocol, server, and cli fit together — plus one turn traced end to end.
On this page
Monorepo layout
Arcturn is a pnpm workspace, ESM throughout, TypeScript 5.x targeting Node ≥ 20:
packages/
types @arcturn/types Zero-dep shared contracts: messages, events, tools,
permissions, session tree, protocol schemas.
ai @arcturn/ai Unified LLM layer over nine provider adapters
(Anthropic, OpenAI Chat Completions, OpenAI
Responses, Google, Bedrock, Vertex, Azure, and the
OpenAI- and Anthropic-compatible endpoint adapters);
streaming, tool calls, thinking, model catalog, cost.
core @arcturn/core Agent runtime: event loop, steering/abort, session
store (JSONL tree, branching), compaction, hooks,
permission engine, sub-agents, state tools. This
is the SDK.
tools @arcturn/tools Built-ins: read, write, edit, bash (+background),
grep, glob, ls, fetch, websearch.
index @arcturn/index Token-optimized code index and hybrid (BM25 +
structural + PageRank) search, used for repo maps.
mcp @arcturn/mcp MCP client integration (official SDK) → Arcturn tools.
tui @arcturn/tui Terminal UI lib: differential renderer, components,
markdown, input editor, autocomplete.
protocol @arcturn/protocol JSON-RPC-style wire protocol for server mode.
server @arcturn/server WebSocket/HTTP server exposing sessions remotely.
cli arcturn (bin: arcturn) Interactive coding agent: TUI app, print/JSON modes,
extensions, skills, themes, config, plan mode, LSP.
evals @arcturn/evals Evaluation harness for scoring agent runs.
web/ This site — Next.js 16 static export, Tailwind v4,
markdown docs.Dependency flow
Read from each package's own package.json:
types ← (zero-dep — everyone else's shared contracts)
ai ← types
core ← types
tools ← types (+ tinyglobby)
index ← types
mcp ← types (+ @modelcontextprotocol/sdk)
protocol ← types
tui ← (zero-dep on @arcturn/* — a standalone terminal UI lib)
server ← types, protocol, core
cli ← types, ai, core, index, mcp, protocol, server, tools, tui
evals ← types, ai, core, arcturn (the cli package)@arcturn/types is the load-bearing package: zero runtime dependencies, just the
shared contracts — Message, AgentEvent, Tool, PermissionRule, SessionEntry,
ModelSpec, the protocol's ClientRequest/ServerMessage — that every other package
codes against. Nothing downstream of it needs to know how another package is
implemented, only what shape it produces and consumes.
@arcturn/core depends only on types — it has no idea tools, mcp, ai, or tui
exist as concrete packages; it takes an LLMClient and a Tool[] as injected
interfaces. Tools, MCP-bridged tools, and the terminal UI are all just consumers of
core's public surface (Agent, AgentEvent, Tool), assembled together by whoever's
embedding it — the cli package, your own SDK usage, or server. cli is the one
package that depends on nearly everything: it's the composition root.
Why it's shaped this way
- The runtime never imports a provider SDK.
Agenttakes an injectedLLMClientfrom@arcturn/ai; core has noimport "@anthropic-ai/sdk"anywhere. Swap providers, mock the client in tests, or add a new one entirely without touching the runtime. - The runtime never imports a UI. Every observable thing — streaming tokens, tool
progress, permission prompts, todo/plan changes, sub-agent activity — is one
AgentEventunion. The TUI renders it to a terminal; the server re-emits it over a WebSocket; your own app can render it however it wants. It's the same stream in every case, described once in@arcturn/types. - Sessions are a tree, not a log.
SessionEntry.parentIdmakes branching a structural property of storage instead of a feature bolted onto a linear transcript — see Sessions. - Everything minimal harnesses call "an extension you write yourself" is a package, not
a pattern. MCP, sub-agents, permissions, and plan/todo state are shipped, tested
packages with their own contracts in
types, not documentation telling you how to build them.
How the CLI composes the runtime
packages/cli is glue, not logic: it builds the pieces @arcturn/core needs and hands
them to createAgent.
- Config (
config.ts) — load and merge~/.arcturn/config.jsonand<cwd>/.arcturn/config.jsoninto anArcturnConfig. See Configuration. - Tools (
@arcturn/tools'screateDefaultTools) — the nine built-ins, plus MCP-bridged tools from any configured server, plustodo/planfrom@arcturn/core. Iflsp: "on",write/editare wrapped with LSP diagnostics (LSP diagnostics); ifverifyis set, they're wrapped with the verify loop too. - LLM client (
@arcturn/ai) — resolve the configured model id to aModelSpecand build the matching provider client, with API keys or OAuth credentials from the environment or~/.arcturn/auth/. - Agent (
@arcturn/core'screateAgent) — wire config, tools, and the LLM client into oneAgent, with aJsonlSessionStorerooted at~/.arcturn/sessions/<hash-of-cwd>/. - Front end — the TUI subscribes to
agent.subscribe(listener)and renders everyAgentEvent;-p/--printmode does the same but writes text or NDJSON to stdout instead (see Getting started);servemode re-emits the same events over a WebSocket via@arcturn/protocol.
Nothing in step 4 or in @arcturn/core knows which of those front ends is listening —
that's the point of the event stream being the entire interface.
One turn, traced end to end
What actually happens between typing a prompt and seeing a result, for a prompt that edits a file:
- Prompt submission. The TUI (or
-p) callsagent.prompt(text).@-mentions in the text are expanded first — file content appended, images attached — see @-mentions & images. The agent emitsrunStart, appends aUserMessageto the conversation, and persists it as a session entry. - Compaction check. Before calling the model, the agent checks whether the
conversation is close to the context window and, if so, compacts older history into a
summary first (
compactionStart/compactionEndevents) — see Sessions. - Model call. The loop (
runLoopin@arcturn/core) streams a turn from the injectedLLMClient, emitting incrementaltext/thinking/tool-call delta events as they arrive so the TUI can render tokens live. - Tool call dispatch. When the model's turn includes a tool call (say,
edit), the loop resolves lifecycle hooks first (preToolUse— can block the call outright), emitstoolStart, then validates the tool's arguments against its JSON Schema. - Permission gate. The loop computes a
subjectfor the call (foredit, the file path) and asks thePermissionEnginetocheck()it. Resolution order: tools inalwaysAllowTools(todo,plan) pass silently;planmode denies anything outside the read-only tools (read,grep,glob,ls); stored rules are matched session > project > user, most-specific first; read-only tools are allowed outright;yoloallows everything andacceptEditsauto-allowswrite/edit; anything still unresolved is asked via the injectedPermissionPrompt— which is what actually shows the TUI's permission prompt, or auto-denies in-pmode. See Permissions for the full rule schema and mode table. - Tool execution. If allowed, the loop builds a
ToolExecutionContext(cwd, abortsignal, a scopedrequestPermissionthe tool can call again for finer-grained decisions, anonUpdateprogress sink,sessionId,toolCallId) and callstool.execute(input, ctx).edititself makes the actual file write here, having already computed and validated the replacement. Iflsp/verifywrapped the tool, diagnostics or a check-command failure are appended to the result now — see LSP diagnostics and Configuration. - Result and events.
postToolUsehooks run, the loop emitstoolEndwith the fullToolResultMessage, and the result is appended to the conversation as a tool-result message and persisted as a session entry. - Repeat or finish. The loop keeps streaming turns — feeding tool results back to the
model, dispatching further tool calls — until the model responds without a tool call,
maxTurnsis hit, the run is aborted, or an unrecoverable error occurs. It then emitsrunEndwith the reason. - Render and persist. Every event along the way was already streamed to whichever
front end subscribed — the TUI updates its differential-rendered transcript live,
-p --output-format jsonhad already written each event as an NDJSON line,servehad already forwarded each one over the WebSocket. Nothing is buffered and replayed after the fact; the render and the run happen concurrently, driven by the sameAgentEventstream. The full turn is now durable in the session's JSONL file, ready for-c/--resume,arcturn replay,arcturn blame, orarcturn audit.
Engineering standards
- ESM only,
NodeNextmodule resolution,strictTypeScript, noanyin public APIs. - Vitest for tests — every package ships unit tests alongside its source.
- Biome for lint and format.
- Apache-2.0 license.