1126 lines
59 KiB
Markdown
1126 lines
59 KiB
Markdown
# Makelore Code OpenCode → Pi 硬切换实施规范
|
||
|
||
## 0. 文档信息
|
||
|
||
| 字段 | 值 |
|
||
| --- | --- |
|
||
| Spec ID | `ML-CODE-PI-001` |
|
||
| 状态 | 已获用户实施授权;实施中 |
|
||
| 日期 | 2026-08-22 |
|
||
| Task | `20260822-pi-runtime-spec-b6e2c9a4` |
|
||
| 基线 | `fba68e86d93c66d2c50f53d215de76a7c69c97a7` |
|
||
| 来源设计 | Task `20260821-pi-runtime-replacement-doc-8e31c4a7` 的 OpenCode → Pi 硬切换改造设计 |
|
||
| 关联票据 | `20260822-pi-runtime-spec-b6e2c9a4__pi-runtime-cutover-tickets.md` |
|
||
| Pi 基线 | `@earendil-works/pi-coding-agent@0.84.2`;上游 tag `v0.84.2` / commit `914cf1472e715297caa30db4b9535d534a9eb718`,已由 `PI-000` 精确固化 |
|
||
| `PI-000` 实施基线 | Commit `2bc423ebc58589307442ebdaf1c81d83ec9dc2d5`(`feat: qualify Pi runtime cutover foundation`);实施任务 `20260822-pi-runtime-qualification-c52e8a14` 已 `ready_for_integration` |
|
||
| `PI-010` 交接 | 实施任务 `20260822-pi-conversation-contracts-a61d9c42` 已从 `2bc423e` 建立,当前状态 `planning` |
|
||
| 发布单元 | 一次完整硬切换;不允许部分能力以双 runtime 形式发布 |
|
||
| Phase-0 范围修订 | 用户于 2026-08-22 明确将 macOS x64/arm64 资格验证延期到 `PI-150`;该平台未通过、也不得记为 Pass |
|
||
| Provider 风险决策 | 用户于 2026-08-22 明确豁免真实外部 Provider Account 与真实 provider 双 worker 验证并接受对应风险;`QG-004`/`QG-005` 不是 Pass |
|
||
| `PI-000` 审计状态 | Done with explicit waivers;`QG-001/002/003/006` Pass,`QG-004/005` Accepted Risk,`QG-007` Pending / not triggered |
|
||
|
||
本文把详细改造设计收敛为可验证的实施合同。它规定“必须交付什么、边界在哪里、如何判定完成”,不规定每个函数的内部写法。
|
||
|
||
本文中的关键词含义如下:
|
||
|
||
- **MUST / 必须**:发布前不可缺少;不满足即 Spec 未完成。
|
||
- **MUST NOT / 禁止**:任何实现均不得出现。
|
||
- **SHOULD / 应当**:默认实现方式;偏离时必须在实施记录中给出可验证理由。
|
||
- **MAY / 可以**:不影响合规的实现选择。
|
||
|
||
## 1. 目标结果
|
||
|
||
### 1.1 产品结果
|
||
|
||
完成后,Pi 是 Makelore Code 唯一 coding-agent runtime。用户仍使用 Makelore 的项目、伙伴和 Conversation 产品模型,不直接感知 Pi CLI、RPC、session 文件或 provider 配置格式。
|
||
|
||
系统必须同时达成以下结果:
|
||
|
||
1. 删除 OpenCode runtime、依赖、插件、Host API、Renderer store、运行资源与打包产物。
|
||
2. 项目、伙伴和空 Conversation 的创建仅写本地产品元数据,不启动 Pi,也不阻塞输入框。
|
||
3. 每个运行中或温热 Conversation 使用一个独立 Pi worker;多个 Conversation 可以真正并行。
|
||
4. Renderer 只消费 Makelore-owned Snapshot/Patch 协议;Pi wire 只存在于 Electron Main 的 Adapter 内。
|
||
5. Pi subagent 由 Makelore 自有 extension 编排,具有全局并发上限、父子 abort 和同项目写租约。
|
||
6. 保留有产品价值的文本、图片、thinking、tool、retry、compaction、queue、interaction、模型、thinking level、文件、changes、browser 和 skill 能力。
|
||
7. 旧 OpenCode Conversation 不在 Pi 中继续或展示;迁移前备份,发布说明明确告知。
|
||
8. packaged runtime、四协议 loopback/provider-shaped 双 worker、资源占用和性能预算都有实际证据;真实外部 Provider 兼容与并发风险以明确 waiver 记录,不伪装成 Pass。
|
||
|
||
### 1.2 成功定义
|
||
|
||
只有第 20 节 Definition of Done 全部通过,才可宣称 OpenCode 已被 Pi 替换。仅能启动 Pi、仅能完成单轮聊天、或仅删除 package dependency 都不构成完成。
|
||
|
||
## 2. 范围
|
||
|
||
### 2.1 包含
|
||
|
||
- Code 模块的项目/伙伴/Conversation 产品持久化升级。
|
||
- Electron Main 的 Pi runtime、worker pool、session registry、provider/resource loader、event projector、extensions 和 Host API。
|
||
- Renderer 的 vendor-neutral store、Chat UI、执行图、工具卡、交互、queue、compaction、subagent、changes 和诊断投影。
|
||
- 一次性 OpenCode 数据备份与 schema 迁移。
|
||
- Pi runtime staging、artifact verifier、真实 smoke、三平台打包验证。
|
||
- 流式渲染、首 Conversation、并发和资源预算的遥测与验收。
|
||
- OpenCode 专属能力的删除或产品语义替换。
|
||
|
||
### 2.2 不包含
|
||
|
||
- OpenCode 与 Pi 双 runtime、fallback、feature flag、compatibility Adapter 或旧 Host API facade。
|
||
- 旧 OpenCode Session 到 Pi Session 的续聊转换。
|
||
- 通用 runtime 插件平台或任意第三方 MCP 市场。
|
||
- 同一项目多个写入 Agent 的 git worktree 产品;本次只实现项目级 mutation lease。
|
||
- Makelore app id、protocol、`niancode` 存储根、通用用户数据或其他产品模块迁移。
|
||
- Works gallery、asset gallery、publish/upload 或 cloud-deploy workbench 的恢复。
|
||
- Pi TUI 所有功能的 GUI 镜像。
|
||
- 自动删除 `<userData>/opencode` 或用户不确定所有权的 `.opencode` 文件。
|
||
|
||
## 3. 不可变架构决定
|
||
|
||
### 3.1 硬切换
|
||
|
||
- `CUT-001`:发布产物 MUST 只包含 Pi runtime,不得包含 OpenCode binary、package、plugin 或可执行 fallback。
|
||
- `CUT-002`:代码 MUST NOT 定义 `OpenCodeRuntime | PiRuntime`、runtime capability negotiation、runtime selector 或 OpenCode Adapter。
|
||
- `CUT-003`:所有 `/api/opencode/*` 路由 MUST 删除;Renderer MUST NOT 继续调用旧路由。
|
||
- `CUT-004`:源码中不得用 `Opencode*` 名称包装 Pi 数据。仅历史文档、迁移提示和旧数据目录说明可以保留 OpenCode 字样。
|
||
- `CUT-005`:硬切换以一个完整应用版本为回退单位,不在同一版本内做 runtime 开关。
|
||
|
||
### 3.2 产品权威数据
|
||
|
||
- `BND-001`:`.niancode/project.json` MUST 继续是项目、伙伴、默认模型和 skill 选择的权威来源。
|
||
- `BND-002`:稳定 Agent ID、`app.niancode.desktop`、现有 URL protocol、`niancode` 存储标识 MUST 保持不变。
|
||
- `BND-003`:不得生成项目内 `.pi/agents/*` 作为伙伴配置的第二权威来源。
|
||
- `BND-004`:项目中任意 `.pi/settings.json`、自动发现的 `.pi/extensions`、`.pi/skills` 或第三方 package MUST NOT 被生产 runtime 隐式加载。
|
||
|
||
### 3.3 Main / Renderer 边界
|
||
|
||
- `BND-005`:Electron Main MUST 拥有 runtime、worker、provider、credential、session 文件、项目文件、browser 和系统集成。
|
||
- `BND-006`:Renderer 只能通过 `src/lib/host-api.ts` 或 `src/lib/api-client.ts` 的 typed facade 访问上述能力;禁止页面/组件直接 IPC 或直连 Pi localhost/stdin。
|
||
- `BND-007`:Renderer MUST NOT 导入 Pi 类型、解析 Pi JSONL、读取 Pi session、接收绝对 session path 或接收 credential。
|
||
- `BND-008`:Pi 实现 MUST 位于一个深的 Main-owned `PiConversationRuntime` Module 后;公共 `CodingConversationRuntime` Interface 只表达 Makelore 产品语义。
|
||
|
||
## 4. 用户行为规范
|
||
|
||
### 4.1 项目、伙伴与 Conversation 创建
|
||
|
||
- `UX-001`:创建项目 MUST 只完成项目目录和产品 metadata 初始化;成功响应不得等待 Pi spawn、provider network、session list 或 runtime diagnostics。
|
||
- `UX-002`:创建/保存伙伴 MUST 只写 `.niancode/project.json` 及受管本地资源;不得启动或重启所有 worker。
|
||
- `UX-003`:创建空 Conversation MUST 生成稳定产品 `conversationId` 并写入 schema v2;此时 `piSessionId` 和 `sessionKey` 可以为空。
|
||
- `UX-004`:选择 Conversation 后 Main MAY 后台 prewarm worker,但 prewarm 失败不得阻止用户编辑 draft。
|
||
- `UX-005`:项目、伙伴和 Conversation metadata 写入失败必须明确失败,不得返回成功后再依赖 runtime 补齐产品记录。
|
||
|
||
### 4.2 Composer
|
||
|
||
- `UX-010`:Textarea 可编辑条件只能依赖 active project 存在、有效伙伴存在、当前项目不处于销毁/迁移临界状态。
|
||
- `UX-011`:Textarea 可编辑性 MUST NOT 依赖 worker state、session/status 请求、全局 loading、其他 Conversation busy、provider request 或 diagnostics。
|
||
- `UX-012`:附件编码、模型不可用或当前 submit 本地校验 MAY 暂时禁用 Send,但不得禁用 Textarea。
|
||
- `UX-013`:cold worker 尚未 ready 时,UI 显示 Conversation 级“正在准备本地 Agent”,不得显示覆盖整个 Chat 的无界 loading。
|
||
- `UX-014`:任何本地准备超过 10 秒 MUST 进入明确、可恢复错误;不得让 Composer 分钟级 disabled。
|
||
|
||
### 4.3 Prompt、queue 与完成
|
||
|
||
- `UX-020`:发送时 Renderer MUST 立即创建带 `clientRequestId` 的 optimistic user node。
|
||
- `UX-021`:HTTP 成功只表示 prompt/steer/follow-up 已接受,不能表示 run 完成。
|
||
- `UX-022`:prompt 接受后,Provider/tool 错误通过 patch stream 投影;不得删除已接受的 user node。
|
||
- `UX-023`:一个 Conversation 的 prompt、abort、retry、queue、model、thinking、error MUST 与其他 Conversation 隔离。
|
||
- `UX-024`:`agent_settled` 是权威 idle;`agent_end`、`turn_end`、`message_end`、`compaction_end` 单独出现时均不得释放 top-level permit 或清除 busy。
|
||
- `UX-025`:steer 与 follow-up 必须是两个明确的 queue mode;UI 不得继续使用含义不明的单一 OpenCode queue。
|
||
- `UX-026`:abort 只影响目标 Conversation 及其 child subagents,不得终止其他 worker。
|
||
|
||
### 4.4 Conversation 设置
|
||
|
||
- `UX-030`:伙伴模型只是新 Conversation 默认值;已有 Conversation 保持自己的 `accountId/modelId/thinkingLevel`。
|
||
- `UX-031`:模型切换只调用目标 worker 的 Pi `set_model`,并原子更新目标 Conversation metadata;不得修改伙伴默认、重启其他 worker或全局 provider 配置。
|
||
- `UX-032`:thinking level 是 Conversation setting,行为与模型切换相同。
|
||
- `UX-033`:目标模型不在当前受管 catalog 时切换必须失败,旧模型保持不变。
|
||
|
||
### 4.5 并发与 subagent
|
||
|
||
- `UX-040`:不同 Conversation 必须使用两个独立 worker,且最终 packaged provider-shaped loopback turn 必须能在时间上重叠;不能通过一个 worker 反复 `switch_session` 冒充并发。真实外部 Provider 可能并发、串行、限流或拒绝,本 Spec 不再以外部 turn 重叠作为 release hard gate。
|
||
- `UX-041`:多个 subagent 支持 `single`、`parallel`、`chain` 三种模式。
|
||
- `UX-042`:subagent 作为父 tool 下的嵌套执行节点展示,不得创建虚构的 `role: subagent` chat message。
|
||
- `UX-043`:只读 subagent 可以并行;同一项目 mutation 必须遵守项目写租约。
|
||
- `UX-044`:abort parent 必须取消所有未完成 child;parallel 中一个 child 失败不得抹掉其他已完成结果;chain 在首个失败处停止。
|
||
|
||
### 4.6 功能删除与改名
|
||
|
||
| OpenCode 旧能力 | Pi 版本要求 |
|
||
| --- | --- |
|
||
| share / unshare | UI、route、type、test 全部删除;未来 sharing 另立 Makelore 合同。 |
|
||
| revert / unrevert | 删除;用“从这里创建新对话分支”表达 Pi fork,不承诺文件回滚。 |
|
||
| todos endpoint | 删除;若需要结构化进度,使用版本化 `task_state` extension details。 |
|
||
| global runtime start/stop/restart | 删除;worker 由 Conversation lazy lifecycle 管理。 |
|
||
| Session diff | 改为 Main-owned `ConversationChangeTracker`,不向 Pi 查询。 |
|
||
| questions / permissions 双模型 | 合并为 Makelore `interactions`;具体高影响 tool 自己发 confirm。 |
|
||
| Playwright MCP | 删除;`agent_browser` extension 直接调用 Main browser seam。 |
|
||
| OpenCode Agent materialization | 删除;伙伴 prompt/manifest 从 `.niancode/project.json` 生成到受管 runtime cache。 |
|
||
|
||
## 5. 目标架构与模块责任
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
UI[Renderer Coding UI]
|
||
API[Main Host API /api/coding]
|
||
CS[Conversation Service]
|
||
PS[Project & Conversation Store]
|
||
FS[Project File / Change Service]
|
||
RT[CodingConversationRuntime]
|
||
POOL[Pi Worker Pool]
|
||
W1[Pi RPC Worker A]
|
||
W2[Pi RPC Worker B]
|
||
EXT[Makelore Pi Extension]
|
||
PR[Provider Service / Secure Storage]
|
||
|
||
UI -->|typed command| API
|
||
API --> CS
|
||
API --> PS
|
||
API --> FS
|
||
CS --> RT
|
||
RT --> POOL
|
||
POOL --> W1
|
||
POOL --> W2
|
||
W1 --> EXT
|
||
W2 --> EXT
|
||
PR --> POOL
|
||
CS -->|Snapshot + ordered Patch| API
|
||
API -->|SSE| UI
|
||
```
|
||
|
||
### 5.1 产品模块
|
||
|
||
| 模块 | 必须拥有 | 禁止拥有 |
|
||
| --- | --- | --- |
|
||
| `electron/coding-projects` | project config/store、conversation metadata、migration、file service、skills registry | Pi RPC、provider turn、Renderer state |
|
||
| `electron/coding-runtime/contracts.ts` | vendor-neutral runtime Interface 和产品 DTO | Pi event/type import |
|
||
| `electron/coding-runtime/conversation-service.ts` | product command orchestration、snapshot/patch subscription、acceptance | child process 细节 |
|
||
| `electron/coding-runtime/pi` | process、RPC、worker pool、session registry、event/session projector、provider/resource input、extensions | project CRUD、Renderer component |
|
||
| `electron/api/routes/coding-*` | request validation、typed response、SSE、安全错误投影 | vendor wire passthrough |
|
||
| Renderer `coding-conversations` | per-Conversation snapshot/patch reducer、draft 和 selection | Pi JSONL、绝对路径、credential |
|
||
|
||
### 5.2 `CodingConversationRuntime` Interface
|
||
|
||
实现 MUST 提供等价于下列产品能力的最小接口:
|
||
|
||
```ts
|
||
interface CodingConversationRuntime {
|
||
prepare(input: PrepareConversationInput): Promise<ConversationRuntimeState>;
|
||
getSnapshot(conversationId: string): Promise<ConversationSnapshot>;
|
||
prompt(input: PromptConversationInput): Promise<PromptAcceptance>;
|
||
steer(input: QueueMessageInput): Promise<QueueAcceptance>;
|
||
followUp(input: QueueMessageInput): Promise<QueueAcceptance>;
|
||
abort(conversationId: string): Promise<void>;
|
||
setModel(input: SetConversationModelInput): Promise<ConversationModelState>;
|
||
setThinking(input: SetThinkingLevelInput): Promise<ConversationModelState>;
|
||
compact(conversationId: string): Promise<void>;
|
||
fork(input: ForkConversationInput): Promise<ForkResult>;
|
||
recover(conversationId: string): Promise<ConversationRuntimeState>;
|
||
dispose(conversationId: string): Promise<void>;
|
||
subscribe(listener: (patch: ConversationPatchEnvelope) => void): () => void;
|
||
}
|
||
```
|
||
|
||
- `ARC-001`:生产 `PiConversationRuntime` 与测试 `InMemoryConversationRuntime` 是该 Interface 的两个 Implementation。
|
||
- `ARC-002`:Interface MUST NOT 包含 vendor enum、Pi CLI option、OpenCode capability 或绝对路径。
|
||
- `ARC-003`:项目 CRUD 和文件读取不属于 runtime Interface,应由对应 Main service 直接提供。
|
||
- `ARC-004`:实现内部 MAY 进一步拆分,但不得把 process/RPC/event/session/provider 生命周期重新堆回单个 Host route 文件。
|
||
|
||
### 5.3 建议目录
|
||
|
||
```text
|
||
electron/
|
||
coding-projects/
|
||
project-config.ts
|
||
project-store.ts
|
||
conversation-store.ts
|
||
project-files.ts
|
||
conversation-change-tracker.ts
|
||
skill-registry.ts
|
||
coding-runtime/
|
||
contracts.ts
|
||
conversation-service.ts
|
||
runtime-errors.ts
|
||
pi/
|
||
runtime.ts
|
||
worker-pool.ts
|
||
worker-process.ts
|
||
rpc-framer.ts
|
||
rpc-client.ts
|
||
event-projector.ts
|
||
session-projector.ts
|
||
provider-config.ts
|
||
resource-loader.ts
|
||
extensions/
|
||
makelore-runtime.ts
|
||
interaction.ts
|
||
write-lease.ts
|
||
subagent.ts
|
||
agent-browser.ts
|
||
game-assets.ts
|
||
api/routes/
|
||
coding-projects.ts
|
||
coding-conversations.ts
|
||
coding-files.ts
|
||
```
|
||
|
||
目录名称可以按仓库约定小幅调整,但责任边界和依赖方向是规范要求。
|
||
|
||
## 6. 产品数据模型
|
||
|
||
### 6.1 产品 ID 与 Pi ID
|
||
|
||
- `DAT-001`:`conversationId` MUST 是 Makelore 生成的稳定 UUID;Renderer、URL、archive、unread、selection、Zustand key 只使用该 ID。
|
||
- `DAT-002`:Pi `sessionId`、session file、entry ID、leaf ID 是 Main 私有实现字段。
|
||
- `DAT-003`:worker 重启、session reopen 或 Pi 升级不得改变产品 `conversationId`。
|
||
- `DAT-004`:任何绝对 session path MUST NOT 出现在 Host API、SSE、Renderer log 或 UI。
|
||
|
||
### 6.2 `.niancode/project.json` schema v2
|
||
|
||
每个 Agent 的模型选择 MUST 迁移为:
|
||
|
||
```ts
|
||
interface ProductModelRef {
|
||
accountId: string;
|
||
modelId: string;
|
||
thinkingLevel: "off" | "minimal" | "low" | "medium" | "high";
|
||
}
|
||
|
||
interface ProductModelSelection {
|
||
model: ProductModelRef | null;
|
||
modelResolution: "resolved" | "required";
|
||
}
|
||
```
|
||
|
||
- `accountId` 指向 Main Provider Service 的稳定账号。
|
||
- `modelId` 是 provider 原生模型 ID,不是 Pi 派生 provider key。
|
||
- runtime provider ID 只在 Main 运行时派生。
|
||
- 旧 string model 只有在 Provider Service 能唯一映射时才自动转换。
|
||
- 不能唯一映射时保留 Agent 其他字段,写入
|
||
`{ model: null, modelResolution: "required" }`;UI 要求用户选择,不得静默使用默认模型。
|
||
- Agent id、name、prompt、skills、archive 状态 MUST 保持。
|
||
|
||
### 6.3 `.niancode/conversations.json` schema v2
|
||
|
||
```ts
|
||
interface CodingConversationFileV2 {
|
||
schemaVersion: 2;
|
||
conversations: Array<{
|
||
id: string;
|
||
agentId: string;
|
||
title: string;
|
||
model: ProductModelRef | null;
|
||
modelResolution: "resolved" | "required";
|
||
piSessionId?: string;
|
||
sessionKey?: string;
|
||
archivedAt: string | null;
|
||
unread: boolean;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}>;
|
||
}
|
||
```
|
||
|
||
- `DAT-010`:`sessionKey` 必须是 Main session root 下的 opaque relative key;禁止 `..`、绝对路径或 Renderer 提供路径。
|
||
- `DAT-011`:Conversation 可以在 Pi session 不存在时持久化。
|
||
- `DAT-012`:第一次 prompt 的 session create/open 与 Conversation 绑定 MUST 使用 per-Conversation single-flight;并发首发不得创建两个 session。
|
||
- `DAT-013`:title、archive、unread 和 model metadata 不依赖 worker 存活。
|
||
- `DAT-014`:删除 Conversation 时先 dispose worker,再把 session 移入 Main-owned trash;Renderer 不直接删除文件。
|
||
- `DAT-015`:产品 metadata 写入 MUST 使用原子替换;session 文件由 Pi 持久化合同负责。
|
||
|
||
### 6.4 受管 Pi 目录
|
||
|
||
```text
|
||
<userData>/coding-runtime/pi/
|
||
config/
|
||
sessions/<project-id>/<opaque-session-key>.jsonl
|
||
prompts/<project-id>/<agent-id>.md
|
||
extensions/
|
||
logs/
|
||
trash/
|
||
```
|
||
|
||
`PI_CODING_AGENT_DIR` MUST 指向受管 config。项目 cwd 仍是实际项目目录,但 Pi 全局配置、session、prompt 和 extension 不写入任意用户 `~/.pi`。
|
||
|
||
## 7. Provider、模型与受管资源
|
||
|
||
### 7.1 Provider 投影
|
||
|
||
Main 内部 MUST 用 vendor-neutral account 生成等价于下列描述:
|
||
|
||
```ts
|
||
interface PiProviderDescriptor {
|
||
accountId: string;
|
||
runtimeProviderId: string;
|
||
api:
|
||
| "openai-completions"
|
||
| "openai-responses"
|
||
| "anthropic-messages"
|
||
| "google-generative-ai";
|
||
baseUrl?: string;
|
||
headers: Record<string, string>;
|
||
apiKeyEnv?: string;
|
||
models: Array<{
|
||
id: string;
|
||
name: string;
|
||
input: Array<"text" | "image">;
|
||
contextWindow?: number;
|
||
maxOutputTokens?: number;
|
||
}>;
|
||
}
|
||
```
|
||
|
||
具体 `api` union 在 `PI-000` 中按锁定 Pi 版本固化。
|
||
|
||
- `PRV-001`:只有认证、base URL 和协议完全匹配时才使用 Pi built-in provider。
|
||
- `PRV-002`:自定义 base URL/header、Works gateway/proxy 和导入模型必须由 Main 生成受管 catalog。
|
||
- `PRV-003`:runtime provider ID 由稳定 account ID 派生,两个同 vendor 账号不得冲突。
|
||
- `PRV-004`:当前 Works gateway `/v1` 归一化、proxy token 与 refresh 语义必须迁移并有 fixture。
|
||
- `PRV-005`:credential 只在 spawn 前从 secure storage 读取,放入单 worker environment 或受管 credential provider;不得进入 command line、snapshot、SSE、Renderer 或日志。
|
||
- `PRV-006`:同 account refresh 使用 Provider Service single-flight;认证恢复最多执行一次 refresh + worker reopen,不得无限循环。
|
||
- `PRV-007`:Provider/config 维护 Main-owned revision。idle stale worker 在下次 prompt 前重建;running worker在 settled 后重建,不得中断当前 run。
|
||
|
||
### 7.2 Prompt、skills 与 context
|
||
|
||
- `RES-001`:伙伴 prompt 从 `.niancode/project.json` 生成到受管 cache;command line 只出现受管路径,不出现 prompt 内容。
|
||
- `RES-002`:内建 coding skills 从 `.opencode/skills` 移到 `resources/coding-skills`,由 skill ID 映射到显式打包路径。
|
||
- `RES-003`:child 只加载当前伙伴选择的 skill;禁止自动发现项目或用户目录的 Pi resources。
|
||
- `RES-004`:正在运行的 worker 保持本 run 的 resource snapshot;新 run/新 worker使用最新 revision。
|
||
- `RES-005`:`AGENTS.md` 等 context file 的加载策略必须由 `PI-000` 在锁定版本上显式决定并测试,不能依赖 Pi 默认值漂移。
|
||
|
||
## 8. Pi 进程、RPC 与 Worker Pool
|
||
|
||
### 8.1 Phase-0 资格门
|
||
|
||
- `QG-001`:在任何产品迁移代码开始前,必须精确 pin 一个 Pi 版本并记录 package、CLI entry、Node engine 和上游 tag/commit。
|
||
- `QG-002`:Phase-0 平台范围内的 Windows x64、Linux x64 packaged qualification artifact 必须证明 Electron Node 能 spawn、RPC ready、open session、accept prompt、abort、settle、reopen、clean exit。
|
||
- `QG-003`:必须证明 Phase-0 平台范围内的 Windows x64、Linux x64 完整 production dependency closure、WASM/native/optional 资源能从 packaged qualification artifact resolve;最终产品 artifact 的同项验证属于 `PI-150`。
|
||
- `QG-004`:真实外部 Provider Account 的四协议验证由用户于 2026-08-22 标记为 **Explicitly Waived / Accepted Risk**,不得记为 Pass。Phase-0 仍必须保留 Windows/Linux packaged loopback 四协议矩阵,验证 request/SSE、base URL/path、header、environment credential plumbing、model 和 image serialization;报告中的 `realTurnVerified` 必须保持 `false`。
|
||
- `QG-005`:真实外部 provider 的双 worker turn overlap/isolation 由用户于 2026-08-22 标记为 **Explicitly Waived / Accepted Risk**,不得记为 Pass。Phase-0 仍必须以两个独立 worker 通过 packaged provider-shaped loopback 证明时间窗口重叠、单侧 abort 后另一侧 settled,以及受控 seam 内的 event/session/model/credential 路由不串线;不得把它表述为真实外部 Provider 并发证据。
|
||
- `QG-006`:必须报告 Phase-0 平台范围内 Windows x64、Linux x64 的 cold/warm latency、RSS、退出清理 p50/p95/max 和样本数。
|
||
- `QG-007`:若 RPC 无法通过未豁免的打包、provider-shaped contract/concurrency/abort 或预算门,必须在业务迁移前停止并修订本 Spec,选择“Pi SDK in Electron utility process”;禁止继续实现双 runtime 或同时维护 RPC/SDK 两条生产路径。缺少已明确豁免的真实外部 Provider 样本不是已确认失败,不触发本条。
|
||
|
||
**Phase-0 macOS waiver(用户范围决策,2026-08-22)**:macOS x64 与
|
||
macOS arm64 在 `PI-000` 中标记为 **Explicitly Waived / Deferred by user**,
|
||
不得标记为 Pass。原 Phase-0 中的 macOS workspace、staged production closure、
|
||
controlled packaged、packaged loopback 和 metrics 全部移交 `PI-150` 实际验证。
|
||
该 waiver 只改变 `QG-002`、`QG-003`、`QG-006` 的 Phase-0 平台范围;
|
||
`QG-004`、`QG-005` 后续由下面独立的 Provider waiver 处理。这会把 macOS
|
||
packaging、native、resource 和 performance defect 的暴露时间推迟到 release
|
||
stage,可能导致更晚返工;这是用户接受的时序风险,不代表风险已经消除。
|
||
|
||
**真实 Provider waiver(用户风险决策,2026-08-22)**:用户明确豁免真实
|
||
外部 Provider Account/credential 与真实 provider 双 worker 验证,并接受以下
|
||
尚未验证的风险:
|
||
|
||
- 真实认证、endpoint、proxy、rate-limit 和 provider-specific response variation;
|
||
- base URL、header、environment credential、model、image 等字段在真实协议端点
|
||
的兼容性;
|
||
- 真实 provider 可能并发、串行、限流或拒绝两个 worker turn;
|
||
- 跨 worker abort、event、session、model、credential 的真实外部隔离。
|
||
|
||
该决定把 `QG-004`、`QG-005` 置为 **Explicitly Waived / Accepted Risk**,
|
||
不是 Pass,也不是已确认失败。Windows/Linux packaged loopback 与
|
||
provider-shaped concurrency/abort smoke 仍是必须通过的替代证据;任何报告均须
|
||
保留 `realTurnVerified: false`。真实外部 Provider Account、凭证或 turn 不再是
|
||
`PI-000`、`PI-150` 或 release 的阻断条件,缺失它们不触发 `QG-007`。
|
||
|
||
### 8.2 启动合同
|
||
|
||
生产实现 SHOULD 使用 Electron executable + `ELECTRON_RUN_AS_NODE=1` 启动 staged Pi CLI RPC entry。锁定版本的实际参数必须由 smoke 确认,语义要求如下:
|
||
|
||
- RPC mode;
|
||
- offline/禁用 Pi 自更新类启动网络,但不阻断 provider;
|
||
- `--no-approve` 或锁定版本等价选项;
|
||
- 禁止自动 extension/skill/prompt-template 发现;
|
||
- 只显式加载 Makelore extension、selected skills、受管 session/provider/model;
|
||
- stdout 只能输出 RPC JSONL,日志只能走 stderr;
|
||
- secret 只进入 child environment;
|
||
- cwd 是当前项目目录;Pi config/session root 是 Main-owned 目录。
|
||
|
||
### 8.3 RPC
|
||
|
||
- `RPC-001`:使用自有严格 LF JSONL framer;不得用 Node `readline`,JSON string 内 U+2028/U+2029 不能分帧。
|
||
- `RPC-002`:command 必须带相关 ID;response 可乱序并通过 ID resolve;event 不得误配到 pending command。
|
||
- `RPC-003`:半行跨 chunk、单 chunk 多行、CRLF 尾部、malformed JSON、oversized line、stdout 非协议文字、stderr、backpressure、exit 和 timeout 都必须覆盖。
|
||
- `RPC-004`:stdout protocol violation 必须 kill 当前 worker、generation +1,并只保留有界且脱敏的诊断片段。
|
||
- `RPC-005`:只读 command 可在 generation 不变时重试一次;`prompt/steer/follow_up/fork/compact` timeout 后禁止自动重发。
|
||
|
||
### 8.4 Worker 生命周期
|
||
|
||
```text
|
||
absent -> queued -> spawning -> ready -> running
|
||
^ | |
|
||
| v v
|
||
crashed <- idle <- settling
|
||
|
|
||
evicted
|
||
```
|
||
|
||
- `RUN-001`:`prepare` 必须是 per-Conversation 幂等 single-flight。
|
||
- `RUN-002`:一个 worker 同一时间只拥有一个产品 Conversation/Pi AgentSession;不得在两个 streaming Conversation 间 `switch_session`。
|
||
- `RUN-003`:running worker 禁止 LRU eviction;idle worker 超预算按最久未使用关闭。
|
||
- `RUN-004`:worker crash/recover 后 generation 加一;所有旧 generation event、response、interaction 和 lease 必须丢弃/取消。
|
||
- `RUN-005`:app quit 先拒绝新 prompt,再给 worker 3 秒 graceful shutdown,超时 kill;退出不得无限等待。
|
||
- `RUN-006`:Session 已持久化时,idle eviction 后可以重新 spawn 和 hydrate。
|
||
- `RUN-007`:worker unexpected exit 必须取消 pending RPC、interaction、write lease 和 child subagent,但保留 session 文件。
|
||
|
||
### 8.5 初始调度预算
|
||
|
||
| 资源 | 初始产品上限 | 必须行为 |
|
||
| --- | ---: | --- |
|
||
| 同时 running top-level Conversation | 4 | 第 5 个进入自己的队列,UI 显示等待。 |
|
||
| warm idle worker | 4 | 超出后 LRU eviction。 |
|
||
| 单次 subagent dispatch task | 8 | schema validation 拒绝第 9 个。 |
|
||
| 全应用 running subagent child | 4 | 所有 parent 共用 semaphore。 |
|
||
| Pi 总进程软上限 | 8 | 4 parent + 4 child;running 不被强杀。 |
|
||
|
||
这些值是 Makelore 初始常量,不是 Pi 引擎保证,也不做用户 feature flag。只有 packaged RSS/latency 证据支持时才可在后续独立变更中调整。
|
||
|
||
### 8.6 项目写租约
|
||
|
||
- `LEASE-001`:同一项目同时最多一个 mutation tool 持有 write lease。
|
||
- `LEASE-002`:`read/grep/find/ls` 和纯推理无需 lease,可以并行。
|
||
- `LEASE-003`:受管 `write`、`edit` 及可执行任意命令的 `bash` 在执行前必须向 Main 获取 lease。
|
||
- `LEASE-004`:等待时 tool card 显示“等待项目写入”,且用户可 abort。
|
||
- `LEASE-005`:complete、error、abort、timeout、worker crash、child exit 都必须释放 lease。
|
||
- `LEASE-006`:不同项目 mutation 可以并行;同 parent 的 coding subagent 也必须遵守租约。
|
||
|
||
## 9. Makelore Pi Extension 合同
|
||
|
||
### 9.1 显式 bundle
|
||
|
||
生产包只显式加载一个版本化 Makelore extension entry。它至少注册:
|
||
|
||
| 能力 | 责任 |
|
||
| --- | --- |
|
||
| `ask_user` | select/confirm/input/editor 与 pending interaction |
|
||
| `project_write_lease` | mutation 前后申请/释放项目租约 |
|
||
| `subagent` | stable Agent manifest、single/parallel/chain、nested progress |
|
||
| `agent_browser` | Main-owned browser seam |
|
||
| `game_asset_browser/review` | 迁移现有游戏资源产品能力 |
|
||
| `task_state` | 版本化任务步骤/进度;不是 Pi core todo |
|
||
| `changed_file` | 上报受管 tool touched path |
|
||
| `runtime_context` | 项目、Conversation、Agent、knowledge、skills;不含 secret |
|
||
|
||
### 9.2 Extension → Main seam
|
||
|
||
- `EXT-001`:extension 不得直连 Renderer。
|
||
- `EXT-002`:Main 提供 loopback internal endpoint 或 child IPC bridge,并为每个 worker生成短期随机 bearer token;worker dispose 后 token 失效。
|
||
- `EXT-003`:该 endpoint 只暴露 interaction、browser、write lease、subagent permit 和受管资源读取;不得复用 Works credential。
|
||
- `EXT-004`:request 必须带 `conversationId/workerGeneration/runId`,Main 必须核对 worker registry。
|
||
- `EXT-005`:同机受管子进程边界只要求随机 bearer token + loopback;不得额外引入签名、哈希或通用远程授权框架。
|
||
- `EXT-006`:extension 误写 stdout 属于 RPC protocol violation。
|
||
|
||
### 9.3 Interaction
|
||
|
||
```ts
|
||
interface ConversationInteraction {
|
||
id: string;
|
||
conversationId: string;
|
||
runId: string;
|
||
kind: "select" | "confirm" | "input" | "editor";
|
||
title: string;
|
||
message?: string;
|
||
options?: Array<{ id: string; label: string; description?: string }>;
|
||
status: "pending" | "answered" | "rejected" | "cancelled";
|
||
}
|
||
```
|
||
|
||
- 常规 read/write/edit/bash 沿用本地协作者默认授权,不建设泛化 permission policy engine。
|
||
- 高影响操作只有在具体产品 tool 明确要求时调用 confirm。
|
||
- worker crash/abort 必须取消其 pending interactions。
|
||
- notify 投影为 toast;status/widget 只接受已注册类型;unknown details 只进入 bounded diagnostics。
|
||
|
||
### 9.4 Subagent details
|
||
|
||
Makelore MUST 自有稳定 schema,不能直接把 Pi 官方示例 details 传给 GUI:
|
||
|
||
```ts
|
||
interface SubagentDetailsV1 {
|
||
schema: "subagent.v1";
|
||
dispatchId: string;
|
||
mode: "single" | "parallel" | "chain";
|
||
tasks: Array<{
|
||
taskId: string;
|
||
agentId: string;
|
||
toolProfile: "read-only" | "coding";
|
||
status: "queued" | "running" | "complete" | "error" | "aborted" | "skipped";
|
||
summary?: string;
|
||
errorCode?: string;
|
||
usage?: PublicUsage;
|
||
}>;
|
||
}
|
||
```
|
||
|
||
- child 使用独立 Pi process/context,默认不成为可恢复的用户 Conversation。
|
||
- parent UI 只消费 `subagent.v1` 及普通嵌套 tool/message progress。
|
||
- unknown schema version 不得 dump raw JSON;显示通用不可用状态并记录诊断。
|
||
|
||
## 10. Snapshot / Patch 领域协议
|
||
|
||
### 10.1 Snapshot
|
||
|
||
```ts
|
||
interface ConversationSnapshot {
|
||
schemaVersion: 1;
|
||
conversation: {
|
||
id: string;
|
||
projectId: string;
|
||
agentId: string;
|
||
title: string;
|
||
model: ConversationModelState;
|
||
};
|
||
nodes: ConversationNode[];
|
||
run: ConversationRunState;
|
||
queue: ConversationQueueState;
|
||
context: ConversationContextState;
|
||
pendingInteractions: ConversationInteraction[];
|
||
worker: PublicWorkerState;
|
||
cursor: {
|
||
workerGeneration: number;
|
||
seq: number;
|
||
leafEntryId?: string;
|
||
};
|
||
}
|
||
```
|
||
|
||
### 10.2 Node
|
||
|
||
```ts
|
||
type ConversationNode =
|
||
| ConversationMessageNode
|
||
| ConversationToolNode
|
||
| ConversationCompactionNode
|
||
| ConversationBoundaryNode
|
||
| ConversationSubagentNode
|
||
| ConversationNoticeNode;
|
||
|
||
interface ConversationMessageNode {
|
||
kind: "message";
|
||
id: string;
|
||
sourceEntryId?: string;
|
||
clientRequestId?: string;
|
||
role: "user" | "assistant";
|
||
status: "optimistic" | "streaming" | "complete" | "error" | "aborted";
|
||
blocks: ConversationContentBlock[];
|
||
usage?: PublicUsage;
|
||
stopReason?: "stop" | "length" | "tool-use" | "error" | "aborted";
|
||
}
|
||
|
||
type ConversationContentBlock =
|
||
| { kind: "text"; id: string; text: string; status: "streaming" | "complete" }
|
||
| { kind: "thinking"; id: string; text: string; status: "streaming" | "complete" }
|
||
| { kind: "image"; id: string; attachmentId: string; mime: string };
|
||
|
||
interface ConversationToolNode {
|
||
kind: "tool";
|
||
id: string;
|
||
toolCallId: string;
|
||
toolName: string;
|
||
title: string;
|
||
inputText: string;
|
||
status: "declared" | "waiting" | "running" | "complete" | "error" | "aborted";
|
||
output: ConversationContentBlock[];
|
||
details?: KnownToolDetails;
|
||
}
|
||
```
|
||
|
||
Pi `toolResult` MUST attach to `ConversationToolNode`;它不是公开 message role,也不得生成独立 bubble。
|
||
|
||
### 10.3 Patch
|
||
|
||
```ts
|
||
type ConversationPatch =
|
||
| { op: "worker.state"; state: PublicWorkerState }
|
||
| { op: "run.state"; run: ConversationRunState }
|
||
| { op: "message.upsert"; node: ConversationMessageNode }
|
||
| { op: "message.block-delta"; messageId: string; blockId: string; delta: string }
|
||
| { op: "tool.upsert"; node: ConversationToolNode }
|
||
| { op: "compaction.upsert"; node: ConversationCompactionNode }
|
||
| { op: "subagent.upsert"; node: ConversationSubagentNode }
|
||
| { op: "queue.replace"; queue: ConversationQueueState }
|
||
| { op: "interaction.upsert"; interaction: ConversationInteraction }
|
||
| { op: "interaction.remove"; interactionId: string }
|
||
| { op: "context.replace"; context: ConversationContextState }
|
||
| { op: "snapshot.invalidated"; reason: string };
|
||
|
||
interface ConversationPatchEnvelope {
|
||
conversationId: string;
|
||
workerGeneration: number;
|
||
runId?: string;
|
||
seq: number;
|
||
at: number;
|
||
patch: ConversationPatch;
|
||
}
|
||
```
|
||
|
||
- `EVT-001`:`seq` 在单 Conversation + generation 内严格递增。
|
||
- `EVT-002`:Renderer 丢弃旧 generation;发现 gap 不猜测缺失内容,GET 目标 Conversation snapshot。
|
||
- `EVT-003`:初次订阅和重连先取 snapshot,再接 live stream;不建设永久 event log。
|
||
- `EVT-004`:live UI ID 在 `message_start` 生成,durable reconcile 只绑定 `sourceEntryId`,不得替换 UI ID。
|
||
- `EVT-005`:禁止使用 timestamp 或 array index 作为唯一 UI identity。
|
||
- `EVT-006`:live assembly 与 hydrated snapshot 必须使用同一 normalization reducer 和 fixture。
|
||
- `EVT-007`:unknown Pi event/content 默认不进入 timeline,只进入有界 diagnostics。
|
||
|
||
### 10.4 Active branch hydration
|
||
|
||
`get_entries` 不能直接按 append order 渲染。Main MUST:
|
||
|
||
1. 读取 `entries` 与当前 `leafId`。
|
||
2. 建立 `id → entry` map。
|
||
3. 从 `leafId` 沿 `parentId` 回溯到 root。
|
||
4. 反转为 active path。
|
||
5. 应用 compaction/retained-tail 规则。
|
||
6. 通过统一 reducer 生成 Snapshot。
|
||
7. 在 `agent_settled` 后用 durable entries/leaf reconcile。
|
||
|
||
废弃 branch 不进入当前 transcript;只有明确的分支历史查询可以读取。
|
||
|
||
## 11. Pi 事件映射
|
||
|
||
| Pi 输入 | 产品投影 | 规范要求 |
|
||
| --- | --- | --- |
|
||
| prompt response success | `PromptAcceptance` | 只表示接受/排队,不是完成。 |
|
||
| prompt response failure | optimistic node rejected/error | 保留 draft/附件恢复能力;不得创建 assistant。 |
|
||
| `agent_start` | run `running` | 验证/绑定预分配 `runId`。 |
|
||
| `agent_end` | low-level checkpoint | 禁止标 idle。 |
|
||
| `agent_settled` | run `idle` + durable reconcile | 唯一权威 idle。 |
|
||
| `turn_start/end` | boundary node | 不重复插入 message/tool result。 |
|
||
| user `message_start` | reconcile optimistic user | 通过 request/run 关联。 |
|
||
| assistant `message_start` | streaming assistant node | 生成稳定 UI ID。 |
|
||
| `text_start/delta/end` | text block | 按 `contentIndex` 组装;end 为权威值。 |
|
||
| `thinking_start/delta/end` | thinking block | 按 `contentIndex`,遵守展示偏好。 |
|
||
| `toolcall_start/delta/end` | declared tool + args buffer | delta 阶段不 parse JSON,end 才解析。 |
|
||
| `tool_execution_start` | tool running | 只更新 `toolCallId` 对应节点。 |
|
||
| `tool_execution_update` | replace cumulative output/details | 必须 replace,禁止 append `partialResult`。 |
|
||
| `tool_execution_end` | complete/error/aborted tool | final result 权威。 |
|
||
| assistant `message_end` | authoritative message | 替换 draft blocks/usage/stop reason。 |
|
||
| `toolResult` `message_end` | attach final result | 不生成独立 bubble。 |
|
||
| `queue_update` | queue replace | UI badge/composer 状态,不进 transcript。 |
|
||
| `compaction_start/end` | compaction node/context | `willRetry` 时 run 保持 active;summary 默认隐藏。 |
|
||
| retry events | retry state/trace | 脱敏 error,显示 attempt/delay。 |
|
||
| extension interaction | pending interaction | request ID 关联 response。 |
|
||
| notify/status/widget/title/editor text | toast/status/known widget/title/draft suggestion | draft revision 已变化时不得覆盖用户输入。 |
|
||
| `bashExecution` | direct command node | 仅产品暴露 direct command 时显示。 |
|
||
| `custom display:false` | hidden | 不显示。 |
|
||
| registered custom | typed product node | 只渲染已注册 schema。 |
|
||
| unknown custom | diagnostic | 不 dump raw details。 |
|
||
| branch/compaction summary | metadata/timeline marker | 不当 user/assistant bubble。 |
|
||
|
||
### 11.1 Usage 与 context
|
||
|
||
- `message_update.usage` 可流式显示,但最终值以 `message_end` 与 `get_session_stats` reconcile。
|
||
- session total 必须包含 compaction/branch 等有效 usage,不能只相加 assistant message。
|
||
- 刚压缩后 context 尚未可算时显示“重新计算中”,不得显示虚假的 0%。
|
||
|
||
## 12. Host API
|
||
|
||
### 12.1 路由
|
||
|
||
所有新路由使用 `/api/coding/*`:
|
||
|
||
| 路由 | 方法 | 语义 |
|
||
| --- | --- | --- |
|
||
| `/api/coding/projects` | GET | 列出项目。 |
|
||
| `/api/coding/projects/open` | POST | 打开已有目录。 |
|
||
| `/api/coding/projects/create` | POST | 创建本地项目;不启动 Pi。 |
|
||
| `/api/coding/projects/remove` | POST | 从 catalog 移除,不删除项目文件。 |
|
||
| `/api/coding/projects/active` | GET/POST | 查询/选择 active project。 |
|
||
| `/api/coding/projects/config` | GET/PUT | `.niancode/project.json`。 |
|
||
| `/api/coding/projects/knowledge` | POST | 更新项目 knowledge。 |
|
||
| `/api/coding/projects/conversations` | GET/POST | metadata;POST 不启动 Pi。 |
|
||
| `/api/coding/conversations/:id` | GET/PATCH/DELETE | metadata 与受管 session lifecycle。 |
|
||
| `/api/coding/conversations/:id/snapshot` | GET | active branch 完整 Snapshot。 |
|
||
| `/api/coding/events` | GET/SSE | 全局有序 envelope stream;seq 按 Conversation。 |
|
||
| `/api/coding/conversations/:id/prompt` | POST | prompt/steer/follow-up;只等待 acceptance。 |
|
||
| `/api/coding/conversations/:id/abort` | POST | abort 目标 Conversation。 |
|
||
| `/api/coding/conversations/:id/model` | POST | 目标 Conversation model。 |
|
||
| `/api/coding/conversations/:id/thinking` | POST | 目标 Conversation thinking level。 |
|
||
| `/api/coding/conversations/:id/compact` | POST | 手动压缩。 |
|
||
| `/api/coding/conversations/:id/fork` | POST | 从 user entry 创建新产品 Conversation。 |
|
||
| `/api/coding/conversations/:id/recover` | POST | 重新创建 worker 并 hydrate。 |
|
||
| `/api/coding/conversations/:id/commands` | GET | Pi command + Makelore command 安全投影。 |
|
||
| `/api/coding/interactions` | GET | 当前 pending interactions。 |
|
||
| `/api/coding/interactions/:id/respond` | POST | 回答/拒绝 interaction。 |
|
||
| `/api/coding/skills` | GET | 产品 skill registry。 |
|
||
| `/api/coding/files/status` | GET | 项目文件状态。 |
|
||
| `/api/coding/files/find` | GET | 文件查找。 |
|
||
| `/api/coding/files/content` | GET | 文件内容预览。 |
|
||
| `/api/coding/search` | GET | 文本搜索。 |
|
||
| `/api/coding/runtime/diagnostics` | GET | 脱敏 worker pool/resource summary。 |
|
||
|
||
### 12.2 Prompt request/acceptance
|
||
|
||
```ts
|
||
interface PromptConversationInput {
|
||
clientRequestId: string;
|
||
conversationId: string;
|
||
mode: "prompt" | "steer" | "follow-up";
|
||
text: string;
|
||
attachments: Array<{ attachmentId: string }>;
|
||
}
|
||
|
||
interface PromptAcceptance {
|
||
accepted: true;
|
||
conversationId: string;
|
||
clientRequestId: string;
|
||
runId: string;
|
||
mode: "prompt" | "steer" | "follow-up";
|
||
queuePosition?: number;
|
||
}
|
||
```
|
||
|
||
- `API-001`:接受时返回 HTTP `202`;产品校验未通过时返回 typed 4xx,且 `accepted` 不得伪造为 true。
|
||
- `API-002`:HTTP 不等待 `agent_settled`、provider first token 或完整 assistant message。
|
||
- `API-003`:同一进程内重复 `clientRequestId` 若已明确 accepted,应返回相同 acceptance;状态不确定时不得自动发送第二次 prompt。
|
||
- `API-004`:请求 attachment 只引用 Main-owned attachment ID;Renderer 不提交任意本地路径。
|
||
- `API-005`:SSE/event stream 断线不得导致 accepted prompt 自动重发。
|
||
|
||
### 12.3 错误合同
|
||
|
||
Renderer 只接收稳定 code、可操作状态和固定中文提示。至少覆盖:
|
||
|
||
| code | 场景 | 可恢复性 |
|
||
| --- | --- | --- |
|
||
| `CODING_RUNTIME_START_FAILED` | child spawn 失败 | 手动 recover |
|
||
| `CODING_RUNTIME_READY_TIMEOUT` | 10 秒未 ready | kill 后 recover |
|
||
| `CODING_RUNTIME_PROTOCOL_ERROR` | stdout/RPC 违规 | kill 后 recover |
|
||
| `CODING_PROVIDER_AUTH_REQUIRED` | credential 不可用/刷新一次仍失败 | 修复账号后 recover |
|
||
| `CODING_MODEL_UNAVAILABLE` | model 不在 catalog | 选择模型 |
|
||
| `CODING_SESSION_UNREADABLE` | session JSONL 无法读取 | 保留文件,新建/诊断 |
|
||
| `CODING_STORAGE_WRITE_FAILED` | product/session persistence 失败 | 停止新 prompt,修复存储 |
|
||
| `CODING_REQUEST_UNCERTAIN` | mutating RPC timeout 后无法确认 | 用户显式 reconcile/retry |
|
||
| `CODING_MIGRATION_MODEL_REQUIRED` | 旧 model 无唯一映射 | 用户选择模型 |
|
||
|
||
原始 stderr、provider body、credential、账号 ID、完整路径、stack 和 extension path 不得穿透 Renderer。
|
||
|
||
## 13. Project File、Changes 与 Browser
|
||
|
||
### 13.1 Project File Service
|
||
|
||
- 文件 status/find/content/search 从 OpenCode route 移入 Main-owned product service。
|
||
- 所有路径必须相对 active project 解析;Renderer 不获得绝对项目根。
|
||
- 行为保持现有受支持功能,不因 runtime 切换扩展为任意文件系统代理。
|
||
|
||
### 13.2 `ConversationChangeTracker`
|
||
|
||
- `CHG-001`:run start 记录项目基线状态和当前 git head(若存在)。
|
||
- `CHG-002`:受管 write/edit tool 通过 `changed_file` 上报 touched relative path。
|
||
- `CHG-003`:bash 可能修改任意文件,因此 settled 后执行一次项目级 git status/diff refresh。
|
||
- `CHG-004`:只读取 changed path 的 diff;untracked 只提供受限 preview。
|
||
- `CHG-005`:结果进入 Conversation changes panel,不写入 Pi message/session。
|
||
- `CHG-006`:不得为每个文件增加哈希/checkpoint 框架;若将来需要文件回滚,另立 git checkpoint 设计。
|
||
|
||
### 13.3 Browser
|
||
|
||
```text
|
||
Pi tool agent_browser
|
||
-> Makelore extension
|
||
-> Main browser service
|
||
-> bounded structured result / screenshot attachmentId
|
||
```
|
||
|
||
- 不启动 Playwright MCP server,不维护 MCP discovery/stdio/second protocol。
|
||
- screenshot 经 Main attachment service 投影,不把大 base64 反复传给 Renderer。
|
||
- browser 继续遵守现有 Main-owned browser seam 的边界。
|
||
|
||
## 14. 渲染与内存性能
|
||
|
||
- `REN-001`:Main 按 16–33 ms 窗口合并 text/thinking delta。
|
||
- `REN-002`:Renderer 只更新当前 Conversation 的目标 node/block;禁止每 token 替换整个 transcript。
|
||
- `REN-003`:tool cumulative partial output 必须 replace;禁止重复拼接。
|
||
- `REN-004`:tool-call args 在 end 前只作为字符串 buffer。
|
||
- `REN-005`:大图片/base64 转成 Main-owned attachment ID/preview URL;SSE/Zustand 不反复复制 base64。
|
||
- `REN-006`:隐藏 Conversation 只更新轻量 run/unread summary,不做 Markdown 全量重渲染。
|
||
- `REN-007`:Markdown、thinking 和大 tool output 使用 memoized block;超长输出默认折叠或虚拟化。
|
||
- `REN-008`:压力 fixture 必须记录 Main patch 数、Renderer commit 数和 IPC/SSE bytes,证明 batching 生效。
|
||
|
||
## 15. 一次性迁移与旧数据政策
|
||
|
||
### 15.1 首次迁移
|
||
|
||
打开 schema v1 项目时,Main MUST 按顺序执行:
|
||
|
||
1. 在 `.niancode/migration-backups/opencode-cutover-<timestamp>/` 复制原 `project.json` 与 `conversations.json`。
|
||
2. 通过 Provider Service 把旧 Agent model string 唯一映射为结构化 model ref。
|
||
3. 不能唯一映射的 Agent 标记 model required,保留其他字段。
|
||
4. 原子写 project schema v2,保持 Agent identity、prompt、skills、archive。
|
||
5. 写空的 Conversation schema v2;旧 OpenCode Session 不进入新 sidebar。
|
||
6. 显示一次性说明:“旧版 OpenCode 对话不能在 Pi 中继续,原数据已保留用于回退或独立导出。”
|
||
7. 成功后写明确 schema version;失败时不覆盖 backup 或原始可恢复数据。
|
||
|
||
- `MIG-001`:迁移是一次性、显式版本分支,不建设通用 migration framework。
|
||
- `MIG-002`:迁移前必须备份;无备份时不得覆写 schema v1。
|
||
- `MIG-003`:旧 Conversation 不显示、不继续、不被 Pi release 读取。
|
||
- `MIG-004`:如果产品需要旧聊天可读,必须在仍含 OpenCode 的旧版本中另做 read-only exporter;Pi release 不保留 OpenCode client/daemon。
|
||
|
||
### 15.2 `.opencode` 项目内容
|
||
|
||
- 只删除 materialization metadata 能证明是 Makelore 生成且未修改的 `.opencode/agent` 文件。
|
||
- 用户修改过或所有权不确定的生成文件移到 migration backup。
|
||
- 项目中其他 `.opencode` 内容不删除,但新应用永不读取。
|
||
- 不创建 `.pi/agents` 替代。
|
||
|
||
### 15.3 userData
|
||
|
||
`<userData>/opencode` 在首个 Pi release 保持原地、惰性且不读取。不得自动递归删除。它只支持完整应用版本回退或人工导出,不代表兼容层。
|
||
|
||
### 15.4 版本回退
|
||
|
||
1. 发布 Pi 版本前保留上一版安装包和 release tag。
|
||
2. schema migration backup 是回退旧 metadata 的唯一输入。
|
||
3. 阻断问题发生时安装上一版,并按 runbook 恢复 backup。
|
||
4. Pi 新建 Conversation 不能在 OpenCode 版本继续,release note 必须明确。
|
||
5. 普通项目文件不自动回滚;用户自行使用 VCS。
|
||
|
||
## 16. 打包和运行时交付
|
||
|
||
### 16.1 依赖
|
||
|
||
- `PKG-001`:删除精确版本 `opencode-ai`、`@opencode-ai/plugin` 和只为 OpenCode MCP 存在的 `@playwright/mcp`。
|
||
- `PKG-002`:`@earendil-works/pi-coding-agent` 必须精确 pin;禁止 `^`/`~`。
|
||
- `PKG-003`:lockfile、许可清单、runtime manifest 和 diagnostics 记录同一精确版本。
|
||
- `PKG-004`:目标 Pi Node engine 必须与每个平台 bundled Electron Node 实测兼容。
|
||
|
||
### 16.2 Staging
|
||
|
||
`scripts/bundle-pi-runtime.mjs` 或等价脚本 MUST 从 frozen lockfile/已安装依赖图生成 `build/pi-runtime`,包含:
|
||
|
||
- Pi 运行所需 dist/resource;
|
||
- 全部 production dependencies;
|
||
- 当前平台 required optional dependencies;
|
||
- WASM/native 资源;
|
||
- Makelore extension bundle;
|
||
- 内建 coding skills;
|
||
- runtime manifest:package、version、entry、Node engine、resource paths。
|
||
|
||
构建阶段禁止在线安装或自动更新 Pi。不得只复制顶层 package。
|
||
|
||
### 16.3 electron-builder 与 artifact verifier
|
||
|
||
- 删除 `build/opencode-ai` 与 `.opencode/skills` 打包映射。
|
||
- 增加 `build/pi-runtime → pi-runtime`。
|
||
- native/WASM 使用正确 `extraResources` 或 `asarUnpack`。
|
||
- 保持 appId、productName、protocol 和现有产品路径。
|
||
|
||
最终 artifact verifier 必须证明:
|
||
|
||
1. OpenCode package/binary/plugin 不存在。
|
||
2. Pi manifest、package.json 与 lockfile 版本一致。
|
||
3. CLI/RPC entry 可由 Electron Node 加载。
|
||
4. production closure 全部 resolve。
|
||
5. Node engine 相容。
|
||
6. extension/skills paths 存在。
|
||
7. 临时空项目 `get_state` 成功。
|
||
8. 无 credential protocol smoke 可启动/退出;四协议受控 loopback/provider-shaped endpoint 使用环境凭据引用完成 prompt、settled、双 worker overlap 与 abort isolation。真实外部 Provider Account 不属于 release hard gate,`realTurnVerified` 保持 `false`。
|
||
9. Windows、macOS x64/arm64、Linux artifact 无开发机绝对路径。
|
||
|
||
`PI-000` 的 macOS waiver 到 `PI-150` 即失效。`PI-150` 必须分别在 macOS
|
||
x64 与 arm64 上以独立 checkout/frozen install 运行 workspace 5+5、staged
|
||
5+5、controlled packaged 5+5、packaged loopback、closure/native/resource
|
||
检查和 metrics,再对最终目标 artifact 运行 verifier/smoke。静态 darwin path
|
||
test、其他平台回归或 Phase-0 waiver 均不能替代 macOS 实跑证据。
|
||
|
||
### 16.4 Real runtime smoke
|
||
|
||
`smoke:pi:real` 中的 `real` 指最终 artifact 内实际 Pi runtime/process seam,不指真实外部 Provider Account。它必须通过受控 loopback/provider-shaped endpoint 覆盖:spawn、`get_state`、persistent session create/open、prompt accepted、text/tool projection、abort、settled、restart/reopen hydration、两个 worker 同时 run、一个 subagent child、clean shutdown。
|
||
|
||
## 17. 可观测性与性能预算
|
||
|
||
### 17.1 Privacy-safe milestones
|
||
|
||
每次 cold/warm 首发记录:
|
||
|
||
| milestone | 含义 |
|
||
| --- | --- |
|
||
| `conversation.local_create` | 产品 Conversation metadata 写入 |
|
||
| `composer.interactive` | 选择到 Textarea 可编辑 |
|
||
| `worker.queue_wait` | worker permit 等待 |
|
||
| `worker.spawn` | child spawn |
|
||
| `rpc.ready` | 首次 `get_state` 成功 |
|
||
| `session.open` | Pi session create/open |
|
||
| `resources.ready` | provider/prompt/skills/extensions ready |
|
||
| `prompt.accepted` | RPC prompt response success |
|
||
| `agent.start` | accepted 到 `agent_start` |
|
||
| `provider.first_event` | agent start 到首 provider event |
|
||
| `renderer.first_commit` | Main 首 patch 到 Renderer commit |
|
||
| `agent.settled` | accepted 到权威 idle |
|
||
|
||
- `OBS-001`:记录匿名 Conversation 局部短 ID、workerGeneration、runId、cold/warm 和 duration。
|
||
- `OBS-002`:不得记录 prompt、文件内容、credential、完整路径、账号 ID、完整 request ID 或原始 provider body。
|
||
- `OBS-003`:本地 runtime overhead 与 provider first-event 延迟必须分开。
|
||
- `OBS-004`:报告 p50/p95/max、样本数和平台/packaged 状态,不只报单次体验。
|
||
|
||
### 17.2 发布候选预算
|
||
|
||
| 指标 | p95 目标 |
|
||
| --- | ---: |
|
||
| 创建项目本地 metadata | ≤ 1,000 ms |
|
||
| 创建伙伴本地 metadata | ≤ 500 ms |
|
||
| 创建空 Conversation metadata | ≤ 500 ms |
|
||
| 选择新 Conversation → Composer 可编辑 | ≤ 500 ms |
|
||
| warm worker `rpc.ready` | ≤ 1,500 ms |
|
||
| cold worker `rpc.ready` | ≤ 3,000 ms |
|
||
| warm prompt accepted | ≤ 250 ms |
|
||
| cold prompt accepted | ≤ 3,000 ms |
|
||
| Main delta → Renderer commit | ≤ 50 ms |
|
||
|
||
Operational limits:metadata Host API 5 秒;spawn/RPC ready 10 秒;graceful shutdown 3 秒;stream batching 16–33 ms。
|
||
|
||
这些预算只衡量本地 runtime/GUI overhead。受控 provider-shaped first event 单独报告,不得用它掩盖本地超时,也不得把它计入本地 p95。若未来自愿收集真实外部 Provider first token 数据,只能作为非阻断诊断样本,不改变本次 Accepted Risk。
|
||
|
||
### 17.3 必测场景
|
||
|
||
1. 全新 userData:创建项目、首伙伴、首 Conversation,不发送,测 Composer。
|
||
2. 首次 prompt:拆分 spawn/RPC/session/resources/accepted/provider/first commit。
|
||
3. 重启同一 Conversation,测 warm restore。
|
||
4. 两个不同项目 Conversation 同时 prompt,通过最终 packaged provider-shaped loopback 验证窗口重叠、事件路由和单侧 abort 隔离;不要求真实外部 Provider turn。
|
||
5. 同项目两个 Conversation 同时只读 tool,验证并行。
|
||
6. 同项目两个 mutation tool,验证 lease 串行和可取消。
|
||
7. parent 并行四个只读 subagent,同时另一个 Conversation 运行,验证全局 cap。
|
||
8. 100 KB cumulative tool output、长 thinking、混合 blocks,测 patch/commit/bytes。
|
||
9. 大图片输入,确认 SSE/Renderer 不重复搬运 base64。
|
||
10. worker crash、SSE reconnect、app quit,确认恢复与进程清理。
|
||
|
||
## 18. 故障与恢复
|
||
|
||
| 故障 | 用户状态 | 自动动作 | 禁止行为 |
|
||
| --- | --- | --- | --- |
|
||
| spawn 失败 | 本地 Agent 启动失败;Composer 可编辑 | 手动 recover | 无限重试/禁用整个 Chat |
|
||
| RPC ready timeout | worker failed | kill、generation +1 | 复用未知 child |
|
||
| malformed stdout | worker crashed | 有界诊断、kill/recover | 当 assistant 文本 |
|
||
| mutating RPC timeout | request uncertain | reconcile session/queue | 静默重发 prompt |
|
||
| provider 429/5xx | retry banner | Pi 有界 retry | Main 叠加无界 retry |
|
||
| credential expired | auth error | refresh once + reopen | 输出 credential |
|
||
| extension error | 对应 tool/run error | 隔离目标 Conversation | 杀所有 worker |
|
||
| worker exit | recoverable | 取消 RPC/interaction/lease/child | 接收旧 generation event |
|
||
| SSE gap | reconnect | GET 目标 snapshot | 清空所有 Conversation |
|
||
| unreadable session | recoverable | 保留文件、允许诊断/新建 | 自动截断/覆盖 |
|
||
| disk write failure | persistence error | 停止目标新 prompt | 继续生成并假装持久化 |
|
||
| write lease wait | tool waiting | 可 abort | 并发 mutation |
|
||
|
||
`recover` MUST:关闭旧 generation → cancel pending 资源 → 3 秒有界退出/kill → 从 registry 读取 sessionKey → 用最新 provider/resource revision spawn → `get_state/get_entries` hydrate → 发布新 generation Snapshot。不得自动重放未确认 prompt。
|
||
|
||
## 19. 验证合同
|
||
|
||
### 19.1 Unit
|
||
|
||
- RPC framing、correlation、protocol violation、timeout、generation invalidation。
|
||
- event projector 的 text/thinking/toolcall、cumulative tool、`toolResult`、`agent_settled`、retry、compaction、queue、interaction、unknown custom。
|
||
- active leaf/parent hydration、abandoned branch、compaction retained tail、live/durable ID reconcile。
|
||
- worker single-flight、caps、LRU、crash cleanup、quit、provider revision。
|
||
- extension explicit resources、interaction cancel、subagent modes/caps/abort、write lease、known schema versions。
|
||
- schema v1 backup/migration、unresolved model、stable Agent ID、atomic failure。
|
||
|
||
### 19.2 Main integration
|
||
|
||
使用可编程 fake Pi child 输出锁定版本 fixture,覆盖 Host acceptance → SSE patches → snapshot reconcile、gap/reconnect、provider redaction/model、persistence/reopen/fork/delete、files/changes、crash/recover。另有少量 real Pi smoke 防止 fixture 漂移。
|
||
|
||
### 19.3 Renderer
|
||
|
||
- metadata 请求 pending 时 selected Agent 仍可输入。
|
||
- 首发 optimistic/reconcile。
|
||
- per-Conversation run/error/queue/model/draft 隔离。
|
||
- message/tool/thinking/compaction/interaction/subagent 映射。
|
||
- seq gap 只刷新目标 Conversation。
|
||
- cumulative tool output 不重复。
|
||
- hidden Conversation 不重渲染全文。
|
||
|
||
### 19.4 Electron E2E
|
||
|
||
至少覆盖:
|
||
|
||
1. 首项目/伙伴/Conversation Composer readiness;
|
||
2. 首 prompt cold path;
|
||
3. 两个 Conversation 同时 streaming;
|
||
4. 模型切换只影响一个 Conversation;
|
||
5. steer/follow-up queue;
|
||
6. compaction retry + settled;
|
||
7. interaction select/confirm/input;
|
||
8. parallel subagent nested UI;
|
||
9. worker crash/recover;
|
||
10. image attachment;
|
||
11. packaged skill/browser tool;
|
||
12. old schema migration notice。
|
||
|
||
### 19.5 必跑命令
|
||
|
||
实现完成后必须运行仓库 pin 的 pnpm:
|
||
|
||
```text
|
||
pnpm run typecheck
|
||
pnpm run lint:check
|
||
pnpm test
|
||
pnpm run build:vite
|
||
pnpm run test:e2e
|
||
```
|
||
|
||
另外必须运行 Pi artifact verifier、real runtime smoke、Windows/macOS x64/arm64/Linux packaged smoke 和第 17 节性能场景。任何未运行项必须阻止 release,不得写成“本地测试通过即可替代”。这里的 real runtime smoke 按第 16.4 节使用实际 packaged Pi + 受控 provider-shaped endpoint,不要求真实外部 Provider Account。`PI-000` 的 macOS waiver 不延续到此发布门;macOS x64 或 arm64 任一缺失时均不得宣称 cross-platform release-ready。
|
||
|
||
## 20. Definition of Done
|
||
|
||
### 20.1 架构
|
||
|
||
- `CUT-*`、`BND-*`、`ARC-*` 全部满足。
|
||
- Main 有 vendor-neutral `CodingConversationRuntime` 和唯一生产 `PiConversationRuntime`。
|
||
- Renderer 不导入或解析任何 Pi wire 类型。
|
||
- `.niancode/project.json`、稳定 Agent ID 和通用 app/storage 标识保持。
|
||
|
||
### 20.2 能力
|
||
|
||
- 文本、图片、thinking、tool、retry、compaction、queue、interaction、模型/thinking、file/changes、browser、skills 全部通过测试与 E2E。
|
||
- 两个独立 Conversation worker 通过最终 packaged provider-shaped loopback turn 重叠且受控状态不串线;真实外部 Provider 隔离仍是明确 Accepted Risk,不得宣称已验证。
|
||
- subagent single/parallel/chain、child abort、失败隔离和全局 cap 正常。
|
||
- 同项目 mutation 受写租约控制。
|
||
- share、revert/unrevert、todos、global runtime controls 已从 UI、route、type、test、文档删除。
|
||
|
||
### 20.3 性能
|
||
|
||
- 第 17 节 p95 预算全部达标。
|
||
- 不存在分钟级无界 Composer disabled/loading。
|
||
- 100 KB tool/长 thinking/大图片压力场景证明 batching、局部更新和 attachment ref 生效。
|
||
- 4 parent + 4 child 场景下主 UI 保持可交互,RSS/CPU 数据进入发布报告。
|
||
|
||
### 20.4 迁移与交付
|
||
|
||
- schema v1 backup、schema v2、unresolved model、一次性 notice、完整版本回退 runbook 通过。
|
||
- package/lock/artifact 只含精确 Pi runtime,OpenCode 为零。
|
||
- typecheck、lint、unit、build、E2E、实际 packaged Pi + provider-shaped loopback、Windows x64/Linux x64/macOS x64/macOS arm64 packaged 与性能验证全部通过;`PI-000` 的 macOS waiver 不构成此处证据,真实外部 Provider waiver 也不得被写成 Pass。
|
||
- README、accepted ADR、canonical architecture/current state/business rules/success criteria 和 superseded OpenCode commitments 由 Integration Gate 更新。
|
||
- release note 明确旧 Conversation 不继续、已删除能力和回退限制。
|
||
|
||
### 20.5 零残留
|
||
|
||
下列搜索只允许命中历史 task/proposal、migration backup/notice 文案:
|
||
|
||
```text
|
||
rg -i "opencode|@opencode-ai|/api/opencode|\.opencode" .
|
||
```
|
||
|
||
`package.json`、lockfile、生产源码、`dist-electron`、`build/pi-runtime`、ASAR/extraResources 和最终安装产物不得命中 OpenCode package、binary、plugin、route 或 runtime resource。
|
||
|
||
## 21. 需求到票据追踪
|
||
|
||
| 需求组 | 主交付票据 |
|
||
| --- | --- |
|
||
| `QG-*`(Phase-0 Windows x64/Linux x64;`QG-004`/`QG-005` explicit waivers) | `PI-000` |
|
||
| Phase-0 延期的 macOS x64/arm64 workspace/staged/controlled packaged/loopback/metrics | `PI-150` |
|
||
| `BND-*` | `PI-010`, `PI-020`, `PI-100`, `PI-110`, `PI-140` |
|
||
| `ARC-*`, `EVT-*` contracts | `PI-010`, `PI-060` |
|
||
| `DAT-*`, `MIG-*` schema/migration | `PI-020`, `PI-140` |
|
||
| `RPC-*`, `RUN-*` process foundation | `PI-030`, `PI-050` |
|
||
| `PRV-*`, `RES-*` | `PI-040` |
|
||
| event mapping/hydration/recovery | `PI-060` |
|
||
| `EXT-*`, `LEASE-*`, interaction | `PI-070` |
|
||
| `UX-*` overall | `PI-020`, `PI-050`, `PI-080`, `PI-100`, `PI-120`, `PI-130`, `PI-150` |
|
||
| `UX-040` true Conversation concurrency | `PI-050`, `PI-150` |
|
||
| `UX-041`~`UX-044`, `subagent.v1` | `PI-080`, `PI-130` |
|
||
| `CHG-*`, browser/game/skills/changes | `PI-090`, `PI-105`, `PI-130` |
|
||
| `API-*` and `/api/coding` | `PI-100`, `PI-105` |
|
||
| Snapshot/Patch Renderer store | `PI-110` |
|
||
| `UX-001`~`UX-005` local-only creation | `PI-020`, `PI-100`, `PI-120` |
|
||
| `UX-010`~`UX-033` Conversation UX | `PI-100`, `PI-120`, `PI-130` |
|
||
| `REN-*` | `PI-120`, `PI-150` |
|
||
| model/queue/compaction/interaction/subagent/changes UI | `PI-130` |
|
||
| `CUT-*`, OpenCode deletion, release migration notice | `PI-140`, `PI-160` |
|
||
| `PKG-*`, `OBS-*`, performance/cross-platform verification | `PI-150` |
|
||
| 全部 DoD、独立审阅、canonical promotion | `PI-160` |
|
||
|
||
依赖、ready frontier、文件所有权和每张票据验收条件见关联票据文档。
|