Claude Code 101|01|Agent Runtime
Claude Code 的核心不是 CLI 外壳,而是一个把模型、工具、上下文、权限、状态和终端体验组织起来的 Agent Runtime。
Claude Code 经常被描述成“命令行里的代码助手”。这个说法适合介绍产品入口,却不适合解释它为什么能处理真实工程任务。真实 coding agent 不是把一段 prompt 发给模型,然后等待一段文本回来。它要在工程目录里读文件、改代码、运行命令、处理权限、接收中断、恢复会话、控制上下文预算,并把中间状态持续展示给用户。
因此,成熟 coding agent 需要的不是“更聪明的 prompt”,而是 Agent Runtime:一个把输入、上下文、模型循环、工具执行、权限、状态、持久化、UI 事件和外部协议组织起来的运行时容器。
这篇把 Claude Code 当成一个 Agent Runtime 案例来理解:CLI 只是入口,模型只是推理核心,真正决定系统能力的是两者之间那层 runtime。

工程问题
如果只是做 demo,run(prompt) 已经足够。用户输入一句话,程序拼 system prompt,调用模型,返回文本。但代码任务不是一次问答,它会跨越三个边界。
第一是环境边界。模型可以提出“读取文件”“修改配置”“运行测试”“搜索文档”,但真正执行这些动作的是宿主进程。宿主必须知道哪些工具存在、工具输入是否合法、是否需要用户授权、输出是否过大、失败如何回填。
第二是时间边界。一次代码修改可能包含十几轮 tool_use / tool_result;用户可能中断;API 可能重试;上下文可能过长而触发 compact;进程可能退出后再 resume。没有持久状态,Agent 的“继续”只是一种幻觉。
第三是产品边界。同一个核心能力要服务交互式 TUI、headless SDK、后台任务、远程会话、子 Agent 和插件命令。把逻辑写死在 UI 里会导致 SDK 路径无法复用;把逻辑写成纯模型函数又无法承载权限和可观测性。
Agent Runtime 的价值,就是把这些边界变成系统结构,而不是让模型在 prompt 里临时猜。
Runtime 处在什么位置
Claude Code 可以理解成三层。
第一层是产品宿主。交互式终端负责输入队列、消息渲染、权限弹窗、后台任务状态和用户中断;SDK / headless 路径则把同样的过程投影成可程序消费的事件流。它们面向不同使用场景,但都不是模型本身。
第二层是 runtime 内核。它维护 turn 生命周期、消息历史、工具协议、上下文投影、权限状态、usage、transcript、compact 边界和恢复路径。这里才是 coding agent 从聊天产品变成工程系统的关键。
第三层是环境适配。文件系统、shell、LSP、Web、MCP、Git、IDE、terminal renderer 和 telemetry exporter 都在这一层。Agent 的动作通过这些边界进入真实世界,也必须把真实世界的结果重新包装成模型能理解的观察。
产品宿主负责交互形态,runtime 负责 turn 生命周期,环境适配负责副作用。Claude Code 的复杂度来自这条边界清晰存在,而不是所有逻辑混在一个 CLI handler 里。
Conversation turn 是基本单位
Claude Code 的基本单位不是命令行参数,也不是单次模型请求,而是 conversation turn。一次 turn 从用户输入开始,经过输入编译、上下文投影、模型采样、工具执行、观察注入、可能的 continuation,最后以 result、错误或中断结束。

把 turn 当成基本单位以后,很多设计会自然浮现:输入被接受后要先进入消息链;工具调用要有 tool_use_id;权限拒绝要成为可记录的状态;中断不能只是 kill process;最终结果要能被 TUI、SDK 或后台任务消费。
这里的重点不是某个类名,而是边界:用户输入一旦被 runtime 接受,conversation 就已经有了可恢复的锚点,即使模型还没有返回。
可迁移的抽象可以写成这样:
type ConversationTurn = {
id: string
acceptedInput: RuntimeMessage[]
modelVisibleContext: ContextProjection
emittedEvents: RuntimeEvent[]
stateDelta: StateMutation[]
result?: TurnResult
}
class AgentRuntime {
async *submit(input: UserInput): AsyncGenerator<RuntimeEvent, TurnResult> {
const compiled = await this.inputPipeline.compile(input, this.snapshot())
await this.transcript.append(compiled.acceptedMessages)
this.state.messages.push(...compiled.acceptedMessages)
if (!compiled.shouldEnterLoop) {
return this.finishLocalTurn(compiled)
}
const projection = await this.contextBuilder.project(this.state)
for await (const event of this.agentLoop.run(projection)) {
this.state.apply(event)
yield this.protocol.project(event)
}
return this.state.currentTurnResult()
}
}
这个草图刻意不绑定 Claude Code 的具体文件。它表达的是一个更通用的 Agent Runtime 形状:输入先被编译,状态先被持久化,模型只看到一次投影,工具和 UI 都通过事件协议回到 runtime。
Runtime 把模型行动约束在工具协议里
在普通聊天产品里,模型输出就是答案。在 coding agent 里,模型输出经常只是行动意图。它可能想读文件、编辑代码、运行测试、搜索网页、调用 LSP 或请求子 Agent。真正执行这些动作的不是模型,而是 runtime。
这就产生了一个关键分工:模型负责提出行动,runtime 负责治理行动。治理包括工具查找、输入校验、权限判断、沙箱执行、输出截断、错误包装、观察回填和事件展示。
工具调用不应该被理解成“模型函数调用”的小功能,而应该被理解成 runtime 的受治理执行协议。
type ToolProposal = {
id: string
name: string
input: unknown
}
type ToolObservation = {
toolUseId: string
content: string | ContentBlock[]
isError?: boolean
metadata?: {
source?: string
truncated?: boolean
permission?: PermissionDecision
}
}
async function executeToolProposal(
proposal: ToolProposal,
ctx: RuntimeContext,
): Promise<ToolObservation> {
const tool = ctx.tools.resolve(proposal.name)
const input = await tool.validate(proposal.input)
const decision = await ctx.permissions.check(tool, input)
if (!decision.allowed) {
return ctx.observations.denied(proposal.id, decision)
}
const raw = await tool.run(input, ctx.toolUseContext)
return ctx.resultBudget.pack(proposal.id, raw)
}
这个协议让 Agent 可以被审计:每次行动都有来源,每次结果都有归属,每次拒绝都能解释。模型不直接拥有 shell、文件系统或网络;它只能通过 runtime 提供的工具边界触达环境。
Runtime 把上下文视为投影
很多 Agent 系统会把 Context Engineering 理解成“把更多东西塞进 prompt”。Claude Code 更接近另一种思路:runtime state 可以很大,但模型每一轮只能看到一个经过选择、排序、压缩和预算治理的投影。

Runtime state 里可能包含消息历史、文件读取缓存、MCP 状态、工具列表、权限规则、额外工作目录、memory attachment、skill discovery、compact 边界、任务状态和 UI 状态。但模型请求不应该直接等于这些状态的字符串拼接。
Context Engineering 的核心不是“获得上下文”,而是“在每个 turn 之前构造一个足够准确、足够稳定、足够省预算、足够可治理的上下文视图”。
type ContextProjection = {
system: string[]
userContext: Record<string, string>
systemContext: Record<string, string>
messages: RuntimeMessage[]
tools: ToolSchema[]
}
async function projectContext(state: RuntimeState): Promise<ContextProjection> {
const controlPlane = await state.promptBuilder.buildSystemParts()
const facts = await state.contextSources.collectUserAndSystemContext()
const messages = await state.compaction.applyBoundaries(state.messages)
const budgeted = await state.resultBudget.rewriteLargeToolResults(messages)
return {
system: controlPlane,
userContext: facts.user,
systemContext: facts.system,
messages: budgeted,
tools: state.tools.visibleSchemas(),
}
}
这个机制解释了为什么成熟 Agent 越做越不像 chat completion wrapper。系统真正维护的是 state graph;模型请求只是这个 state graph 的一次可见切片。
Runtime 把权限、中断和恢复放进主路径
文件修改、shell 执行、Web 访问和 MCP 调用都是有副作用的动作。如果权限只是 UI 弹窗,系统很快会失控:SDK 怎么表达权限拒绝?后台任务怎么请求授权?子 Agent 的权限由谁批准?被拒绝的工具调用是否进入 transcript?用户中断后 turn 是失败、取消还是可恢复?

权限拒绝不是异常;它是工具观察的一种。用户中断不是崩溃;它是 turn 的一种终止路径。只有把这些路径放进 runtime,系统才能稳定地恢复、报告和继续。
这也是 coding agent 和普通自动化脚本的重要区别。自动化脚本通常假设路径顺利:输入确定、命令可跑、输出可读、错误可抛。Agent Runtime 必须默认路径不顺利:模型可能提出危险动作,工具可能失败,输出可能过长,权限可能被拒绝,上下文可能爆掉,用户可能随时停止。
成熟 runtime 不会把这些情况都交给外层 try/catch。它会把它们建模成状态:permission_denied、aborted、prompt_too_long、tool_result_over_budget、compact_required、retryable_api_error、recoverable_session_state。状态一旦明确,UI 可以解释,SDK 可以消费,resume 可以恢复,telemetry 可以观察。
Runtime 把 UI 变成可观测性层
Claude Code 的终端 UI 不是薄薄的 stdout printer。交互式体验里有 assistant 文本、thinking、tool use、tool result、progress、permission dialog、compact boundary、后台任务和错误恢复。它还要处理输入队列、桥接消息、MCP 工具刷新和用户中断。
一个成熟 Agent Runtime 至少需要两种投影:给模型的上下文投影,给用户或 SDK 的事件投影。前者决定模型能否继续推理,后者决定人和外部系统能否理解正在发生什么。
type RuntimeEvent =
| { type: 'assistant_delta'; text: string }
| { type: 'tool_started'; name: string; id: string }
| { type: 'tool_finished'; id: string; output: string }
| { type: 'permission_request'; tool: string; input: unknown }
| { type: 'compact_boundary' }
| { type: 'turn_result'; status: 'success' | 'error' | 'aborted' }
interface RuntimeHost {
render(event: RuntimeEvent): void
requestPermission(event: RuntimeEvent): Promise<PermissionDecision>
report(result: TurnResult): void
}
TUI 和 SDK 可以是不同宿主,但它们都不应该直接窥探 runtime 内部变量。更稳妥的方式是让 runtime 产生事件,再由宿主投影成屏幕、日志、SDK stream 或 telemetry span。
Claude Code 作为 Runtime 案例
从 Agent Runtime 的角度看,Claude Code 不是“终端里多接了一个模型”,而是一套围绕 turn lifecycle 组织起来的工程系统。交互式终端、SDK、工具调用、上下文构建、权限判断、会话恢复和事件投影,都是这个 runtime 的不同表面。
这也是为什么 Claude Code 值得放进 Agent Engineering 101:它展示的不是某个模型有多会写代码,而是 coding agent 如何被组织成一个可治理、可恢复、可观察的运行时系统。
如果只从产品入口看,它像 CLI;如果只从模型调用看,它像 chat completion wrapper;如果从 runtime 视角看,它更像一个小型操作系统:有输入编译,有调度循环,有权限,有状态,有 I/O,有事件投影,也有恢复机制。
工程启发
第一,不要从 run(prompt) 设计长期接口。它会诱导你把输入、状态、权限、工具、日志和 UI 全部塞进一个函数。更稳妥的抽象是 conversation object 或 runtime object。
第二,把模型循环和宿主分开。交互式终端、SDK、后台任务、远程会话可以共享 Agent Loop,但不应该共享所有 UI 状态。
第三,把工具当作受治理的协议,而不是普通函数。工具输入校验、权限、执行、输出压缩、错误回填、可观测事件和 transcript 都应该是协议的一部分。
第四,把上下文当作投影。Runtime state 可以长期存在,模型上下文必须按 turn 生成、按预算裁剪、按恢复策略维护边界。
第五,把失败路径放进主设计。权限拒绝、用户中断、API retry、输出过长、prompt too long、compact、工具失败和进程退出都不是边缘情况。Agent Runtime 的质量,往往取决于这些路径是否被建模为状态机,而不是 try/catch 后打印错误。
小结
Claude Code 展示的不是“CLI + LLM API + tools”的简单组合,而是一个终端里的 Agent Runtime。它把用户输入编译成运行时事件,把状态投影成模型上下文,把模型行动约束在工具协议里,把权限和中断纳入主路径,再把内部事件投影给 TUI 或 SDK。
理解这一层后,后面的文章就有了共同坐标。第 02 篇会进入 Input Pipeline:一次看似普通的输入,如何被编译成 messages、控制信号和本地执行分支。第 03 篇会看 QueryEngine:这些输入如何被组织成可恢复的 conversation turn。第 04 篇再进入更内层的 Agent Loop:模型与工具如何在 continuation / recovery 状态机中推进。