mirror of
https://github.com/Narcooo/inkos.git
synced 2026-08-31 01:42:58 +08:00
chore: remove local planning docs from pr
This commit is contained in:
@@ -1,52 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
InkOS 专业 agent 设计采用渐进式披露:先读 `docs/agents/agent-extraction-guide.md`,再按需深入 `docs/agents/<AgentName>.md`。
|
||||
|
||||
默认使用中文回答问题,并使用中文撰写计划和 SPEC 文件。
|
||||
|
||||
InkOS pi-agent 持久化模块设计在 `docs/superpowers/specs/2026-04-27-pi-agent-jsonl-persistence-design.md`,需要了解 JSONL transcript、requestId、toolResult、thinking、cache 和 legacy migration 时先读该文件。
|
||||
|
||||
## 回答结构规范
|
||||
|
||||
回答技术问题时要体现清晰的思考过程,而非堆叠语义相近的描述。
|
||||
|
||||
1. **先抽象分类**:先把问题归纳成少数几个大类,说明每一类解决什么核心矛盾。
|
||||
2. **再建立推演链路**:按“问题背景 -> 关键数据结构 -> 执行步骤 -> 结果影响”的顺序解释机制。
|
||||
3. **用证据支撑判断**:涉及源码时给出关键函数、字段、调用链或文件位置,让结论可以被验证。
|
||||
4. **解释因果关系**:说明某个设计为什么能产生对应结果,以及缺少这个设计会触发什么失败场景。
|
||||
5. **避免空泛对比**:不要用简单二分对比替代解释;对比只能作为结论,不能作为证明过程。
|
||||
6. **控制信息密度**:每一层只放支撑当前结论所需的信息,细节服务于推理,不做无目的罗列。
|
||||
|
||||
## TypeScript 代码实践
|
||||
|
||||
回答或改写 TypeScript 代码时,先说明数据如何从“不可信输入”变成“可信领域对象”。每个判断都要回答四件事:它消除什么非法状态;由哪个类型、schema 或函数保证;下游因此获得什么前置条件;缺少它会触发什么失败场景。
|
||||
|
||||
### 1. 核心分类
|
||||
|
||||
1. **边界收敛**:外部输入不可信。文件、网络、JSON、数据库、第三方库返回值可以先是 `unknown`,但必须在模块边界通过 schema、parser 或 type guard 收窄。
|
||||
2. **领域建模**:业务状态要可区分。字段集合随 `role`、`type`、`kind` 改变时,用 discriminated union,不用 optional 字段堆出一个宽接口。
|
||||
3. **合法状态约束落地**:系统声称某份数据合法时必须满足的规则要有归属。单个对象自身就能判断的规则,放进类型或 schema;必须结合事件顺序、上下文或外部状态才能判断的规则,放进命名清楚的校验、转换或清理函数。
|
||||
4. **演进验证**:新增分支不能静默漏处理。union 要穷尽处理,非法状态要有测试覆盖。
|
||||
|
||||
### 2. 推演链路
|
||||
|
||||
解释 TypeScript 代码时按这条链路展开:
|
||||
|
||||
```text
|
||||
问题背景:哪些输入不可信,或哪些业务状态容易混在一起。
|
||||
关键数据结构:raw input、validated event、domain message、cleaned message 分别是什么。
|
||||
执行步骤:哪个函数负责读取,哪个函数负责收窄,哪个函数负责合法状态校验,哪个函数返回可信结果。
|
||||
结果影响:下游少做哪些重复判断;缺少该设计时,非法状态会在哪里爆炸。
|
||||
```
|
||||
|
||||
例如 transcript 恢复:`readTranscriptEvents()` 读取并解析事件,`committedMessageEvents()` 排除未提交请求,`cleanRestoredAgentMessages()` 清理孤立 `toolResult`、空 assistant 和非法 trailing thinking。推理重点不是“调用了这些函数”,而是这些函数分别阻断了哪些非法历史进入模型上下文。
|
||||
|
||||
### 3. 代码准则
|
||||
|
||||
1. 宽类型止于边界:`unknown`、`any`、`Record<string, unknown>` 不进入核心业务流程。
|
||||
2. 互斥状态用 union:不要用大量 optional 字段模拟状态机。
|
||||
3. 合法状态约束集中实现:不要把“必须 committed”“必须有对应 toolCall”“assistant 不能为空”散落成临时 `if`。
|
||||
4. 类型断言要有证据:`as SomeType` 前面必须有 schema、parser 或 guard 支撑。
|
||||
5. 分支处理要穷尽:用 `switch`、明确窄化和 `assertNever` 暴露新增分支。
|
||||
6. 函数名表达可信度:`parse*` 表示可能失败,`normalize*` 表示转换形状,`clean*` 表示执行合法性修复。
|
||||
7. 测试覆盖非法状态:恢复、迁移、IO、LLM message、tool loop 必须测坏数据、缺字段、未提交请求、孤立 `toolResult`、空 assistant、trailing thinking。
|
||||
@@ -1,630 +0,0 @@
|
||||
# Agent Native Google 与模型切换上下文修复计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
## 目标
|
||||
|
||||
修复 InkOS pi-agent 在 Google Gemini / DeepSeek / MiniMax 等模型之间切换时的上下文污染、工具调用失败和错误信息失真问题。
|
||||
|
||||
## 执行记录
|
||||
|
||||
2026-04-27 已执行:
|
||||
|
||||
- Google service 默认调用协议已切到 pi-ai native `google-generative-ai`。
|
||||
- Agent cache identity 已从 `modelId` 升级为 `api + provider + baseUrl + modelId`。
|
||||
- JSONL transcript 继续保存 raw `AgentMessage`;恢复时按目标模型协议投影上下文。
|
||||
- 同模型同协议保留 native thinking/tool history;跨模型/跨协议把旧工具结果文本化为普通 user 语义上下文。
|
||||
- Studio `/api/v1/agent` 已优先透传 final assistant error,避免被 fallback probe 改写成 `LLM returned empty response`。
|
||||
|
||||
验证结果:
|
||||
|
||||
- `pnpm --filter @actalk/inkos-core exec vitest run src/__tests__/service-resolver.test.ts src/__tests__/provider.test.ts src/__tests__/effective-llm-config.test.ts src/__tests__/config-loader.test.ts` 通过。
|
||||
- `pnpm --filter @actalk/inkos-core exec vitest run src/__tests__/session-transcript-restore.test.ts src/__tests__/agent-session.test.ts` 通过。
|
||||
- `pnpm --filter @actalk/inkos-studio exec vitest run src/api/server.test.ts` 通过。
|
||||
- `pnpm --filter @actalk/inkos-core typecheck` 通过。
|
||||
- `pnpm --filter @actalk/inkos-studio typecheck` 通过。
|
||||
- `git diff --check` 通过。
|
||||
- 真实 `createLLMClient + chatCompletion` 调 `google/gemini-pro-latest` 三次,3/3 HTTP success,3/3 包含 `【完整结束】`。
|
||||
- 真实 Studio agent + Gemini 工具调用普通提示三次,3/3 HTTP 200,未出现 XML `<function-calls>` 泄漏,JSONL 中 assistant tool call 为 `api: "google-generative-ai"` / `provider: "google"`。
|
||||
- 严格“最终回复必须完全等于固定字符串”的工具提示下,观测到 1 次 native Google provider 返回 `An unknown error occurred`;transcript 显示这是 pi-ai Google provider 在 `stopReason: "error"` 且无内容时给出的 generic upstream error,不是 JSONL 恢复污染,也不是 OpenAI-compatible `MALFORMED_FUNCTION_CALL`。
|
||||
|
||||
根因分两层:
|
||||
|
||||
1. **Google 传输协议选错**:pi-ai 已有 native `google-generative-ai` provider,但 InkOS 当前 `google` service 走 `openai-completions + https://generativelanguage.googleapis.com/v1beta/openai`。这会让 Gemini 工具调用落在 OpenAI-compatible 适配层,现场已复现 `function_call_filter: MALFORMED_FUNCTION_CALL`、XML `<function-calls>` 文本泄漏和空 assistant。
|
||||
2. **Agent cache identity 太窄**:当前 cache 只比较 `modelId`。同一个 `gemini-pro-latest` 从 OpenAI-compatible 切到 native Google 时,模型 id 不变,但 `api/provider/baseUrl` 已经变了,继续复用旧 Agent 会保留错误协议的内存态。
|
||||
|
||||
最终状态:
|
||||
|
||||
- Google service 默认走 pi-ai native `google-generative-ai`。
|
||||
- Agent cache 使用完整模型协议身份:`api + provider + baseUrl + modelId`。
|
||||
- JSONL transcript 继续保存 raw `AgentMessage`。
|
||||
- 恢复上下文时,同一完整模型协议身份保留原生 history;跨协议/跨模型只保留语义文本。
|
||||
- OpenAI-compatible Gemini 只作为 legacy/custom fallback,不再作为 InkOS Google 默认路径。
|
||||
- Studio API 透传 pi-agent-core / pi-ai 的真实上游错误,不再全部折叠成 `LLM returned empty response`。
|
||||
|
||||
## 架构边界
|
||||
|
||||
本计划只处理 Studio agent chat 这条链路:
|
||||
|
||||
`Studio /api/v1/agent -> resolveServiceModel/createLLMClient -> runAgentSession -> pi-agent-core Agent -> pi-ai streamSimple -> JSONL transcript`
|
||||
|
||||
不处理专业 agent pipeline 的写作质量策略,不调整章节生成的业务 prompt。
|
||||
|
||||
## 关键设计
|
||||
|
||||
### 1. Google Provider 默认 native
|
||||
|
||||
当前事实:
|
||||
|
||||
- pi-ai `@mariozechner/pi-ai` 已注册 `api: "google-generative-ai"`。
|
||||
- `getModel("google", "gemini-2.5-flash")` 返回:
|
||||
- `api: "google-generative-ai"`
|
||||
- `provider: "google"`
|
||||
- `baseUrl: "https://generativelanguage.googleapis.com/v1beta"`
|
||||
- InkOS 当前 `packages/core/src/llm/providers/endpoints/google.ts` 仍配置为 `openai-completions` 和 `/v1beta/openai`。
|
||||
|
||||
修改原则:
|
||||
|
||||
- `google` endpoint 的主调用协议改为 native `google-generative-ai`。
|
||||
- `modelsBaseUrl` 可以继续指向 OpenAI-compatible `/v1beta/openai`,只服务模型列表探测;模型调用必须走 native baseUrl。
|
||||
- 不新增 `maxTokensField` 之类 OpenAI-compatible 参数。`maxTokens` 仍由 model card 的 `maxOutput` 决定。
|
||||
|
||||
### 2. Cache Identity 从 modelId 升级为模型协议身份
|
||||
|
||||
旧规则:
|
||||
|
||||
```ts
|
||||
cached.modelId !== requestedModelId
|
||||
```
|
||||
|
||||
新规则:
|
||||
|
||||
```ts
|
||||
agentModelIdentity(model) =
|
||||
`${model.api}::${model.provider}::${model.baseUrl ?? ""}::${model.id}`
|
||||
```
|
||||
|
||||
推导:
|
||||
|
||||
1. `modelId` 只描述模型名称。
|
||||
2. agent 的 LLM 调用行为由 `api/provider/baseUrl/modelId` 共同决定。
|
||||
3. Google 从 OpenAI-compatible 切到 native 后,`modelId` 不变,`api/baseUrl/provider` 改变。
|
||||
4. cache 比较必须覆盖所有影响 LLM message schema 的字段。
|
||||
|
||||
### 3. Transcript 恢复只在同协议身份下保留原生状态
|
||||
|
||||
保留规则:
|
||||
|
||||
- `assistant.api === target.api`
|
||||
- `assistant.provider === target.provider`
|
||||
- `assistant.model === target.id`
|
||||
|
||||
跨协议/跨模型规则:
|
||||
|
||||
- `user` 文本保留。
|
||||
- assistant 可见 `text` 保留。
|
||||
- `thinking` 丢弃,尤其是 DeepSeek `reasoning_content` 和 Gemini thought signature。
|
||||
- `toolCall + toolResult` 合并为普通 `user` 上下文文本,例如 `[Tool results] read(tool-1): ...`。
|
||||
- synthetic bridge 文本不进入最终 LLM context。
|
||||
|
||||
原因:
|
||||
|
||||
1. thinking / reasoning signature 是 provider 协议状态,不是可跨 provider 携带的语义。
|
||||
2. tool call id 只在对应 assistant tool call 的协议回合里有意义。
|
||||
3. 跨模型恢复的目标是保存“模型已经看过哪些工具结果”的语义,不恢复旧协议的 pending state。
|
||||
|
||||
### 4. OpenAI-compatible Gemini 只保留 legacy 防线
|
||||
|
||||
`convertAgentMessagesForModel()` 对 `baseUrl.includes("generativelanguage.googleapis.com") && api === "openai-completions"` 的文本化逻辑可以保留,但它是 fallback:
|
||||
|
||||
- 用于旧 transcript、custom provider 或临时回滚。
|
||||
- native Google 不应该触发这层文本化。
|
||||
- 真实工具调用由 pi-ai Google provider 的 `part.functionCall -> toolCall` 处理。
|
||||
|
||||
## 文件结构
|
||||
|
||||
- Modify: `packages/core/src/llm/providers/types.ts`
|
||||
- `ApiProtocol` 增加 `google-generative-ai`。
|
||||
- Modify: `packages/core/src/llm/providers/endpoints/google.ts`
|
||||
- Google 主协议切到 native。
|
||||
- 保留 `modelsBaseUrl` 或测试中明确 fallback 行为。
|
||||
- Modify: `packages/core/src/llm/service-presets.ts`
|
||||
- `resolveServicePiProvider("google")` 返回 `google`。
|
||||
- Modify: `packages/core/src/llm/service-resolver.ts`
|
||||
- `resolveServiceModel("google", ...)` 产出 native `Model<Api>`。
|
||||
- OpenAI-compatible compat 只在 `api === "openai-completions"` 时注入。
|
||||
- Modify: `packages/core/src/llm/provider.ts`
|
||||
- `createLLMClient()` 构造的 Google `_piModel` 使用 native `api/provider/baseUrl`。
|
||||
- Modify: `packages/core/src/agent/agent-session.ts`
|
||||
- cache identity 使用完整模型协议身份。
|
||||
- legacy Gemini OpenAI-compatible `convertToLlm` 只作为 fallback。
|
||||
- final assistant error 继续透传。
|
||||
- Modify: `packages/core/src/interaction/session-transcript-restore.ts`
|
||||
- 按目标模型身份恢复 raw transcript。
|
||||
- Modify: `packages/studio/src/api/server.ts`
|
||||
- `/api/v1/agent` 使用 resolved native Google model。
|
||||
- 保留真实错误返回。
|
||||
- Tests:
|
||||
- `packages/core/src/__tests__/service-resolver.test.ts`
|
||||
- `packages/core/src/__tests__/provider.test.ts`
|
||||
- `packages/core/src/__tests__/effective-llm-config.test.ts`
|
||||
- `packages/core/src/__tests__/config-loader.test.ts`
|
||||
- `packages/core/src/__tests__/agent-session.test.ts`
|
||||
- `packages/core/src/__tests__/session-transcript-restore.test.ts`
|
||||
- `packages/studio/src/api/server.test.ts`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 用测试锁定 Google native provider
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/core/src/__tests__/service-resolver.test.ts`
|
||||
- Modify: `packages/core/src/__tests__/provider.test.ts`
|
||||
- Modify: `packages/core/src/__tests__/effective-llm-config.test.ts`
|
||||
- Modify: `packages/core/src/__tests__/config-loader.test.ts`
|
||||
|
||||
- [ ] **Step 1: service resolver 测试 Google native identity**
|
||||
|
||||
更新 `resolveServiceModel("google", "gemini-pro-latest", root)` 断言:
|
||||
|
||||
```ts
|
||||
expect(result.model.api).toBe("google-generative-ai");
|
||||
expect(result.model.provider).toBe("google");
|
||||
expect(result.model.baseUrl).toBe("https://generativelanguage.googleapis.com/v1beta");
|
||||
```
|
||||
|
||||
同时断言不再带 OpenAI-compatible compat:
|
||||
|
||||
```ts
|
||||
expect(result.model.compat).toBeUndefined();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: provider client 测试 Google native `_piModel`**
|
||||
|
||||
在 `provider.test.ts` 覆盖:
|
||||
|
||||
```ts
|
||||
const client = createLLMClient({
|
||||
service: "google",
|
||||
provider: "openai",
|
||||
model: "gemini-pro-latest",
|
||||
apiKey: "test",
|
||||
});
|
||||
|
||||
expect(client._piModel?.api).toBe("google-generative-ai");
|
||||
expect(client._piModel?.provider).toBe("google");
|
||||
expect(client._piModel?.baseUrl).toBe("https://generativelanguage.googleapis.com/v1beta");
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 更新配置类测试的 baseUrl 期望**
|
||||
|
||||
把 `effective-llm-config.test.ts` 和 `config-loader.test.ts` 里 Google service 的默认 baseUrl 期望从:
|
||||
|
||||
```ts
|
||||
https://generativelanguage.googleapis.com/v1beta/openai
|
||||
```
|
||||
|
||||
改为:
|
||||
|
||||
```ts
|
||||
https://generativelanguage.googleapis.com/v1beta
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行失败测试**
|
||||
|
||||
```bash
|
||||
pnpm --filter @actalk/inkos-core exec vitest run \
|
||||
src/__tests__/service-resolver.test.ts \
|
||||
src/__tests__/provider.test.ts \
|
||||
src/__tests__/effective-llm-config.test.ts \
|
||||
src/__tests__/config-loader.test.ts
|
||||
```
|
||||
|
||||
Expected before implementation: Google 相关断言失败。
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 实现 Google native provider
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/core/src/llm/providers/types.ts`
|
||||
- Modify: `packages/core/src/llm/providers/endpoints/google.ts`
|
||||
- Modify: `packages/core/src/llm/service-presets.ts`
|
||||
- Modify: `packages/core/src/llm/service-resolver.ts`
|
||||
- Modify: `packages/core/src/llm/provider.ts`
|
||||
|
||||
- [ ] **Step 1: 扩展 InkOS provider schema**
|
||||
|
||||
在 `ApiProtocol` 增加:
|
||||
|
||||
```ts
|
||||
| "google-generative-ai"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改 Google endpoint**
|
||||
|
||||
把 `GOOGLE` endpoint 改为:
|
||||
|
||||
```ts
|
||||
api: "google-generative-ai",
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
modelsBaseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
||||
```
|
||||
|
||||
删除 Google endpoint 上 OpenAI-compatible 专用的:
|
||||
|
||||
```ts
|
||||
compat: { supportsStore: false, requiresAssistantAfterToolResult: true }
|
||||
```
|
||||
|
||||
该 compat 只属于 `/v1beta/openai`,不属于 native Google。
|
||||
|
||||
- [ ] **Step 3: 修正 service -> pi provider 映射**
|
||||
|
||||
`resolveServicePiProvider("google")` 必须返回:
|
||||
|
||||
```ts
|
||||
"google"
|
||||
```
|
||||
|
||||
`SERVICE_TO_PI_PROVIDER` 也要包含:
|
||||
|
||||
```ts
|
||||
google: "google"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 修正 `createLLMClient()` 的 pi provider 选择**
|
||||
|
||||
`packages/core/src/llm/provider.ts` 当前默认会把非特殊 provider 映射成 `openai`。增加 Google 分支:
|
||||
|
||||
```ts
|
||||
if (inkosProvider?.id === "google") piProvider = "google";
|
||||
```
|
||||
|
||||
该分支必须早于默认 `else piProvider = provider`。
|
||||
|
||||
- [ ] **Step 5: 修正 `resolveServiceModel()` 的 model 合成**
|
||||
|
||||
`resolveServiceModel()` 对 Google 应优先使用:
|
||||
|
||||
```ts
|
||||
getModel("google", modelId)
|
||||
```
|
||||
|
||||
如果 pi-ai 内置 model 能命中,保留它的 `reasoning/input/contextWindow/maxTokens`,再覆盖 `id/name/baseUrl` 以匹配 InkOS provider bank。
|
||||
|
||||
- [ ] **Step 6: 运行 Task 1 测试**
|
||||
|
||||
```bash
|
||||
pnpm --filter @actalk/inkos-core exec vitest run \
|
||||
src/__tests__/service-resolver.test.ts \
|
||||
src/__tests__/provider.test.ts \
|
||||
src/__tests__/effective-llm-config.test.ts \
|
||||
src/__tests__/config-loader.test.ts
|
||||
```
|
||||
|
||||
Expected: pass。
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add \
|
||||
packages/core/src/llm/providers/types.ts \
|
||||
packages/core/src/llm/providers/endpoints/google.ts \
|
||||
packages/core/src/llm/service-presets.ts \
|
||||
packages/core/src/llm/service-resolver.ts \
|
||||
packages/core/src/llm/provider.ts \
|
||||
packages/core/src/__tests__/service-resolver.test.ts \
|
||||
packages/core/src/__tests__/provider.test.ts \
|
||||
packages/core/src/__tests__/effective-llm-config.test.ts \
|
||||
packages/core/src/__tests__/config-loader.test.ts
|
||||
git commit -m "fix: use native google provider"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 升级 Agent cache identity
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/core/src/agent/agent-session.ts`
|
||||
- Modify: `packages/core/src/__tests__/agent-session.test.ts`
|
||||
|
||||
- [ ] **Step 1: 写 cache identity 回归测试**
|
||||
|
||||
覆盖三种情况:
|
||||
|
||||
1. 同 `sessionId + bookId + readPermission + api/provider/baseUrl/modelId`:复用 Agent。
|
||||
2. 同 `modelId` 但 `api` 从 `openai-completions` 变为 `google-generative-ai`:重建 Agent。
|
||||
3. 同 `modelId/api` 但 `baseUrl` 改变:重建 Agent。
|
||||
|
||||
断言方式:
|
||||
|
||||
- 用 mock `streamSimple` 记录 `context.model` 或构造时状态。
|
||||
- 或检查 `streamCalls` 中第二次请求没有继承第一次 Agent 的旧 messages。
|
||||
|
||||
- [ ] **Step 2: 实现 `agentModelIdentity()`**
|
||||
|
||||
在 `agent-session.ts` 中增加局部函数:
|
||||
|
||||
```ts
|
||||
function agentModelIdentity(model: Model<Api>): string {
|
||||
return [
|
||||
model.api,
|
||||
model.provider,
|
||||
model.baseUrl ?? "",
|
||||
model.id,
|
||||
].join("::");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: cache value 保存完整 identity**
|
||||
|
||||
把 `CachedAgent` 中的:
|
||||
|
||||
```ts
|
||||
modelId: string | undefined;
|
||||
```
|
||||
|
||||
替换为:
|
||||
|
||||
```ts
|
||||
modelIdentity: string;
|
||||
```
|
||||
|
||||
创建 Agent 前先解析本次 `model`,用 `agentModelIdentity(model)` 比较 cache。
|
||||
|
||||
- [ ] **Step 4: 保留原有 book/readPermission 失效规则**
|
||||
|
||||
cache 失效条件为:
|
||||
|
||||
```ts
|
||||
modelIdentityChanged || bookChanged || readPermissionChanged
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 运行 agent-session 测试**
|
||||
|
||||
```bash
|
||||
pnpm --filter @actalk/inkos-core exec vitest run src/__tests__/agent-session.test.ts
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/core/src/agent/agent-session.ts packages/core/src/__tests__/agent-session.test.ts
|
||||
git commit -m "fix: key agent cache by model protocol"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 保留 raw JSONL,并按目标协议恢复上下文
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/core/src/interaction/session-transcript-restore.ts`
|
||||
- Modify: `packages/core/src/__tests__/session-transcript-restore.test.ts`
|
||||
- Modify: `packages/core/src/agent/agent-session.ts`
|
||||
- Modify: `packages/core/src/__tests__/agent-session.test.ts`
|
||||
|
||||
- [ ] **Step 1: 测同协议保留 native Google tool/thought**
|
||||
|
||||
构造 assistant:
|
||||
|
||||
```ts
|
||||
{
|
||||
role: "assistant",
|
||||
api: "google-generative-ai",
|
||||
provider: "google",
|
||||
model: "gemini-pro-latest",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "plan", thinkingSignature: "google-signature" },
|
||||
{ type: "toolCall", id: "tool-1", name: "ls", arguments: { subdir: "story/roles" } },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
目标模型同为 `api/provider/id` 时,恢复结果必须保留原始 blocks。
|
||||
|
||||
- [ ] **Step 2: 测跨协议文本化**
|
||||
|
||||
用旧 OpenAI-compatible Gemini assistant:
|
||||
|
||||
```ts
|
||||
api: "openai-completions",
|
||||
provider: "openai",
|
||||
model: "gemini-pro-latest",
|
||||
content: [{ type: "toolCall", id: "tool-1", name: "ls", arguments: {} }]
|
||||
```
|
||||
|
||||
目标模型是 native Google 或 DeepSeek 时:
|
||||
|
||||
- 不出现 `toolCall`。
|
||||
- 不出现 `toolResult` role。
|
||||
- 出现 `[Tool results]` user 文本。
|
||||
|
||||
- [ ] **Step 3: 测 DeepSeek reasoning 不跨到 Google**
|
||||
|
||||
DeepSeek 历史中 `thinkingSignature: "reasoning_content"`,目标模型为 native Google 时:
|
||||
|
||||
- `reasoning_content` 不进入目标 context。
|
||||
- assistant 可见 text 保留。
|
||||
- 工具结果文本化。
|
||||
|
||||
- [ ] **Step 4: 实现恢复投影**
|
||||
|
||||
恢复规则:
|
||||
|
||||
- `isSameAssistantModel(message, target)` 只比较 `api/provider/model`。
|
||||
- 同模型:保留 raw assistant/toolResult。
|
||||
- 异模型:丢弃 thinking,保留 text,工具结果合并为 user 文本。
|
||||
- synthetic bridge 文本过滤。
|
||||
|
||||
注意:不要把只服务单一线性流程的步骤拆成多个无复用价值的顶层 helper;保持函数内局部 helper。
|
||||
|
||||
- [ ] **Step 5: legacy OpenAI-compatible Gemini runtime projection 只作为 fallback**
|
||||
|
||||
`agent-session.ts` 中如果保留 `convertAgentMessagesForModel()`,触发条件必须严格为:
|
||||
|
||||
```ts
|
||||
model.api === "openai-completions" &&
|
||||
model.baseUrl?.includes("generativelanguage.googleapis.com")
|
||||
```
|
||||
|
||||
native Google 不触发该分支。
|
||||
|
||||
- [ ] **Step 6: 运行恢复与 agent 测试**
|
||||
|
||||
```bash
|
||||
pnpm --filter @actalk/inkos-core exec vitest run \
|
||||
src/__tests__/session-transcript-restore.test.ts \
|
||||
src/__tests__/agent-session.test.ts
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add \
|
||||
packages/core/src/interaction/session-transcript-restore.ts \
|
||||
packages/core/src/__tests__/session-transcript-restore.test.ts \
|
||||
packages/core/src/agent/agent-session.ts \
|
||||
packages/core/src/__tests__/agent-session.test.ts
|
||||
git commit -m "fix: project restored history by model protocol"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 真实错误透传
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/core/src/agent/agent-session.ts`
|
||||
- Modify: `packages/core/src/__tests__/agent-session.test.ts`
|
||||
- Modify: `packages/studio/src/api/server.ts`
|
||||
- Modify: `packages/studio/src/api/server.test.ts`
|
||||
|
||||
- [ ] **Step 1: `AgentSessionResult` 保留 final assistant error**
|
||||
|
||||
如果最终 assistant:
|
||||
|
||||
```ts
|
||||
stopReason === "error" || stopReason === "aborted"
|
||||
```
|
||||
|
||||
且带 `errorMessage`,`runAgentSession()` 返回:
|
||||
|
||||
```ts
|
||||
errorMessage
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Studio API 优先返回真实错误**
|
||||
|
||||
`/api/v1/agent` 中,如果:
|
||||
|
||||
```ts
|
||||
!result.responseText && result.errorMessage
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```ts
|
||||
status 502
|
||||
error.code = "AGENT_LLM_ERROR"
|
||||
response = result.errorMessage
|
||||
```
|
||||
|
||||
该分支必须早于 fallback probe 和 `LLM returned empty response`。
|
||||
|
||||
- [ ] **Step 3: 覆盖真实错误测试**
|
||||
|
||||
测试至少覆盖:
|
||||
|
||||
- `400 The reasoning_content ... must be passed back`
|
||||
- `Provider finish_reason: function_call_filter: MALFORMED_FUNCTION_CALL`
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add \
|
||||
packages/core/src/agent/agent-session.ts \
|
||||
packages/core/src/__tests__/agent-session.test.ts \
|
||||
packages/studio/src/api/server.ts \
|
||||
packages/studio/src/api/server.test.ts
|
||||
git commit -m "fix: surface agent upstream errors"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 验证真实 Google 场景
|
||||
|
||||
**Files:**
|
||||
- No code changes expected.
|
||||
|
||||
- [ ] **Step 1: 单纯 Google 文本流三次**
|
||||
|
||||
使用 InkOS `createLLMClient + chatCompletion` 调 `google/gemini-pro-latest` 三次,提示词要求输出结束标记:
|
||||
|
||||
```text
|
||||
【完整结束】
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 3/3 HTTP success。
|
||||
- 3/3 包含结束标记。
|
||||
- 不出现 `LLM returned empty response from stream`。
|
||||
|
||||
- [ ] **Step 2: Studio agent + Gemini 工具调用三次**
|
||||
|
||||
对新 session 发送三次:
|
||||
|
||||
```text
|
||||
请调用工具查看当前书的 story/roles 目录。工具调用完成后,只回复:Google agent 测试通过。【完整结束】
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 3/3 HTTP 200。
|
||||
- 3/3 包含结束标记。
|
||||
- 不出现 XML `<function-calls>`。
|
||||
- 不出现 `MALFORMED_FUNCTION_CALL`。
|
||||
- JSONL 中 assistant tool call 的 `api` 为 `google-generative-ai`,`provider` 为 `google`。
|
||||
|
||||
- [ ] **Step 3: 污染 transcript 恢复验证**
|
||||
|
||||
用旧 session `1777003372863-8uq0sb` 或构造等价 fixture,包含:
|
||||
|
||||
- 旧 Gemini OpenAI-compatible `toolCall/toolResult`
|
||||
- DeepSeek `reasoning_content`
|
||||
- 空 error assistant
|
||||
- XML `<function-calls>` 文本
|
||||
|
||||
分别切到:
|
||||
|
||||
- native Google
|
||||
- DeepSeek
|
||||
|
||||
Expected:
|
||||
|
||||
- 请求不因为旧 thinking/tool protocol 400。
|
||||
- 恢复 context 中跨协议工具结果是普通 user 文本。
|
||||
- 旧 XML 文本不被当成可执行 tool call。
|
||||
|
||||
- [ ] **Step 4: 类型检查和 diff 检查**
|
||||
|
||||
```bash
|
||||
pnpm --filter @actalk/inkos-core typecheck
|
||||
pnpm --filter @actalk/inkos-studio typecheck
|
||||
git diff --check
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-Review Checklist
|
||||
|
||||
- [ ] `google` service 默认 `api` 是 `google-generative-ai`。
|
||||
- [ ] `google` service 默认 `provider` 是 `google`。
|
||||
- [ ] `google` service 默认调用 baseUrl 是 `https://generativelanguage.googleapis.com/v1beta`。
|
||||
- [ ] OpenAI-compatible `/v1beta/openai` 不再用于默认 Gemini agent 调用。
|
||||
- [ ] Agent cache identity 覆盖 `api/provider/baseUrl/modelId`。
|
||||
- [ ] 同协议同模型恢复保留 native thinking/tool history。
|
||||
- [ ] 跨协议恢复不传递 DeepSeek `reasoning_content` 或 Gemini thought signature。
|
||||
- [ ] 跨协议工具结果降级为普通 user 语义文本。
|
||||
- [ ] `LLM returned empty response` 不再吞掉真实上游错误。
|
||||
- [ ] 真实 Gemini 工具调用三次稳定通过。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,377 +0,0 @@
|
||||
# InkOS Pi-Agent JSONL 持久化设计
|
||||
|
||||
状态:已确认进入 SPEC
|
||||
日期:2026-04-27
|
||||
|
||||
## 1. 目标
|
||||
|
||||
InkOS pi-agent session 持久化收敛为一层:追加式 JSONL transcript,路径为 `.inkos/sessions/{sessionId}.jsonl`。
|
||||
|
||||
transcript 是 agent 历史的唯一持久事实源。UI session 对象、聊天消息、thinking 展示、工具执行面板、session 列表摘要、恢复后的 `Agent.state.messages` 都从 transcript 派生。
|
||||
|
||||
这个设计替换当前两层行为:`BookSession` JSON 持久化 UI 形状的消息,`agentCache` 在短时间内保存更完整的内存态 `AgentMessage` 历史。
|
||||
|
||||
## 2. 源码对齐
|
||||
|
||||
### 2.1 Claude Code Main
|
||||
|
||||
Claude Code 用 JSONL transcript entry 存储对话历史。恢复主链围绕 `uuid`、`parentUuid`、`sessionId` 展开,没有把 transcript 级 `turnId` 作为恢复主结构。
|
||||
|
||||
关键源码位置:
|
||||
|
||||
- `/Users/fanghanjun/claude-code-main/src/types/logs.ts`:`SerializedMessage` 携带 `sessionId`、`timestamp`、`version`、`cwd` 等元数据。
|
||||
- `/Users/fanghanjun/claude-code-main/src/types/logs.ts`:`TranscriptMessage` 增加 `parentUuid`、`isSidechain` 等图结构字段。
|
||||
- `/Users/fanghanjun/claude-code-main/src/utils/sessionStorage.ts`:`insertMessageChain()` 写入消息并分配 `parentUuid`。
|
||||
- `/Users/fanghanjun/claude-code-main/src/utils/sessionStorage.ts`:tool result 通过 `sourceToolAssistantUUID` 绑定到发出 tool use 的 assistant message。
|
||||
- `/Users/fanghanjun/claude-code-main/src/utils/sessionStorage.ts`:`buildConversationChain()` 从 leaf message 沿 `parentUuid` 回溯恢复主链。
|
||||
- `/Users/fanghanjun/claude-code-main/src/utils/sessionStorage.ts`:`recoverOrphanedParallelToolResults()` 修复并行工具调用导致的 sibling assistant/tool-result 分支遗漏。
|
||||
|
||||
Claude Code 在 query 和 compact tracking 中有 `turnId`,但 transcript 恢复不依赖它。InkOS 应采用同类结构:用图字段和顺序字段支撑持久恢复,用 request 级标识做分组和诊断。
|
||||
|
||||
### 2.2 pi-agent-core
|
||||
|
||||
pi-agent-core 有内部 `turn_start` / `turn_end` 事件。这个 turn 边界表示一次 assistant response cycle,粒度小于一次用户请求。
|
||||
|
||||
一次 `agent.prompt()` 可以包含多个 pi-agent turn:
|
||||
|
||||
1. user prompt 进入 `currentContext.messages`。
|
||||
2. assistant 生成 `toolCall`。
|
||||
3. 工具执行返回 `toolResult` message。
|
||||
4. `toolResult` 被 push 进 `currentContext.messages`。
|
||||
5. 下一次 assistant response 带着 tool result 继续运行。
|
||||
|
||||
关键源码位置:
|
||||
|
||||
- `node_modules/.../@mariozechner/pi-agent-core/dist/agent-loop.js`:`runAgentLoop()` 构造 `currentContext.messages = [...context.messages, ...prompts]`。
|
||||
- `node_modules/.../@mariozechner/pi-agent-core/dist/agent-loop.js`:`runLoop()` 围绕每次 assistant response cycle 发出 `turn_start` 和 `turn_end`。
|
||||
- `node_modules/.../@mariozechner/pi-agent-core/dist/agent-loop.js`:`emitToolCallOutcome()` 构造 `role: "toolResult"` message。
|
||||
- `node_modules/.../@mariozechner/pi-agent-core/dist/agent.js`:`processEvents()` 在每个 `message_end` 时把 message push 到 `Agent.state.messages`。
|
||||
|
||||
InkOS 外层用户请求字段命名为 `requestId`。这样可以让 pi-agent-core 的内部 turn 语义继续保留给 `piTurnIndex`。
|
||||
|
||||
## 3. 核心决策
|
||||
|
||||
1. 使用 JSONL,一行一个事件。
|
||||
2. 使用 `.inkos/sessions/{sessionId}.jsonl` 作为 canonical path。
|
||||
3. 保留 `.inkos/sessions/{sessionId}.json` 的 legacy 读取能力。
|
||||
4. 新 session 和已迁移 session 停止写完整 `BookSession` JSON。
|
||||
5. 持久化原始 `AgentMessage`,避免写入压平后的 UI message。
|
||||
6. 使用 `requestId` 表示一次 InkOS 用户请求。
|
||||
7. 使用可选 `piTurnIndex` 表示 pi-agent-core 内部 turn 分组。
|
||||
8. 使用 `uuid`、`parentUuid`、`seq` 支撑持久排序和未来图恢复。
|
||||
9. 使用 `request_committed` 作为恢复栅栏。
|
||||
10. UI 状态从 transcript event 派生,不单独持久化 `UiMessageEvent`。
|
||||
|
||||
## 4. 事件模型
|
||||
|
||||
### 4.1 事件联合类型
|
||||
|
||||
```ts
|
||||
type TranscriptEvent =
|
||||
| SessionCreatedEvent
|
||||
| SessionMetadataUpdatedEvent
|
||||
| RequestStartedEvent
|
||||
| MessageEvent
|
||||
| RequestCommittedEvent
|
||||
| RequestFailedEvent
|
||||
```
|
||||
|
||||
### 4.2 消息事件
|
||||
|
||||
```ts
|
||||
type MessageEvent = {
|
||||
type: "message"
|
||||
version: 1
|
||||
|
||||
sessionId: string
|
||||
requestId: string
|
||||
|
||||
uuid: string
|
||||
parentUuid: string | null
|
||||
seq: number
|
||||
|
||||
role: "user" | "assistant" | "toolResult" | "system"
|
||||
timestamp: number
|
||||
|
||||
piTurnIndex?: number
|
||||
toolCallId?: string
|
||||
sourceToolAssistantUuid?: string
|
||||
legacyDisplay?: {
|
||||
thinking?: string
|
||||
toolExecutions?: unknown[]
|
||||
}
|
||||
|
||||
message: AgentMessage
|
||||
}
|
||||
```
|
||||
|
||||
`message` 必须保留 pi-agent-core 原始 `AgentMessage`。assistant 的 `thinking`、`text`、`toolCall` content block,以及 `toolResult.content`、`toolResult.details`、`toolResult.isError` 都必须完整写入。
|
||||
|
||||
`legacyDisplay` 只服务于旧 JSON 迁移后的 UI 派生,例如旧 `InteractionMessage.thinking`。它不进入模型恢复路径,避免把没有 provider signature 的旧 thinking 伪造成可回放给模型的 signed thinking block。
|
||||
|
||||
### 4.3 请求事件
|
||||
|
||||
```ts
|
||||
type RequestStartedEvent = {
|
||||
type: "request_started"
|
||||
version: 1
|
||||
sessionId: string
|
||||
requestId: string
|
||||
seq: number
|
||||
timestamp: number
|
||||
input: string
|
||||
}
|
||||
|
||||
type RequestCommittedEvent = {
|
||||
type: "request_committed"
|
||||
version: 1
|
||||
sessionId: string
|
||||
requestId: string
|
||||
seq: number
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
type RequestFailedEvent = {
|
||||
type: "request_failed"
|
||||
version: 1
|
||||
sessionId: string
|
||||
requestId: string
|
||||
seq: number
|
||||
timestamp: number
|
||||
error: string
|
||||
}
|
||||
```
|
||||
|
||||
`request_committed` 是恢复时使用的 durable boundary。`request_started` 后面的 message event 只有在匹配的 `request_committed` 存在时才进入模型恢复。
|
||||
|
||||
## 5. 写入路径
|
||||
|
||||
写入路径在 `runAgentSession()` 内订阅 pi-agent-core event。
|
||||
|
||||
每次 `/api/v1/agent` 请求执行以下步骤:
|
||||
|
||||
1. 分配 `requestId`。
|
||||
2. append `request_started`。
|
||||
3. 执行 `agent.prompt(instruction)`。
|
||||
4. 每次收到 pi-agent-core `message_end` 时 append 一个 `message` event。
|
||||
5. 根据 pi-agent-core `turn_start` / `turn_end` 在内存中维护 `piTurnIndex`。
|
||||
6. `agent.prompt()` 正常结束后 append `request_committed`。
|
||||
7. 执行失败或中断时 append `request_failed`。
|
||||
|
||||
带工具调用的请求会形成这类事件序列:
|
||||
|
||||
```text
|
||||
request_started
|
||||
message(user)
|
||||
message(assistant: thinking + toolCall)
|
||||
message(toolResult)
|
||||
message(assistant: thinking + text)
|
||||
request_committed
|
||||
```
|
||||
|
||||
writer 必须按 session 串行化 append。第一版使用进程内 per-session queue 即可满足 Studio 单 Node 进程模型。每次 append 写入一个 JSON object,再写入 `\n`。
|
||||
|
||||
## 6. 恢复路径
|
||||
|
||||
恢复路径从 JSONL transcript 构造 `Agent.state.messages`。
|
||||
|
||||
算法:
|
||||
|
||||
1. 读取 `.inkos/sessions/{sessionId}.jsonl`。
|
||||
2. 按文件顺序解析合法 JSONL event。
|
||||
3. 构造已 committed 的 `requestId` 集合。
|
||||
4. 收集 `requestId` 已 committed 的 `message` event。
|
||||
5. 按 `seq` 排序。
|
||||
6. 执行模型合法性清理。
|
||||
7. 返回 `messageEvent.message[]`。
|
||||
8. 在 `agent.prompt()` 前赋值给 `agent.state.messages`。
|
||||
|
||||
第一版使用 committed `seq` 顺序恢复。schema 同时写入 `uuid`、`parentUuid`、`toolCallId`、`sourceToolAssistantUuid`,后续可以增加 Claude Code 风格 leaf recovery,文件格式无需迁移。
|
||||
|
||||
## 7. thinking 持久化
|
||||
|
||||
thinking 必须作为 assistant 原始 content 持久化,不能只存 UI 字符串。
|
||||
|
||||
正确 transcript 数据形状:
|
||||
|
||||
```ts
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "thinking", thinking: "...", signature: "..." },
|
||||
{ type: "text", text: "..." }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
UI thinking 面板从 assistant thinking block 派生。
|
||||
|
||||
模型恢复使用合法性清理后的原始 assistant message。清理需要覆盖:
|
||||
|
||||
1. 丢弃没有有效 sibling assistant content 的孤立 thinking-only assistant message。
|
||||
2. 当 provider 拒绝以 thinking 结尾的 history 时,从最后一条 assistant message 移除 trailing thinking block。
|
||||
3. 当 model/provider/auth fallback 使 signature 失效时,移除带 signature 的 thinking block。
|
||||
|
||||
legacy `.json` session 可能有 `InteractionMessage.thinking`,但该字符串没有 provider signature。迁移可以保留它用于 UI 展示,不能把它伪造成可回放给模型的 signed thinking block。
|
||||
|
||||
## 8. tool result 持久化
|
||||
|
||||
tool result 必须作为一等 `message` event 进入 transcript。
|
||||
|
||||
当前 InkOS 信息损耗点在 `agentMessagesToPlain()`:它跳过 `ToolResult` message。这个行为导致两种上下文结果:
|
||||
|
||||
1. cache 存活时,模型能看到之前的 tool result,因为 `Agent.state.messages` 仍在内存中。
|
||||
2. cache 过期或进程重启后,模型只能看到压平后的 user/assistant 文本。
|
||||
|
||||
JSONL transcript 通过持久化原始 message 消除这种差异。恢复后的 `Agent.state.messages` 包含 committed request 结束时内存中存在的 user、assistant、toolResult message。
|
||||
|
||||
`restoreAgentMessagesFromTranscript()` 只恢复 committed 且清理后的 raw `AgentMessage`。
|
||||
provider-specific 兼容投影发生在 `adaptRestoredAgentMessagesForModel()`:
|
||||
跨模型会把外来 tool call/result 文本化;只有目标 provider 的 `compat.requiresAssistantAfterToolResult`
|
||||
为 true 时,才在 restored `toolResult` 后补 synthetic assistant bridge。
|
||||
|
||||
`sourceToolAssistantUuid` 在可获得时指向发出对应 tool call 的 assistant message。后续图恢复可以借此把 tool result 绑定到来源 tool use,降低对顺序恢复的依赖。
|
||||
|
||||
## 9. 缓存角色
|
||||
|
||||
`agentCache` 保留为加速层,不承担持久化职责。
|
||||
|
||||
缓存值调整为:
|
||||
|
||||
```ts
|
||||
type CachedAgent = {
|
||||
agent: Agent
|
||||
bookId: string | null
|
||||
modelId: string | null
|
||||
lastCommittedSeq: number
|
||||
lastActive: number
|
||||
}
|
||||
```
|
||||
|
||||
复用条件:
|
||||
|
||||
1. `sessionId` 匹配。
|
||||
2. `bookId` 匹配。
|
||||
3. `modelId` 匹配。
|
||||
4. transcript 最新 committed seq 等于 `lastCommittedSeq`。
|
||||
|
||||
任一条件不满足时,从 JSONL 重建 Agent。cache eviction 不会丢上下文,因为 JSONL 是持久事实源。
|
||||
|
||||
## 10. UI 派生
|
||||
|
||||
不引入独立的 `AgentMessageEvent | UiMessageEvent` 双事件线。
|
||||
|
||||
UI 状态从 transcript event 派生:
|
||||
|
||||
- user chat bubble:`message.role === "user"`。
|
||||
- assistant chat bubble:assistant text block。
|
||||
- thinking display:assistant thinking block。
|
||||
- tool execution panel:assistant toolCall block 按 `toolCallId` 关联 `toolResult`。
|
||||
- title:第一条 user message 或 `session_metadata_updated`。
|
||||
- book binding:`session_created` 和 `session_metadata_updated`。
|
||||
- session list summary:最后一条可见 user/assistant 文本与 metadata。
|
||||
|
||||
纯 `toolResult` message 不作为独立 user message 展示。它服务于工具执行面板和模型恢复。
|
||||
|
||||
## 11. 旧数据兼容
|
||||
|
||||
现有 `.inkos/sessions/{sessionId}.json` 文件作为 legacy session 读取。
|
||||
|
||||
迁移行为:
|
||||
|
||||
1. JSONL 存在时优先读取 JSONL。
|
||||
2. 只有 JSON 存在时,读取 legacy `BookSession`。
|
||||
3. 把 legacy user/assistant message 转成 transcript `message` event。
|
||||
4. 把生成的 request 标记为 committed。
|
||||
5. 保留 `sessionId`、`bookId`、`title`、`createdAt`、`updatedAt` 等 metadata。
|
||||
6. 保留 legacy assistant `thinking` 给 UI 展示。
|
||||
7. 不生成 legacy 数据里不存在的 tool result、tool call、signed thinking。
|
||||
8. 迁移成功后,运行时只写 JSONL。
|
||||
|
||||
旧 JSON 文件可以继续留在磁盘上。迁移后它不再被更新。
|
||||
|
||||
如果 Studio plain chat fallback 需要补写 assistant 文本,使用 synthetic committed request 写入 transcript。该补写仍然产生 `request_started -> message -> request_committed`,不恢复 legacy JSON 写入路径。
|
||||
|
||||
## 12. 错误处理
|
||||
|
||||
格式错误的 JSONL 行不能导致 session 加载失败。reader 记录 warning 并跳过不可解析行。
|
||||
|
||||
缺少 `request_committed` 的 request 被视为 interrupted tail:
|
||||
|
||||
- 不进入模型恢复。
|
||||
- 保留给诊断。
|
||||
- 后续可以在 UI 中展示为 interrupted request。
|
||||
|
||||
如果最后一条 committed message 让模型 history 处于非法状态,恢复前执行清理。第一版清理 unresolved tool use、孤立 tool result、孤立 thinking-only message、空 assistant message、trailing thinking。
|
||||
|
||||
## 13. 测试策略
|
||||
|
||||
这个改动必须用 TDD。核心风险是重启、cache 过期、工具调用循环后发生静默上下文丢失。
|
||||
|
||||
测试分层:
|
||||
|
||||
1. JSONL codec 单测
|
||||
- 一行 append 一个 event。
|
||||
- 保留原始 `AgentMessage` 内的未知字段。
|
||||
- 拒绝非法 schema 形状。
|
||||
- 分配单调递增 `seq`。
|
||||
|
||||
2. restore 单测
|
||||
- 只恢复 committed request。
|
||||
- 忽略 interrupted tail。
|
||||
- 保留 user、assistant、toolResult 顺序。
|
||||
- 返回原始 `AgentMessage[]`。
|
||||
|
||||
3. thinking 单测
|
||||
- 保留 assistant thinking text。
|
||||
- 保留 provider signature 字段。
|
||||
- 不把 legacy UI thinking 转成 signed model thinking。
|
||||
- 过滤非法 trailing thinking 或孤立 thinking。
|
||||
|
||||
4. tool loop 单测
|
||||
- 持久化 `assistant(toolCall) -> toolResult -> assistant(text)`。
|
||||
- 恢复 toolResult 到 `Agent.state.messages`。
|
||||
- 通过 `toolCallId` 绑定 toolResult 和 toolCall。
|
||||
|
||||
5. legacy migration 单测
|
||||
- 读取旧 BookSession JSON。
|
||||
- 派生 UI 兼容 message。
|
||||
- 迁移后写入 JSONL。
|
||||
- 停止写 legacy JSON。
|
||||
|
||||
6. cache 单测
|
||||
- 最新 committed seq 匹配时复用 cache。
|
||||
- cache 落后时从 JSONL 重建。
|
||||
- book 或 model 变化时重建。
|
||||
|
||||
7. API 集成测试
|
||||
- `/api/v1/agent` 写 transcript event。
|
||||
- session list/detail API 从 JSONL 派生。
|
||||
- 前端响应形状保持兼容。
|
||||
|
||||
## 14. 实现边界
|
||||
|
||||
预期 core 模块:
|
||||
|
||||
- `packages/core/src/interaction/session-transcript.ts`
|
||||
- `packages/core/src/interaction/session-transcript-schema.ts`
|
||||
- `packages/core/src/interaction/session-transcript-restore.ts`
|
||||
- `packages/core/src/interaction/session-transcript-legacy.ts`
|
||||
|
||||
预期集成点:
|
||||
|
||||
- `packages/core/src/agent/agent-session.ts`
|
||||
- `packages/core/src/interaction/book-session-store.ts`
|
||||
- `packages/studio/src/api/server.ts`
|
||||
|
||||
第一版不实现 compaction、branching UI、remote sync、完整 Claude Code graph recovery。schema 预留后续能力所需字段。
|
||||
|
||||
## 15. 验收标准
|
||||
|
||||
1. 新 session 写入 `.inkos/sessions/{sessionId}.jsonl`。
|
||||
2. 已迁移或新建 session 不再写 `.inkos/sessions/{sessionId}.json`。
|
||||
3. Studio 重启后能恢复包含 tool result 的原始 `AgentMessage[]`。
|
||||
4. assistant thinking block 经过 JSONL 写入和恢复后仍然存在。
|
||||
5. legacy JSON session 仍然可以加载和展示。
|
||||
6. cache 过期不会改变模型可见的 conversation context。
|
||||
7. 测试覆盖 committed restore、interrupted tail 排除、tool result 恢复、thinking 持久化、legacy migration、cache invalidation。
|
||||
Reference in New Issue
Block a user