状态:设计稿,未实现。目标:同一个 Prometheus 会话界面、同一套插件和工具,可以选择由 PI、Claude Code 或 Codex 驱动,并且网关可以用 runtime 自带的,也可以用 Prometheus 的。
在任意 runtime 下,下面这些都必须成立。做不到的,必须由能力声明显式降级(UI 置灰并给出原因),不能静默失效:
- 现有 Web UI / TUI 的消息流、工具卡片、diff、bash 输出、todo 面板、子智能体面板不需要为某个 runtime 写特判。
- Prometheus 的所有插件工具(记忆、知识库、todo、SSH、connectors、subagent、open_file、插件安装……)在三个 runtime 里都能调用,并且行为一致。
- 每轮的上下文注入(记忆、知识库、模式、权限提示、技能索引)在三个 runtime 里都生效。
- 权限只有一套:同一个
decide()、同一个审批弹窗、同一个"总是允许"规则集,不会重复弹窗,也不会漏拦。 - 面罩(veil)的脱敏和出口代理,在 Prometheus 网关模式下对所有 runtime 都生效。
- 会话可以列出、改名、恢复、中止、统计用量,自动化(无人值守)运行也能用。
- 有一套一致性测试,用同一组场景跑三个 runtime,靠它保证上面几条,而不是靠人工点。
| 位置 | 耦合方式 |
|---|---|
core/src/host.ts |
createAgentSession / AgentSession,直接用于 prompt、abort、compact、模型、thinking、fork、树导航、用量统计 |
core/src/events.ts |
把 PI 的 AgentSessionEvent 转成线上的 SessionEvent;ChatMessage 本身就是 PI 的消息形状 |
core/src/permission-gate.ts |
通过 PI 的 tool_call hook 拦截;exit_plan_mode 是一个 PI 工具 |
core/src/ui-bridge.ts |
实现 PI 的 ExtensionUIContext |
| 插件 | 全部通过 ctx.extension((pi) => …) 接入 |
插件实际用到的 PI API 很少(统计自 plugins/*、packages/*):
| API | 次数 | 使用方 | 在外部 runtime 下的去处 |
|---|---|---|---|
registerTool |
22 | 几乎所有插件 | 工具总线(§4) |
on("before_agent_start") 写 sections/guidelines |
7 | memory、knowledge、connectors、workbench、permissions、modes | 上下文提供者(§5) |
before_agent_start 返回 systemPrompt |
1 | modes(replace/strict) | 整段替换 system prompt(§5.3) |
on("session_start" / "session_shutdown") |
3 | modes、permissions、subagents | 会话生命周期钩子 |
on("tool_call") |
1 | permissions | 统一权限(§6) |
getAllTools / getActiveTools / setActiveTools |
4 | modes、permissions | 工具白名单(§4.5) |
registerMcpServer |
1 | mcp | 经工具总线转出,或直接传给 runtime(§4.6) |
registerProvider |
2 | 网关、veil | 网关中继(§7) |
on("before_provider_request" / "before_provider_headers") |
2 | veil | 网关中继的请求钩子(§7) |
on("resources_discover") |
1 | skills | 技能索引注入(§5.4) |
sendMessage(triggerTurn, followUp) |
1 | subagents(后台完成通知) | runtime.enqueue(followUp) |
registerCommand |
2 | — | 由 Prometheus 宿主解析 /命令(§8.3) |
工具上下文里的 ctx.ui.*、ctx.cwd、hasUI |
— | — | 总线把同一个 UIBridge 交给工具(§4.2) |
结论:难点不在工具调用本身,而在 上下文注入、权限、工具白名单、provider 钩子 这四类。下面逐个给出方案。
Web UI / TUI (协议不变:SessionEvent + ChatMessage + UIRequest)
│
PrometheusHost(会话索引、RPC)
│ AgentRuntime 接口
┌────────────────────────┼─────────────────────────┐
PiRuntime ClaudeCodeRuntime CodexRuntime
(进程内 AgentSession) (Claude Agent SDK, 子进程) (codex app-server, JSON-RPC/stdio)
│ │ │
│ ┌───────────────┴──────────────┬───────────┘
│ │ Prometheus 工具总线 │ 网关中继(可选)
│ │ MCP Streamable HTTP │ 127.0.0.1,veil 钩子,鉴权注入
│ │ /mcp/<session> (token) │ 格式转换(后期)
│ └──────────────┬───────────────┘
└── 共享:插件工具注册表、上下文提供者、PermissionGate、UIBridge、ConfigStore
原则:
- 线上协议不变。 适配器负责把各 runtime 的事件转成现有的
SessionEvent/ChatMessage。这样前端零改动,pi 的行为就是金标准。 - Prometheus 自己的能力只实现一次。 工具、上下文、权限、UI 弹窗都放在 runtime 之外,各 runtime 只是接入。
- 每个 runtime 用它最原生、最完整的接口。 Claude Code 用 Agent SDK(流式输入、canUseTool、hooks、resume/fork、interrupt、setPermissionMode)。Codex 用
codex app-server(JSON-RPC:thread/turn、审批请求、中断、事件流;TS 类型由codex app-server generate-ts生成,并锁定版本)。现有的cli-runner.ts(-p/exec --json一次性模式)只保留给子智能体。
// packages/protocol: 线上可见的部分
export type RuntimeKind = "pi" | "claude-code" | "codex";
export interface RuntimeCapabilities {
steering: boolean; // 回合中插话
followUpQueue: boolean;
compaction: "manual+auto" | "auto" | "none";
fork: boolean;
treeNavigation: boolean; // 仅 pi
thinkingLevels: ThinkingLevel[];
images: boolean;
liveToolListChange: boolean; // 会话中途增减工具
systemPromptReplace: "live" | "restart" | "none";
contextBreakdown: boolean; // 精确的上下文分项
permissionModes: PermissionMode[];
nativeTools: CanonicalToolKind[];
}
// packages/core: 宿主侧
export interface AgentRuntime {
readonly kind: RuntimeKind;
probe(): Promise<RuntimeHealth>; // 已安装?版本?已登录?
listModels(gw: GatewayChoice): Promise<ModelInfo[]>;
create(opts: SessionInit): Promise<RuntimeSession>;
resume(ref: SessionRef, opts: SessionInit): Promise<RuntimeSession>;
}
export interface RuntimeSession {
readonly id: string; // Prometheus 会话 id
readonly nativeId: string; // runtime 自己的会话/线程 id
readonly capabilities: RuntimeCapabilities;
prompt(input: UserInput, delivery: "send" | "steer" | "followUp"): Promise<Disposition>;
abort(): Promise<void>;
compact?(instructions?: string): Promise<void>;
fork?(atMessage?: string): Promise<SessionRef>;
setModel(m: ModelRef): Promise<void>;
setThinking(l: ThinkingLevel): Promise<void>;
setPermissionMode(m: PermissionMode): Promise<void>;
refreshTools(): Promise<void>; // 插件热装或模式切换后调用
messages(): ChatMessage[]; // 规范化后的对话记录
usage(): SessionUsage;
subscribe(fn: (e: SessionEvent) => void): () => void;
dispose(): Promise<void>;
}
export interface SessionInit {
cwd: string;
gateway: GatewayChoice; // { mode: "native" } | { mode: "prometheus", gatewayId, model }
permission: PermissionGate; // 共享的同一套
ui: UIBridge;
tools: ToolBusSession; // 本会话的总线端点
context: ContextComposer; // 上下文提供者的合成器
unattended?: RunAccess;
}PiRuntime 是第一个实现,由 host.ts 现有逻辑原样搬过去(纯重构)。host.ts 只保留会话索引、RPC 分发、工作区。
- 原生记录是恢复的唯一依据。 PI 的 session 文件、Claude Code 的
~/.claude/projects/*.jsonl、Codex 的 rollout 文件,由各 runtime 自己负责。 - Prometheus 新增 会话索引
<data>/sessions/index.json,记录{ id, runtime, nativeId, cwd, name, model, gateway, createdAt, modified }。会话列表、改名、归档都只读写这个索引,不依赖各 runtime 的列表能力。 - 另外维护一份 规范化的镜像记录(
ChatMessage[]的 jsonl),用于界面首屏、搜索和用量统计(usage.stats现在直接解析 PI 的 session 文件,以后改为读镜像)。镜像只追加,与原生记录不一致时,以原生记录为准重建。 - 不支持切换已有会话的 runtime。后期可以提供"在另一个 runtime 中继续":把记录摘要后开一个新会话。
Prometheus 在本机开一个 MCP Streamable HTTP 服务(127.0.0.1:随机端口),每个会话一个端点 /mcp/<sessionId>,用一次性 bearer token 鉴权(通过环境变量交给子进程,不写入磁盘配置)。
为什么选 HTTP,而不是 Claude Agent SDK 的进程内 createSdkMcpServer:
- 两个外部 runtime 共用一份实现。
- 插件工具的参数本来就是 JSON Schema,可以直接透传;进程内方式要求 zod。
- 端点按会话区分,工具执行时天然拿到正确的
cwd、sessionId、UIBridge、signal。
接入方式:
- Claude Code:
mcpServers: { prometheus: { type: "http", url, headers: { Authorization } } },并设置strictMcpConfig,避免和用户自己的同名配置冲突。 - Codex:thread 启动时传
-c mcp_servers.prometheus.url=…,并配bearer_token_env_var=PROMETHEUS_MCP_TOKEN。
| PI 工具语义 | MCP 映射 |
|---|---|
execute(id, params, signal, onUpdate, ctx) |
tools/call;signal 对应 MCP 取消通知;onUpdate 对应 notifications/progress |
返回 content[](text/image) |
MCP content(text/image) |
返回 details(UI 用的结构化数据,比如 todo) |
MCP structuredContent,并且总线自己发 tool_execution_* 事件(见 4.3) |
ctx.ui.confirm/select/input |
总线把该会话的 UIBridge 交给工具,弹窗走原有的 ui.request 通道 |
ctx.cwd / hasUI |
从会话上下文取;hasUI = !unattended |
annotations.readOnlyHint |
MCP tool annotations,原样透传 |
promptSnippet / promptGuidelines |
不进 MCP description,进静态上下文(§5),保证三个 runtime 的提示一致 |
同一次 Prometheus 工具调用,runtime 会报一遍(CC 的 tool_use,Codex 的 mcp_tool_call item),总线也知道一遍。规则如下:
- 总线是 Prometheus 工具的权威来源。
args、partialResult、result、details、durationMs以总线为准。适配器按toolCallId关联,runtime 侧的同一调用只用来定位它在消息里的位置。关联方式:CC 的tool_use_id会出现在 MCP 请求的_meta里吗?这个需要 spike;如果没有,就用"会话内按顺序加参数哈希"匹配。 - 工具名规范化。 CC 会把工具显示为
mcp__prometheus__memory_search,Codex 是{server, tool}。适配器统一还原成memory_search。权限规则、模式白名单、UI 渲染器都只认规范名。
外部 runtime 的内置工具(CC 的 Bash/Read/Edit/Write/Grep/Glob/WebFetch/WebSearch/TodoWrite/Task/ExitPlanMode,Codex 的 command_execution/file_change/web_search/plan 更新)由适配器映射到 规范工具类型,让现有的 diff、bash 输出、todo 渲染器直接复用:
type CanonicalToolKind = "exec" | "read" | "edit" | "write" | "search" | "fetch" | "todo" | "task" | "plan_exit" | "mcp" | "other";
// 例:CC Edit{file_path, old_string, new_string} → name "edit", args { path, edits:[{oldText,newText}] }
// Codex file_change{changes} → name "edit"/"write",args 由 unified diff 还原
// Codex command_execution{command, aggregated_output, exit_code} → name "bash"映射表放在 packages/runtimes/<kind>/tool-map.ts,用录制的真实事件做快照测试。
重叠工具的取舍:用 runtime 原生的,把结果映射进 Prometheus 的面板。 模型对自家工具训练得最好,原生的效果更稳。
- todo:CC 的
TodoWrite、Codex 的 plan 更新,转成 todo 面板状态。Prometheus 的todo_write在这两个 runtime 下不导出。 - 计划模式:CC 的
ExitPlanMode转成现有的计划审批弹窗。Codex 没有对应工具,就导出 Prometheus 的exit_plan_mode。 - 子智能体:CC 的
Task默认禁用(disallowedTools),统一用 Prometheus 的subagent,这样子智能体面板、多 runtime 子智能体、后台运行都保持一致。在设置里可以切换。 codemode、tool_search、pi_settings是 PI 专属,不导出。CC 有自己的工具搜索;Codex 下工具数量多时,靠总线的deferred暴露(§4.6)。
- 模式里写的工具名一律用 规范名。每个适配器提供
toNative(canonical[]) → { allow, deny }:- CC:
allowedTools/disallowedTools;运行中切换时,如果 SDK 不支持热改,就在下一回合以 resume 重启 query(liveToolListChange=false)。 - Codex:Prometheus 工具在总线侧过滤
tools/list。原生工具只能通过 sandbox 和功能开关粗粒度控制(比如只读 =sandbox read-only),能力矩阵会如实标注"原生工具只能粗粒度限制"。
- CC:
- 插件热装或模式切换后,总线发
notifications/tools/list_changed。runtime 不支持的话(需要 spike),就在下一回合开始前重启连接或 thread(用 resume 保留上下文)。
默认 经总线转出:Prometheus 仍是 MCP 客户端,再把这些工具通过总线导出。这样 codemode/deferred/direct/hidden 四种暴露方式、权限规则、veil 在三个 runtime 下一致。
deferred在总线上实现为一个mcp_search+mcp_call工具对,不把全部工具塞进上下文。- 每个服务器可以单独选择"直连",把配置原样传给 runtime。代价是失去统一权限和脱敏,UI 会提示。
把 before_agent_start 拆成两类,因为它们在外部 runtime 里的落点不同:
ctx.context.provide(async ({ prompt, cwd, sessionId, runtime, messageType, permissionMode, signal }) => ({
static?: { sections?: Record<string,string>; guidelines?: string[] }, // 会话级,稳定,可缓存
turn?: string, // 本回合,随用户消息变化
}));ContextRequest 的字段:
prompt:本回合用户消息;不是用户触发的回合为空串。cwd、sessionId?、runtime:工作目录、会话 id、当前 runtime。messageType?:PI 的触发类型(user、custom等);其他 runtime 不设。permissionMode?: PermissionMode:调用方知道会话权限模式时传入。PI 桥接拿不到它(模式由 PermissionGate 持有,它自己在插件之后写unattended/auto_mode/plan_mode段落),所以 PI 下为undefined;外部 runtime 的宿主应当传。PluginHost.collectContext/collectContextExternal原样转给提供者。signal?: AbortSignal:提供者超时(默认 4 秒,PluginHostOptions.contextTimeoutMs可改)时被中止,提供者应据此停止 I/O;超时后的返回值会被忽略,并输出[plugin:<name>] 上下文提供者超时。
宿主并发启动所有提供者,再按插件顺序合并(同名段落后者覆盖)。提供者可以返回任意东西,宿主会容错:
-
段落名必须匹配
/^[a-z][a-z0-9_-]*$/,否则 PI 在构造 system prompt 时会抛错,导致该会话每次 prompt 都失败;不合法的名字会被丢弃并警告。 -
保留名不可用:
preamble、tools、rules、docs、addendum、project_context、skills、cwd(PI 自带),以及turn_context、unattended、auto_mode、plan_mode(Prometheus 自己写;权限闸门的段落优先)。 -
空串、非字符串的段落值,非数组的
guidelines,非对象的返回值一律忽略。 -
static进 system prompt,内容变了才更新。这样提示缓存不会每轮失效。 -
turn跟随用户消息注入,比如记忆命中、知识库命中、权限模式提示。
内置插件这样迁移(P2-c 已完成前四项):memory 的命中项和索引全部放 turn(memory_save 会在会话中途改变二者,放 static 会让提示缓存失效或内容过期);knowledge 的知识库清单放 static.sections.knowledge、connectors 的服务清单放 static.sections.connectors、workbench 的用法说明放 static.guidelines;技能索引放 static;权限、计划、自动、无人值守放 turn(模式会中途变化)。knowledge 原先在 before_agent_start 里返回 systemPrompt,会冻结整段提示、使之后的段落丢失,迁移后改为静态段落,属有意修复。memory、knowledge、connectors、workbench、todos、ssh、automations 的工具都已改用 ctx.tools.register。modes、veil、mcp、技能的 resources_discover、子代理 runtime、权限闸门仍是 PI 扩展,留待后续阶段。
顺序约束:PI 桥接的 before_agent_start 处理器必须排在所有旧式(ctx.extension)处理器之前(PluginHost.dynamicExtension() 和 TUI 的 extensions 都先注册它)。modes 的 replace/strict 会把 event.systemPrompt(即此前所有处理器累加的结果)冻结为整段替换;若桥接在它之后才写入段落,桥接写入的段落(包括放进 turn_context 的 memory)就会在这两种模式下消失。
| static | turn | |
|---|---|---|
| PI | systemPromptOptions.sections/guidelines(现状) |
同左(PI 每轮重建 prompt) |
| Claude Code | systemPrompt: { type: "preset", preset: "claude_code", append } |
UserPromptSubmit hook 返回 additionalContext |
| Codex | thread 启动时的 developer_instructions |
本回合输入前插入一段 <prometheus_context> 文本项 |
PI 的 turn 不是跟在用户消息后面,而是作为 system prompt 段落 turn_context 交付:PI 每轮把变化的段落转成一条 system patch,历史里会保留旧的 patch;CC 和 Codex 则把 turn 放在用户消息旁边(hook 的 additionalContext / 前置文本项)。
static 内容变化时:PI 下一轮自动生效;CC 和 Codex 在下一回合以 resume 重启(成本是一次缓存未命中,只在内容变化时发生)。
modes 的 replace/strict 会返回完整的 systemPrompt。
- CC:
systemPrompt: string,不用 preset,systemPromptReplace = "restart"。 - Codex:
base_instructions,同样是 restart。 - 由于会丢掉 runtime 自带的工具使用说明,UI 会提示:"在 Claude Code/Codex 下替换系统提示可能降低工具调用质量"。
- AGENTS.md / CLAUDE.md 由各 runtime 自己加载,Prometheus 不重复注入,否则内容会出现两遍。PI 下仍按现状处理。
- Prometheus 技能:注入"名称、描述、SKILL.md 路径"索引(和 PI 现在的格式一样),由模型按需读取。三个 runtime 都有读文件能力,所以行为一致。不往
.claude/skills里写文件,避免污染用户目录。
decide(mode, call, env)、"总是允许"集合、UIBridge.askPermission 三者保持不变。各 runtime 只负责"把待决定的调用交给 gate,再把结论交回去"。
| 原生工具 | Prometheus 工具(总线) | |
|---|---|---|
| PI | tool_call hook(现状) |
同左 |
| Claude Code | canUseTool(name, input) → 规范化 → decide → 必要时弹窗;返回 allow/deny,拒绝原因原样回传 |
CC 侧配置 allowedTools: ["mcp__prometheus__*"] 免询问,由总线在 tools/call 内做 decide;保证只问一次 |
| Codex | app-server 的命令/补丁审批请求 → 规范化 → decide → 回复 approved/denied |
Codex 对该 MCP server 免审批,由总线决定 |
模式映射:
| Prometheus | Claude Code | Codex |
|---|---|---|
| default | default + canUseTool |
workspace-write + approval_policy=untrusted(尽可能多问) |
| acceptEdits | acceptEdits |
workspace-write + on-request,补丁审批由 gate 自动通过 |
| plan | plan |
read-only + 导出 exit_plan_mode |
| auto / bypassPermissions | bypassPermissions |
danger-full-access + never(auto 仍保留自动选择的 UI 行为) |
- 运行中切换模式:CC 用
setPermissionMode();Codex 在下一回合以新的 sandbox/approval 参数发起 turn(app-server 的 turn 级覆盖参数,需要 spike)。 - 已知差异,不掩盖: Codex 在
workspace-write下,工作区内的写操作不会发审批请求,所以 default 模式下"每次编辑都询问"做不到。能力矩阵和 UI 会标注:"Codex:工作区内编辑由沙箱约束,不逐次询问"。 - 无人值守:CC 用
canUseTool直接按ACCESS_MODE判定,不弹窗;Codex 用对应的 sandbox 加never,总线侧照常判定。
每个会话选一种 GatewayChoice:
- runtime 原生:使用 CLI 自己的登录和配置,Prometheus 不注入任何东西。veil 在此模式下不可用,UI 会明示。
- Prometheus 网关:一律 经本地网关中继,不把网关地址和 key 直接交给子进程。
网关中继(从 veil 的 VeilRelay 抽出来,作为核心服务):
- 监听
127.0.0.1,按会话签发一次性 token。子进程只拿到base_url=中继和这个 token。真实 key 由中继注入上游请求,不会进入子进程环境,也不会出现在 CLI 的日志或配置里。 - 把 veil 的
before_provider_request/headers变成中继的请求钩子ctx.hooks.providerRequest,三个 runtime 都能脱敏。PI 也可以改走中继,这样脱敏只剩一条路径。 - 顺便统一用量统计(按会话记录 token 和费用),出口代理也自然复用。
接入方式:
- CC:
env: { ANTHROPIC_BASE_URL: 中继, ANTHROPIC_AUTH_TOKEN: 会话token, ANTHROPIC_MODEL, ANTHROPIC_SMALL_FAST_MODEL }。同时要防止用户~/.claude/settings.json里的env覆盖这些值(spike:SDK 的settingSources和 env 的优先级)。 - Codex:
-c model_provider=prometheus -c model_providers.prometheus.base_url=中继/v1 -c model_providers.prometheus.env_key=PROMETHEUS_GW_TOKEN -c model_providers.prometheus.wire_api="responses"。
格式兼容:
| runtime | 可用的网关格式(第一版) | 后期 |
|---|---|---|
| Claude Code | anthropic-messages |
中继做 openai → anthropic 转换 |
| Codex | openai-responses |
中继做 chat/anthropic → responses 转换 |
| PI | 全部 | — |
不兼容的网关和模型在选择器里置灰,并说明原因。
- 原生模式:CC 用 SDK 的
supportedModels(),Codex 用 app-server 的模型列表接口。 - Prometheus 网关模式:用网关配置的模型,按格式过滤。
- thinking 档位映射:CC 用 effort / thinking token 预算;Codex 用
model_reasoning_effort。不支持的档位不显示。
| Claude Code | Codex | |
|---|---|---|
| 中止 | query.interrupt() |
turn/interrupt |
| 插话 | 流式输入里追加用户消息 | 不支持就退化为 followUp 队列(spike) |
| 压缩 | 发送 /compact,或依赖自动压缩;结束后 messages_reset |
对应的 compact 接口(spike) |
| 分叉 | resume + forkSession |
thread fork(spike) |
| 树导航 | 不支持 | 不支持 |
| 用量 | result.usage 和每条消息的 usage |
turn.completed.usage / token 计数事件 |
/xxx先由宿主匹配 Prometheus 命令(插件registerCommand、prompt 模板、skill:技能)。- 没命中就原样交给 runtime,这样 CC 和 Codex 自己的命令(
/compact等)照常可用。 commands.list合并两个来源,并标注来源。
- 每个会话一个子进程(CC SDK 进程或 Codex app-server)。空闲 N 分钟后回收,下次发消息时 resume,保持进程数可控。
- Windows:复用
resolveExecutable的.cmd垫片解析;中止时用taskkill /T杀掉整棵进程树。 probe()检查:是否安装、版本是否在支持范围内、是否已登录(仅原生模式需要)。结果显示在 runtime 选择器上。
- runtime 是服务端能力。 新增 RPC
runtime.list返回后端的probe()结果(可用性、版本、登录状态、不可用原因)。客户端只按这份结果渲染选项,不写任何平台判断。 - 瘦客户端(连接服务器): runtime 运行在服务器上,手机只走现有的 WS 协议,三个 runtime 都可用,体验与桌面一致。外部 runtime 的子进程在服务端,空闲回收后发消息时 resume,所以手机断线重连、切到后台再回来,会话都还能继续。
- 本机模式(内置 Termux Node): 默认只提供 PI。Claude Code、Codex 的发布二进制面向 glibc/musl Linux,在 Android(bionic)上不保证能运行,原生登录也需要浏览器 OAuth,所以
probe()报告为不可用,原因写“需连接服务器”。如果日后验证 Codex 的静态 musl 版本能在 Android 上跑,再放开,但仍只允许 Prometheus 网关模式。 - 资源: 本机模式不启动 MCP 工具总线(PI 在进程内直接调用工具)。网关中继按需启动:只有 veil 开启或有外部 runtime 会话时才开。
- 界面:
- runtime 和网关选择放在新建会话面板里,不放在顶栏。会话进行中只显示一个小标记,比如
CC、Codex。 - 审批弹窗、计划审批、能力降级提示都要适配窄屏:用底部抽屉,diff 和命令详情默认折叠。
- 能力置灰用一行说明,点开看原因,不用 hover 提示(触屏没有 hover)。
- runtime 和网关选择放在新建会话面板里,不放在顶栏。会话进行中只显示一个小标记,比如
- 新增与 runtime 无关的 API(plugin-sdk):
ctx.tools.register、ctx.context.provide、ctx.commands.register、ctx.hooks.session、ctx.hooks.toolCall、ctx.hooks.providerRequest。内置插件全部迁移过去,插件安装器的 GUIDE 改为推荐新 API。 - 老的
ctx.extension((pi) => …)继续支持:- PI 下照旧,传入真实的 pi。
- 外部 runtime 下,把同一个工厂函数跑在一个 PiFacade 上。它记录
registerTool/on(...)/registerCommand/registerMcpServer/registerProvider,再按 §1 的表转接到总线、上下文、中继。 - 无法转接的调用(比如自定义
registerProvider的流式实现、setActiveTools之外的会话操作)记录为警告,插件页面显示"在 Claude Code 下部分不可用:xxx"。 before_agent_start的 facade 版本拿到的是一个可变的systemPromptOptions草稿,写进去的 sections 和 guidelines 默认算作static,所以老插件也能工作。
packages/runtimes/conformance/:同一组场景,对每个适配器都跑一遍,断言 规范化后的事件序列、ChatMessage 形状、UI 请求 和 PI 的金标准一致。不支持的能力必须由 capabilities 显式跳过。
- 确定性模型: 写一个假的 LLM 服务(脚本化的 anthropic-messages / openai-responses SSE),挂在网关中继后面。三个 runtime 都走"Prometheus 网关模式"连它,不消耗真实 API,结果可复现。这也顺带验证了中继和 veil。
- 场景清单:
- 纯文本流
- thinking
- Prometheus 工具往返(memory_search、带
ctx.ui.confirm的工具、带 progress 的工具、返回图片的工具) - 原生 bash:允许、拒绝、"总是允许"
- 原生编辑的 diff 渲染
- 计划模式:提交计划 → 批准 → 切到 acceptEdits
- 中途中止(工具运行中、流式输出中)
- 插话和 followUp
- 插件热装后工具出现
- 模式切换后白名单生效
- 记忆的 static 和 turn 注入都出现在上游请求里(在假模型端断言)
- veil 脱敏往返
- 进程重启后 resume
- fork
- 压缩后
messages_reset - 图片输入
- 无人值守运行
- 上游错误和重试
- 用量上报
- 录制回放: 适配器的事件解析用真实 CLI 录制的事件流做快照测试,CI 上跑。
- 版本守护: 声明支持的 CLI 版本范围;Codex 的 TS 绑定从锁定版本生成;夜间任务用最新 CLI 跑一致性测试,提前发现上游协议变化。
图例:✅ 已验证可行;
| 能力 | PI | Claude Code(Agent SDK 0.3.x / CLI 2.1.27x+) | Codex(app-server,0.133+) |
|---|---|---|---|
| Prometheus 工具 | ✅ | ✅ HTTP MCP + headers | ✅ HTTP MCP + bearer_token_env_var |
| 调用关联(总线 ↔ runtime) | ✅ | ✅ _meta["claudecode/toolUseId"] |
turn_id/thread_id,按回合内顺序 + 参数关联 |
| 工具列表热更新 | ✅ | ✅ 响应 tools/list_changed |
config/mcpServer/reload |
| 每轮上下文注入 | ✅ | ✅ UserPromptSubmit.additionalContext |
✅ 输入前置文本项 |
| 静态上下文更新 | ✅ 每轮 | systemPrompt.snapshot:false |
thread/resume 时传新的 developerInstructions |
| 统一权限 / 逐次询问 | ✅ | ✅ 以 PreToolUse hook 为准(canUseTool 对自动放行的只读调用不触发) |
|
| 运行中切换权限模式 | ✅ | ✅ setPermissionMode() |
✅ turn/start 时覆盖 approvalPolicy/sandboxPolicy |
| 计划模式 | ✅ | ✅ 原生 ExitPlanMode | ✅ 导出 exit_plan_mode + read-only 沙箱 |
| 工具白名单 | ✅ | tools/disallowedTools 只能在启动时设,切换要 resume 重启 |
|
| 插话 | ✅ | ✅ 流式输入(并入当前回合) | ✅ turn/steer(需要 expectedTurnId) |
| 中止 | ✅ | ✅ interrupt(),会话可以继续使用 |
✅ turn/interrupt |
| 手动压缩 | ✅ | /compact(待验证) |
✅ thread/compact/start |
| 分叉 / 树导航 | ✅ / ✅ | ✅ resume + forkSession / ❌ |
✅ thread/fork / thread/rollback(只能按回合数回退) |
| 切换模型 | ✅ | ✅ setModel() |
✅ turn/start.model |
| 模型列表 | ✅ | ✅ supportedModels() |
✅ model/list( |
| 流式文本 / 思考 | ✅ | ✅ includePartialMessages → 原始 Messages 流事件 |
✅ item/agentMessage/delta、item/reasoning/* |
| diff 渲染 | ✅ | ✅ 由 Edit/Write 参数构造 | ✅ fileChange.changes[].diff(统一 diff) |
| todo 面板 | ✅ | ✅ TodoWrite 映射 | ✅ turn/plan/updated 映射 |
| 用量 | ✅ | ✅ result.modelUsage(累计)、usage(单回合) |
✅ thread/tokenUsage/updated(含上下文窗口) |
| veil(Prometheus 网关) | ✅ | ✅ 中继 | ✅ 中继 |
| veil(原生网关) | — | ❌ | ❌ |
| 任意格式网关 | ✅ | ||
| 上下文分项 | ✅ | ||
| Android 连接服务器 | ✅ | ✅ | ✅ |
| Android 本机模式 | ✅ | ❌ 需连接服务器 | ❌ 需连接服务器(静态 musl 待验证) |
| 协议稳定性 | — | 正式 SDK | 🧪 整个 app-server 标注为 experimental;只用稳定子集,锁定版本范围 |
都用模拟的模型服务(Anthropic Messages SSE 和 OpenAI Responses)验证,runtime 和 MCP 一侧是真实代码。
- 接入:
query({ prompt: AsyncIterable<SDKUserMessage>, options }),用流式输入模式。pathToClaudeCodeExecutable指定用户安装的 CLI(2.1.273 可用),默认使用 SDK 自带的二进制(依赖 optional dependency)。 - 插话: 回合中追加的用户消息会并进当前回合的下一个请求,不会新开回合,也会触发
UserPromptSubmit。 - 权限:
canUseTool(name, input, { toolUseID, signal, mcpServer, ... })只在需要询问时才调用,只读命令会被自动放行。所以统一权限放在PreToolUsehook(返回permissionDecision: allow|deny|ask和updatedInput),canUseTool只用来兜底。 - MCP: http 类型的 headers 每个请求都会带上;会响应
tools/list_changed,约 6ms 后重新列出工具;tools/call._meta["claudecode/toolUseId"]就是 tool_use id,可以直接关联。alwaysLoad: true可以避免工具被放到 ToolSearch 后面延迟加载。 - system prompt: preset
claude_code加append。默认snapshot:true会把第一次的提示固化下来,resume 时传新的 append 不生效,必须设置snapshot:false。 ⚠️ 网关注入陷阱: 用户~/.claude/settings.json里的env会覆盖 SDK 的env选项。本机实测:用户设置里的代理地址覆盖了我们注入的ANTHROPIC_BASE_URL。对策:Prometheus 网关模式下使用settingSources: ["project", "local"],不加载 user 层,并在probe时校验 init 消息里的实际出口。CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST=1也有效,但这是未公开的内部开关,只作备选。原生网关模式照常加载全部设置。env选项会整体替换子进程环境,需要显式合并process.env。- 工具集: 子智能体工具现在叫
Agent(Task是旧名),两个都要禁用;默认工具还有 CronCreate、Workflow、ScheduleWakeup、Monitor 等,用显式的tools: [...]白名单控制。strictMcpConfig: true生效。 - 会话: 记录存放在
~/.claude/projects/<cwd 规范化>/<id>.jsonl(会跟随CLAUDE_CONFIG_DIR);SDK 提供listSessions/getSessionMessages/forkSession/deleteSession/renameSession。interrupt()后结果是error_during_execution/aborted_streaming,之后会话还能继续用。 - 用量:
result.usage只算单回合主循环;modelUsage和total_cost_usd是累计值,统计时要取差值。
- 传输: stdio,每行一个 JSON,消息里没有
jsonrpc字段。握手顺序是initialize→ 通知initialized。TS 类型用codex app-server generate-ts --out生成(加不加--experimental各生成一份),只使用稳定那份里的方法。 - 生命周期(稳定):
thread/start|resume|fork|list|read|rollback|archive|name/set、turn/start|steer|interrupt、thread/compact/start、model/list、config/mcpServer/reload、mcpServerStatus/list。 - 覆盖参数: thread 级可以设
model/cwd/approvalPolicy/sandbox/baseInstructions/developerInstructions/config;turn 级可以设approvalPolicy/sandboxPolicy/model/effort/cwd,设置后对之后的回合也生效。turn 级不能改 instructions。 - 审批:
item/commandExecution/requestApproval和item/fileChange/requestApproval,回复{ decision: "accept"|"acceptForSession"|"decline"|"cancel" }。文件变更的审批请求里不带 diff,要按itemId和item/started里的 fileChange 关联。 - 事件:
item/started|completed(commandExecution、fileChange(带统一 diff)、mcpToolCall(server 和 tool 是分开的字段)、webSearch、plan、contextCompaction……)、item/agentMessage/delta、item/reasoning/*、item/commandExecution/outputDelta、turn/plan/updated、turn/diff/updated、thread/tokenUsage/updated、turn/completed(status 为 completed、interrupted 或 failed,加 error;没有单独的失败事件)、error(带willRetry)。 - MCP: 需要配置
mcp_servers.<id>.url、bearer_token_env_var,以及default_tools_approval_mode="approve"(不配的话调用会被自动取消)。tools/call._meta只带x-codex-turn-metadata(thread_id、turn_id),不带 call_id。不响应list_changed。token 环境变量没设时,MCP 服务器会静默消失,所以 probe 必须先检查。Codex 会去掉 schema 里的minimum、default等关键字,所以 Prometheus 工具不能依赖这些关键字做校验,执行端要自己校验。 - 备选的工具通道: 🧪
thread/start.dynamicTools加item/tool/call可以在协议内直接提供工具,自带 call id,不需要 MCP。等它稳定后可以替换 Codex 下的 MCP 总线。 - 本机环境问题(和设计无关,需要用户处理):
~/.codex/config.toml第 1 行格式错误,codex 的所有命令都会失败。Prometheus 的 probe 要把配置解析错误展示给用户。
- §4.3 关联:CC 用
_meta精确关联;Codex 用(turn_id, 回合内第 n 次调用该工具, 参数哈希)关联,对不上时以总线数据为准,单独生成工具卡片。 - §4.5 热更新:CC 依赖
list_changed;Codex 在每个回合开始前,如果工具集有变化,就调用config/mcpServer/reload。 - §6 权限:CC 的拦截点从
canUseTool改为PreToolUse;Codex 模式切换用 turn 级的approvalPolicy/sandboxPolicy覆盖。 - §7 网关:CC 在 Prometheus 网关模式下排除 user 设置层;Codex 在 Prometheus 网关模式下使用 Prometheus 自己的
CODEX_HOME(<data>/runtimes/codex-home),不读用户的 config.toml 和 auth.json,原生模式才用用户的CODEX_HOME。 - §5.2 static 上下文:CC 设置
snapshot:false,内容变化后的下一回合 resume 重启;Codex 在thread/resume时更新developerInstructions。 - 仍待验证:CC 的
/compact在流式输入模式下是否可用、2.1.273 的_meta和list_changed行为、Codex 跨回合的list_changed、冷启动性能、Android 上的 musl。 - Codex 原生登录模式:用户的 ChatGPT 令牌可能很久没刷新,首次以原生模式启动 Codex 可能触发令牌轮换,存在把用户本机 Codex 登出的风险。
probe()只检查登录文件是否存在,不发真实请求;首次使用原生模式前在界面上提示。 - 会话列表只读 Prometheus 自己的会话索引(§3.1),不依赖
thread/list(默认只返回交互式会话)。 - 模型选择:
model/list的默认模型不一定可用(实测 gpt-5.5 在 ChatGPT 登录下不被支持),所以不直接采用其默认值,由用户选择,并以第一次成功的回合为准记录。 - 合规(未决):在 Prometheus 里调用用户本机 CLI 的订阅登录是否符合 Anthropic / OpenAI 条款。在确认之前,“原生登录模式”默认关闭,由用户在设置里自行开启并看到提示;使用 API key 或 Prometheus 网关的路径不受影响。
- TodoWrite 已被禁用(CLI 报 "TodoWrite is disabled for this session",它换成了任务工具)。所以 todo 面板改由总线上的 Prometheus
todo_write驱动,不再用原生工具;§4.4 里"todo 用原生"的结论作废。 - ExitPlanMode 只在 Claude Code 自己处于 plan 模式时可用,否则报 "You are not in plan mode"。所以 Prometheus 切到计划模式时,要同步
query.setPermissionMode("plan");批准后切回default。其余权限判断仍然只在PreToolUse钩子里做。 - ExitPlanMode 的
plan字段到不了钩子(输入里的 plan 被剥掉,Claude Code 把计划放在计划文件里)。批准弹窗退而使用模型在本回合写的文字作为计划;还没在真实模型下验证这种取舍是否够用。 tools/call._meta["claudecode/toolUseId"]确实带着 tool_use id,总线事件据此与模型的tool_use关联,前端看到的是同一个toolCallId。PreToolUse返回permissionDecision: "allow"后,Claude Code 不会再走自己的询问流程;canUseTool只做兜底。- 总线上的拒绝(权限门不放行)以
isError结果返回给模型,不会卡住会话。 - 会话 resume 用 SDK 的
resume: <id>,新会话用sessionId: <uuid>,Prometheus 的会话 id 直接等于 Claude Code 的会话 id。 - 网关流量钩子(
ctx.hooks.gatewayTraffic):veil 的脱敏和还原挂在核心中继上,对所有外部 runtime 生效;PI 仍走 veil 原来的路径。
第四个运行时,位于 packages/core/src/agent/opencode。不引入新依赖:直接用 HTTP 加 SSE 调它的服务进程。
- 传输: 每个会话一个
opencode serve --hostname=127.0.0.1 --port=0子进程,从输出里的opencode server listening on <url>取地址。用随机的OPENCODE_SERVER_PASSWORD做 HTTP Basic 认证,本机其他进程不能驱动它。事件走GET /event(SSE),断开后自动重连,并用GET /session/status补一次状态。 - 配置: 通过环境变量
OPENCODE_CONFIG_CONTENT叠加在用户自己的配置之上,不写文件:mcp.prometheus指向工具总线(type: "remote",令牌用{env:PROMETHEUS_MCP_TOKEN}引用),permission里把edit / bash / webfetch / external_directory / doom_loop设为ask,让所有会改东西的操作都到权限门,由 Prometheus 的权限模式决定。 - 网关: Prometheus 网关作为自定义 provider
prometheus注册(@ai-sdk/openai-compatible、@ai-sdk/openai或@ai-sdk/anthropic,按网关支持的协议依次选),baseURL是中继的/v1,apiKey是会话令牌;同时设置enabled_providers: ["prometheus"],这个会话不会走用户自己的 provider。模型决定路由:换到别的网关或回到自带登录时,换一个进程,按同一个 opencode 会话继续。 - 自带登录: 不覆盖用户的配置和数据目录,
opencode auth login的结果直接可用,会话也存在用户自己的 opencode 里。模型列表来自opencode models,另有一项“配置的默认模型”(不传 model,由 opencode 自己选)。 - 消息:
POST /session/:id/prompt_async。静态上下文放在system字段;本回合的上下文和切换运行时的交接内容作为synthetic文本片段发送,模型能看到,聊天记录里不显示。计划模式用 opencode 的plan智能体,其余用build。 - 事件映射:
message.updated先于它的片段到达,用来区分用户消息、助手消息和压缩产生的摘要(摘要不显示);message.part.delta的text字段是增量。text、reasoning、tool 三种片段各生成一条助手消息,工具的pending状态没有参数,等到running才生成工具卡片。step-finish的 token 数用于上下文用量。session.status: idle结束一轮。 - 工具名: opencode 的
bash/read/edit/write/grep/glob/list/webfetch/websearch/todowrite/task规范化成bash/read/edit/write/grep/find/ls/web_fetch/web_search/todo_write/subagent;我们自己的工具显示为prometheus_<tool>,还原成<tool>。edit的结果带 opencode 给出的统一 diff。opencode 不把调用 id 传给 MCP 服务,所以和 Codex 一样按(工具名,参数哈希)把总线调用和工具卡片对上。 - 权限:
permission.asked交给权限门,回复POST /permission/:id/reply(once或reject)。question.asked(opencode 的提问工具):有人在线时用ui.select问用户,无人值守或 auto 模式取第一个选项。 - 压缩:
POST /session/:id/summarize;自动压缩产生的compaction片段和session.compacted事件转成compaction_start/end。 - 会话: 会话列表由 Prometheus 自己的索引维护(和 Codex 一样);恢复时
GET /session/:id/message重建聊天记录;删除调用opencode session delete。 - 未做: 思考档位(opencode 的 variant)、回退到某条消息、分叉、斜杠命令列表、作为子智能体的执行器。
opencode 本来就是“起一个服务、多个客户端连上来用”的形态,所以 OpenCode Agent 可以不再每个会话起一个进程:
- 连接已有服务(
server.url,可带username/password,即对方的OPENCODE_SERVER_USERNAME/OPENCODE_SERVER_PASSWORD):probe 调GET /global/health判断可用与版本;模型列表取该服务的GET /config/providers。 - 固定端口的共享服务(只填
server.port):Prometheus 第一次用到时在127.0.0.1:<port>起一个opencode serve,这个 Agent 的所有会话共用,宿主退出时停掉;端口上已经有一个凭据相同的 opencode 服务时直接用它。密码留空则用随机密码(外部客户端接不进来),填了就能用opencode attach http://127.0.0.1:<port>接入同一个服务,和 Prometheus 里的会话互相可见。 - 每个会话的隔离:服务是共享的,所以不能再用
OPENCODE_CONFIG_CONTENT。工具总线用POST /mcp动态注册,名字是prometheus-<会话 id 前 12 位>(工具名前缀随之变为prometheus-xxxx_,映射器按会话的名字还原);会话关闭时POST /mcp/<name>/disconnect。opencode 会把所有 MCP 工具给所有会话,而它只认“精确工具 id + pattern*+ deny”的会话规则来隐藏工具(通配符不行;prompt_async的tools字段会整个替换会话规则,也不能用),所以每次发消息前PATCH /session/:id写入:edit/bash/webfetch/external_directory/doom_loop为ask,再逐个 deny 其它会话总线上的工具 id(从GET /mcp取服务名,工具名按本会话的工具列表推出)。 - 限制:服务模式只用服务自己配置的模型和登录,不能选 Prometheus 网关(网关是按会话注入的 provider);Prometheus 工具总线只监听 127.0.0.1,所以连接其它机器上的服务时没有 Prometheus 工具(会记录警告)。删除会话走
DELETE /session/:id。 - 测试:
packages/core/test/opencode-server-mode.test.ts(真实 opencode + 假模型,覆盖两种模式、工具隔离和权限门)。
第五种运行时 acp,一份适配器支持所有实现了 ACP 的 Agent:内置 Qwen Code(qwen --acp),设置里另有 Goose(goose acp)、Kimi CLI、Auggie、Claude Code / Codex 的 ACP 适配器等预设,其它 Agent 选“ACP”填命令和参数即可。内置与预设定义在 packages/protocol 的 BUILTIN_AGENTS / ACP_PRESETS(含一键安装用的 npm 包)。
- 传输: stdio 上的 JSON-RPC 2.0,一行一条,双向请求(
acp-connection.ts)。Windows 上 npm 的.cmd垫片解析成真实脚本或二进制,避免 shell 改写参数。每个会话一个进程,崩溃后下一条消息重启并session/load。 - 握手:
initialize声明fs、terminal都不提供,Agent 用自己的工具读写和执行;probe 也是一次握手(拿agentInfo.version),结果缓存 5 分钟,runtime.list并行探测。 - 工具总线:
session/new|load的mcpServers里放prometheus:Agent 声明mcpCapabilities.http时用 HTTP(Authorization头带令牌),否则用 stdio 桥(mcp-bridge.ts写到<data>/runtimes/acp/mcp-bridge.cjs,由本机 Node 运行,地址和令牌走环境变量)。ACP 不给 MCP 调用 id,按工具名关联:标题第一个词(echo_tool (prometheus MCP Server))、mcp__x__name形式、代理工具输入里的name(Qwen Code 把 MCP 工具藏在tool_search/tool_call后面)或_meta.toolName。 - 权限: Agent 被切到它自己的
default模式(凡改动都发session/request_permission),由权限门按 Prometheus 的模式决定,回复allow_once/reject_once;我们自己的工具直接放行(总线会再判定,不问两次);中止时未决的请求回cancelled。计划模式与其它运行时一样:导出exit_plan_mode,写操作由权限门拒绝。 - 上下文: ACP 没有 system prompt 字段:静态上下文随进程的第一条消息、每轮上下文和交接内容每条消息,都放在一个
<prometheus-context>文本块里,聊天记录里不显示(回放时也会剥掉)。 - 事件映射(
acp-mapper.ts):agent_message_chunk/agent_thought_chunk流式成文字和思考;tool_call/tool_call_update按kind规范化(execute→bash,read,edit 的 diff→edit 或 write,search→grep,fetch→web_fetch/web_search……),参数到齐(rawInput非空、或有 diff/路径、或已开始执行)才画卡片;plan喂 todo 面板;available_commands_update进/菜单;usage_update进上下文用量。 - 回合与队列: 一次
session/prompt就是一回合,返回的stopReason结束它(cancelled为中止,refusal/max_tokens/max_turn_requests报错)。ACP 不能往进行中的回合插话,所以回复中发送的消息排队,回合结束后自动发出(followUpQueue,可撤回);中止会清空队列。 - 模型: “Agent 配置的默认模型”加上 Agent 在
session/new里报告的models.availableModels;切换用session/set_model,不支持时退到session/set_config_option。 - 网关: ACP 本身不能指定端点,所以按 Agent 适配(
acp-gateway.ts)。目前 Qwen Code 支持 Prometheus 网关(openai-completions,经中继:OPENAI_BASE_URL/OPENAI_API_KEY/--auth-type openai --model),其它 ACP Agent 只用自己的登录。 - 会话: 会话列表由 Prometheus 索引维护;界面上的记录另存一份在
<data>/sessions/acp/<id>.json。恢复时优先session/load(Agent 回放的内容静默重建,界面用存下的记录);Agent 不支持或加载失败时开新会话,并把旧记录作为交接内容随第一条消息交给它。 - 未做: 压缩、分叉、回退、思考档位、
fs/terminal客户端能力(让 Agent 通过 Prometheus 读写文件和跑命令)、作为子智能体的执行器。 - 测试:
acp-runtime.test.ts(脚本化的假 ACP Agent,含 HTTP 与 stdio 桥两种总线接法、权限、队列、中止、崩溃重启、恢复)、conformance-acp-qwen.test.ts(真实 Qwen Code 经中继连假模型)。
| 阶段 | 内容 | 验收 |
|---|---|---|
| P0 | 做完 spike(§12),修订本文的能力矩阵 | 每个 ❓ 有结论 |
| P1 | 抽出 AgentRuntime,PiRuntime 实现,加会话索引;host.ts 只做分发 |
现有测试全过,行为零变化 |
| P2 | 新插件 API + PiFacade + 内置插件迁移;上下文分成 static/turn | PI 下行为不变;新 API 的单测 |
| P3 | 工具总线(MCP HTTP)+ 网关中继(含 veil 钩子),先让 PI 走中继验证 | 一致性测试框架和假模型可以跑通 PI |
| P4 | ClaudeCodeRuntime(Prometheus 网关模式优先,再做原生模式) | 一致性测试全绿,或只有显式声明的跳过 |
| P5 | CodexRuntime(app-server) | 同上 |
| P6 | UI:新建会话时选 runtime 和网关、健康检查、能力置灰、插件兼容性提示(含移动端窄屏适配,§8.5);文档 | 桌面和 Android(连接服务器、本机模式)各一份手工验收清单 |
| P7(可选) | 中继里的格式转换;"在另一个 runtime 中继续" | — |
P1 到 P3 对现有用户不可见,但做完之后,每接一个 runtime 就只是写一个适配器加通过一致性测试。
P0 到 P5 已完成。P6 已完成:新建会话时选运行时(RuntimePicker)、运行时自带登录或 Prometheus 网关(按格式过滤:Claude Code 只列 anthropic-messages,Codex 只列 openai-responses)、按运行时和网关取模型列表(runtime.models)、按 capabilities 置灰(不支持插话时回复中发送改为排队,不支持图片时禁用附件)、插件页的兼容性提示(plugin.compatibility)。
会话快照带有 gateway,打开已有的非 PI 会话时模型列表按它取。P7 未开始。
- 设置页能看到 PI、Claude Code、Codex;未安装的置灰并显示原因。
- 新建会话选 Claude Code + 自带登录:能对话,bash/编辑的卡片和 diff 正常,todo 面板随
todo_write更新。 - 同上选 Codex。
- 选 Prometheus 网关:模型列表只含对应格式的网关;开启 veil 后上游请求里敏感串已被替换。
- 回复中发送:Claude Code 和 Codex 可插话;中止后会话可继续。
- 权限模式切换(默认、自动编辑、计划、完全放行)在三个运行时下都生效。
- 插件页:有不兼容项的插件显示"在 Claude Code / Codex 下部分不可用"。
- 重启后会话仍在侧栏,打开后能续聊。
- 连接服务器模式:重复上面 1 到 8,新建会话页的选择器在窄屏下不溢出,弹层贴边显示。
- 本机模式:Claude Code 与 Codex 不可用,选择器里置灰并提示"Android 本机模式不可用,请连接服务器"。
- 多协议网关:
GatewayConfig.alternates记录同一密钥和模型可走的其它协议及各自的 Base URL,format/baseUrl仍是主协议(PI 用它)。gatewayFormats、gatewayAs、pickGatewayFormat(packages/protocol)负责解析。Claude Code 取anthropic-messages,Codex 取openai-responses,中继按所选协议换上对应的 Base URL 和鉴权方式;选择器里按"是否支持该运行时的协议"过滤网关。 - 在另一个运行时继续:
session.switchRuntime在同一目录用新运行时接着聊,列表里只保留这一个会话:原会话随即删除。原对话整理成文字记录(最近约 4 万字符,工具结果截断),作为不显示的上下文随第一条消息交给新运行时(Claude Code 走additionalContext,Codex 走前置输入项,PI 前置在消息文本里,所以 PI 下会可见)。新会话的界面不显示旧消息。
- Agent 菜单只列 Agent(PI、Claude Code、Codex 和自定义 Agent),不再有"模型来源"一级。模型选择器像 PI 一样列出该 Agent 能用的全部模型:自带登录的模型,加上每个兼容网关的模型(组名是网关名,
provider即网关 id)。runtime.models因此变为{ runtime, agent? },返回合并列表。 - 模型决定路由:
session.create/session.switchRuntime的model的provider若是某个网关 id,就走 Prometheus 网关,否则走 Agent 自带登录。会话内用model.set换到另一个网关的模型时,Claude Code 会停掉进程,下一条消息用新端点 resume 同一会话;Codex 会替换 app-server 进程并 resume 同一线程。回复进行中不能换网关。新路由会写回会话索引,重开后保持。 - 自定义 Agent(设置 → Agent):基于 Claude Code 或 Codex,可设可执行文件路径和环境变量;
config.json的agents保存,内置两个也可以加覆盖。agent.list/save/delete管理,改动对之后新建的会话生效。 - Codex 自带登录:每个会话用独立的
CODEX_HOME,复制账号的auth.json和config.toml,再追加 Prometheus 工具总线的配置;模型为账号默认(codex/default)。账号自己的config.toml无法解析时,Codex 会报配置错误。
- 安装位置: 一键安装用
npm install --global --prefix <data>/agents,不碰用户全局的 npm;外部运行时把<data>/agents(Windows)或<data>/agents/bin放在 PATH 最前。可安装的包来自BUILTIN_AGENTS/ACP_PRESETS的npm字段,基于 Claude Code / Codex / OpenCode 的自定义 Agent 用同一个包。 - 用哪个: 不看是谁装的,只做一次 PATH 查找:Agent 里指定的可执行文件优先,否则取 PATH 上第一个,Prometheus 一键安装的目录(
<data>/agents)排在 PATH 最后,所以用户自己装的永远优先,没有时才用一键安装的那份。Claude Code 和 Codex 在这里解析(否则 Claude Code 会用 SDK 自带的 CLI,Codex 只看服务进程自己的 PATH),OpenCode 和 ACP Agent 按该 Agent 的 PATH 查找;Claude Code 都找不到时才退回 SDK 自带的(开发环境)。 - 检查更新:
agent.checkUpdates(设置 → Agent 打开时自动查,“检查更新”按钮强制刷新)。当前版本:对正在用的那个 CLI 执行--version(按文件修改时间缓存),Claude Code 找不到 CLI 时取 SDK 的claudeCodeVersion(来源bundled)。最新版本用npm view <包> version,所以.npmrc里的镜像生效,缓存 1 小时。指定了可执行文件(custom)、连接外部 OpenCode 服务(server)、不在 npm 上的 Agent(noPackage)不提供更新,界面上说明原因。 - 更新: 就是
agent.install(安装@latest)。只有跑的那份在 Prometheus 目录里(source: prometheus)才显示“更新”按钮;跑的是 PATH 上别处的(path),只提示有新版本,请用原来的方式更新,因为 Prometheus 装一份也排在它后面,不会生效。Claude Code 只有 SDK 自带版本(bundled)时,更新是装一份到 Prometheus 目录并从此使用。Windows 上正在运行的可执行文件被锁定时 npm 会失败,错误里提示先关闭使用它的会话。 - 测试:
agent-install.test.ts(版本解析与比较、已装版本与入口),agent-update.e2e.test.ts(PROMETHEUS_TEST_UPDATES=1:真实 npm,旧版 OpenCode → 检查 → 更新 → 运行的是新版)。 - 安装包不带 Claude Code: 桌面端打包(
bundle-server.ts)会删掉 Agent SDK 的各平台二进制(@anthropic-ai/claude-agent-sdk-<平台>,约 280 MB),以及 LanceDB 可选依赖带来的@huggingface/transformers、onnxruntime-*、sharp(约 100 MB,向量由网关计算,用不到)。没有二进制又没指定可执行文件时,Claude Code 的probe返回不可用,设置页显示“安装”,装好后走上面“装的就是跑的”的路径。开发环境(pnpm 安装)仍带二进制,行为不变。