Harness · Coding Agent Book17 / Prime Agent
研究总览
M17 · SOURCE-GROUNDED TUTORIAL

Prime Agent
从源码学会它怎么工作

把 provider-neutral Agent Loop、可扩展 AgentSession、结构化 compaction、RLM 子 Agent 与 resident daemon 组合成一个可嵌入的 TypeScript coding harness。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。

TypeScript · Extension-first Pi Harness / RLM Daemon AgentMITa3b3e753490d29 个结论 · 84 处引用
这门课怎么读

先建立直觉,再沿一条任务链钻进代码

参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 Prime Agent 的 29 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。

你会得到一张可复述的架构地图一次完整任务的链路追踪能迁移到自研 Harness 的设计判断
你不会得到把 README 功能当成已验证事实把 prompt 约束说成 OS 沙箱把一次双模型调用夸成多 Agent 平台
M00 · MAP

先看全景:这个 Agent 的控制面在哪里

下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。

架构图全屏打开 ↗
一轮执行链路全屏打开 ↗
核心机制

AgentMessage loop;steer → tool batch → follow-up → continuation

上下文

动态 transform;reserve 16K、keep 20K;结构化 summary + branch tree

适用建设

可嵌入 SDK、长任务恢复、TUI/RPC/print/daemon 共享运行时

M00.5 · TRACE

跟踪一个任务:从输入到交付

把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。

读图提醒

箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。

M01 · ORIENTATION

先把 Agent 看成一台会交付的机器

如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。

先用一个生活比喻

把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。

本节阅读法先问问题读事实看代码做迁移判断
读源码时先问本课的判断方式
这一层有没有独立证据?没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。
谁拥有最终控制权?区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念

固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。

小练习 1

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M02 · LOOP

主循环:模型为什么会继续动

一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?

先用一个生活比喻

像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。

这套实现先回答了什么?

Prime Agent 把“模型怎么流式回答、什么时候执行工具、用户插话后是否继续”抽成一个不依赖 TUI 的小内核,上层入口只负责喂配置和消费事件。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
低层 Agent Loop 是可复用的 provider-neutral 状态机packages/agent/src/agent-loop.ts:178Prime Agent 把“模型怎么流式回答、什么时候执行工具、用户插话后是否继续”抽成一个不依赖 TUI 的小内核,上层入口只负责喂配置和消费事件。
Steering、follow-up、continuation 是三个不同的队列语义packages/agent/src/agent-loop.ts:317用户正在打断时是一种消息,用户等 Agent 停下来再追加是另一种消息,系统为了长目标自动继续又是第三种消息,三者不会混成一个 pending 数组。
最值得借鉴的是“纯 loop + coding host + extension bus”三层分离packages/coding-agent/src/core/agent-session.ts:1这套分层让我们既能复用基础 loop,又能按产品需要装配 TUI、RPC、daemon 或插件,而不是把所有能力塞进一个巨型 Agent 类。
01
L1 · fact · prime-arch-001

低层 Agent Loop 是可复用的 provider-neutral 状态机

先看源码事实

agentLoop 只接收 prompts、AgentContext、AgentLoopConfig 和可选 streamFn;runLoop 把 steering、tool calls、follow-up、continuation 和 abort 编排成外层/内层循环,并最终发出 agent_end。

翻译成白话

Prime Agent 把“模型怎么流式回答、什么时候执行工具、用户插话后是否继续”抽成一个不依赖 TUI 的小内核,上层入口只负责喂配置和消费事件。

为什么这对自研重要

自研时可把 loop 做成纯运行时,再让 CLI、RPC、桌面 UI 共用;不要让界面组件自己复制一套 tool loop。

固定提交源码摘录
  178   * Start an agent loop with a new prompt message.
  179   * The prompt is added to the context and events are emitted for it.
  180   */
  181  export function agentLoop(
  182  	prompts: AgentMessage[],
  183  	context: AgentContext,
  184  	config: AgentLoopConfig,
  185  	signal?: AbortSignal,
  186  	streamFn?: StreamFn,
  187  ): EventStream<AgentEvent, AgentMessage[]> {
  188  	const stream = createAgentStream();
  189  
  190  	endAgentStreamOnError(
  191  		stream,
  192  		runAgentLoop(
  193  			prompts,
  194  			context,
  195  			config,
  196  			async (event) => {
  197  				stream.push(event);
  198  			},
  199  			signal,
  200  			streamFn,
  201  		),
  202  	);
  203  
  204  	return stream;
  205  }
为什么相信这条结论?查看 3 处证据
02
L1 · fact · prime-loop-003

Steering、follow-up、continuation 是三个不同的队列语义

先看源码事实

runLoop 在当前 assistant turn 完成工具后轮询 steering;没有工具和 steering 时才轮询 follow-up;最后通过 getContinuationMessages 处理宿主拥有的长任务续跑策略。

翻译成白话

用户正在打断时是一种消息,用户等 Agent 停下来再追加是另一种消息,系统为了长目标自动继续又是第三种消息,三者不会混成一个 pending 数组。

为什么这对自研重要

长任务产品应给消息定义明确的 admission boundary,才能保证“插话”不会跳过当前工具调用或错误地重放。

固定提交源码摘录
  317  	// Check for steering messages at start (user may have typed while waiting)
  318  	let pendingMessages: AgentMessage[] = await pollMessagesUnlessAborted(config.getSteeringMessages, signal);
  319  
  320  	const shouldStopBeforeTurn = (): boolean => !firstTurn && (config.shouldStopBeforeTurn?.() ?? false);
  321  
  322  	// Outer loop: continues when queued follow-up messages arrive after agent would stop
  323  	while (true) {
  324  		throwIfAborted(signal);
  325  		let hasMoreToolCalls = true;
  326  
  327  		// Inner loop: process tool calls and steering messages
  328  		while (hasMoreToolCalls || pendingMessages.length > 0) {
  329  			throwIfAborted(signal);
  330  			if (!firstTurn) {
  331  				await emit({ type: "turn_start" });
  332  			} else {
  333  				firstTurn = false;
  334  			}
  335  
  336  			// Process pending messages (inject before next assistant response)
  337  			if (pendingMessages.length > 0) {
  338  				for (const message of pendingMessages) {
  339  					await emit({ type: "message_start", message });
  340  					await emit({ type: "message_end", message });
  341  					currentContext.messages.push(message);
  342  					newMessages.push(message);
  343  				}
  344  				pendingMessages = [];
  345  			}
为什么相信这条结论?查看 3 处证据
03
L2 · inference · prime-recommend-001

最值得借鉴的是“纯 loop + coding host + extension bus”三层分离

先看源码事实

低层 agent-loop 只处理消息/工具/流;AgentSession 组合 compaction、resource loader、MCP、RLM、session manager;ExtensionRunner 再把 context/provider/tool/session 变换做成可插拔 bus。

翻译成白话

这套分层让我们既能复用基础 loop,又能按产品需要装配 TUI、RPC、daemon 或插件,而不是把所有能力塞进一个巨型 Agent 类。

为什么这对自研重要

自研架构建议沿此边界拆包:Core Loop、Harness Session、Policy/Extension Bus、Execution Adapters、Persistence/Control Plane。

边界与风险
  • 这是跨模块架构归纳,不是仓库作者的单一官方标签。
固定提交源码摘录
    1  /**
    2   * AgentSession - Core abstraction for agent lifecycle and session management.
    3   *
    4   * This class is shared between all run modes (interactive, print, rpc).
    5   * It encapsulates:
    6   * - Agent state access
    7   * - Event subscription with automatic session persistence
    8   * - Model and thinking level management
    9   * - Compaction (manual and auto)
   10   * - Bash execution
   11   * - Session switching and branching
   12   *
   13   * Modes use this class and add their own I/O layer on top.
为什么相信这条结论?查看 3 处证据
小练习 2

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M03 · MODEL

模型调用:流式输出如何变成可执行步骤

模型输出的文字、思考、工具调用和错误,经过哪些转换才进入 Agent 状态?

先用一个生活比喻

模型像电话另一端的同事:你听到的不是一整段录音,而是一串实时片段;Harness 要边听边拼装,还要能在电话断线时留下可恢复的记录。

这套实现先回答了什么?

上下文不会在会话开始时被一次性拍扁,长任务中每次请求都可以重新裁剪、换系统提示和刷新短期 token。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
Provider 边界前才做上下文变换和密钥解析packages/agent/src/agent-loop.ts:467上下文不会在会话开始时被一次性拍扁,长任务中每次请求都可以重新裁剪、换系统提示和刷新短期 token。
运行时支持 provider 动态系统提示和过期 token 刷新packages/agent/src/types.ts:170长时间工具执行后,下一次模型调用仍能拿到最新授权和最新资源说明。
04
L1 · fact · prime-arch-002

Provider 边界前才做上下文变换和密钥解析

先看源码事实

streamAssistantResponse 在每次 provider 请求前依次执行 transformContext、convertToLlm、动态 getApiKey、动态 system prompt,然后把 Context 交给 streamFunction;流事件逐步回写 context.messages,并对 abort 生成最终 aborted message。

翻译成白话

上下文不会在会话开始时被一次性拍扁,长任务中每次请求都可以重新裁剪、换系统提示和刷新短期 token。

为什么这对自研重要

上下文压缩、OAuth refresh 和 provider 适配应放在真正发请求的边界,避免 token 过期或 stale prompt。

固定提交源码摘录
  467  async function streamAssistantResponse(
  468  	context: AgentContext,
  469  	config: AgentLoopConfig,
  470  	signal: AbortSignal | undefined,
  471  	emit: AgentEventSink,
  472  	streamFn?: StreamFn,
  473  ): Promise<AssistantMessage> {
  474  	let partialMessage: AssistantMessage | null = null;
  475  	let addedPartial = false;
  476  	const finishAbortedMessage = async () => {
  477  		const finalMessage = createAbortedAssistantMessage(config, partialMessage);
  478  		if (addedPartial) {
  479  			context.messages[context.messages.length - 1] = finalMessage;
  480  		} else {
  481  			context.messages.push(finalMessage);
  482  			await emit({ type: "message_start", message: { ...finalMessage } });
  483  		}
  484  		await emit({ type: "message_end", message: finalMessage });
  485  		return finalMessage;
  486  	};
  487  
  488  	try {
  489  		throwIfAborted(signal);
  490  		// Apply context transform if configured (AgentMessage[] → AgentMessage[])
  491  		let messages = context.messages;
      … 19 lines omitted; exact range 467–521 …
  511  			tools: context.tools,
  512  		};
  513  
  514  		const response = await maybePromiseWithAbort(
  515  			streamFunction(config.model, llmContext, {
  516  				...config,
  517  				apiKey: resolvedApiKey,
  518  				signal,
  519  			}),
  520  			signal,
  521  		);
为什么相信这条结论?查看 3 处证据
05
L1 · fact · prime-provider-001

运行时支持 provider 动态系统提示和过期 token 刷新

先看源码事实

AgentLoopConfig 文档明确 getSystemPrompt 每次 LLM call 解析,getApiKey 可为每次调用获取短期 OAuth token;streamAssistantResponse 在请求前执行两者。

翻译成白话

长时间工具执行后,下一次模型调用仍能拿到最新授权和最新资源说明。

为什么这对自研重要

多 provider harness 应把 credential refresh 和 system prompt resolution 做成 request-time hook,而不是启动时静态读取。

固定提交源码摘录
  170  	transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
  171  
  172  	/** Resolves the system prompt immediately before each LLM call. */
  173  	getSystemPrompt?: () => string;
  174  
  175  	/**
  176  	 * Resolves an API key dynamically for each LLM call.
  177  	 *
  178  	 * Useful for short-lived OAuth tokens (e.g., GitHub Copilot) that may expire
  179  	 * during long-running tool execution phases.
  180  	 *
  181  	 * Contract: must not throw or reject. Return undefined when no key is available.
  182  	 */
  183  	getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
为什么相信这条结论?查看 2 处证据
小练习 3

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M04 · TOOLS

工具系统:Agent 的手脚怎样被注册和调度

模型看见的工具说明,和真正执行工具的代码,是不是同一个东西?并发、编辑和失败结果怎么处理?

先用一个生活比喻

工具系统像机场:模型提交登机牌,注册表确认航班,权限闸机检查证件,调度器决定跑道,最后才允许真正起飞。

这套实现先回答了什么?

多个独立查询可以并发跑,但带副作用的工具能强制串行;模型给错参数时不会直接进 shell,而是先变成错误 tool result。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
工具调用先预检再执行,支持串行和并行两条路径packages/agent/src/agent-loop.ts:608多个独立查询可以并发跑,但带副作用的工具能强制串行;模型给错参数时不会直接进 shell,而是先变成错误 tool result。
06
L1 · fact · prime-tools-001

工具调用先预检再执行,支持串行和并行两条路径

先看源码事实

executeToolCalls 根据全局 toolExecution 或单个工具 executionMode 决定串行/并行;两条路径都先 prepareToolCall,验证参数后才执行,结果 message 保持工具调用源顺序。

翻译成白话

多个独立查询可以并发跑,但带副作用的工具能强制串行;模型给错参数时不会直接进 shell,而是先变成错误 tool result。

为什么这对自研重要

工具调度应把“可并行性”和“参数校验”写进 ToolDefinition,而不是让模型提示词决定。

固定提交源码摘录
  608  async function executeToolCalls(
  609  	currentContext: AgentContext,
  610  	assistantMessage: AssistantMessage,
  611  	config: AgentLoopConfig,
  612  	signal: AbortSignal | undefined,
  613  	emit: AgentEventSink,
  614  ): Promise<ExecutedToolCallBatch> {
  615  	const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
  616  	const hasSequentialToolCall = toolCalls.some(
  617  		(tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === "sequential",
  618  	);
  619  	if (config.toolExecution === "sequential" || hasSequentialToolCall) {
  620  		return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit);
  621  	}
  622  	return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);
  623  }
为什么相信这条结论?查看 3 处证据
小练习 4

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M05 · CONTEXT

上下文:有限窗口怎样装下长任务

当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?

先用一个生活比喻

上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。

这套实现先回答了什么?

它不会等到 provider 报 context overflow 才处理,而是提前留出一块回答空间,再保留最近工作集。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
默认压缩预留 16384 token,尾部保留 20000 tokenpackages/coding-agent/src/core/compaction/compaction.ts:122它不会等到 provider 报 context overflow 才处理,而是提前留出一块回答空间,再保留最近工作集。
Token 估算融合 provider usage 与 trailing message 估算packages/coding-agent/src/core/compaction/compaction.ts:138刚从模型拿到真实 token 账单就用真实数,刚塞进来的工具结果还没账单就先用保守估算,不会因为只看旧 usage 而漏算最新输入。
Cut point 避开孤立 tool result,保留完整 tool turnpackages/coding-agent/src/core/compaction/compaction.ts:303压缩不会只删掉工具调用上半段或回执下半段,避免下一轮看到一张没有出处的工具结果。
摘要提示词固定为可恢复的结构化 checkpointpackages/coding-agent/src/core/compaction/compaction.ts:465摘要不是一句“我们做了很多事”,而是下一任模型可以按清单接手的交接卡。
07
L1 · fact · prime-context-001

默认压缩预留 16384 token,尾部保留 20000 token

先看源码事实

DEFAULT_COMPACTION_SETTINGS 将 enabled 设为 true、reserveTokens 设为 16384、keepRecentTokens 设为 20000;shouldCompact 在 contextTokens > contextWindow - reserveTokens 时触发。

翻译成白话

它不会等到 provider 报 context overflow 才处理,而是提前留出一块回答空间,再保留最近工作集。

为什么这对自研重要

自研应把 reserve/keep 做成可见配置并在模型切换时重新计算,不要硬编码单一窗口。

固定提交源码摘录
  122  export interface CompactionSettings {
  123  	enabled: boolean;
  124  	reserveTokens: number;
  125  	keepRecentTokens: number;
  126  }
  127  
  128  export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {
  129  	enabled: true,
  130  	reserveTokens: 16384,
  131  	keepRecentTokens: 20000,
  132  };
为什么相信这条结论?查看 2 处证据
08
L1 · fact · prime-context-002

Token 估算融合 provider usage 与 trailing message 估算

先看源码事实

estimateContextTokens 使用最后一个有效 assistant usage 作为锚点,对其后的消息用 estimateTokens 累加;没有 usage 时按角色估算,文本采用 chars/4,图片按约 4800 字符计。

翻译成白话

刚从模型拿到真实 token 账单就用真实数,刚塞进来的工具结果还没账单就先用保守估算,不会因为只看旧 usage 而漏算最新输入。

为什么这对自研重要

上下文预算应允许“真实 usage + 未结算尾巴”混合,且图片/工具输出需要有明确估算策略。

固定提交源码摘录
  138  /**
  139   * Calculate total context tokens from usage.
  140   * Uses the native totalTokens field when available, falls back to computing from components.
  141   *
  142   * Includes output: the assistant's response becomes part of the prompt on the next
  143   * request, so it counts toward the context the next turn will send.
  144   */
  145  export function calculateContextTokens(usage: Usage): number {
  146  	return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
  147  }
为什么相信这条结论?查看 3 处证据
09
L1 · fact · prime-context-003

Cut point 避开孤立 tool result,保留完整 tool turn

先看源码事实

findValidCutPoints 允许在 user/assistant/custom/bashExecution 等消息切分,明确跳过 toolResult;findCutPoint 还能检测切在 turn 中间的情况并保留 turn prefix。

翻译成白话

压缩不会只删掉工具调用上半段或回执下半段,避免下一轮看到一张没有出处的工具结果。

为什么这对自研重要

对话裁剪需要 message role invariant;“按 N 条消息截断”对 coding agent 不够安全。

固定提交源码摘录
  303  /**
  304   * Find valid cut points: indices of user, assistant, custom, or bashExecution messages.
  305   * Never cut at tool results (they must follow their tool call).
  306   * When we cut at an assistant message with tool calls, its tool results follow it
  307   * and will be kept.
  308   * BashExecutionMessage is treated like a user message (user-initiated context).
  309   */
  310  function findValidCutPoints(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {
  311  	const cutPoints: number[] = [];
  312  	for (let i = startIndex; i < endIndex; i++) {
  313  		const entry = entries[i];
  314  		switch (entry.type) {
  315  			case "message": {
  316  				const role = entry.message.role;
  317  				switch (role) {
  318  					case "bashExecution":
  319  					case "custom":
  320  					case "branchSummary":
  321  					case "compactionSummary":
  322  					case "user":
  323  					case "assistant":
  324  						cutPoints.push(i);
  325  						break;
  326  					case "toolResult":
  327  						break;
      … 1 lines omitted; exact range 303–339 …
  329  				break;
  330  			}
  331  			case "thinking_level_change":
  332  			case "model_change":
  333  			case "compaction":
  334  			case "branch_summary":
  335  			case "custom":
  336  			case "custom_message":
  337  			case "label":
  338  			case "session_info":
  339  				break;
为什么相信这条结论?查看 3 处证据
10
L1 · fact · prime-context-004

摘要提示词固定为可恢复的结构化 checkpoint

先看源码事实

SUMMARIZATION_PROMPT 强制 Goal、Constraints & Preferences、Progress/Done/In Progress/Blocked、Key Decisions、Next Steps、Critical Context 七个区块,并要求保留精确路径、函数名和错误信息;更新摘要会合并 previous-summary。

翻译成白话

摘要不是一句“我们做了很多事”,而是下一任模型可以按清单接手的交接卡。

为什么这对自研重要

长任务的摘要模板应和恢复协议一起版本化,并对路径、错误和下一步做保真约束。

固定提交源码摘录
  465  const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.
  466  
  467  Use this EXACT format:
  468  
  469  ## Goal
  470  [What is the user trying to accomplish? Can be multiple items if the session covers different tasks.]
  471  
  472  ## Constraints & Preferences
  473  - [Any constraints, preferences, or requirements mentioned by user]
  474  - [Or "(none)" if none were mentioned]
  475  
  476  ## Progress
  477  ### Done
  478  - [x] [Completed tasks/changes]
  479  
  480  ### In Progress
  481  - [ ] [Current work]
  482  
  483  ### Blocked
  484  - [Issues preventing progress, if any]
  485  
  486  ## Key Decisions
  487  - **[Decision]**: [Brief rationale]
  488  
  489  ## Next Steps
  490  1. [Ordered list of what should happen next]
  491  
  492  ## Critical Context
  493  - [Any data, examples, or references needed to continue]
  494  - [Or "(none)" if not applicable]
  495  
  496  Keep each section concise. Preserve exact file paths, function names, and error messages.`;
为什么相信这条结论?查看 3 处证据
11
L2 · inference · prime-recommend-002

适合长程 coding,但要为宿主 shell 补强 sandbox policy

先看源码事实

项目同时提供 request-time context transform、结构化 compaction、branch-aware JSONL、RLM/daemon continuation 和截断 artifact;但默认 bash 是 host spawn,隔离依赖 BashOperations 或更高层。

翻译成白话

它很适合做“能停下来、能恢复、能继续”的工程 Agent;真正接入不可信代码库时,安全边界要由部署方补齐。

为什么这对自研重要

自研落地可以直接借鉴它的 loop/session/compaction 机制,同时把执行适配器设计成强制 sandbox backend,而不是默认本机 shell。

边界与风险
  • “适合长程”是基于实现组合的工程判断;不能替代对具体部署的安全评审。
固定提交源码摘录
  636  export function prepareCompaction(
  637  	pathEntries: SessionEntry[],
  638  	settings: CompactionSettings,
  639  ): CompactionPreparation | undefined {
  640  	if (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === "compaction") {
  641  		return undefined;
  642  	}
  643  
  644  	let prevCompactionIndex = -1;
  645  	for (let i = pathEntries.length - 1; i >= 0; i--) {
  646  		if (pathEntries[i].type === "compaction") {
  647  			prevCompactionIndex = i;
  648  			break;
  649  		}
  650  	}
  651  
  652  	let previousSummary: string | undefined;
  653  	let boundaryStart = 0;
  654  	if (prevCompactionIndex >= 0) {
  655  		const prevCompaction = pathEntries[prevCompactionIndex] as CompactionEntry;
  656  		previousSummary = prevCompaction.summary;
  657  		const firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId);
  658  		boundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;
  659  	}
  660  	const boundaryEnd = pathEntries.length;
      … 41 lines omitted; exact range 636–712 …
  702  		for (const msg of turnPrefixMessages) {
  703  			extractFileOpsFromMessage(msg, fileOps);
  704  		}
  705  	}
  706  
  707  	return {
  708  		firstKeptEntryId,
  709  		messagesToSummarize,
  710  		turnPrefixMessages,
  711  		isSplitTurn: cutPoint.isSplitTurn,
  712  		tokensBefore,
为什么相信这条结论?查看 3 处证据
小练习 5

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M06 · SECURITY

权限与沙箱:能做什么,在哪里做

审批按钮、规则引擎、容器和操作系统沙箱分别解决什么问题?为什么“问过用户”不等于“隔离了风险”?

先用一个生活比喻

审批像门卫问你有没有预约,沙箱像把访客关在指定房间;前者决定是否放行,后者限制放行后能摸到什么。

这套实现先回答了什么?

审批、审计、脱敏、工具白名单和“这个结果是否要终止 Agent”都可以在一个统一钩子里实现,而且 block 发生在真正执行前。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
before/after tool hook 是可编程的策略门packages/agent/src/agent-loop.ts:795审批、审计、脱敏、工具白名单和“这个结果是否要终止 Agent”都可以在一个统一钩子里实现,而且 block 发生在真正执行前。
默认 bash 是宿主 shell,不等于 OS 沙箱packages/coding-agent/src/core/tools/bash.ts:36它能可靠地超时和杀掉进程树,但默认命令就是在当前机器的 shell 里跑;源码没有在这里自动启动容器、Seatbelt 或 bubblewrap。
Bash 输出有行/字节上限并把完整内容留在临时文件packages/coding-agent/src/core/tools/bash.ts:250大日志不会把上下文撑爆,模型还能拿到一个完整输出文件路径继续查。
Extension runner 按注册顺序串行执行,session-before 可以取消packages/coding-agent/src/core/extensions/runner.ts:670扩展链像一条可观察的中间件管线:早期扩展能阻止 session switch 或 tool call,后面的扩展看到前面已经变换过的 context。
12
L1 · fact · prime-tools-002

before/after tool hook 是可编程的策略门

先看源码事实

prepareToolCall 在参数 schema 验证后调用 beforeToolCall,返回 block 就生成错误结果而不执行;执行结束后 afterToolCall 可覆盖 content、details、isError 和 terminate。

翻译成白话

审批、审计、脱敏、工具白名单和“这个结果是否要终止 Agent”都可以在一个统一钩子里实现,而且 block 发生在真正执行前。

为什么这对自研重要

策略要留在 dispatch pipeline,而不是散落在每个工具内部;同时要审计扩展是否能覆盖结果或终止状态。

固定提交源码摘录
  795  async function prepareToolCall(
  796  	currentContext: AgentContext,
  797  	assistantMessage: AssistantMessage,
  798  	toolCall: AgentToolCall,
  799  	config: AgentLoopConfig,
  800  	signal: AbortSignal | undefined,
  801  ): Promise<PreparedToolCall | ImmediateToolCallOutcome> {
  802  	const tool = currentContext.tools?.find((t) => t.name === toolCall.name);
  803  	if (!tool) {
  804  		return {
  805  			kind: "immediate",
  806  			result: createErrorToolResult(`Tool ${toolCall.name} not found`),
  807  			isError: true,
  808  		};
  809  	}
  810  
  811  	try {
  812  		const preparedToolCall = prepareToolCallArguments(tool, toolCall);
  813  		const validatedArgs = validateToolArguments(tool, preparedToolCall);
  814  		if (config.beforeToolCall) {
  815  			const beforeResult = await maybePromiseWithAbort(
  816  				config.beforeToolCall(
  817  					{
  818  						assistantMessage,
  819  						toolCall,
      … 18 lines omitted; exact range 795–848 …
  838  			tool,
  839  			args: validatedArgs,
  840  		};
  841  	} catch (error) {
  842  		return {
  843  			kind: "immediate",
  844  			result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
  845  			isError: true,
  846  		};
  847  	}
  848  }
为什么相信这条结论?查看 4 处证据
13
L1 · limitation · prime-exec-001

默认 bash 是宿主 shell,不等于 OS 沙箱

先看源码事实

createLocalBashOperations 使用 getShellConfig 得到的 shell,spawn 时传入当前 cwd 与 shell env,detached 子进程由 killProcessTree 管理;BashOperations 只是可插拔执行接口。

翻译成白话

它能可靠地超时和杀掉进程树,但默认命令就是在当前机器的 shell 里跑;源码没有在这里自动启动容器、Seatbelt 或 bubblewrap。

为什么这对自研重要

如果产品面向不可信仓库,必须显式注入远端/容器 BashOperations 或在更上层做 sandbox policy,不能把 timeout 当隔离。

边界与风险
  • 上层配置或 extension 可以替换 operations;本结论只针对这个默认实现。
固定提交源码摘录
   36  /**
   37   * Pluggable operations for the bash tool.
   38   * Override these to delegate command execution to remote systems (for example SSH).
   39   */
   40  export interface BashOperations {
   41  	/**
   42  	 * Execute a command and stream output.
   43  	 * @param command The command to execute
   44  	 * @param cwd Working directory
   45  	 * @param options Execution options
   46  	 * @returns Promise resolving to exit code (null if killed)
   47  	 */
   48  	exec: (
   49  		command: string,
   50  		cwd: string,
   51  		options: {
   52  			onData: (data: Buffer) => void;
   53  			signal?: AbortSignal;
   54  			timeout?: number;
   55  			env?: NodeJS.ProcessEnv;
   56  		},
   57  	) => Promise<{ exitCode: number | null }>;
   58  }
为什么相信这条结论?查看 3 处证据
14
L1 · fact · prime-exec-002

Bash 输出有行/字节上限并把完整内容留在临时文件

先看源码事实

Bash tool description 宣布输出按 DEFAULT_MAX_LINES 或 DEFAULT_MAX_BYTES 截断;OutputAccumulator 在流式 update 和结束时生成 truncation/fullOutputPath details。

翻译成白话

大日志不会把上下文撑爆,模型还能拿到一个完整输出文件路径继续查。

为什么这对自研重要

执行工具应把展示输出和可恢复 artifact 分离,并在 tool result 中明确截断范围。

固定提交源码摘录
  250  	const truncation = result.details?.truncation;
  251  	const fullOutputPath = result.details?.fullOutputPath;
  252  	if (truncation?.truncated || fullOutputPath) {
  253  		const warnings: string[] = [];
  254  		if (fullOutputPath) {
  255  			warnings.push(`Full output: ${fullOutputPath}`);
  256  		}
  257  		if (truncation?.truncated) {
  258  			if (truncation.truncatedBy === "lines") {
  259  				warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
  260  			} else {
  261  				warnings.push(
  262  					`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,
  263  				);
  264  			}
  265  		}
  266  		component.addChild(new Text(`\n${theme.fg("warning", `[${warnings.join(". ")}]`)}`, 0, 0));
为什么相信这条结论?查看 3 处证据
15
L1 · fact · prime-ext-002

Extension runner 按注册顺序串行执行,session-before 可以取消

先看源码事实

runner.emit 遍历 extension 和 handler,捕获异常写入 ExtensionError;session_before_* 事件收到 cancel 会立即返回。emitToolCall 也按顺序处理 block,emitContext 与 before_provider_request 允许前一个结果成为下一个输入。

翻译成白话

扩展链像一条可观察的中间件管线:早期扩展能阻止 session switch 或 tool call,后面的扩展看到前面已经变换过的 context。

为什么这对自研重要

要把 extension handler 的顺序、异常隔离、阻断和结果变换写进契约测试,避免插件之间互相覆盖。

固定提交源码摘录
  670  	private isSessionBeforeEvent(event: RunnerEmitEvent): event is SessionBeforeEvent {
  671  		return (
  672  			event.type === "session_before_switch" ||
  673  			event.type === "session_before_fork" ||
  674  			event.type === "session_before_compact" ||
  675  			event.type === "session_before_tree"
  676  		);
  677  	}
  678  
  679  	async emit<TEvent extends RunnerEmitEvent>(event: TEvent): Promise<RunnerEmitResult<TEvent>> {
  680  		const ctx = this.createContext();
  681  		let result: SessionBeforeEventResult | undefined;
  682  
  683  		for (const ext of this.extensions) {
  684  			const handlers = ext.handlers.get(event.type);
  685  			if (!handlers || handlers.length === 0) continue;
  686  
  687  			for (const handler of handlers) {
  688  				try {
  689  					const handlerResult = await handler(event, ctx);
  690  
  691  					if (this.isSessionBeforeEvent(event) && handlerResult) {
  692  						result = handlerResult as SessionBeforeEventResult;
  693  						if (result.cancel) {
  694  							return result as RunnerEmitResult<TEvent>;
      … 6 lines omitted; exact range 670–711 …
  701  						extensionPath: ext.path,
  702  						event: event.type,
  703  						error: message,
  704  						stack,
  705  					});
  706  				}
  707  			}
  708  		}
  709  
  710  		return result as RunnerEmitResult<TEvent>;
  711  	}
为什么相信这条结论?查看 4 处证据
16
L2 · risk · prime-risk-001

扩展可以获得 shell/exec 和 active tool 控制权,权限面很宽

先看源码事实

ExtensionAPI 同时暴露 exec、registerTool、setActiveTools、provider registration、appendEntry 和 session actions;资源 loader 还会加载用户/项目/CLI extension。

翻译成白话

插件一旦被加载,不只是加一段说明,它可以执行命令、改工具面、持久化状态和注册模型。

为什么这对自研重要

生产部署应对 extension 来源做 allowlist/签名/隔离,至少在报告和审批面把 extension 能力显式列出来。

边界与风险
  • 本风险是插件权限面的架构推断,不等于每个 extension 都恶意。
固定提交源码摘录
 1080  	/** Register a tool that the LLM can call. */
 1081  	registerTool<TParams extends TSchema = TSchema, TDetails = unknown, TState = any>(
 1082  		tool: ToolDefinition<TParams, TDetails, TState>,
 1083  	): void;
 1084  
 1085  	// =========================================================================
 1086  	// Command, Shortcut, Flag Registration
 1087  	// =========================================================================
 1088  
 1089  	/** Register a custom command. */
 1090  	registerCommand(name: string, options: Omit<RegisteredCommand, "name" | "sourceInfo">): void;
 1091  
 1092  	/** Register a keyboard shortcut. */
 1093  	registerShortcut(
 1094  		shortcut: KeyId,
 1095  		options: {
 1096  			description?: string;
 1097  			handler: (ctx: ExtensionContext) => Promise<void> | void;
 1098  		},
 1099  	): void;
 1100  
 1101  	/** Register a CLI flag. */
 1102  	registerFlag(
 1103  		name: string,
 1104  		options: {
      … 48 lines omitted; exact range 1080–1163 …
 1153  	/** Set or clear a label on an entry. Labels are user-defined markers for bookmarking/navigation. */
 1154  	setLabel(entryId: string, label: string | undefined): void;
 1155  
 1156  	/** Execute a shell command. */
 1157  	exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
 1158  
 1159  	/** Get the list of currently active tool names. */
 1160  	getActiveTools(): string[];
 1161  
 1162  	/** Get all configured tools with parameter schema and source metadata. */
 1163  	getAllTools(): ToolInfo[];
为什么相信这条结论?查看 2 处证据
小练习 6

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M07 · ECOSYSTEM

指令、MCP、Skills 与插件:能力如何接进来

一条系统指令、一个 Skill、一个 MCP server 和一个插件,分别在什么时候进入上下文和执行路径?

先用一个生活比喻

这像给工作室接设备:说明书不是设备,设备也不等于电源;成熟 Harness 会分别治理发现、信任、加载、调用和卸载。

这套实现先回答了什么?

指令、技能、主题和扩展不是四套互不相干的扫描器,而是在 reload 时形成一份带诊断的资源快照。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
ResourceLoader 统一管理 skills、prompts、themes、extensions 和 AGENTS 文件packages/coding-agent/src/core/resource-loader.ts:23指令、技能、主题和扩展不是四套互不相干的扫描器,而是在 reload 时形成一份带诊断的资源快照。
资源来源带 user/project/temporary metadata 并去重冲突packages/coding-agent/src/core/resource-loader.ts:646系统知道一个 skill 是用户级、项目级还是临时 CLI 注入的;同名 prompt 不会静默覆盖,而是留下谁赢、谁输的诊断。
Extension API 覆盖生命周期、工具、命令、provider 和持久化packages/coding-agent/src/core/extensions/types.ts:1024扩展不是只能加一个 prompt;它可以加入工具、CLI 命令、模型 provider、上下文变换和 session state。
MCP host 侧把 OAuth provider 与 kernel 请求分开packages/coding-agent/src/core/mcp/mcp-manager.ts:36登录凭据、UI 交互和真正的 MCP kernel 连接没有混成一条黑盒;host 只做身份和配置桥接。
17
L1 · fact · prime-resources-001

ResourceLoader 统一管理 skills、prompts、themes、extensions 和 AGENTS 文件

先看源码事实

ResourceLoader 暴露 getSkills/getPrompts/getThemes/getAgentsFiles/getSystemPrompt、extendResources 和 reload;reload 解析启用路径、加载 extensions、skills、prompts、themes,再按 cwd/agentDir 加载项目上下文文件。

翻译成白话

指令、技能、主题和扩展不是四套互不相干的扫描器,而是在 reload 时形成一份带诊断的资源快照。

为什么这对自研重要

插件/技能热更新要有单一 loader 和明确的 reload boundary,才能通知 session 资源变了。

固定提交源码摘录
   23  export interface ResourceExtensionPaths {
   24  	skillPaths?: Array<{ path: string; metadata: PathMetadata }>;
   25  	promptPaths?: Array<{ path: string; metadata: PathMetadata }>;
   26  	themePaths?: Array<{ path: string; metadata: PathMetadata }>;
   27  }
   28  
   29  export interface ResourceLoader {
   30  	getExtensions(): LoadExtensionsResult;
   31  	getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[] };
   32  	getPrompts(): { prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[] };
   33  	getThemes(): { themes: Theme[]; diagnostics: ResourceDiagnostic[] };
   34  	getAgentsFiles(): { agentsFiles: Array<{ path: string; content: string }> };
   35  	getSystemPrompt(): string | undefined;
   36  	getAppendSystemPrompt(): string[];
   37  	extendResources(paths: ResourceExtensionPaths): void;
   38  	reload(): Promise<void>;
   39  }
为什么相信这条结论?查看 3 处证据
18
L1 · fact · prime-resources-002

资源来源带 user/project/temporary metadata 并去重冲突

先看源码事实

resource-loader 根据 agentDir 和 cwd 下的 skills/prompts/themes/extensions 根目录给资源标注 scope;mergePaths 用 canonical path 去重,dedupePrompts 对同名 prompt 生成 collision diagnostics。

翻译成白话

系统知道一个 skill 是用户级、项目级还是临时 CLI 注入的;同名 prompt 不会静默覆盖,而是留下谁赢、谁输的诊断。

为什么这对自研重要

自研扩展系统需保留来源元数据和碰撞报告,否则排查“为什么这条指令生效”会非常困难。

固定提交源码摘录
  646  		const normalizedPath = resolve(filePath);
  647  		const agentRoots = [
  648  			join(this.agentDir, "skills"),
  649  			join(this.agentDir, "prompts"),
  650  			join(this.agentDir, "themes"),
  651  			join(this.agentDir, "extensions"),
  652  		];
  653  		const projectRoots = [
  654  			join(this.cwd, CONFIG_DIR_NAME, "skills"),
  655  			join(this.cwd, CONFIG_DIR_NAME, "prompts"),
  656  			join(this.cwd, CONFIG_DIR_NAME, "themes"),
  657  			join(this.cwd, CONFIG_DIR_NAME, "extensions"),
  658  		];
  659  
  660  		for (const root of agentRoots) {
  661  			if (this.isUnderPath(normalizedPath, root)) {
  662  				return { path: filePath, source: "local", scope: "user", origin: "top-level", baseDir: root };
  663  			}
  664  		}
  665  
  666  		for (const root of projectRoots) {
  667  			if (this.isUnderPath(normalizedPath, root)) {
  668  				return { path: filePath, source: "local", scope: "project", origin: "top-level", baseDir: root };
  669  			}
  670  		}
  671  
  672  		return {
  673  			path: filePath,
  674  			source: "local",
  675  			scope: "temporary",
  676  			origin: "top-level",
  677  			baseDir: statSync(normalizedPath).isDirectory() ? normalizedPath : resolve(normalizedPath, ".."),
  678  		};
为什么相信这条结论?查看 3 处证据
19
L1 · fact · prime-ext-001

Extension API 覆盖生命周期、工具、命令、provider 和持久化

先看源码事实

ExtensionAPI 支持 session/context/provider/agent/turn/message/tool/input 等事件,registerTool、registerCommand、registerShortcut、registerFlag、appendEntry、exec、setActiveTools 和 provider registration。

翻译成白话

扩展不是只能加一个 prompt;它可以加入工具、CLI 命令、模型 provider、上下文变换和 session state。

为什么这对自研重要

这种能力适合构建插件生态,但必须把 extension 来源、冲突和权限当作一等公民。

固定提交源码摘录
 1024  /** Handler function type for events */
 1025  // biome-ignore lint/suspicious/noConfusingVoidType: void allows bare return statements
 1026  export type ExtensionHandler<E, R = undefined> = (event: E, ctx: ExtensionContext) => Promise<R | void> | R | void;
 1027  
 1028  /**
 1029   * ExtensionAPI passed to extension factory functions.
 1030   */
 1031  export interface ExtensionAPI {
 1032  	// =========================================================================
 1033  	// Event Subscription
 1034  	// =========================================================================
 1035  
 1036  	on(event: "resources_discover", handler: ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult>): void;
 1037  	on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
 1038  	on(
 1039  		event: "session_before_switch",
 1040  		handler: ExtensionHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>,
 1041  	): void;
 1042  	on(event: "session_before_fork", handler: ExtensionHandler<SessionBeforeForkEvent, SessionBeforeForkResult>): void;
 1043  	on(
 1044  		event: "session_before_compact",
 1045  		handler: ExtensionHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,
 1046  	): void;
 1047  	on(event: "session_compact", handler: ExtensionHandler<SessionCompactEvent>): void;
 1048  	on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): void;
      … 15 lines omitted; exact range 1024–1074 …
 1064  	on(event: "message_end", handler: ExtensionHandler<MessageEndEvent, MessageEndEventResult>): void;
 1065  	on(event: "tool_execution_start", handler: ExtensionHandler<ToolExecutionStartEvent>): void;
 1066  	on(event: "tool_execution_update", handler: ExtensionHandler<ToolExecutionUpdateEvent>): void;
 1067  	on(event: "tool_execution_end", handler: ExtensionHandler<ToolExecutionEndEvent>): void;
 1068  	on(event: "model_select", handler: ExtensionHandler<ModelSelectEvent>): void;
 1069  	on(event: "thinking_level_select", handler: ExtensionHandler<ThinkingLevelSelectEvent>): void;
 1070  	on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
 1071  	on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
 1072  	on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
 1073  	on(event: "input", handler: ExtensionHandler<InputEvent, InputEventResult>): void;
 1074  	on(event: "refine_complete", handler: ExtensionHandler<RefineCompleteEvent>): void;
为什么相信这条结论?查看 4 处证据
20
L1 · fact · prime-mcp-001

MCP host 侧把 OAuth provider 与 kernel 请求分开

先看源码事实

McpManager refresh/config/begin_login host request 会读取 auth storage、重新获取 API key,并把解析后的 server URL/config 交给 kernel;stdio server 由 Python 自己管理,HTTP 用户 server 才在 host 侧注册 OAuth provider。

翻译成白话

登录凭据、UI 交互和真正的 MCP kernel 连接没有混成一条黑盒;host 只做身份和配置桥接。

为什么这对自研重要

连接器设计应区分 credential plane、control plane 和 tool execution plane,避免把 token 直接塞进模型上下文。

固定提交源码摘录
   36  export class McpManager {
   37  	private readonly authStorage: AuthStorage;
   38  	private readonly getUserServers: () => Record<string, McpServerConfig> | undefined;
   39  	private readonly beginLogin?: (server: string) => Promise<void>;
   40  	private integrations = new Map<string, ResolvedIntegration>();
   41  	/** Provider ids we registered for user servers, so refresh can drop removed ones. */
   42  	private registeredUserProviderIds = new Set<string>();
   43  
   44  	constructor(options: McpManagerOptions) {
   45  		this.authStorage = options.authStorage;
   46  		this.getUserServers = options.getUserServers ?? (() => undefined);
   47  		this.beginLogin = options.beginLogin;
   48  		this.resolveIntegrations();
   49  		this.registerProviders();
   50  	}
   51  
   52  	/** Re-read settings and re-register providers; call after a session reload. */
   53  	refresh(): void {
   54  		this.resolveIntegrations();
   55  		this.registerProviders();
   56  	}
   57  
   58  	private providerId(server: string): string {
   59  		return `mcp:${server}`;
   60  	}
      … 7 lines omitted; exact range 36–78 …
   68  				url: entry.url,
   69  				usesOAuth: entry.oauth?.kind === "oauth",
   70  			});
   71  		}
   72  		for (const [server, config] of Object.entries(this.getUserServers() ?? {})) {
   73  			if (config.type !== "http") continue; // stdio servers self-manage in Python
   74  			integrations.set(server, {
   75  				server,
   76  				label: server,
   77  				url: config.url,
   78  				usesOAuth: config.oauth === true,
为什么相信这条结论?查看 2 处证据
小练习 7

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M08 · COLLABORATION

子 Agent:把一个大任务拆成可治理的协作

什么时候是普通工具调用,什么时候才算子 Agent?子 Agent 的上下文、预算、取消和结果怎样回到父 Agent?

先用一个生活比喻

不是把同事叫来聊天就叫协作;真正的协作要有工单、权限、截止时间、交付物和回收机制。

这套实现先回答了什么?

子 Agent 不是简单 `Promise.all`:父子关系、状态、目录、模型、深度和完成后回收都有可查询的对象。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
RLM child runtime 有显式 registry、深度和完成释放协议packages/coding-agent/src/core/rlm-runtime.ts:14子 Agent 不是简单 `Promise.all`:父子关系、状态、目录、模型、深度和完成后回收都有可查询的对象。
Daemon supervisor 是 resident worker 控制面而不是一次性 subprocesspackages/coding-agent/src/modes/daemon/daemon-supervisor.ts:158后台 Agent 可以脱离前台 UI 常驻,客户端断线后仍能通过 socket 找回 worker,恢复 journal 和 heartbeat。
Prompt admission 与 heartbeat 让 daemon 交互可取消且可观测packages/coding-agent/src/modes/daemon/daemon-supervisor.ts:1125用户点了等待式 prompt 后断网,不会留下一个永远占着队列的请求;心跳也能告诉控制面任务还活着还是已断开。
Daemon/RLM 的恢复正确性依赖多个 journal、lease 和 session 入口协同packages/coding-agent/src/modes/daemon/daemon-supervisor.ts:49它很强,但出错时要同时判断进程身份、socket、worker 状态、session 文件和 child registry,排障门槛高。
21
L1 · fact · prime-collab-001

RLM child runtime 有显式 registry、深度和完成释放协议

先看源码事实

RlmSubagentRegistryEntry 记录 child id、active/session id、name、dir、status;CreateRlmSubagentRuntimeOptions 携带 parent、model、tools、rlmDepth、rlmMaxDepth 和 parent node;SubagentRuntimeHost 要求 create、complete、release、delete 等生命周期动作。

翻译成白话

子 Agent 不是简单 `Promise.all`:父子关系、状态、目录、模型、深度和完成后回收都有可查询的对象。

为什么这对自研重要

多 Agent 系统要把 child 生命周期持久化,才能支持观察、取消、恢复和成本归因。

固定提交源码摘录
   14  export interface RlmSpawnHandle {
   15  	rlm_child_id: string;
   16  	name: string;
   17  	session_dir: string;
   18  	model: string;
   19  }
   20  
   21  export type RlmSubagentRegistryStatus = "running" | "completed" | "error";
   22  
   23  export interface RlmSubagentRegistryEntry {
   24  	rlm_child_id: string;
   25  	active_session_id: string | null;
   26  	session_id: string | null;
   27  	session_name: string;
   28  	session_dir: string;
   29  	status: RlmSubagentRegistryStatus;
   30  }
   31  
   32  export interface RlmListSubagentsResult {
   33  	subagents: RlmSubagentRegistryEntry[];
   34  }
   35  
   36  export interface RlmDeleteSubagentResult {
   37  	subagent: RlmSubagentRegistryEntry;
   38  	outcome?: "deleted" | "skipped_running";
   39  }
为什么相信这条结论?查看 2 处证据
22
L1 · fact · prime-collab-002

Daemon supervisor 是 resident worker 控制面而不是一次性 subprocess

先看源码事实

DaemonSupervisor 启动时取得 socket lease、创建 0700 descriptor dir、写 supervisor config/journal,加载 worker descriptors 并 adoptOrRecoverWorker;协议包含 prompt/prompt_and_wait、cancel、heartbeats、retry_worker、session tree/stats 等命令。

翻译成白话

后台 Agent 可以脱离前台 UI 常驻,客户端断线后仍能通过 socket 找回 worker,恢复 journal 和 heartbeat。

为什么这对自研重要

需要远程继续、移动端查看或长时间任务时,resident supervisor 比每次 CLI 启新进程更合适,但运维复杂度也会显著上升。

固定提交源码摘录
  158  	"list_saved_sessions",
  159  	"create",
  160  	"attach",
  161  	"reattach",
  162  	"detach",
  163  	"complete_owned_session",
  164  	"promote_owned_session",
  165  	"kill",
  166  	"rename",
  167  	"prompt",
  168  	"cancel_prompt_admission",
  169  	"prompt_and_wait",
  170  	"steer",
  171  	"follow_up",
  172  	"restore_next_turn",
  173  	"restore_actions",
  174  	"append_custom_message",
  175  	"resume_queue",
  176  	"send_message",
  177  	"agent_messages_status",
  178  	"agent_messages_pause",
  179  	"agent_messages_resume",
  180  	"agent_messages_clear",
  181  	"abort",
  182  	"start_side_question",
      … 19 lines omitted; exact range 158–212 …
  202  	"mutate_queued_message",
  203  	"clear_queue",
  204  	"abort_and_clear_queue",
  205  	"cron_list",
  206  	"heartbeats_list",
  207  	"heartbeat_manage",
  208  	"cron_add",
  209  	"cron_cancel",
  210  	"heartbeat_get",
  211  	"heartbeat_set",
  212  	"heartbeat_update",
为什么相信这条结论?查看 3 处证据
23
L1 · fact · prime-collab-003

Prompt admission 与 heartbeat 让 daemon 交互可取消且可观测

先看源码事实

daemon supervisor 为每个 client/activeSession/admissionId 维护 waiting/owned/cancelled 状态,断开时转发 cancel_prompt_admission;heartbeats_list 会汇总 ready workers 的 heartbeat snapshot。

翻译成白话

用户点了等待式 prompt 后断网,不会留下一个永远占着队列的请求;心跳也能告诉控制面任务还活着还是已断开。

为什么这对自研重要

异步控制面需要 admission id、幂等 key 和 client disconnect cleanup,不应只靠 websocket 生命周期。

固定提交源码摘录
 1125  	private promptAdmissionKey(activeSessionId: string, publicAdmissionId: string): string {
 1126  		return `${activeSessionId}\0${publicAdmissionId}`;
 1127  	}
 1128  
 1129  	private promptAdmissionsFor(client: DaemonSocketClient): Map<string, SupervisorPromptAdmission> {
 1130  		let admissions = this.promptAdmissions.get(client);
 1131  		if (!admissions) {
 1132  			admissions = new Map();
 1133  			this.promptAdmissions.set(client, admissions);
 1134  		}
 1135  		return admissions;
 1136  	}
 1137  
 1138  	private getPromptAdmission(
 1139  		client: DaemonSocketClient,
 1140  		activeSessionId: string,
 1141  		publicAdmissionId: string,
 1142  	): SupervisorPromptAdmission | undefined {
 1143  		return this.promptAdmissions.get(client)?.get(this.promptAdmissionKey(activeSessionId, publicAdmissionId));
 1144  	}
 1145  
 1146  	private deletePromptAdmission(admission: SupervisorPromptAdmission): void {
 1147  		const admissions = this.promptAdmissions.get(admission.client);
 1148  		const key = this.promptAdmissionKey(admission.activeSessionId, admission.publicAdmissionId);
 1149  		if (admissions?.get(key) !== admission) return;
      … 24 lines omitted; exact range 1125–1184 …
 1174  							: undefined;
 1175  					if (status === "owned") admission.status = "owned";
 1176  					else if (status === "cancelled") admission.status = "cancelled";
 1177  				})
 1178  				.catch((error: unknown) => {
 1179  					this.log(
 1180  						`Could not cancel prompt admission ${admission.workerAdmissionId} on disconnected client: ${String(error)}`,
 1181  					);
 1182  				});
 1183  		}
 1184  	}
为什么相信这条结论?查看 3 处证据
24
L2 · risk · prime-risk-002

Daemon/RLM 的恢复正确性依赖多个 journal、lease 和 session 入口协同

先看源码事实

daemon supervisor 同时维护 supervisor config、worker descriptor、command journal、recovery journal、orphan journal、socket identity 与 session file;RLM 还把 child depth、parent node 和完成释放交给 host。

翻译成白话

它很强,但出错时要同时判断进程身份、socket、worker 状态、session 文件和 child registry,排障门槛高。

为什么这对自研重要

自研要先定义故障状态机和可重放事件,再扩展常驻 worker;否则“恢复”可能变成重复执行或孤儿进程。

边界与风险
  • 这是基于多个实现模块的维护复杂度推断;并非源码声称的 bug。
固定提交源码摘录
   49  import type { PrivateFrame } from "../session-worker/private-framing.js";
   50  import { createActiveSessionId, type DaemonSocketClient } from "./active-session-state.js";
   51  import { CommandRecoveryJournal, createCommandIdempotencyKey } from "./command-recovery-journal.js";
   52  import { CompactAssistantStreamReconstructor, isCompactAssistantDelta } from "./compact-session-stream.js";
   53  import { DAEMON_CATALOG_ROLE_ENV, DaemonCatalogClient } from "./daemon-catalog-process.js";
为什么相信这条结论?查看 3 处证据
小练习 8

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M09 · STATE

会话、持久化与观测:让一次运行变成可追溯事实

如果进程崩了、用户刷新了、任务跑了一夜,系统凭什么恢复并解释“刚才究竟发生了什么”?

先用一个生活比喻

内存像白板,数据库像目录,append-only journal 像监控录像;可靠 Harness 不只保存最后答案,还保存每次转弯。

这套实现先回答了什么?

会话文件不是一条只能向后追加的聊天记录,而是一棵可导航的树;扩展可以持久化自己的 entry,又不会把内部状态强塞给模型。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
SessionManager 用带 parentId 的 JSONL 树表达分支、压缩和扩展状态packages/coding-agent/src/core/session-manager.ts:33会话文件不是一条只能向后追加的聊天记录,而是一棵可导航的树;扩展可以持久化自己的 entry,又不会把内部状态强塞给模型。
Context 重建沿 parent tree,并把 compaction summary 放在 retained messages 前packages/coding-agent/src/core/session-manager.ts:472从任意分支恢复时,模型看到的是“摘要→保留的旧消息→新分支”,UI 还可以知道真实的树边界。
写盘采用临时文件 rename,普通 entry 采用 append-onlypackages/coding-agent/src/core/session-manager.ts:1345完整重写时不会留下半个 JSONL;常规消息又不必每次重写整个文件,兼顾可靠性与成本。
Agent events 覆盖 agent、turn、message 和 tool execution 四层packages/agent/src/types.ts:399UI 可以画出模型流式文本、工具进度和每个 turn 的边界,而不是只能等最终字符串。
25
L1 · fact · prime-persistence-001

SessionManager 用带 parentId 的 JSONL 树表达分支、压缩和扩展状态

先看源码事实

SessionEntry 包含 message、compaction、branch_summary、custom、session_state、agent_status 等类型;SessionHeader 保存 version、cwd、parentSession、rlmDepth 和 git context。

翻译成白话

会话文件不是一条只能向后追加的聊天记录,而是一棵可导航的树;扩展可以持久化自己的 entry,又不会把内部状态强塞给模型。

为什么这对自研重要

要支持 fork、resume、compaction 和子 Agent,持久化格式需要 parent link、版本号和“不进入 LLM context”的 entry 类型。

固定提交源码摘录
   33  export const CURRENT_SESSION_VERSION = 3;
   34  const SESSION_LIST_SEARCH_TEXT_MAX_CHARS = 64 * 1024;
   35  const SESSION_LIST_PARSE_MAX_LINE_CHARS = 1024 * 1024;
   36  const SESSION_LIST_LARGE_MESSAGE_PREVIEW_MAX_CHARS = 256;
   37  const SESSION_STREAMING_LOAD_THRESHOLD_BYTES = 128 * 1024 * 1024;
   38  const SESSION_ASYNC_PARSE_YIELD_BYTES = 4 * 1024 * 1024;
   39  
   40  // Entry types that can represent user intent (vs. daemon bookkeeping like
   41  // session_state/agent_status/git_state/child_usage_attributed). Used by
   42  // hasUserContent to decide whether a message-less draft is safe to discard.
   43  const CONTENT_ENTRY_TYPES = new Set([
   44  	"message",
   45  	"custom_message",
   46  	"custom",
   47  	"model_change",
   48  	"thinking_level_change",
   49  	"service_tier_change",
   50  	"session_info",
   51  	"label",
   52  	"compaction",
   53  	"branch_summary",
   54  ]);
为什么相信这条结论?查看 3 处证据
26
L1 · fact · prime-persistence-002

Context 重建沿 parent tree,并把 compaction summary 放在 retained messages 前

先看源码事实

buildSessionContext 从 leaf 沿 parentId 回溯再 reverse,找到 compaction 后把 summary message 与 retainedMessages 放在前面,再追加 compaction 之后的消息;同时恢复 model、thinking level 和 service tier。

翻译成白话

从任意分支恢复时,模型看到的是“摘要→保留的旧消息→新分支”,UI 还可以知道真实的树边界。

为什么这对自研重要

恢复逻辑应是持久化格式的一部分,而不是 UI 临时拼接;压缩和分支要共享同一 ancestry traversal。

固定提交源码摘录
  472  /**
  473   * Build the session context from entries using tree traversal.
  474   * If leafId is provided, walks from that entry to root.
  475   * Handles compaction and branch summaries along the path.
  476   */
  477  export function buildSessionContext(
  478  	entries: SessionEntry[],
  479  	leafId?: string | null,
  480  	byId?: Map<string, SessionEntry>,
  481  ): SessionContext {
  482  	// Build uuid index if not available
  483  	if (!byId) {
  484  		byId = new Map<string, SessionEntry>();
  485  		for (const entry of entries) {
  486  			byId.set(entry.id, entry);
  487  		}
  488  	}
  489  
  490  	// Find leaf
  491  	let leaf: SessionEntry | undefined;
  492  	if (leafId === null) {
  493  		// Explicitly null - return no messages (navigated to before first entry)
  494  		return { messages: [], thinkingLevel: "off", serviceTier: "default", model: null };
  495  	}
  496  	if (leafId) {
      … 28 lines omitted; exact range 472–535 …
  525  			thinkingLevel = entry.thinkingLevel;
  526  		} else if (entry.type === "service_tier_change") {
  527  			serviceTier = entry.serviceTier;
  528  		} else if (entry.type === "model_change") {
  529  			model = { provider: entry.provider, modelId: entry.modelId };
  530  		} else if (entry.type === "message" && entry.message.role === "assistant") {
  531  			model = { provider: entry.message.provider, modelId: entry.message.model };
  532  		} else if (entry.type === "compaction") {
  533  			compaction = entry;
  534  		}
  535  	}
为什么相信这条结论?查看 2 处证据
27
L1 · fact · prime-persistence-003

写盘采用临时文件 rename,普通 entry 采用 append-only

先看源码事实

_rewriteFile 将完整 JSONL 写入带随机名的临时文件后 rename;_persist 在已有 assistant 后追加一行,尚未有 assistant 时暂缓非状态 entry,避免半成品会话。

翻译成白话

完整重写时不会留下半个 JSONL;常规消息又不必每次重写整个文件,兼顾可靠性与成本。

为什么这对自研重要

会话持久化要显式处理“第一条 assistant 尚未出现”的预模型状态,并用原子替换防崩溃损坏。

固定提交源码摘录
 1345  	private _rewriteFile(): void {
 1346  		if (!this.persist || !this.sessionFile) return;
 1347  		const content = `${this.fileEntries.map((e) => JSON.stringify(e)).join("\n")}\n`;
 1348  		const targetPath = realpathIfPresent(this.sessionFile);
 1349  		const directory = dirname(targetPath);
 1350  		mkdirSync(directory, { recursive: true });
 1351  		const tempPath = join(directory, `.${basename(targetPath)}.${process.pid}.${randomUUID()}.tmp`);
 1352  		try {
 1353  			const metadata = statMetadataIfPresent(targetPath);
 1354  			writeFileSync(tempPath, content, metadata === undefined ? undefined : { mode: metadata.mode });
 1355  			if (metadata !== undefined) {
 1356  				chownSync(tempPath, metadata.uid, metadata.gid);
 1357  				chmodSync(tempPath, metadata.mode);
 1358  			}
 1359  			renameSync(tempPath, targetPath);
 1360  		} finally {
 1361  			rmSync(tempPath, { force: true });
 1362  		}
 1363  		this._notifyPersistListeners();
 1364  	}
为什么相信这条结论?查看 3 处证据
28
L1 · fact · prime-observe-001

Agent events 覆盖 agent、turn、message 和 tool execution 四层

先看源码事实

AgentEvent 类型至少包括 agent_start/end、turn_start/end、message_start/update/end、tool_execution_start/update/end;AgentSession 还扩展 compaction、RLM child update 等宿主事件。

翻译成白话

UI 可以画出模型流式文本、工具进度和每个 turn 的边界,而不是只能等最终字符串。

为什么这对自研重要

观测模型应把 lifecycle、partial output、tool artifact 和终态分层,便于 replay 与故障定位。

固定提交源码摘录
  399  /**
  400   * Events emitted by the Agent for UI updates.
  401   *
  402   * `agent_end` is the last event emitted for a run, but awaited `Agent.subscribe()`
  403   * listeners for that event are still part of run settlement. The agent becomes
  404   * idle only after those listeners finish.
  405   */
  406  export type AgentEvent =
  407  	// Agent lifecycle
  408  	| { type: "agent_start" }
  409  	| { type: "agent_end"; messages: AgentMessage[] }
  410  	// Turn lifecycle - a turn is one assistant response + any tool calls/results
  411  	| { type: "turn_start" }
  412  	| { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] }
  413  	// Message lifecycle - emitted for user, assistant, and toolResult messages
  414  	| { type: "message_start"; message: AgentMessage }
  415  	// Only emitted for assistant messages during streaming
  416  	| { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent }
  417  	| { type: "message_end"; message: AgentMessage }
  418  	// Tool execution lifecycle
  419  	| { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any }
  420  	| { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any }
  421  	| { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean };
为什么相信这条结论?查看 2 处证据
小练习 9

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M10 · ENGINEERING

测试、恢复与工程取舍:把漂亮机制变成可靠产品

哪些行为有测试证明?哪些只是配置或 prompt 约定?当安全、性能、可恢复性冲突时,源码选择了什么?

先用一个生活比喻

这像验收一座桥:图纸说明结构,测试证明承重,故障演练证明断电后还能不能让人安全回来。

这套实现先回答了什么?

项目没有只测“模型能回答一句话”,而是把长会话、后台 worker、子 Agent 和工具执行拆成可回归的测试文件。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
测试布局覆盖 agent、session、daemon、RLM、MCP 与 telemetrypackages/coding-agent/test/acp-rlm-subagents.test.ts:1项目没有只测“模型能回答一句话”,而是把长会话、后台 worker、子 Agent 和工具执行拆成可回归的测试文件。
29
L3 · fact · prime-maturity-001

测试布局覆盖 agent、session、daemon、RLM、MCP 与 telemetry

先看源码事实

coding-agent test 目录包含 acp-rlm-subagents、daemon runtime lease、daemon supervisor eviction/side-question、agent-session tree/concurrent、MCP/auth、telemetry、exec、kernel startup 等契约测试。

翻译成白话

项目没有只测“模型能回答一句话”,而是把长会话、后台 worker、子 Agent 和工具执行拆成可回归的测试文件。

为什么这对自研重要

建设自研 harness 时应把恢复、并发、权限和观测作为一等测试维度,而不是上线后再补。

固定提交源码摘录
    1  import { mkdirSync, rmSync } from "node:fs";
    2  import { tmpdir } from "node:os";
    3  import { join } from "node:path";
    4  import * as acp from "@agentclientprotocol/sdk";
    5  import { Agent } from "@earendil-works/pi-agent-core";
    6  import {
    7  	type AssistantMessage,
    8  	type Context,
    9  	createAssistantMessageEventStream,
   10  	getModel,
   11  	type TextContent,
   12  	type Usage,
为什么相信这条结论?查看 4 处证据
小练习 10

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M11 · PRACTICE

把读懂变成会判断

下面的练习不要求你先写一个完整 Agent,而是训练你检查设计边界:事实是什么、推断是什么、如果换成自研产品要补哪一层。

Q1

低层 Agent Loop 是可复用的 provider-neutral 状态机

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

自研时可把 loop 做成纯运行时,再让 CLI、RPC、桌面 UI 共用;不要让界面组件自己复制一套 tool loop。

证据:packages/agent/src/agent-loop.ts:178
Q2

Provider 边界前才做上下文变换和密钥解析

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

上下文压缩、OAuth refresh 和 provider 适配应放在真正发请求的边界,避免 token 过期或 stale prompt。

证据:packages/agent/src/agent-loop.ts:467
Q3

Steering、follow-up、continuation 是三个不同的队列语义

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

长任务产品应给消息定义明确的 admission boundary,才能保证“插话”不会跳过当前工具调用或错误地重放。

证据:packages/agent/src/agent-loop.ts:317
Q4

工具调用先预检再执行,支持串行和并行两条路径

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

工具调度应把“可并行性”和“参数校验”写进 ToolDefinition,而不是让模型提示词决定。

证据:packages/agent/src/agent-loop.ts:608
Q5

before/after tool hook 是可编程的策略门

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

策略要留在 dispatch pipeline,而不是散落在每个工具内部;同时要审计扩展是否能覆盖结果或终止状态。

证据:packages/agent/src/agent-loop.ts:795
APPENDIX · SOURCE INDEX

本课读过的实现文件

文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。

  1. 01packages/agent/src/agent-loop.tsL178–205, L307–373, L422–460, L467–521, L526–603, L317–345, L385–419, L608–623, L630–688, L690–735, L795–848, L850–904, L906–945, L501–519
  2. 02packages/agent/src/types.tsL148–183, L206–244, L246–277, L399–421, L170–183
  3. 03packages/coding-agent/src/core/compaction/compaction.tsL122–132, L226–233, L138–147, L192–224, L243–297, L303–339, L434–458, L618–634, L465–496, L501–538, L552–611, L636–712
  4. 04packages/coding-agent/src/core/session-manager.tsL33–54, L75–85, L126–163, L472–535, L537–596, L1345–1364, L1442–1475, L1477–1499, L1345–1475
  5. 05packages/coding-agent/src/core/tools/bash.tsL36–58, L60–117, L276–298, L250–266, L276–287, L303–314, L66–117
  6. 06packages/coding-agent/src/core/resource-loader.tsL23–39, L336–365, L409–479, L646–678, L681–690, L811–830, L413–435
  7. 07packages/coding-agent/src/core/extensions/types.tsL1024–1074, L1080–1110, L1140–1163, L1188–1199, L1080–1163, L1028–1058
  8. 08packages/coding-agent/src/core/extensions/runner.tsL670–711, L805–826, L857–887, L889–920
  9. 09packages/coding-agent/src/core/mcp/mcp-manager.tsL36–78, L147–198
  10. 10packages/coding-agent/src/core/rlm-runtime.tsL14–39, L201–241, L205–241
  11. 11packages/coding-agent/src/modes/daemon/daemon-supervisor.tsL158–212, L641–705, L920–944, L1125–1184, L1186–1210, L1635–1678, L49–53, L633–660
  12. 12packages/coding-agent/src/core/agent-session.tsL318–390, L1–13, L412–500
  13. 13packages/coding-agent/test/acp-rlm-subagents.test.tsL1–12
  14. 14packages/coding-agent/test/daemon-runtime-lease.test.tsL1–12
  15. 15packages/coding-agent/test/agent-session-tree-navigation.test.tsL1–12
  16. 16packages/coding-agent/test/telemetry.test.tsL1–12
下一步

从课程回到报告,做一次反向核验

教程负责让你读懂,报告负责让你查证。打开报告页,任选一个章节,尝试只靠源码摘录复述它的边界。

查看 Prime Agent 报告 ↗