Compare commits

...
Author SHA1 Message Date
CoderLambert 294ecae6f2 docs: integrate professional knowledge notes
* docs: add professional notes index

* docs: add end-to-end review note

* docs: classify legacy architecture summary

* docs: classify integration notes

* docs: classify development plan

* docs: classify validation snapshot

* docs: classify task documents
2026-09-11 20:23:16 +08:00
CoderLambert eacc20467a docs: add reliability and security notes 2026-09-11 20:18:05 +08:00
CoderLambert 8412831f53 docs: add TanStack streaming state notes 2026-09-11 20:17:48 +08:00
CoderLambert b9b4eb9d4a docs: add Codex app-server streaming notes 2026-09-11 20:17:26 +08:00
CoderLambert 8632e0702e docs: add system architecture notes 2026-09-11 20:16:51 +08:00
11 changed files with 7195 additions and 0 deletions
+2
View File
@@ -1,5 +1,7 @@
# Architecture
> **文档定位:V0 架构摘要 / 项目记录。** 本文保留用于快速查看当前架构边界,不承担系统教学职责。完整的架构心智模型、设计理由、替代方案、失败模式与复习内容请阅读 [`docs/notes/01-system-architecture.md`](./notes/01-system-architecture.md);端到端串联请阅读 [`docs/notes/05-end-to-end-review.md`](./notes/05-end-to-end-review.md)。
## Boundary
The Codex app-server is server-only. Browser code must never import the SDK/protocol client or read local Codex authentication files.
+2
View File
@@ -1,5 +1,7 @@
# Five-task development plan
> **文档定位:开发拆分与协作记录。** 本文描述当时如何把实现拆成五个任务,不是技术知识笔记。长期学习请从 [`docs/notes/README.md`](./notes/README.md) 开始;该索引按“架构 → Runtime → Web 状态 → 可靠性 → 端到端”组织。
The repository is intentionally split into four parallel implementation tracks plus one integration track. Parallel tasks should minimize overlapping file ownership.
| Task | Scope | Primary file ownership | Deliverable |
+2
View File
@@ -1,5 +1,7 @@
# Task 05 integration notes
> **文档定位:集成实施记录。** 本文记录 Task 05 当时如何收敛模块边界,不作为长期学习入口。关于 Runtime/App Server 的系统知识请阅读 [`docs/notes/02-codex-app-server-streaming.md`](./notes/02-codex-app-server-streaming.md);关于 TanStack/React 状态模型请阅读 [`docs/notes/03-tanstack-streaming-state.md`](./notes/03-tanstack-streaming-state.md);完整链路请阅读 [`docs/notes/05-end-to-end-review.md`](./notes/05-end-to-end-review.md)。
## Resolved module boundaries
### Task 01 -> Task 02
+2
View File
@@ -1,5 +1,7 @@
# Validation report
> **文档定位:某次集成验证快照。** 这里记录当时实际跑过的检查与修复,不应被视为当前所有可靠性结论。关于“测试通过仍不能证明什么”、fake app-server、correlation、interrupt、process cleanup 与安全边界,请阅读 [`docs/notes/04-testing-reliability-security.md`](./notes/04-testing-reliability-security.md)。
Task 05 integration was performed against the common base and the Task 01-04 outputs were reconciled into one transport/state/UI pipeline.
## Final checks
+931
View File
@@ -0,0 +1,931 @@
# Codex + TanStack Start:系统架构学习笔记
> 目标:这不是项目进度记录,而是一篇可以脱离当前上下文独立复习的架构笔记。它回答三个问题:**系统边界在哪里、数据如何流动、为什么要这样分层。**
## 1. 问题背景:我们真正要解决什么
这个项目不是在“做一个聊天框”,而是在验证一套本地 Agent Web 架构:浏览器负责交互,TanStack Start 负责 Web/RPC 边界,Codex app-server 负责 Agent runtime,本机已有的 Codex/ChatGPT 登录态负责认证。
核心约束有四个:
1. 浏览器不能拿到 Codex 凭据、`~/.codex`、原始协议对象或本地敏感信息。
2. Agent 回复必须是真正的流式文本,而不是请求完成后一次性返回。
3. Web 层不能直接绑定 Codex 协议,否则以后换 Claude、Pi、Qwen 或其他 runtime 会牵动整个 UI。
4. V0 必须保持只读:可以分析仓库,但不允许文件写入、网络访问或交互式审批绕过。
因此,架构目标不是“最少代码”,而是建立几个清晰的边界:
```text
Browser/UI boundary
Transport/RPC boundary
Application event boundary
Agent runtime boundary
Local machine / workspace boundary
```
真正重要的是:每一层只理解自己需要理解的协议。
---
## 2. 总体架构
```mermaid
flowchart TD
U[User] --> UI[React Chat UI]
UI --> C[useChatController]
C --> SF[TanStack Start createServerFn]
SF --> BR[Streaming Bridge]
BR --> N[Codex Event Normalizer]
N --> RT[CodexRuntime interface]
RT --> AR[CodexAppServerRuntime]
AR --> AS[codex app-server]
AS --> FS[Workspace / Git Repository]
AS --> AUTH[Local Codex / ChatGPT Auth]
N -->|ChatEvent| BR
BR -->|Async stream| SF
SF --> C
C --> AD[ChatEvent adapter]
AD --> R[chatReducer]
R --> UI
```
从浏览器看,它只知道:
```text
ChatRequest -> stream<ChatEvent>
```
它不知道:
```text
JSON-RPC
codex app-server
thread/start
turn/start
item/agentMessage/delta
~/.codex
子进程
stdio
```
这就是架构中的第一原则:**把基础设施协议封装成应用协议。**
---
## 3. 三层系统边界
### 3.1 Browser:只负责产品状态
浏览器职责:
- 收集用户输入;
- 调用 `streamChat()`
- 消费 `ChatEvent`
- 维护消息、运行状态、活动状态;
- 保存最小会话信息;
- 渲染 UI。
浏览器不应该:
- 调用 `codex app-server`
- 读取 Codex 登录文件;
- 接触 MCP 参数/result
- 接触 command stdout/stderr
- 接触 reasoning 原文;
- 决定 sandbox 权限。
**前端状态不是 Agent runtime 状态。** 前端保存的是“产品需要展示的投影”。
### 3.2 TanStack Start Server:安全网关 + 协议翻译层
Server 层做三类工作:
1. 输入验证;
2. runtime 调用;
3. 将 runtime 事件变成浏览器安全的应用事件。
`src/server-functions/chat.ts` 很关键,因为它是浏览器可导入模块,但真正的 Codex runtime 被隔离在 `*.server.ts` 后面。
```text
client import
|
v
createServerFn()
|
| server execution only
v
chat.runtime.server.ts
|
v
Codex runtime
```
这解决了一个典型全栈框架问题:**同一个 TypeScript 工程不等于所有模块都可以进入 browser bundle。**
### 3.3 Codex Runtime:负责 Agent 生命周期
Runtime 层负责:
- spawn `codex app-server --stdio`
- initialize
- start/resume thread
- start turn
- 接收 item notification
- 接收 assistant delta
- 收集 usage
- 清理进程;
- 把协议对象转换为内部 `CodexThreadEvent`
这里最重要的架构点不是 Codex,而是 `CodexRuntime` interface。
```ts
interface CodexRuntime {
streamTurn(input: StreamCodexTurnInput): AsyncGenerator<CodexThreadEvent>
}
```
UI 不依赖 `CodexAppServerRuntime`server function 也不应该依赖 JSON-RPC 细节。
---
## 4. thread / turn / item:必须建立的心智模型
这是理解 Codex Agent runtime 的基础。
### Thread
`thread` 是长期会话上下文。
它类似:
```text
Conversation / Agent Session
```
第一条消息:
```text
thread/start
```
后续继续聊天:
```text
thread/resume(threadId)
```
浏览器 localStorage 保存 `threadId` 的原因,就是要把产品侧会话重新连接到 Codex 侧的长期上下文。
### Turn
`turn` 是 thread 中一次用户输入对应的一轮 Agent 工作。
```text
Thread
├── Turn 1
├── Turn 2
└── Turn 3
```
一次 turn 可能包含:
- reasoning
- command execution
- MCP tool call
- web search
- assistant message
- token usage
- error。
因此:
> 一个 HTTP/RPC 请求不等于一个 assistant message,而更接近一个完整 turn。
### Item
`item` 是 turn 内部的工作单元。
```text
Turn
├── reasoning item
├── command item
├── tool item
└── agentMessage item
```
Assistant 文本本身也是一个 item,它有生命周期:
```text
item/started
item/agentMessage/delta × N
item/completed
```
这也是为什么 UI 应使用:
```text
assistant.started
assistant.delta
assistant.completed
```
而不是只使用一个:
```text
assistant.message
```
---
## 5. 一条消息的完整生命周期
```mermaid
sequenceDiagram
participant User
participant React
participant Start as TanStack Start
participant Runtime
participant Codex as codex app-server
User->>React: 输入 prompt
React->>Start: streamChat({message, threadId})
Start->>Runtime: streamTurn()
Runtime->>Codex: initialize
Codex-->>Runtime: initialize result
Runtime->>Codex: initialized
alt 新会话
Runtime->>Codex: thread/start
else 已有会话
Runtime->>Codex: thread/resume(threadId)
end
Codex-->>Runtime: thread id
Runtime-->>React: thread.started
Runtime->>Codex: turn/start
Codex-->>Runtime: item/started(agentMessage)
Runtime-->>React: assistant.started
loop 模型生成文本
Codex-->>Runtime: item/agentMessage/delta
Runtime-->>React: assistant.delta
React->>React: reducer append delta
end
Codex-->>Runtime: item/completed(agentMessage)
Runtime-->>React: assistant.completed(full snapshot)
Codex-->>Runtime: thread/tokenUsage/updated
Codex-->>Runtime: turn/completed
Runtime-->>React: turn.completed
```
这里有两个特别值得记住的设计:
### Delta 用于体验
`assistant.delta` 提供实时输出。
### Completed snapshot 用于校准
最终 `item/completed` 里的完整文本不是多余的。
它可以校准:
- delta 丢失;
- delta 重复;
- snapshot 修订;
- 中间状态异常。
设计原则:
```text
delta = 实时体验
completed snapshot = 最终事实
```
---
## 6. 为什么需要 application-owned `ChatEvent`
这是整个架构最值得迁移到其他项目的设计之一。
如果 UI 直接消费 Codex
```ts
if (event.method === 'item/agentMessage/delta') { ... }
```
那么 UI 已经被 Codex 协议绑死。
现在使用:
```ts
ChatEvent =
| assistant.started
| assistant.delta
| assistant.completed
| activity.started
| activity.updated
| activity.completed
| turn.completed
| error
```
于是关系变成:
```text
Codex protocol
↓ adapter
Application protocol
React UI
```
未来换 runtime
```text
Claude events ─┐
Pi events ├─> ChatEvent ─> UI
Qwen events ─┘
```
UI 不变。
### `ChatEvent` 不是简单 DTO
它承担三个职责:
1. **解耦**:隔离供应商协议;
2. **安全**:只允许白名单数据进入浏览器;
3. **产品语义**:把 Agent 底层事件转换成 UI 真正关心的生命周期。
这比“直接透传原始 event,然后前端自己判断”健壮得多。
---
## 7. 为什么还要有 `ChatStateEvent`
项目里实际上有两个事件层:
```text
ChatEvent -> transport contract
ChatStateEvent -> reducer contract
```
它们不是重复设计。
例如 server 只需要告诉浏览器:
```text
assistant.started(id)
```
而 reducer 创建消息时需要:
```text
createdAt
```
于是 adapter 可以在客户端补产品状态需要的信息:
```text
ChatEvent
↓ toChatStateEvent(receivedAt)
ChatStateEvent
Reducer
```
这遵循一个很重要的原则:
> Transport model、domain model、view state model 不应因为字段看起来相似就强行合并。
---
## 8. Server-only 边界为什么重要
全栈 TypeScript 很容易制造一种错觉:
> “既然都是 TS 文件,直接 import 不就行了吗?”
问题是 browser bundle 一旦导入 runtime 模块,就可能把:
- Node API
- 本地路径;
- 子进程逻辑;
- 协议类型;
- 甚至认证相关实现
带进浏览器构建图。
当前项目的边界是:
```text
chat.ts browser-importable RPC declaration
chat.runtime.server.ts server-only seam
src/server/codex/** server-only runtime
```
`createServerFn` 的价值之一,就是让客户端引用一个“函数形状”,实际执行发生在服务器。
---
## 9. 持久化边界:谁保存什么
当前有两套状态所有者。
### Browser 持久化
只保存:
```text
threadId
messages[]
```
这解决的是产品体验:刷新页面后仍然能看到聊天记录并继续原线程。
### Codex 持久化
Codex 自己维护 thread/session 数据,例如本地 session。
这解决的是 Agent 上下文。
两者不能混为一谈:
```text
Browser transcript ≠ Codex thread state
```
浏览器消息只是 UI 投影,不应该被当成 Agent 的唯一真实上下文。
### New Chat 的语义
当前 New Chat 做的是:
```text
清浏览器 active conversation
```
不是:
```text
删除 Codex 历史 session
```
这是正确的职责分离。
---
## 10. V0 read-only 安全模型
当前策略:
```text
model: luna
effort: high
sandbox: read-only
sandboxPolicy.networkAccess: false
approvalPolicy: never
```
安全思路不是依赖一个开关,而是多层防线。
### 第一层:权限限制
```text
read-only sandbox
network disabled
```
### 第二层:无交互审批
```text
approvalPolicy: never
```
避免 runtime 临时要求更高权限后由 Web UI 放行。
### 第三层:数据最小化
浏览器不接收:
```text
reasoning raw text
command stdout/stderr
MCP arguments/results
auth data
raw runtime event
```
### 第四层:workspace root
工作目录需要 canonicalize,并限制在允许 root 下。
安全原则:
> Agent 的权限控制和 UI 的数据脱敏是两个不同问题,二者都必须做。
只读 sandbox 防止 Agent 修改系统;事件白名单防止敏感信息泄漏到 Browser。
---
## 11. 为什么不直接把 Codex 协议给前端
看起来直接转发 JSON-RPC 最省代码:
```text
Codex -> WebSocket -> Browser
```
但会带来几个长期问题。
| 方案 | 优点 | 代价 |
|---|---|---|
| 原始协议直传 | 开发快、信息完整 | 前端强耦合 Codex、敏感字段难控制、协议升级影响 UI |
| Server normalize | 安全边界清楚、UI 稳定、可换 runtime | server adapter 代码更多 |
| Browser 自己 adapter | server 简单 | 安全与兼容逻辑散落前端,不推荐 |
对于 Agent 产品,推荐:
```text
Raw runtime protocol
Server-side normalization
Stable app protocol
```
因为 runtime event 通常比 UI 所需的数据丰富得多。
---
## 12. 当前关键源码映射
| 关注点 | 文件 | 职责 |
|---|---|---|
| Web RPC | `src/server-functions/chat.ts` | 输入校验、streaming server function、错误收敛 |
| Runtime seam | `src/server-functions/chat.runtime.server.ts` | Web 层进入 Codex 层的唯一桥梁 |
| Streaming bridge | `src/server-functions/chat-stream.ts` | 消费 runtime event 并输出 ChatEvent |
| Event normalization | `src/server-functions/codex-event-normalizer.ts` | Codex event -> application event |
| Runtime contract | `src/server/codex/codex-runtime.ts` | 定义可替换 runtime interface |
| App-server client | `src/server/codex/codex-app-server.server.ts` | 子进程、JSON-RPC、thread/turn/item 生命周期 |
| Workspace policy | `src/server/codex/workspace.server.ts` | 工作目录约束 |
| App transport types | `src/features/chat/chat.types.ts` | Browser-safe ChatEvent contract |
| State adapter | `src/features/chat/chat-event.adapter.ts` | transport event -> reducer event |
| Reducer | `src/features/chat/chat.reducer.ts` | 确定性状态变化 |
| Controller | `src/features/chat/use-chat-controller.ts` | 调 RPC、消费 stream、generation guard、持久化 |
| Storage | `src/features/chat/chat.storage.ts` | localStorage versioned persistence |
| UI | `src/features/chat/components/**` | 纯展示与交互 |
复习源码时推荐按这个顺序:
```text
chat.ts
-> chat-stream.ts
-> codex-event-normalizer.ts
-> codex-runtime.ts
-> codex-app-server.server.ts
-> chat-event.adapter.ts
-> chat.reducer.ts
-> use-chat-controller.ts
```
这样是在顺着数据流读,而不是按目录读。
---
## 13. 当前架构里的几个关键 trade-off
### process-per-turn vs long-lived app-server
当前 `streamTurn()` 每次创建一个 app-server connection。
优点:
- 生命周期简单;
- 故障隔离强;
- turn 结束即可清理;
- V0 易调试。
缺点:
- 重复 initialize
- 多 turn 成本更高;
- interrupt/steer/多并发管理不自然;
- 不适合未来复杂 Agent desktop runtime。
成熟版本通常更适合:
```text
App lifecycle
long-lived AppServerClient
├── Thread A / Turn 1
├── Thread A / Turn 2
└── Thread B / Turn 1
```
### async generator RPC vs SSE/WebSocket
当前使用 TanStack Start async generator。
| 技术 | 适合场景 |
|---|---|
| async generator RPC | 请求-流式响应、类型整合好、当前 Demo 简洁 |
| SSE | 单向服务器推送,协议简单,浏览器原生支持 |
| WebSocket | 双向长期会话、interrupt/steer/approval/实时协作 |
| raw fetch stream | 控制力高,但协议、解析、类型都需要自己维护 |
当前需求主要是:
```text
user request -> server stream response
```
因此 async generator 很合理。
当未来加入:
```text
steer
approval
interrupt
multi-agent events
```
WebSocket 或独立 runtime transport 的价值会提高。
---
## 14. 典型失败场景
### 14.1 Codex 模型不可用
表现:
```text
turn 很快失败
没有 agentMessage delta
```
调试顺序:
1. 先直接测试本机 Codex
2. 验证账号模型权限;
3. 记录 app-server event type,而不是直接猜 UI
4. 检查 `turn/completed` status/error。
### 14.2 浏览器不是流式输出
分层排查:
```text
Codex 是否产生 delta
↓ yes
Normalizer 是否产生 assistant.delta
↓ yes
TanStack stream 是否逐事件到达?
↓ yes
Reducer 是否 append
↓ yes
UI 是否被 memo/render 阻断?
```
不要一开始就在 React 层加“打字机动画”。伪流式会掩盖真正的数据链路问题。
### 14.3 New Chat 后旧回复出现
这是 stale stream 问题。
当前客户端用 generation guard
```text
stream generation != current generation
=> ignore
```
它解决 UI 污染,但不等于 runtime 已取消。
### 14.4 turn 事件串线
未来存在并发 turn 时,不能只看:
```text
message.method === 'turn/completed'
```
应该同时关联:
```text
threadId + turnId
```
否则别的 turn completed 可能误结束当前流。
### 14.5 子进程退出异常
需要区分:
```text
JSON-RPC error
protocol parse error
app-server process exit
stderr diagnostics
user abort
turn failure
```
如果全部变成一个 `Error('failed')`,系统后续会很难观测。
---
## 15. 调试方法:按边界观察,而不是全链路乱打日志
推荐在开发期临时记录结构化信息:
```text
method
threadId
turnId
item.type
item.id
delta.length
status
```
不要记录:
```text
完整 reasoning
command output
MCP payload
auth token
敏感文件内容
```
### 一条标准调试链
```text
1. 本机 codex CLI 是否正常
2. app-server initialize 是否成功
3. thread/start 或 resume 是否成功
4. turn/start 是否返回
5. item/started 是否出现
6. agentMessage delta 是否持续出现
7. item/completed 是否包含最终 snapshot
8. turn/completed 是否正确
9. ChatEvent 是否正确映射
10. reducer 是否按 id 更新
```
这是比“浏览器没显示,先看 React”更专业的定位方式。
---
## 16. 架构演进路线
### Stage 1:当前 V0
```text
read-only
single workspace
single active browser conversation
process-per-turn
streaming assistant
```
### Stage 2:可靠 runtime client
增加:
- `turnId` correlation
- `turn/interrupt`
- 强制进程退出兜底;
- fake app-server integration tests
- 结构化错误类型。
### Stage 3:长期 app-server
```text
one runtime process
multiple threads
multiple turns
```
增加:
- turn registry
- notification routing
- concurrency control
- reconnect/recovery。
### Stage 4:可写 Agent
必须新增:
- approval UI
- permission model
- diff preview
- write sandbox
- destructive action confirmation。
不能简单把:
```text
read-only -> workspace-write
```
当成一个配置切换。
### Stage 5:多 Runtime
保持:
```text
CodexAdapter ─┐
ClaudeAdapter ├─> ChatEvent
PiAdapter ─┘
```
这时今天设计的 application-owned contract 才真正体现价值。
---
## 17. 可迁移的架构原则
这套项目最值得记住的不是某个 Codex API,而是下面这些模式:
1. **Agent runtime 必须放在可信 server boundary 后面。**
2. **供应商协议和产品协议要分开。**
3. **流式 UI 应基于真实 delta,而不是字符串动画。**
4. **delta 是实时状态,completed snapshot 是最终事实。**
5. **Thread、Turn、Item 必须分层理解。**
6. **Transport state 与 UI state 不应该强行共用同一模型。**
7. **权限隔离和数据脱敏是两条不同安全防线。**
8. **持久化必须明确数据所有者:Browser transcript 和 Agent thread 不是同一份状态。**
9. **并发系统必须使用稳定 correlation id,而不是靠事件顺序猜归属。**
10. **调试应该沿系统边界逐层验证。**
---
## 18. 复习检查表
如果可以不看代码回答下面问题,说明架构已经基本掌握:
- [ ] 为什么 `codex app-server` 必须运行在 server-side
- [ ] `thread``turn``item` 分别表示什么?
- [ ] 为什么一个 turn 不等于一个 assistant message
- [ ] `item/agentMessage/delta` 在系统里经过了哪些层?
- [ ] 为什么 `ChatEvent` 不直接复用 Codex 协议类型?
- [ ] `ChatEvent``ChatStateEvent` 为什么要分开?
- [ ] `assistant.completed` 已经有最终文本,为什么还需要 delta?
- [ ] 有 delta 以后为什么仍需要 completed snapshot
- [ ] localStorage 和 Codex session 分别保存什么?
- [ ] New Chat 为什么不应该等同于删除 Codex session
- [ ] read-only sandbox 与浏览器数据脱敏分别解决什么问题?
- [ ] generation guard 能解决什么,不能解决什么?
- [ ] 为什么并发 turn 必须按 `turnId` correlation
- [ ] process-per-turn 的优缺点是什么?
- [ ] 什么时候应该考虑 SSE 或 WebSocket
- [ ] 如果未来替换成 Claude/Pi,哪些层应该变化、哪些层应该保持不变?
---
## 19. 思考题
1. 如果用户同时打开两个浏览器 Tab,两个 Tab resume 同一个 thread,会有哪些竞态?应该在哪一层解决?
2. 如果 `assistant.delta` 已经追加了 200 字,但最终 `assistant.completed.text` 只有 180 字,Reducer 应该怎么处理?为什么?
3. 如果未来允许 Agent 写文件,仅增加 `sandbox: workspace-write` 为什么不够?至少还需要哪些产品能力?
4. 如果 app-server 变成长驻进程,一个 JSON-RPC reader 如何同时服务多个 turn?你需要哪些 registry/correlation 数据结构?
5. 如果换成一个只提供 SSE 的 Agent provider,现有 `ChatEvent` 层还能否保留?哪些 adapter 需要变化?
6. 为什么“浏览器永远不接触 raw runtime event”不仅是解耦设计,也是安全设计?
7. 哪些错误应该展示给用户,哪些错误只应该进入 server log?如何给它们建立稳定 error code?
---
## 20. 最终心智模型
可以把整个系统压缩成一句话:
> **浏览器维护产品状态,TanStack Start 建立可信 RPC 边界,Application Event Contract 隔离产品与供应商协议,CodexRuntime 管理 Agent 生命周期,Codex app-server 负责真正的 thread/turn/item 执行。**
再进一步抽象:
```text
External Agent Runtime
Trusted Adapter
Stable Application Events
Deterministic Client State
UI
```
这才是这个 Demo 最有价值的架构成果。
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+896
View File
@@ -0,0 +1,896 @@
# 端到端总复习:一条用户消息如何穿过整个 Agent 系统
> 这篇不是第五个孤立专题,而是对前四篇的收口。目标是回答一个工程问题:**用户点击发送以后,系统到底发生了什么;每一层为什么存在;哪里最容易出错;哪些模式值得迁移到别的 Agent 产品。**
---
## 1. 先看全链路
```mermaid
sequenceDiagram
participant U as User
participant UI as React UI
participant C as useChatController
participant SF as TanStack Start ServerFn
participant B as Streaming Bridge
participant R as CodexRuntime
participant A as codex app-server
participant S as chatReducer
participant LS as localStorage
U->>UI: 输入 prompt
UI->>C: sendMessage(prompt)
C->>S: user.message.added
C->>S: turn.started
C->>SF: streamChat({message, threadId})
SF->>B: streamNormalizedChatEvents()
B->>R: streamTurn()
R->>A: initialize / initialized
alt 没有 threadId
R->>A: thread/start
else 已有 threadId
R->>A: thread/resume(threadId)
end
A-->>R: canonical thread id
R-->>B: thread.started
B-->>C: ChatEvent(thread.started)
C->>S: event.received
R->>A: turn/start
A-->>R: item/started(agentMessage)
R-->>B: item.started
B-->>C: assistant.started
C->>S: 创建空 assistant message
loop 文本生成
A-->>R: item/agentMessage/delta
R-->>B: assistant delta
B-->>C: assistant.delta
C->>S: append delta
S-->>UI: React re-render
end
A-->>R: item/completed(agentMessage)
B-->>C: assistant.completed(full text)
C->>S: final reconciliation
A-->>R: tokenUsage / turn.completed
B-->>C: turn.completed
C->>S: running -> idle
S-->>LS: persist threadId + transcript
```
这条链路可以压缩成一句话:
```text
用户意图
→ Server RPC
→ Provider Protocol
→ Application Event
→ State Event
→ Deterministic State
→ UI Projection
```
真正稳定的地方不是“Codex 能生成文本”,而是每一次跨边界都发生了**协议收敛**。
---
## 2. 第一阶段:React 不直接“请求答案”,而是启动一个 Turn
`useChatController()` 发送消息时首先做两件本地状态变化:
```text
user.message.added
turn.started
```
这样 UI 不需要等待网络:用户消息立即出现,输入区进入 running 状态。
传统聊天应用容易写成:
```ts
const answer = await request(prompt)
setMessages([...messages, answer])
```
Agent 应用更适合:
```text
start turn
consume events
fold events into state
```
因为一个 turn 期间不仅有文本,还可能有 reasoning activity、command、MCP、file change、usage、interrupt 和 error。
### 可迁移原则
> **Agent UI 的最小单位应该是“Turn 生命周期”,而不是“HTTP response”。**
---
## 3. 第二阶段:`createServerFn` 是信任边界,不只是调用语法糖
浏览器调用:
```text
streamChat({ message, threadId })
```
看起来像本地函数,实际跨越 Browser → Server。
这层至少负责三件事:
1. 输入验证;
2. 隐藏 Node-only Runtime
3. 把 async generator 作为流返回。
关键模块边界:
```text
src/server-functions/chat.ts
浏览器可 import 的 RPC declaration
src/server-functions/chat.runtime.server.ts
server-only integration seam
src/server/codex/**
Node child process / protocol / auth boundary
```
### 为什么不能直接 import Runtime
因为浏览器不应该理解:
```text
child_process
stdio
~/.codex
local workspace path
approval request
raw Codex notification
```
### 可迁移原则
> **全栈 TypeScript 的“同语言”不代表“同信任域”。模块边界必须按运行环境和安全责任划分。**
---
## 4. 第三阶段:Runtime 先建立 Codex 会话,再启动 Turn
Runtime 与 `codex app-server --stdio` 通信。
最小生命周期:
```text
spawn
initialize
initialized
thread/start OR thread/resume
turn/start
notifications
turn/completed / failed
close
```
### Thread
长期对话上下文。
```text
Thread
├── Turn 1
├── Turn 2
└── Turn 3
```
浏览器只持久化 `threadId`,用它在后续请求中 resume。
### Turn
一次用户请求对应的一整轮 Agent 工作。
### Item
Turn 内部工作单元:
```text
reasoning
commandExecution
mcpToolCall
fileChange
agentMessage
...
```
### 最重要的纠正
```text
Thread ≠ Message[]
Turn ≠ Assistant Message
Item ≠ Token
```
如果这三个概念混淆,后续做 interrupt、tool timeline、多 Agent、thread history 时一定会返工。
---
## 5. 第四阶段:真流式来自 Provider 的 Delta,不来自前端动画
实际关键事件:
```text
item/started(agentMessage)
item/agentMessage/delta × N
item/completed(agentMessage)
```
映射为应用协议:
```text
assistant.started
assistant.delta
assistant.completed
```
### 为什么不是“打字机动画”?
伪流式:
```text
服务端先拿到完整文本
→ 前端 setInterval 一字一字显示
```
真实流式:
```text
模型产生 delta
→ Runtime 立即收到
→ ServerFn 立即 yield
→ Browser 立即 dispatch
→ React 立即 render
```
两者用户观感可能相似,但工程语义完全不同。真正 delta streaming 会改善首字延迟,也允许同步展示工具执行和中断状态。
---
## 6. 第五阶段:为什么需要 `ChatEvent`
Codex 的原始通知不应该直接进入 React:
```text
item/agentMessage/delta
thread/tokenUsage/updated
turn/completed
...
```
应用先收敛成:
```text
ChatEvent
```
例如:
```text
assistant.started
assistant.delta
assistant.completed
activity.started
activity.updated
activity.completed
turn.completed
error
```
### `ChatEvent` 同时承担三种职责
**协议防腐层**UI 不绑定 Codex。
**安全白名单**raw reasoning、stdout/stderr、MCP payload 不越界。
**产品语言**UI 看见的是“Assistant 开始/增量/完成”,而不是 Provider 的 item 类型。
### 如果未来换 Provider
```text
Codex ─┐
Claude ├─ Runtime Adapter ─> ChatEvent ─> UI
Pi ─┤
Qwen ─┘
```
产品层不需要跟着底层协议重写。
### 可迁移原则
> **第三方协议应该在服务端边界被翻译成 application-owned contract。**
---
## 7. 第六阶段:Transport Event 还不是 Reducer Event
Browser 收到 `ChatEvent` 后,还经过:
```text
toChatStateEvent()
```
原因是网络协议与状态协议关注点不同。
例如:
```text
assistant.started(id)
```
传输层只需要 id。
而状态层可能需要:
```text
assistant.started(id, createdAt)
```
因此:
```text
Provider Event
Runtime Event
ChatEvent transport semantics
↓ adapter
ChatStateEvent state semantics
↓ reducer
ChatState product projection
```
这不是“层太多”,而是每层拥有不同责任。
---
## 8. 第七阶段:Reducer 是事件折叠器
Assistant 的状态机:
```mermaid
stateDiagram-v2
[*] --> Started: assistant.started
Started --> Streaming: assistant.delta
Streaming --> Streaming: assistant.delta
Started --> Completed: assistant.completed
Streaming --> Completed: assistant.completed
Completed --> [*]
```
Turn 状态机:
```mermaid
stateDiagram-v2
Idle --> Running: turn.started
Running --> Running: assistant/activity event
Running --> Idle: turn.completed
Running --> Error: error
Error --> Running: next user turn
```
### Delta 是增量
```text
old content + event.delta
```
### Completed 是最终权威快照
```text
content = event.text
```
这形成一个非常有价值的可靠性模式:
```text
incremental optimistic projection
+
authoritative final reconciliation
```
中间流负责体验,最终 snapshot 负责正确性。
---
## 9. Reducer purity:为什么时间不能偷偷在 reducer 里生成
理想 reducer
```text
(state, event) -> nextState
```
相同输入必须有相同输出。
不能在内部调用:
```text
Date.now()
Math.random()
localStorage
network
```
否则会破坏:
- 可重复测试;
- event replay
- time-travel debugging
- 并发渲染下的可预测性。
当前代码中的 `Date.now()` fallback 是值得继续修正的工程缺口。正确方向是 controller/adapter 在 reducer 外注入时间。
### 可迁移原则
> **Controller 管副作用,Reducer 管确定性状态转换。**
---
## 10. PersistenceUI Transcript 与 Agent Context 是两套真相
浏览器当前保存:
```text
threadId
messages
```
Codex 自己保存:
```text
thread/session context
```
因此:
```text
Browser transcript ≠ Agent context source of truth
```
浏览器 transcript 是产品投影,Codex thread 是执行上下文。
### 刷新页面
```text
localStorage restore messages
threadId restore
下一条消息 thread/resume(threadId)
```
### New Chat
应该理解为:
```text
clear current UI conversation pointer
```
而不是:
```text
delete Codex historical thread
```
这种边界使 UI 生命周期与 Agent 历史生命周期解耦。
---
## 11. Stale Stream`generationRef` 解决了什么,没有解决什么
用户在旧请求还没完全结束时 New Chat:
```text
Old Turn -> late event -> New Conversation
```
如果不防护,旧 delta 会写入新页面。
当前 generation guard
```text
send 时记录 generation
New Chat -> generation++
旧 stream 收到事件 -> generation 不匹配 -> ignore
```
它解决的是:
```text
stale UI write
```
但没有解决:
```text
后台 turn 继续生成
后台 token 继续消耗
child process 继续运行
```
因此:
```text
generation guard ≠ cancellation
```
下一阶段应该增加真正的 `turn/interrupt` 或 signal propagation。
---
## 12. Correlation:为什么长期必须使用 `threadId + turnId`
一个 long-lived app-server 连接上可能同时存在多个 Thread / Turn。
只判断:
```text
message.method === turn/completed
```
长期不够。
应建立:
```ts
ActiveTurn {
threadId
turnId
}
```
并过滤:
```text
notification.threadId == active.threadId
notification.turnId == active.turnId
```
否则会出现:
```text
Turn B 的 completed
误结束 Turn A 的 stream
```
当前 process-per-turn 架构降低了这个风险,但没有从协议语义上消除它。
### 可迁移原则
> **在异步事件系统里,合法事件不代表属于当前操作。必须显式 correlation。**
---
## 13. Safety:权限控制和数据泄漏是两个问题
当前 V0 安全策略:
```text
sandbox: read-only
sandboxPolicy.networkAccess: false
approvalPolicy: never
```
这限制 Agent 能做什么。
但还需要另一条独立防线:限制 Browser 能看到什么。
不应该进入浏览器:
```text
raw reasoning
command stdout/stderr
MCP arguments/results
auth material
arbitrary local paths
raw Provider events
```
所以:
```text
Execution permission boundary
!=
Data exposure boundary
```
即使 Agent 是 read-only,也仍可能读取敏感文件路径或输出,因此 normalizer 仍然必须做字段白名单和错误收敛。
---
## 14. Process LifecycleV0 与长期形态的 trade-off
当前偏向:
```text
1 turn
→ spawn app-server
→ initialize
→ execute
→ close
```
### 优点
- 隔离简单;
- cleanup 清晰;
- 一个进程只服务一轮,事件串线风险较低;
- Demo 容易验证。
### 缺点
- 每轮重复启动和握手;
- interrupt / steer 较难;
- 多线程复用差;
- 不适合复杂 approval / MCP 生命周期。
长期更合理:
```text
Application
long-lived AppServerClient
├── Thread A / Turn 1
├── Thread A / Turn 2
└── Thread B / Turn 1
```
这时必须升级:
```text
single reader loop
pending request map
notification subscribers
thread/turn correlation
interrupt
server request routing
bounded shutdown
```
---
## 15. 错误路径应该怎么走
正常链路很容易理解,真正决定系统质量的是异常链路。
典型故障:
```text
codex binary 不存在
initialize RPC error
thread resume 失败
turn failed
child process crash
stdout 非法 JSON
stderr 爆量
stream disconnect
用户 New Chat
network/browser disconnect
```
推荐错误边界:
```text
Raw Runtime Error
↓ server log / diagnostics
normalizeCodexRuntimeError
Sanitized application error
ChatEvent.error
Reducer status=error
Browser generic message
```
不要把“方便调试”作为把任意 server error 直接发给 Browser 的理由。开发日志与产品错误是两个输出通道。
---
## 16. 调试一条不流式的消息
不要直接猜 React。沿链路逐层定位。
### 第 1 层:Provider 是否产生 delta
检查真实 App Server 序列:
```text
item/started(agentMessage)
item/agentMessage/delta
item/completed(agentMessage)
```
没有 delta,前端不可能真流式。
### 第 2 层:Runtime 是否保留 delta
确认 `item/agentMessage/delta` 被转换成内部 delta event。
### 第 3 层:Normalizer 是否过滤掉?
确认产生:
```text
assistant.delta
```
### 第 4 层:ServerFn 是否逐事件 yield
不能先收集数组再返回。
### 第 5 层:Browser 是否 `for await` 即时 dispatch
### 第 6 层:Reducer 是否 append 而不是覆盖?
### 第 7 层:UI 是否真的根据 messages render
这套排查顺序适用于几乎所有流式 Agent UI。
---
## 17. 测试应该沿同一条链路分层
```text
Unit
- validation
- normalizer
- reducer
- storage
Integration
- fake app-server
- request/response id
- notification buffering
- turn correlation
- process exit / abort
Smoke
- real codex app-server
- real auth
- real delta sequence
E2E
- user send
- streaming render
- refresh resume
- New Chat stale-stream isolation
```
一个常见错误是用很多 reducer unit test 代替 Runtime integration test。测试总数不是覆盖面的替代品。
---
## 18. 这套项目最值得迁移的 8 个模式
1. **Runtime Adapter**:Provider 协议封装在服务端。
2. **Application-owned Event Contract**UI 不消费第三方事件。
3. **Streaming Async Iterator**:全链路不聚合事件。
4. **Started / Delta / Completed Lifecycle**:把流式内容作为实体生命周期建模。
5. **Final Reconciliation**:最终 snapshot 校准增量状态。
6. **Transport Event / State Event 分层**:网络语义与状态语义解耦。
7. **Generation Guard + 真 Cancellation 分治**:一个保护 UI,一个停止资源。
8. **Permission Boundary + Data Boundary 双防线**:能做什么与能看到什么分别控制。
这八个模式比任何单一 Codex API 更有长期价值。
---
## 19. 什么时候应该换技术方案?
### TanStack ServerFn vs SSE
当前 ServerFn async generator 类型整合简单。如果未来需要跨非 TanStack 客户端、独立 API 网关或标准化 HTTP stream,可考虑 SSE。
### SSE vs WebSocket
只需要 Server → Client 连续事件:SSE 足够。
需要真正双向实时控制:
```text
interrupt
steer
approval
实时 tool interaction
```
WebSocket 或长连接协议会更自然。
### process-per-turn vs long-lived App Server
Demo/单用户/隔离优先:process-per-turn 简单。
桌面 Agent/多会话/interrupt/steerlong-lived client 更合理。
不要为了“更先进”过早复杂化;技术切换应该由交互需求和生命周期要求驱动。
---
## 20. 最终复习题
1. 为什么 `runStreamed()` 有事件不代表一定支持 Assistant 正文真流式?
2. Thread、Turn、Item 分别是谁的生命周期?
3. 为什么 `ChatEvent` 是安全边界,而不仅是类型定义?
4. 为什么还要从 `ChatEvent` 转成 `ChatStateEvent`
5. `assistant.completed` 已有完整文本,为什么前面还要处理 delta?
6. 为什么 reducer 内的 `Date.now()` 是工程问题?
7. `generationRef` 为什么不能替代 `turn/interrupt`
8. 为什么 `threadId` 不能完全替代 `turnId` correlation
9. read-only sandbox 能否保证浏览器不会看到敏感路径?为什么?
10. 如果改成长连接 App Server`AppServerConnection` 最需要增加哪些基础设施?
11. 为什么 Browser transcript 不能作为 Agent context 的唯一 source of truth
12. 一个真实 Agent Runtime 的 integration test 应该模拟哪些消息乱序和故障?
如果能脱离源码准确回答这 12 个问题,就已经掌握了这套项目真正有迁移价值的架构知识。
---
## 21. 一页总结
```text
Browser
只拥有产品状态
TanStack Start
建立可信 Server boundary
ChatEvent
建立应用自己的协议
CodexRuntime
隔离 Provider / process / auth
codex app-server
产生 thread / turn / item / delta
```
返回方向:
```text
Provider notification
→ Runtime normalization
→ ChatEvent
→ ChatStateEvent
→ reducer
→ UI projection
→ local persistence
```
可靠性补充:
```text
correlation
cancellation
process cleanup
final reconciliation
error sanitization
fake transport tests
```
安全补充:
```text
read-only / no network / no approval
+
Browser event whitelist
```
最终原则:
> **不要让 UI 理解 Runtime,不要让第三方协议定义产品状态,不要让流式体验牺牲最终一致性,也不要把“能运行”误认为“生命周期已经正确”。**
+128
View File
@@ -0,0 +1,128 @@
# 专业技术笔记索引
这套笔记用于长期复习 `Codex + TanStack Start` 本地 Agent 架构。它与 `docs/tasks/*``DEVELOPMENT_PLAN.md``VALIDATION.md` 的定位不同:后者记录“项目怎么做、当时做到了什么”,这里回答“为什么这样做、这些知识如何迁移到其他 Agent 工程”。
## 推荐阅读顺序
| 顺序 | 笔记 | 主要问题 | 学习目标 |
|---|---|---|---|
| 1 | [01-system-architecture.md](./01-system-architecture.md) | 系统边界在哪里? | 建立 Browser / TanStack Start / Runtime / Codex 的整体心智模型,理解 thread / turn / item 与 application-owned event contract |
| 2 | [02-codex-app-server-streaming.md](./02-codex-app-server-streaming.md) | 真流式从哪里来? | 掌握 app-server、stdio/JSON-RPC、`item/agentMessage/delta`、进程生命周期、安全策略与协议适配 |
| 3 | [03-tanstack-streaming-state.md](./03-tanstack-streaming-state.md) | 流式事件如何变成稳定 UI? | 掌握 `createServerFn`、async generator、Transport/State contract、reducer 状态机、stale stream 与 persistence |
| 4 | [04-testing-reliability-security.md](./04-testing-reliability-security.md) | “能跑”为什么不等于“可靠”? | 掌握 correlation、abort/interrupt、fake transport、child process、错误收敛、安全边界和故障注入 |
| 5 | [05-end-to-end-review.md](./05-end-to-end-review.md) | 一条消息完整经历了什么? | 把前四篇重新串成端到端模型,并提炼可迁移的工程模式 |
## 先修知识
建议先具备以下基础,再阅读效果最好:
- TypeScriptunion type、type narrowing、async iterator、`AsyncGenerator`
- Reactrender/commit、state snapshot、`useReducer``useRef`、副作用边界;
- Node.js`child_process.spawn`、stdio、process signal
- WebRPC、streaming、SSE/WebSocket 的基本区别;
- Agentconversation/session、tool call、streaming response 的基本概念。
如果只想快速理解项目,从 **01 → 05** 即可;如果要修改 Runtime,必须完整读 **02 + 04**;如果主要维护 Web/UI,重点读 **03 + 05**
## 统一术语
整套笔记统一采用下面的含义:
```text
Thread = Codex 长期会话上下文
Turn = Thread 内一次用户请求对应的一轮 Agent 执行
Item = Turn 内部的工作单元
Delta = Assistant 正文增量
Snapshot = 某个时刻/完成态的完整正文
ChatEvent = Server -> Browser 的应用级传输协议
ChatStateEvent = 进入 reducer 的状态事件
Runtime = 对具体 Agent 后端的适配层
```
事件命名以当前应用协议为准:
```text
thread.started
assistant.started
assistant.delta
assistant.completed
activity.started
activity.updated
activity.completed
turn.completed
error
```
Codex 原始协议只在 Runtime/协议专题中出现,例如:
```text
thread/start
thread/resume
turn/start
item/started
item/agentMessage/delta
item/completed
thread/tokenUsage/updated
turn/completed
```
不要把两组名字混用:前者是应用协议,后者是 Provider 协议。
## 阅读方法
每篇都按相同模板组织:
1. 背景与问题;
2. 心智模型;
3. 生命周期 / 数据流 / 状态机;
4. 当前源码映射;
5. 为什么这样设计;
6. 替代方案与 trade-off
7. 失败模式;
8. 调试方法;
9. 测试策略;
10. 可迁移结论;
11. 复习题。
阅读时不要只记 API。优先回答三个问题:
```text
这层拥有什么状态?
这层信任哪些数据?
这层允许把什么信息交给下一层?
```
这三个问题比记住某个函数名更能帮助你迁移到 Claude、Pi、Qwen、OpenCode 或其他 Runtime。
## 文档分区
### 学习笔记
`docs/notes/*` 是长期维护的知识文档。要求与当前源码一致,并解释设计理由和适用边界。
### 项目记录
以下文件保留为历史/实施记录,不作为系统学习入口:
- `docs/ARCHITECTURE.md`V0 架构摘要;
- `docs/INTEGRATION.md`Task 05 集成记录;
- `docs/DEVELOPMENT_PLAN.md`:五任务开发拆分;
- `docs/VALIDATION.md`:某次集成验证快照;
- `docs/tasks/*`:各开发任务的实施说明。
项目记录可以保留当时的事实,但不应承担“专业知识笔记”的职责。
## 当前源码审阅重点
当前文档已经明确区分“已经实现”和“应继续完善”的内容。阅读时尤其注意:
- 真流式来自 `item/agentMessage/delta`
- `item/completed` 的完整文本用于最终校准;
- 当前安全策略是 read-only、network disabled、approval never
- Browser 不能收到 raw reasoning、command output、MCP 参数/result 或本地认证信息;
- `generationRef` 解决 stale UI write,但不等于真正中断底层 turn;
- Runtime 长期形态应使用 `threadId + turnId` 做 correlation
- reducer 应保持纯函数,时间/随机数/IO 必须在 reducer 外生成;
- process-per-turn 对 V0 简单可靠,但 long-lived app-server 更适合 interrupt、steer、多 thread 和 approval。
这些“缺口”不是文档错误,而是当前实现与下一阶段工程化之间的边界。
+25
View File
@@ -0,0 +1,25 @@
# `docs/tasks` 文档定位
本目录保存项目开发阶段的**任务实施记录**,用于回答:某个阶段负责什么、交付了哪些模块、当时有哪些约束。
它不是长期学习笔记目录。专业技术学习入口请使用:
- [`../notes/README.md`](../notes/README.md) — 总索引与阅读路线;
- [`../notes/01-system-architecture.md`](../notes/01-system-architecture.md) — 系统架构;
- [`../notes/02-codex-app-server-streaming.md`](../notes/02-codex-app-server-streaming.md) — Codex App Server 与真实流式;
- [`../notes/03-tanstack-streaming-state.md`](../notes/03-tanstack-streaming-state.md) — TanStack Start 与 React 状态;
- [`../notes/04-testing-reliability-security.md`](../notes/04-testing-reliability-security.md) — 测试、可靠性、安全;
- [`../notes/05-end-to-end-review.md`](../notes/05-end-to-end-review.md) — 端到端总复习。
## 本目录保留的文件
| 文件 | 定位 |
|---|---|
| `01-codex-runtime.md` | Runtime 开发任务说明 |
| `02-streaming-bridge.md` | Streaming bridge 开发任务说明 |
| `02-integration-notes.md` | Task 02 集成过程记录 |
| `03-chat-ui.md` | UI 开发任务说明 |
| `04-state-persistence-tests.md` | State / persistence / tests 开发任务说明 |
| `05-integration.md` | 最终集成任务说明 |
这些文件可以保留历史事实,但后续不要继续向其中堆叠概念教程。新的原理性内容应优先写入 `docs/notes/`,并从任务记录链接过去。