diff --git a/.gitignore b/.gitignore index cf8a7a3ff..9b8183c81 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ __pycache__/ # C extensions *.so +# Claude AI +.claude/ + # Distribution / packaging .Python build/ diff --git a/learn-pi-agent/s01_minimal_agent_core/README.md b/learn-pi-agent/s01_minimal_agent_core/README.md index b2e14fa27..79ce03149 100644 --- a/learn-pi-agent/s01_minimal_agent_core/README.md +++ b/learn-pi-agent/s01_minimal_agent_core/README.md @@ -1,7 +1,7 @@ # s01: Agent Core — 先存住一轮对话 -> *一轮,先存下来。* -> **Pi 边界**:provider 输入边界 —— core 的内部状态和 provider 调用之间的第一道分隔。 +> *把一轮对话,先存下来。* +> **Pi Agent 核心的边界**: provider 输入边界 —— core 的内部状态和 provider 调用之间的第一道隔断。 `s01` → [下一节:s02](../s02_tool_contract/) @@ -9,13 +9,13 @@ ## 问题 -你让模型回答一句话,模型给了回复,然后就停了。 +你让模型回答一个问题,模型给了回复,然后就停了。 -如果只是一次问答,这没问题。但你想让它"接着刚才的话继续",就会遇到第一个麻烦:**每一次调用都是独立的,模型自己并不记得上一句说了什么。** +如果只是一轮问答,这没问题。但你想让它"接着刚才的话继续",就会遇到第一个麻烦:**每一次调用都是独立的,模型自己并不记得上一句说了什么。** -所以 core 要做的第一件事,不是让模型变得更聪明,而是**先把这一轮对话记下来**——用户说了什么、模型回了什么,按顺序存在 core 里。有了这份记录,模型才接得上"刚才",下一轮才有继续的基础。 +所以 core 要做的第一件事,不是让模型变得更聪明,而是**先把这一轮对话记下来**——用户说了什么、模型回了什么,按顺序存在 core 里。有了这份记录,模型才接得上"刚才的对话",下一轮才有继续的基础。 -s01 只做这一件事:存下一轮对话。 +s01 从存一轮对话开始,同时建立起 core 和 provider 的界线。 --- @@ -24,21 +24,24 @@ s01 只做这一件事:存下一轮对话。 一轮对话进入 core,中间要经过一条边界: ```text -AgentState(core 内部) ──ProviderInput──> Provider +AgentState(core 内部) ── ProviderInput ──> Provider ``` -core 内部怎么存,是 core 自己的事;provider 能看到什么,由 ProviderInput 决定。这两边**故意不同**:provider 拿不到 core 的内部状态,只拿到一份为它准备好的输入。 +core 内部怎么存,是 core 自己的事;provider 能看到什么,由 ProviderInput 决定。这两边**故意不同**:provider 拿不到 core 的内部状态,只能拿到一份为它准备好的输入信息。 -用 `runOneTurn` 一个函数串起这一轮: +用 `runOneTurn` 函数串起这轮流程: | 步骤 | 动作 | | --- | --- | -| 1 | 用户消息进 AgentState | +| 1 | 用户消息进入 AgentState | | 2 | 从 AgentState 构造 ProviderInput | -| 3 | 调 Provider,拿到 AssistantMessage | -| 4 | assistant 消息存回 AgentState | +| 3 | 调 Provider,拿到 assistant 消息 | +| 4 | 把 assistant 消息存回 AgentState | -另外有两处设计先提一下,后面会用到:assistant 消息带一个**停止原因**(这一轮是正常结束,还是出了错);core 的输出统一走一层 **Output**,不直接打印。 +另外有两处设计先提一下,后面会用到: + + 1. assistant 消息带一个**停止原因**(这一轮是正常结束,还是出了错); + 2. core 的输出统一走一层 **Output**,不直接打印。 --- @@ -46,7 +49,7 @@ core 内部怎么存,是 core 自己的事;provider 能看到什么,由 Pr 从 core 内部往外,一步步把这条边界搭出来。 -**core 先存什么。** 一条消息要么来自用户,要么来自 provider。provider 的回复还要带停止原因,这样 core 才知道这一轮是正常结束,还是出了错。 +**core 先存什么。** 一条消息要么来自用户,要么来自 provider 。provider 的回复还要带停止原因,这样 core 才知道这一轮是正常结束,还是出了错。 ```ts export type StopReason = "stop" | "error"; @@ -56,7 +59,7 @@ export type AssistantMessage = { role: "assistant"; content: string; stopReason: export type AgentMessage = UserMessage | AssistantMessage; ``` -core 用一个数组按顺序把它们存起来。现在 state 只有一个字段,但后面所有的对话历史都从这里长出来。 +core 用一个数组按顺序把它们存起来。现在 state 只有一个字段,但后面所有的对话历史都会从这里长出来。 ```ts export type AgentState = { messages: AgentMessage[] }; @@ -65,7 +68,7 @@ export function createInitialState(): AgentState { return { messages: [] }; } export function createUserMessage(content: string): UserMessage { return { role: "user", content }; } ``` -**然后是边界。** provider 不直接拿 AgentState,而是把每条消息转成它需要的 role 和 content,组成 ProviderInput。这一步只是做了一次转换,但它就是那道墙——core 的内部结构不会漏给 provider。 +**然后是边界。** provider 不会直接拿到 AgentState,而是把每条消息转成 provider 需要的 role 和 content,组成 ProviderInput。这一步看起来只是做了格式转换,但它就是那道墙 —— core 的内部结构不会漏给 provider 。 ```ts export type ProviderMessage = { role: "user" | "assistant"; content: string }; @@ -78,7 +81,7 @@ export function buildProviderInput(state: AgentState): ProviderInput { } ``` -provider 这边的约定就一句话:给我 ProviderInput,我还你 AssistantMessage。 +provider 这边的约定就一句话:给我 ProviderInput,我还你 AssistantMessage 。 ```ts export interface Provider { @@ -86,14 +89,14 @@ export interface Provider { } ``` -**最后收口。** core 不直接决定结果怎么展示,先留一层 Output——现在它只是包了一下 console,但把这层单独拎出来,后面有用。 +**最后收口。** core 不直接决定结果怎么展示,先留一层 Output,现在只包了一层 console,但把这层单独拎出来,后面有用。 ```ts export type Output = { log(line: string): void }; export function createConsoleOutput(): Output { return { log: (line) => console.log(line) }; } ``` -一轮的推进就是把上面几步连起来:存入用户消息 → 构造输入 → 调 provider → 存回 assistant 消息。 +每一轮的推进就是把上面几步连起来:存入用户消息 → 构造输入 → 调 provider → 存回 assistant 消息。 ```ts export async function runOneTurn( @@ -107,7 +110,7 @@ export async function runOneTurn( } ``` -> 这一节真正交付的东西,不是 `runOneTurn` 这个函数,而是那条边界——AgentState 和 ProviderInput 之间的转换。后面每一节都会往 ProviderInput 里加东西,但"core 的内部状态不直接暴露给 provider"这条规矩,从 s01 定下来就不会再变。 +> 这一节真正要讲的,不是 `runOneTurn` 这个函数,而是 AgentState 和 ProviderInput 之间那条边界:格式转换。后面每一节都会往 ProviderInput 里加入新东西,但“ core 的内部状态永远不会直接暴露给 provider ”这条规矩,从 s01 定下来就不会再变。 --- @@ -156,12 +159,12 @@ npm run s01 -- --case error ## 接入主线 -s01 是 mini Pi 的第 1 版,是后面 11 节的地基。本节确立的、后续**只扩展不改写**的永驻基础: +s01 是 mini Pi 的起点,是后面 11 节的地基。本节定下来的类型和接口,后面**只扩展不改写**: | 基础 | 后续怎么演化 | | --- | --- | | `UserMessage` / `AssistantMessage` / `AgentMessage` | 消息三类型,union 只增(s04 加 ToolResultMessage) | -| `AssistantMessage.stopReason` | 永驻,取值只增(s04 加 toolUse) | +| `AssistantMessage.stopReason` | 字段不变,取值只增(s04 加 toolUse) | | `AgentState.messages` | 先是数组,s07 升级为 SessionTree(U1) | | `ProviderInput` | 字段只增(s02 加 tools、s08 加 systemPrompt);对齐 Pi Context,model 在 AgentState 不进 input | | `Provider` | s03 从 complete 升级为 stream(U1) | @@ -171,7 +174,7 @@ s01 是 mini Pi 的第 1 版,是后面 11 节的地基。本节确立的、后 ## 接下来 -现在 ProviderInput 里只有 messages。下一节会往里加东西,让 provider 看到的不只是对话,还有 core 能提供的本地能力。进入下一节:[s02](../s02_tool_contract/)。 +现在 ProviderInput 里只有 messages ,下一节会加入其他东西,让 provider 看到的不只是对话,还有 core 能提供的本地能力。进入下一节:[s02](../s02_tool_contract/)。 --- @@ -182,9 +185,9 @@ s01 是 mini Pi 的第 1 版,是后面 11 节的地基。本节确立的、后 ### 源码在哪 -- `packages/agent/src/types.ts:317` — `AgentState` 类型 -- `packages/agent/src/agent.ts:166` — `Agent` 类(状态持有 + 生命周期) -- `packages/agent/src/agent-loop.ts:155` — `runAgentLoop`(核心循环) +- [`packages/agent/src/types.ts:317`](https://github.com/earendil-works/pi/blob/main/packages/agent/src/types.ts#L317) — `AgentState` 类型 +- [`packages/agent/src/agent.ts:166`](https://github.com/earendil-works/pi/blob/main/packages/agent/src/agent.ts#L166) — `Agent` 类(状态持有 + 生命周期) +- [`packages/agent/src/agent-loop.ts:155`](https://github.com/earendil-works/pi/blob/main/packages/agent/src/agent-loop.ts#L155) — `runAgentLoop`(核心循环) ### AgentState 的真实形状 @@ -238,11 +241,11 @@ private async runWithLifecycle(executor) { - **outer loop**:消费 follow-up 消息队列(用户后续追加的话)。 - **inner loop**:处理工具调用,以及 steering 消息(执行中途插入、用来"引导"方向的)。 -教学版的循环只有"工具来回"一条线;Pi 把"用户中途插话"和"工具来回"分成两个队列,各有优先级——这是真实交互场景必须的(用户不会老老实实等工具跑完)。 +教学版的循环只有"工具来回"一条线;Pi 把"用户中途插话"和"工具来回"分成两个队列,各有优先级,这是真实交互场景必须的(用户不会老老实实等工具跑完)。 ### 消息带时间戳,内容是数组 -教学版 `content` 是字符串。Pi 的 `AgentMessage` 是 `content: Array` 加 `timestamp`——一条消息能同时含文本、图片、工具调用,且按时间排序。教学版先把 content 简化成 string,s04 加 tool_call 时才会碰到"一条消息多种内容"的真实形态。 +教学版 `content` 是字符串。Pi 的 `AgentMessage` 是 `content: Array` 加 `timestamp`,一条消息能同时含文本、图片、工具调用,且按时间排序。教学版先把 content 简化成 string,s04 加 tool_call 时才会碰到"一条消息多种内容"的真实形态。 ### 一句话