Agent Source Code Analysis
Module Overview
In the TypeScript version, Agent is no longer an execution engine, but a pure configuration definition. It describes a “role’s” behavioral parameters — name, permission rules, system prompt, model preference, temperature setting — but contains no execution logic.
The actual execution loop is driven by the Session module. Session launches the main loop via SessionPrompt.loop(), which in each iteration calls SessionProcessor.process() -> LLM.stream() -> Vercel AI SDK streamText() to complete the LLM call, then SessionProcessor handles streaming events, tool calls, context compaction, and so on.
This “separation of configuration and execution” design means:
- Adding or modifying an Agent only requires declaring configuration, with no execution logic to write
- All Agents share the same Session execution loop; tool calling, error handling, and context management are implemented only once
- Agent permissions, tool sets, and system prompts are all controlled through declarative configuration
Key Files
| File Path | Lines | Responsibility |
|---|---|---|
src/agent/agent.ts | ~230 | Agent configuration definition: Agent.Info schema, built-in Agent list, state() factory, generate() dynamic creation |
src/session/index.ts | ~400 | Session management: CRUD, message persistence, cost calculation, paginated queries |
src/session/prompt.ts | ~580 | Core entry point: loop() main loop, user message construction, tool resolution, subtask dispatch |
src/session/llm.ts | ~200 | LLM invocation: LLM.stream() wraps Vercel AI SDK streamText(), handles system prompt assembly and parameter merging |
src/session/processor.ts | ~220 | Message processor: streaming event dispatch, tool lifecycle, Doom Loop detection, snapshot tracking |
src/session/compaction.ts | ~220 | Context compaction: overflow detection, message pruning, summary generation |
src/session/overflow.ts | ~20 | Overflow check: pure function that checks whether token usage exceeds the model’s context window |
src/session/retry.ts | ~100 | Retry strategy: error classification, exponential backoff, retry-after header parsing |
src/session/revert.ts | ~160 | Message revert: snapshot restoration, message deletion, diff calculation |
src/session/status.ts | ~80 | State management: idle/busy/retry tri-state switching, broadcast via Bus |
src/session/message-v2.ts | ~600 | Message types: Zod schemas defining all message and Part types, persistence, streaming queries |
Type System
Agent.Info — Agent Configuration Schema
export const Info = z
.object({
name: z.string(),
description: z.string().optional(),
mode: z.enum(["subagent", "primary", "all"]),
native: z.boolean().optional(),
hidden: z.boolean().optional(),
topP: z.number().optional(),
temperature: z.number().optional(),
color: z.string().optional(),
permission: PermissionNext.Ruleset,
model: z
.object({
modelID: z.string(),
providerID: z.string(),
})
.optional(),
variant: z.string().optional(),
prompt: z.string().optional(),
options: z.record(z.string(), z.any()),
steps: z.number().int().positive().optional(),
})
.meta({
ref: "Agent",
})
Key field descriptions:
mode:"primary"indicates a primary Agent (user converses directly),"subagent"indicates a sub-Agent (invoked by the Task tool),"all"indicates a custom Agentpermission:PermissionNext.Ruleset, defines the tool permission rule chain for this Agentprompt: Custom system prompt that overrides the Provider’s default promptmodel: Optional fixed model binding; if not specified, the user’s currently selected model is used
Built-in Agent List
| Agent Name | mode | hidden | Purpose |
|---|---|---|---|
build | primary | No | Default Agent with full tool permissions, supports question and plan_enter |
plan | primary | No | Planning mode, disables all editing tools, only allows writing to the plans directory |
explore | subagent | No | Quick code exploration with read-only tools only (grep, glob, read, bash, webfetch, etc.) |
general | subagent | No | General-purpose sub-Agent for parallel multi-step task execution, disables the todo tool |
compaction | primary | Yes | Dedicated to context compaction, disables all tools, generates conversation summaries |
title | primary | Yes | Generates titles for sessions, disables all tools, temperature fixed at 0.5 |
summary | primary | Yes | Generates summaries for sessions, disables all tools |
Session.Info — Session Schema
export const Info = z
.object({
id: Identifier.schema("session"),
slug: z.string(),
projectID: z.string(),
directory: z.string(),
parentID: Identifier.schema("session").optional(),
summary: z
.object({
additions: z.number(),
deletions: z.number(),
files: z.number(),
diffs: Snapshot.FileDiff.array().optional(),
})
.optional(),
share: z.object({ url: z.string() }).optional(),
title: z.string(),
version: z.string(),
time: z.object({
created: z.number(),
updated: z.number(),
compacting: z.number().optional(),
archived: z.number().optional(),
}),
permission: PermissionNext.Ruleset.optional(),
revert: z
.object({
messageID: z.string(),
partID: z.string().optional(),
snapshot: z.string().optional(),
diff: z.string().optional(),
})
.optional(),
})
Concurrency Control — Runner Mechanism
Each SessionID is bound to a Runner instance created by Effect’s Runner.make(). The Runner internally maintains a task queue, ensuring that operations on the same Session execute serially:
const runners = new Map<string, Runner<MessageV2.WithParts>>()
const getRunner = (runners, sessionID) => {
const existing = runners.get(sessionID)
if (existing) return existing
const runner = Runner.make<MessageV2.WithParts>(scope, {
onIdle: Effect.gen(function* () {
runners.delete(sessionID) // Auto-cleanup when idle, releasing resources
yield* status.set(sessionID, { type: "idle" })
}),
onBusy: status.set(sessionID, { type: "busy" }),
onInterrupt: lastAssistant(sessionID),
busy: () => { throw new Session.BusyError(sessionID) },
})
runners.set(sessionID, runner)
return runner
}
Key design points:
- Serialization guarantee: All operations on the same Session are queued through the Runner, with no concurrent race conditions
- Auto-cleanup: The Runner is removed from the Map when entering idle state, avoiding memory leaks
- Interrupt callback:
onInterrupttriggerslastAssistant()on cancellation, ensuring consistent state after interruption - Busy rejection: If the Session is busy and doesn’t support queuing, it throws
BusyErrordirectly
Race Condition — SyncEvent Event Source
Message writes don’t operate on the database directly, but execute synchronously through the SyncEvent event source:
const updateMessage = <T extends MessageV2.Info>(msg: T): Effect.Effect<T> =>
Effect.gen(function* () {
yield* Effect.sync(() => SyncEvent.run(MessageV2.Event.Updated, { sessionID: msg.sessionID, info: msg }))
return msg
})
This design eliminates the possibility of race conditions:
SyncEvent.run()executes synchronously: first writes to SQLite WAL, then publishes BusEvent- Effect’s single-threaded coroutine model guarantees operation atomicity
- Operations on the same Session are already serialized by the Runner, so there are no multi-threaded races
- Part updates have delta optimization: streaming text only sends BusEvent without writing to DB, reducing IO overhead
Core Flow — Session Execution Loop
Complete Call Chain
User input -> SessionPrompt.prompt()
-> SessionPrompt.loop()
|-- First message? -> ensureTitle() generates title asynchronously
|-- Pending subtask? -> TaskTool executes directly
|-- Pending compaction? -> SessionCompaction.process()
|-- Context overflow? -> SessionCompaction.create() -> continue
`-- Normal processing:
|-- SessionProcessor.create()
|-- resolveSystemPrompt() assembles system prompt
|-- resolveTools() registers built-in + MCP tools
`-- processor.process()
`-- LLM.stream()
`-- ai.streamText() (Vercel AI SDK)
`-- for await (stream.fullStream) process streaming events
|-- text-start / text-delta / text-end -> Text Part
|-- reasoning-start / reasoning-delta / reasoning-end -> Reasoning Part
|-- tool-input-start / tool-call / tool-result / tool-error -> Tool Part
|-- start-step / finish-step -> Snapshot tracking + token billing
`-- Error? -> SessionRetry.retryable() determines whether to retry
SessionPrompt.loop() — Main Loop in Detail
loop() is the heart of the entire system. It is called within SessionPrompt.prompt(), registering an AbortController via start() for cancellation control. Key loop logic:
- Read message stream:
MessageV2.filterCompacted(MessageV2.stream(sessionID))traverses messages in reverse, skipping compacted history - Check loop exit conditions: If the last Assistant message’s
finishis not"tool-calls"or"unknown", and its ID is greater than the last User message ID, exit the loop - Prioritize pending tasks: Check if the last message’s parts contain
compactionorsubtasktype Parts - Context overflow detection: Check whether compaction is needed via
SessionCompaction.isOverflow() - Normal processing: Create Assistant message, resolve tools, call
processor.process()
SessionProcessor.process() — Streaming Event Handling
SessionProcessor is a stateful object that holds the current Assistant message, tool call mapping table, and snapshot reference. The process() method contains a while(true) loop internally:
async process(streamInput: LLM.StreamInput) {
while (true) {
const stream = await LLM.stream(streamInput)
for await (const value of stream.fullStream) {
abort.throwIfAborted()
switch (value.type) {
// Text stream -> create/append TextPart
// Reasoning stream -> create/append ReasoningPart
// Tool call -> create ToolPart (pending -> running -> completed/error)
// Step tracking -> Snapshot.track() + StepStartPart/StepFinishPart
}
if (needsCompaction) break // Break on context overflow
}
// Error handling -> retryable? continue loop : mark error and exit
// Normal end -> return "continue" | "stop" | "compact"
}
}
LLM.stream() — Vercel AI SDK Wrapper
LLM.stream() is a wrapper around the Vercel AI SDK streamText(), responsible for:
- Getting the language model:
Provider.getLanguage(model)returns an AI SDK-compatibleLanguageModel - Assembling the system prompt: Merges Agent prompt, Provider default prompt, and user-customized system prompt by priority
- Parameter merging pipeline:
base->model.options->agent.options->variant, with each layer overriding the previous - Tool resolution:
resolveTools()filters tools based on the user’s disable list and permission rules - Calling
streamText(): Passes inwrapLanguageModel()middleware for message format transformation
export async function stream(input: StreamInput) {
const [language, cfg, provider, auth] = await Promise.all([
Provider.getLanguage(input.model),
Config.get(),
Provider.getProvider(input.model.providerID),
Auth.get(input.model.providerID),
])
// ... system prompt assembly, parameter merging ...
return streamText({
model: wrapLanguageModel({ model: language, middleware: [...] }),
messages: [...system.map(x => ({ role: "system", content: x })), ...input.messages],
tools,
abortSignal: input.abort,
maxRetries: input.retries ?? 0,
// ... other parameters
})
}
Doom Loop Detection Mechanism
SessionProcessor implements Doom Loop detection internally to prevent the LLM from entering an infinite loop of repeatedly calling the same tool:
const DOOM_LOOP_THRESHOLD = 3
// In the tool-call event handler
const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD)
if (
lastThree.length === DOOM_LOOP_THRESHOLD &&
lastThree.every(
(p) =>
p.type === "tool" &&
p.tool === value.toolName &&
p.state.status !== "pending" &&
JSON.stringify(p.state.input) === JSON.stringify(value.input),
)
) {
await PermissionNext.ask({
permission: "doom_loop",
patterns: [value.toolName],
sessionID: input.assistantMessage.sessionID,
// ...
})
}
Detection condition: the last 3 tool calls are all the same tool name with the same input parameters. When triggered, PermissionNext.ask() requests user confirmation. The user can choose to “always allow” doom loops for that tool.
Subtask Execution Model
Subtasks do not create new Sessions. Instead, they create new Assistant Messages + Tool Parts within the current Session:
const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input) {
// 1. Create a standalone assistant message (mode = task.agent)
// 2. Create a tool part (status: "running")
// 3. Execute taskTool.execute() within the same session
// Uses the sub-agent's permission rules
// Passes the current session's message history
})
Key design points:
- Subtasks reuse the current Session’s message history, avoiding redundant loading
- Uses the sub-Agent’s permission rules (merged with Session-level permissions), rather than the primary Agent’s permissions
- If triggered via
/command, a summary User Message is automatically injected upon completion, letting the primary Agent know the subtask’s execution result - Tool Part lifecycle:
pending->running->completed/error, consistent with other tool calls
Plugin Hook System
The Session execution loop injects Plugin Hooks at multiple points, allowing external extensions:
| Hook Point | Trigger Timing | Purpose |
|---|---|---|
chat.system.transform | During system prompt construction | Transform/inject system prompt content |
chat.params | Before LLM parameters are passed | Modify temperature, maxTokens, and other parameters |
tool.execute.before | Before tool execution | Intercept, log, modify tool input |
tool.execute.after | After tool execution | Post-process tool output, audit logging |
command.execute.before | Before command execution | Intercept/modify command parameters |
chat.message | During message construction | Transform/augment message content |
shell.env | During shell command execution | Inject environment variables |
These hooks enable Plugins to intervene in the execution flow without modifying core code. For example, an audit Plugin can log all tool call inputs via tool.execute.before, or inject project-specific environment variables for shell commands via shell.env.
Context Compaction — Two-Layer Optimization
Compaction is divided into two phases, each with fine-grained protection mechanisms:
1. Overflow Detection (SessionCompaction.isOverflow()):
// overflow.ts
export function isOverflow(input) {
const reserved = input.cfg.compaction?.reserved ?? Math.min(20_000, maxOutputTokens(model))
const usable = input.model.limit.input
? input - reserved
: context - maxOutputTokens(model)
return count >= usable
}
- Calculates total token count of
input + output + cache.read + cache.write - Usable space =
model.limit.input - reserved(reserved defaults tomin(20000, maxOutputTokens)) - Triggers compaction when token usage >= usable space
2. Message Pruning (SessionCompaction.prune()) — Two-Layer Protection:
First layer — minimum threshold:
PRUNE_MINIMUM = 20,000tokens; pruning is not triggered below this value- Avoids meaningless pruning operations on short conversations
Second layer — protection band:
PRUNE_PROTECT = 40,000tokens, protecting recent tool outputs from being prunedPRUNE_PROTECTED_TOOLS = ["skill"], tool outputs marked as protected are never pruned- Traverses from the latest messages backward, skipping the most recent 2 rounds of conversation
Pruning execution:
- Tool outputs exceeding the threshold are marked as
compacted(settingstate.time.compacted = Date.now()) - Subsequent calls to
toModelMessages()replace pruned outputs with[Old tool result content cleared] - Executed asynchronously at the end of each prompt loop (
Effect.forkIn(scope)), without blocking the main loop
3. Summary Generation (SessionCompaction.process()):
- Uses the
compactionAgent (with all tools disabled) - Sends the entire message history + summary instructions to the LLM
- The generated summary message is marked
summary: true; subsequent calls tofilterCompacted()use this as the pruning point - If auto-compaction succeeds, a “Continue” message is automatically appended to resume the conversation
Error Handling and Edge Cases
LLM Call Retry Strategy
The SessionRetry module implements a complete retry strategy:
Retryable Error Classification (retryable()):
APIErrorwithisRetryable === true: includes 429 rate limiting and 500 server errorsContextOverflowError: not retried, triggers compaction directlyFreeUsageLimitError: returns a prompt message, not retried- Errors with response bodies containing
"exhausted"/"unavailable"/"rate_limit"
Backoff Algorithm (delay()):
- First reads response headers
retry-after-ms(milliseconds) orretry-after(seconds/HTTP date) - Without headers, uses exponential backoff:
2000ms * 2^(attempt-1), capped at 30 seconds - With headers, the cap is
2^31 - 1(maximum 32-bit signed integer)
export const RETRY_INITIAL_DELAY = 2000
export const RETRY_BACKOFF_FACTOR = 2
export const RETRY_MAX_DELAY_NO_HEADERS = 30_000
In the catch block of SessionProcessor.process():
const error = MessageV2.fromError(e, { providerID: input.model.providerID })
const retry = SessionRetry.retryable(error)
if (retry !== undefined) {
attempt++
const delay = SessionRetry.delay(attempt, ...)
SessionStatus.set(sessionID, { type: "retry", attempt, message: retry, next: ... })
await SessionRetry.sleep(delay, input.abort)
continue // Return to top of while(true) to retry
}
Tool Execution Error Handling
When tool execution fails (tool-error event), the handler takes different actions based on error type:
PermissionNext.RejectedError: User rejected the permission request; based on configuration, decides whether to break the loopQuestion.RejectedError: User rejected the question confirmation; similarly decides based on configuration- Other errors: Logs the error information, sets tool status to
error, continues processing subsequent events
After the loop ends, all tools in pending or running state are marked with the "Tool execution aborted" error.
Message Revert — Complete Mechanism
The SessionRevert module uses the Effect Service pattern, providing three operations. The core of revert lies in precisely restoring file system state:
revert() Complete Flow:
- Traverses messages and Parts to locate the user-specified revert point
- Starting from the revert point, collects all subsequent Patch Parts (file edit operations)
- Captures the current file system Snapshot as the revert baseline
- Applies the reverse Patch operations:
snap.revert(patches)undoes all file modifications in reverse order - Writes revert metadata (messageID, partID, snapshot, diff) to the Session.revert field
// Simplified core logic
const revert = Effect.fn("SessionRevert.revert")(function* (input) {
// 1. Locate revert point
// 2. Collect subsequent patches
// 3. Capture current snapshot
// 4. snap.revert(patches) — reverse-apply all edits
// 5. Update session.revert metadata
})
revert(): Reverts to the specified message/Part, restores file system state via Snapshot, calculates diffunrevert(): Undoes the revert, restores to the pre-revert snapshotcleanup(): Cleans up revert state at the start of the next conversation (withinprompt()call), deleting all messages after the revert point
This design ensures that revert is reversible — unrevert can precisely restore to the pre-revert state, because snapshots and diffs are saved at every step.
Degradation on Provider Unavailability
When Provider authentication fails, MessageV2.fromError() generates an AuthError. SessionRetry.retryable() returns undefined for authentication errors (not retryable), and the loop terminates immediately. Context overflow errors (ContextOverflowError) are also not retried; instead, they are detected via isOverflow() in loop() to trigger automatic compaction.
Precise Cost Calculation
Cost calculation in Session has multiple details requiring precise handling:
- Cache write tokens: Extracted from Provider-specific fields across multiple sources including anthropic, vertex, bedrock, venice, etc.
- Cached token deduplication: AI SDK v6’s
inputTokensalready includes cached tokens; they must be subtracted to avoid double billing - Pricing above 200K: Uses differential pricing via the
experimentalOver200Kfield - Reasoning tokens: Billed at output price (rather than separately priced)
- Precision guarantee: Uses
Decimal.jsfor precise floating-point calculations, avoiding JavaScript’s native floating-point errors
Tool Call Repair Mechanism
Tool call names returned by the LLM may have case errors. experimental_repairToolCall provides automatic repair:
async experimental_repairToolCall(failed) {
const lower = failed.toolCall.toolName.toLowerCase()
// Case repair: convert tool name to lowercase for matching
// If still unmatched -> route to "invalid" tool
// "invalid" tool returns a friendly error message
}
This prevents tool call failures caused by the model outputting Read_File instead of read_file — the system automatically attempts case-insensitive matching, and if that fails, the “invalid” tool provides a clear error message.
State Management
InstanceState Cache
The Agent list implements lazy-loaded caching via Instance.state():
const state = Instance.state(async () => {
const cfg = await Config.get()
// ... build Agent list ...
return result as Record<string, Agent.Info>
})
export async function get(agent: string) {
return state().then((x) => x[agent])
}
Instance.state() returns an async function that executes the factory function on first call and caches the result. Subsequent calls return the cached value directly. The cache is automatically cleaned up when the Instance is destroyed.
Session State Tri-State
SessionStatus manages each Session’s state through an Effect Service:
export const Info = z.union([
z.object({ type: z.literal("idle") }),
z.object({ type: z.literal("busy") }),
z.object({ type: z.literal("retry"), attempt: z.number(), message: z.string(), next: z.number() }),
])
State changes are broadcast via Bus.publish(Event.Status, ...), and the UI layer subscribes to these events to update the interface.
Cancellation Control and Precise Interruption
SessionPrompt.loop() registers an AbortController for each Session via start():
function start(sessionID: string) {
const controller = new AbortController()
s[sessionID] = { abort: controller, callbacks: [] }
return controller.signal
}
cancel() calls controller.abort() to interrupt all in-progress LLM calls and tool executions. If the same Session has queued requests (callbacks), they are rejected.
AbortController Precise Interruption Points: The stream method of LLM.Service creates an AbortController via Effect.acquireRelease, performing precise resource cleanup on interruption:
- Incomplete snapshots: Computes a patch for the current file system state, saving completed edits
- Incomplete text: Saves streaming text that has been received but not yet written
- Reasoning parts: Cleans up incomplete reasoning content
- Tool markers: All tools in
running/pendingstate are marked aserror("Tool execution aborted")
This fine-grained interruption handling ensures that cancellation operations don’t leave inconsistent state — incomplete edits are rolled back or saved, and tool calls are gracefully terminated.
Call Chain Examples
Chain 1: User Sends Message -> LLM Call -> Tool Execution -> Response Rendering
1. SessionPrompt.prompt({ sessionID, parts: [{ type: "text", text: "read main.ts" }] })
|-- createUserMessage() -> write User message + TextPart to Storage
`-- loop(sessionID)
|-- start() -> register AbortController
|-- MessageV2.filterCompacted(stream) -> load uncompacted message history
|-- Agent.get("build") -> get build Agent configuration
|-- SessionProcessor.create() -> create empty Assistant message
|-- resolveSystemPrompt() -> [header, agentPrompt + environment + custom]
|-- resolveTools() -> register ReadTool, BashTool, EditTool and other built-in tools + MCP tools
`-- processor.process()
|-- LLM.stream() -> ai.streamText() initiates streaming request
|-- Streaming events:
| |-- text-delta -> write TextPart, Bus broadcast PartUpdated
| |-- tool-input-start -> create ToolPart (status: "pending")
| |-- tool-call -> update ToolPart (status: "running", input: { filePath: "main.ts" })
| |-- ReadTool.execute() -> read file contents
| `-- tool-result -> update ToolPart (status: "completed", output: file contents)
`-- return "continue" (finish reason is not tool-calls)
2. UI layer subscribes to MessageV2.Event.PartUpdated via Bus for real-time rendering
3. loop() next iteration detects Assistant is complete -> break -> return final message
Chain 2: Context Compaction Triggered -> Summary Generation -> Message Pruning
1. finish-step event in processor.process():
|-- Session.getUsage() -> calculate token usage: { input: 180000, output: 8000, ... }
|-- SessionCompaction.isOverflow() -> 180000 + 8000 >= 190000 -> true
`-- needsCompaction = true -> break interrupts streaming
2. process() returns "compact" -> loop() continues
3. loop() next iteration:
|-- Detects isOverflow() -> true
`-- SessionCompaction.create() -> write User message + CompactionPart
4. loop() iterates again:
|-- Detects pending compaction task
`-- SessionCompaction.process()
|-- Agent.get("compaction") -> get compaction-dedicated Agent
|-- Create Assistant message (mode: "compaction", summary: true)
|-- SessionProcessor.create()
`-- processor.process()
|-- Historical messages + summary instructions -> LLM generates structured summary
| (Goal / Instructions / Discoveries / Accomplished / Relevant files)
`-- return "continue"
5. Subsequent filterCompacted() uses the summary:true message as the pruning point
6. SessionCompaction.prune() -> clean up old tool outputs (preserving the most recent 40000 tokens)
Design Tradeoffs
Why is Agent a Configuration Instead of a Class?
The TypeScript version changed Agent from the Go version’s “interface + implementation” pattern to a pure configuration object. Reasons:
- Eliminate code duplication: In the Go version, the Coder Agent and Task Agent had nearly identical execution loops, differing only in their tool sets. In the TS version, all Agents share the
SessionPrompt.loop()->SessionProcessor.process()execution path - Declarative composition: Permissions are combined via
PermissionNext.merge(), tools are filtered viaToolRegistry.enabled(), model/temperature are overridden via configuration — no subclassing needed - Runtime extensibility: The
generate()method can dynamically create new Agents via LLM, and user configurations candisablebuilt-in Agents or add custom Agents
Why Vercel AI SDK?
The ai package (Vercel AI SDK) provides a unified streamText() API that abstracts away protocol differences between providers (Anthropic, OpenAI, Google, etc.). OpenCode builds on this by injecting its own message transformation logic via wrapLanguageModel() middleware (ProviderTransform.message()), and handling provider-specific parameter formats via ProviderTransform.providerOptions().
Why is prompt.ts So Large (~580 Lines)?
prompt.ts consolidates responsibilities that were spread across agent.go, tools.go, and session.go in the Go version:
- User message construction (file reading, Agent reference resolution, directory listing)
- Tool resolution and registration (built-in tools + MCP tools + permission filtering)
- Subtask direct execution
- Command (
/command) template expansion - Shell command execution
- Automatic title generation
Relationships with Other Modules
- Provider (
src/provider/):LLM.stream()obtains an AI SDK-compatibleLanguageModelviaProvider.getLanguage(); obtains model metadata (context window, pricing, etc.) viaProvider.getModel() - Config (
src/config/): The Agent list’sstate()readsConfig.get()to get user-customized Agent configurations and permission overrides - Permission (
src/permission/):resolveTools()callsPermissionNext.disabled()to filter disabled tools; Doom Loop detection requests user confirmation viaPermissionNext.ask() - Storage (
src/storage/): All messages, Parts, and Sessions are persisted viaStorage.write/update/read - Bus (
src/bus/): Message updates, Part updates, and state changes are all broadcast via Bus; the UI layer subscribes to these events to drive rendering - Snapshot (
src/snapshot/):SessionProcessortracks file system snapshots at each step (step-start/step-finish) for diff calculation and revert - ToolRegistry (
src/tool/):resolveTools()gets the built-in tool list fromToolRegistry.tools(), merging the Agent’s tool enable configuration viaToolRegistry.enabled() - MCP (
src/mcp/):resolveTools()gets external tools fromMCP.tools(), wrapping them in AI SDKtool()format - Plugin (
src/plugin/): Provides hook points at multiple stages including system prompt construction, before/after tool execution, and chat parameters - Instance (
src/project/):Instance.state()provides lifecycle management for the Agent list cache;Instance.directory/worktreeprovides working directory information