Files
analysis_claude_code/learn-pi-agent/EVOLUTION.md
2026-06-16 00:10:35 +08:00

20 KiB
Raw Blame History

累积演进宪法

本文件是 learn-pi-agent 的工程对照基准。每一节的 code.tsREADME.md 都必须对照它。 目标:学习者从 s01 学到 s12累积实现出一个机制健全、心智与 Pi 一致的 mini Pi。


0. 核心立场

现有课程确立了「每节独立、最小化」P2但丢了「累积实现」P0。 本宪法把 P0 立起来,同时不毁掉「每节聚焦一个机制」的教学性。

一句话原则:每一节 = 给正在生长的 mini Pi 装一个零件。

  • 代码是单轨累积的:sXX/code.ts 是 mini Pi 的第 N 个版本,是第 N-1 节的超集
  • 文档负责聚焦:每节 README 末尾的「接入主线」段用 diff 风格展示本节焊上了什么零件。

不维护「聚焦 demo」和「主线」两份代码。两份代码必然漂移漂移就是现在所有类型退化问题的根源。


1. 元规则(约束所有类型演化)

规则 内容 修复的现有问题
R1 字段只增不删ProviderInputProviderEventStopReason 取值集、AgentMessage union 成员、ToolSpecTurnSnapshot 字段,一旦引入永久保留 s03 删 tools、s04 删 message_start、s06/s08 删 stopReason、s04/s06 删 ToolSpec.input
R2 方法只增ToolRegistry 等类的成员只增 registry 在 s02/s04/s06 间反复变方法集
R3 只允许两类受控升级,且必须在当节 README 显式声明「这是升级,不是新增」:
U1 接口语义升级(不可避免的 breaking change
U2 同名类型全局唯一
现有「同名不同义」「5 节 5 种 Tool 形状」
R4 错误不崩溃,转结构化消息(见 §4 Gap3 错误传播缺失
R5 循环有终止保证maxTurns 上限) Gap3 工具循环无保护
R6 加载执行分开管(对齐 Pi 真实设计trust 控制资源加载;执行边界不内置、靠部署层 containerizationhook 是唯一的执行细化拦截点 Gap3 trust/policy/hook 三个孤立 demo
R7 core 通过 output 抽象输出,不直接 console.log。从 s01 起就有最小间接层 Gap3 输出/执行分离s10 才分离却已焊死
R8 每节 code.ts 是前一节的超集README「接入主线」段以 diff 展示增量 不累积

U1 受控升级清单(宪法允许的全部 breaking change

升级 理由
s03 Provider.complete()Provider.stream() provider 输出形态本质改变(一次性→流式),无法并存
s07 AgentState.messages: AgentMessage[]SessionTree 历史从线性升级为可分支树;currentPath() 仍产出 AgentMessage[],对外构造方式不变
s11 ResourceLoader.load()load(trust) 加信任参数过滤资源(参数升级)

除以上三处外,任何对已有类型/接口的删改都违反宪法。

U2 同名类型全局唯一清单

ResourceLoaderRuntimeEventToolOutput 在主线中各自只有一个定义,所有章节复用它。


2. 核心类型字典

终态定义 + 引入节 + 演变。每节 code.ts 必须与字典一致。

// —— 消息s01 起union 只增 R1——
type StopReason = "stop" | "error";              // s01
//           = "stop" | "toolUse" | "error";     // s04 起加 toolUse之后稳定

type UserMessage      = { role: "user";       content: string };                       // s01稳定
type AssistantMessage = { role: "assistant";  content: string; stopReason: StopReason };// s01stopReason 永驻
type ToolResultMessage= { role: "toolResult"; toolCallId: string; content: string };    // s04稳定
type AgentMessage = UserMessage | AssistantMessage            // s01
                  | ToolResultMessage;                        // s04 起并入(只增)

// —— core 状态(对齐 Pi AgentState——
type AgentState = {
  messages: AgentMessage[];   // s01 起s07 升级为 SessionTreeU1
  model: string;              // s06 起加:跨轮配置,对齐 Pi不在 ProviderInput/snapshot
};

// —— Provider 对外形态(对齐 Pi Context——
type ProviderMessage =
  | { role: "user" | "assistant"; content: string }           // s01
  | { role: "toolResult"; toolCallId: string; content: string };// s04 起并入(只增)

// Pi 的 Context = { systemPrompt?, messages, tools }。教学版对齐systemPrompt(s08 起) + messages + tools。model 不在这里,在 AgentState。
type ProviderInput = {
  systemPrompt: string;                                        // s08 起加(项目资料组装进去)
  messages: ProviderMessage[];                                 // s01
  tools: ToolSpec[];                                           // s02 起加s03 不许删)
};

// —— 工具契约s02 起,全局唯一形状 U2——
type ToolSpec    = { name: string; description: string; input: Record<string, string> };// input 永驻
type ToolHandler = (input: Record<string, string>) => string;                          // 同步;抛错由 R4 捕获
type Tool        = { spec: ToolSpec; handler: ToolHandler };                           // 全局唯一形状
type ToolCall    = { id: string; name: string; input: Record<string, string> };        // s04

class ToolRegistry {                                           // 方法只增 R2
  register(tool: Tool): void {}                                // s02
  getSpecs(): ToolSpec[] {}                                    // s02
  run(call: ToolCall): string {}                               // s04
}

// —— Provider 事件流 ——
type ProviderEvent =
  | { type: "message_start" }                                  // s03s04 不许删)
  | { type: "text_delta"; text: string }                       // s03
  | { type: "message_end"; stopReason: StopReason }            // s03
  | { type: "tool_call"; call: ToolCall };                     // s04 起加

interface Provider {                                           // U1s03 由 complete 升级为 stream
  stream(input: ProviderInput): AsyncGenerator<ProviderEvent>;
}

// —— Turn 快照(对齐 Pi AgentContext固定 systemPrompt/messages/toolsmodel 在 state 不进快照)——
type TurnSnapshot = { systemPrompt: string; messages: ProviderMessage[]; tools: ToolSpec[] };

// —— 会话树U1s07 取代扁平 messages 数组)——
type SessionEntry = { id: string; parentId: string | null;
                      role: "user" | "assistant" | "toolResult"; content: string };
class SessionTree {
  append(msg): SessionEntry {}
  moveTo(id: string): void {}
  currentPath(): AgentMessage[] {}                             // 产出线性消息供 ProviderInput 使用
  allEntries(): SessionEntry[] {}
}

// —— 上下文资源s08U2 全局唯一 ResourceLoader——
type ContextResource = { kind: "agents" | "skill" | "prompt"; name: string; content: string };
class ResourceLoader {
  constructor(private resources: ContextResource[]) {}
  load(trust?: ProjectTrust): ContextResource[] {}             // U1s11 加 trust 参数
}
// s08资源组装进 systemPrompt对齐 Pi buildSystemPrompt不是独立 context 字段
function buildSystemPrompt(resources: ContextResource[]): string {}

// —— Hooks05外层装饰 registry.run不进 registry——
type BeforeToolCallResult = { type: "allow" } | { type: "block"; reason: string };
type ToolHooks = {
  beforeToolCall?: (call: ToolCall) => BeforeToolCallResult;
  afterToolCall?:  (call: ToolCall, result: string) => string;
};
function executeToolCall(registry, hooks, call): ToolResultMessage {}  // s05 起,稳定(不加 policy

// —— 扩展运行时s09复用前面的 Tool/ToolRegistry——
type Command = { name: string; run: () => string };
type RuntimeEvent = { type: "message"; content: string } | { type: "done" }; // U2 全局唯一
type ExtensionAPI = {
  on(type: RuntimeEvent["type"], handler: (e: RuntimeEvent) => void): void;
  registerTool(tool: Tool): void;                              // 复用 s02 的 Tool注入现有 ToolRegistry
  registerCommand(cmd: Command): void;
};
type Extension = (api: ExtensionAPI) => void;
class ExtensionRuntime { use(ext: Extension): void; /* 内部持有 ToolRegistry */ }

// —— 输出抽象R7s01 最小形态 → s10 正式化)——
type Output = { log(line: string): void };                     // s01 起最小间接层
// s10 升级为:
type RuntimeMode = { render(events: RuntimeEvent[]): void };   // PrintMode / JsonMode 是两个实现

// —— 信任与执行边界s11对齐 Pitrust 控加载,执行靠 containerization——
type ProjectTrust = "trusted" | "untrusted";
// 注Pi 不内置执行权限系统。ExecutionPolicy/Executor 已移除——执行边界靠部署层
// containerization 三方案OpenShell / Gondolin / Plain Dockercore 内只有 trust 控制资源加载。

// —— 能力分发s12注入既有 registry/commands/loader——
type PackageManifest = { name: string; tools: string[]; commands: string[]; resources: string[] };
type Package = { manifest: PackageManifest; contents: Record<string, string> };
type LoadedPackage = { name: string; tools: Record<string,string>;
                       commands: Record<string,string>; resources: Record<string,string> };
function loadPackage(pkg: Package): LoadedPackage {}

3. 十二节累积演进主表

每节三栏:累积骨架(不变) 本节新增零件 接入点(焊在哪)

累积骨架(不变) 本节新增零件 接入点
s01 AgentState、消息三类型、StopReason(stop/error)、ProviderInput{messages}、Provider.complete、runOneTurn、Output.log(R7) 地基
s02 messages、provider、runOneTurn ToolSpec{name;description;input}、ToolHandler、Tool、ToolRegistry(register/getSpecs) buildProviderInput 接收 registryProviderInput 加 tools=registry.getSpecs()
s03 全部(含 toolsR1 不删 [U1] Provider complete→stream、ProviderEvent(message_start/text_delta/message_end)、collectAssistantMessage Provider 接口升级runOneTurn 内 complete→stream+collect
s04 事件流、tools ToolCall、ToolResultMessage、tool_call 事件、StopReason+toolUse、ToolRegistry.run、runEventedToolLoop(maxTurns R5)、错误捕获(R4) 循环内 registry.run(call),结果入 messagestools 仍取 registry.getSpecs()不硬编码
s05 工具循环 ToolHooks、beforeToolCall/afterToolCall、BeforeToolCallResult(allow/block)、executeToolCall(registry,hooks,call) 循环内 registry.run(call)executeToolCall(registry,hooks,call)registry 不变(R2)
s06 循环+hook TurnSnapshot{messages,tools}、createTurnSnapshot、AgentState+model(跨轮配置,对齐 Pi runEventedToolLoop 开头先 createTurnSnapshotmodel 在 AgentState 不进 snapshot对齐 Pi AgentContextsnapshot 只固定 messages/tools
s07 snapshot、循环、hook [U1] messages 数组→SessionTree、SessionEntry{parentId}、append/moveTo/currentPath buildProviderInput 用 session.currentPath() 取线性消息;对外构造不变
s08 tree、snapshot、tools ContextResource、ResourceLoader.load()、buildSystemPrompt、ProviderInput+systemPrompt(资料组装进去,对齐 Pitools 保留 R1 buildProviderInput 调 buildSystemPrompt(loader.load()) 拼 systemPromptsnapshot 跟随加 systemPrompt
s09 全部主线 Extension、ExtensionAPI、ExtensionRuntime、Command、RuntimeEvent、on/registerTool/registerCommand ExtensionRuntime 内部持有现有 ToolRegistryregisterTool 注入的 Tool 走同一执行链(s05 hook)
s10 全部主线 [R7 收获] Output.log → RuntimeMode、PrintMode/JsonMode、render(RuntimeEvent[]) core 的 output.log 升级为 mode.render(events)s01 起就没直连 console此处只是命名+多态化
s11 全部主线 ProjectTrust、[U1] load(trust)、containerization 三方案(执行边界靠部署层,对齐 Pi trust→loader.load(trust) 控制资源加载;执行边界不内置,靠 containerizationREADME 讲 OpenShell/Gondolin/Docker 三方案)
s12 全部主线 PackageManifest、Package、LoadedPackage、loadPackage、pick loadPackage 产出注入 registry(s02)/commands(s09)/loader(s08)。能力分发闭环

4. Gap3 健全心智补全(宪法硬规定)

缺口 宪法规定
循环终止 (R5) runEventedToolLoopmaxTurns(默认 8)。终止条件 = provider 不再发 tool_call stopReason≠toolUse 达上限。达上限时返回 stopReason="stop" 并附注 "max turns reached"
错误传播 (R4) ① provider stream 抛错 → 捕获,本轮 AssistantMessage.stopReason="error"、content=错误说明,写回 state。
② tool handler 抛错 → executeToolCall 捕获ToolResultMessage.content="error: <msg>"循环继续(让 provider 看到错误自行决定)。
StopReason 维持三值 `stop
加载/执行分离 (R6) 两件事分开(对齐 Pi 真实设计):
加载靠 trustloader.load(trust)untrusted 返回空(防恶意资源)
执行不内置权限Pi 不在 core 里限制文件/进程/网络,执行边界靠部署层 containerization 三方案OpenShell / Gondolin / Plain Docker
细化拦截靠 hookbeforeToolCall allow/block 具体工具core 内唯一的执行拦截点)
教学版不再发明 ExecutionPolicy——它在 Pi 里没有对应物
systemPrompt 层级 ProviderInput.systemPromptsystem 级装配(项目资料组装进去),与 messages对话级历史)是两个独立维度。对齐 Pi 的 Context.systemPrompt(资料进 systemPrompt不是独立 context 字段)
输出/执行分离 (R7) 从 s01 起 core 通过 Output.log 输出,不直连 consoles10 升级为 RuntimeMode.render。前 9 节就不存在"core 与 IO 焊死"的债

5. 完整 Turn 执行链(总装蓝图)

这是根 README.md(总装章)的灵魂。一条链接上全部 12 节 + Gap3。

newTurn(userInput):
  1. systemPrompt = buildSystemPrompt(loader.load(trust)) # s08 资源组装 + s11 trust 过滤 (R6)
  2. session.append({role:"user", content:userInput})     # s07 历史树 (U1)
  3. snapshot = createTurnSnapshot(                        # s06 快照
       session.currentPath(), registry.getSpecs(), systemPrompt)
     # model 在 AgentState不进 snapshot对齐 Pi AgentContext
  4. for turn in 0..maxTurns:                              # s04 循环 (R5)
       input  = buildProviderInput(snapshot, state)        # s01/s02/s08systemPrompt 从 snapshotmodel 从 state
       events = provider.stream(input)                     # s03 事件流 (U1)Pi 真实 stream(model,context),教学 fake provider 简化)
       for event in events:
         message_start / text_delta → 累加 content
         message_end               → stopReason
         tool_call →
           result = executeToolCall(                       # s05 hookR4 错误捕获)
                       registry, hooks, call)              # 无 policy执行边界靠 containerizationR6
           session.append(toolResultMessage)               # s07
       if 无 tool_call 或 stopReason≠toolUse: break
  5. assistant = {role:"assistant", content, stopReason}  # stopReason 永驻 (R1)
  6. session.append(assistant)                            # s07
  7. mode.render(allEvents)                               # s10 输出分离 (R7)

  旁路extension(s09) 可注册额外 tool/command
        package(s12) 可分发 tool/resource 注入 registry/loader。

每一步都能追溯到某一节 + 某条规则。这就是「心智健全」的可验证证据。


6. 文件结构与每节 README 规范

6.1 文件结构

learn-pi-agent/
  README.md       总装章执行链全景图§5+ 累积演进表索引§3+ 如何从 s01 读到 s12
  EVOLUTION.md    本文件(宪法)
  sXX_*/
    code.ts       累积主线第 N 版前一节的超集R8
    README.md     按下方规范

6.2 每节 README 模板

# sXX: <主题> — <一句话点题>
> <motto 格言>
> Pi 边界:<本节对应的设计边界>

## 问题          ← 真痛点 / 上一节方案的缺陷(不提前命名下节概念)
## 解决方案      ← 核心洞察 + 设计取舍表(不重复工作原理步骤)
## 工作原理      ← 递进叙事:每步「为什么需要 + 代码」,末尾点睛
## 试一下        ← 运行命令 + 输出 + 观察重点
## 接入主线      ← 相对上节的变更表(前后对比)+ 焊接点
## 接下来        ← 引出下一节痛点(只描述,不命名)
## Pi 源码溯源    ← 折叠,只对照本章词汇

6.3 写作规则(硬约束)

  • R-写①「问题」必须有痛或张力:场景痛点 或 上一节方案的缺陷。不准写任务描述("core 要保存对话"这种不算问题)。
  • R-写②「解决方案」是洞察不是目录:一句话说清本质 + 设计取舍表。不准预告实现步骤(那是「工作原理」的事)。
  • R-写③「工作原理」是叙事不是清单:每个类型/函数出场时回答"为什么现在需要它"递进展开末尾必点睛这一节真正交付的东西是什么。U1 升级必须显式标注「这是升级」。
  • R-写④ 用自然中文,不直译英文意象motto 若源自英文,中文必须重写(catch one turn ✗ → 不译成"接住一轮");不为对仗生造动词搭配("穿过 core""接住一轮");用程序员实际会说的词("存下来""转成")。每节写完通读一遍,读起来"像翻译"的句子都要改。
  • R-写⑤ 词汇纪律(叙事段):「问题」「解决方案」「工作原理」「接下来」只用本章已解释词汇;不提前命名下节概念;"本章不讲 X"也把 X 引进来了,不要写。源码溯源同样控词。
    • 例外:「接入主线」段是工程对照表,允许路标式提及未来章节的术语,但必须标注出现章节(如 ToolResultMessages04 加)),作为前瞻路标,不是教学展开。

6.4「接入主线」段的写法

变更表(组件维度,前节 vs 本节),再加一句焊接点本节零件焊在主线的哪个位置。s01 作为地基节无前节可比,改列「本节确立的永驻基础 + 后续怎么演化」。后续节严格用「变更表 + 焊接点」。


7. 已落实的修正

  • R1s03 保留 toolss04 保留 message_start、ToolSpec.input并从 registry.getSpecs() 取 toolss06/s08 保留 stopReasons08 的 systemPrompt 与 tools 并列进入 ProviderInput。
  • R2s04 之后的 ToolRegistry 统一为 register + getSpecs + run
  • U2ResourceLoader、RuntimeEvent、Tool 全链路保持单一形状;后续章节复用同名类型,不另起含义。
  • U1s03、s07、s11 的三处受控升级在 README 的「接入主线」中显式标注。
  • R4/R5/R6/R7:工具错误转 ToolResultMessage、工具循环有 maxTurns、trust 只控加载、输出从 s01 起经过 Output 并在 s10 长成 RuntimeMode。
  • R8:每节 code.ts 是前一节的超集;每节 README 都有「接入主线」段。
  • 幽灵名词toProviderMessageContextBlock 这类文档里出现但代码里没有的名词已移除或落到实际实现上。

8. 待继续检查的点

  • s10 事件源:本节按教学 A 方案使用 createDemoRuntimeEvents() 作为最小事件源,避免重讲完整 tool loop。后续如果读者仍误解为另起 core再把「演示事件源」说明前移到「问题」段。
  • s12 安装闭环:本节按教学 A 方案保留 tools / commands / resources 三类教学对象,并通过 installLoadedPackage() 接回主线。真实 Pi 的 extensions / skills / prompts / themes 只在源码溯源里讲,不进入教学主线。
  • s06/s07 心智负担snapshot 与 SessionTree 是课程里的两次陡坡,后续润色时优先看这两节的「问题」段是否足够具体。