06. 主机 RPC 协议
翻译说明: 本页是与 英文源规格 一一对应的机器辅助翻译。代码、协议字段和标识符保持原文;如翻译与英文源事实有歧义,以英文版本为准。
1. Goal
定义以下之间的本地协议:
- Electron 主要(协调器)
- Rust host-core(特权后端)
- Node pi 代理 sidecar(通过主机的工具请求者/事件源使用者)
MVP 传输决策 (D001):
stdio JSON-RPC 优于 NDJSON
2. 交通
- 流程:Electron 主要生成 Rust host-core sidecar
- 通道:子进程 stdin/stdout
- 成帧:每行一个以 LF 分隔的 JSON 对象(NDJSON);接受 CRLF。 JSON 字符串内的 U+2028 与 U+2029 属于载荷,不是帧分隔符。 所有 Node stdio 读取器会跨输入块保留 UTF-8 字符,并在传输关闭时释放缓冲片段和监听器。 为兼容起见,EOF 时接受最后一帧未以换行结束的情况。
- 非法 JSON 帧会先产出仅含字节长度、不含载荷文本的诊断,然后丢弃。后续完整帧仍可读。现有会话文本不会被改写或迁移。
- 编码:UTF-8
- Request/response:JSON-RPC 2.0 风格
控制管道在 host-core 内部是资源隔离的。一个专用操作系统 线程读取 stdin,并且一个专用操作系统线程序列化 stdout;请求和 工具任务从不执行 Tokio stdio 操作。这会保留临时操作系统 将管道 read/write 转换为 Tokio 阻塞池导致线程耗尽 恐慌。线程重试中断和瞬态非阻塞错误 保留每行一条消息的框架;不可恢复的管道错误结束 主机并由正常的 Electron 监控路径处理。
2. 1 运行时准入和背压
Host-core 不会为每个请求创建无限的任务或子进程。 RPC 调度程序将活动请求上限限制为 32。然后,tools.execute 输入 有界执行预算:
- 总共 16 次工具执行
- 全局 4 个并发
Bash进程,每个会话 2 个 - 全球 8 个 read/search 工具
- 2 个全局变异工具,每个会话 1 个
- 全球4个插件工具
- 每个会话执行 4 次工具
- 全局 64 个排队工具执行
权限提示不占用执行槽。满队列返回 HOST_OVERLOADED 在工具结果中具有可重试语义,而不是 无限期地等待或产生更多工作。限制是主机拥有的,所以 Electron 和 sidecar 不能独立过度接纳相同的资源。 每会话突变许可是在全局突变槽之前获取的; 因此,排队的 Bash/read/search 调用在等待时不会保留全局容量 对于同一会话中的较早突变。
请求
{
"jsonrpc": "2.0",
"id": "req_01H...",
"method": "tools.execute",
"params": {}
}回应
{
"jsonrpc": "2.0",
"id": "req_01H...",
"result": {}
}错误
{
"jsonrpc": "2.0",
"id": "req_01H...",
"error": {
"code": 1003,
"message": "PATH_OUTSIDE_WORKSPACE",
"data": {
"errorCode": "PATH_OUTSIDE_WORKSPACE",
"details": {}
}
}
}通知(服务器 → 客户端,无 id)
{
"jsonrpc": "2.0",
"method": "permissions.request",
"params": {}
}3. 握手
在生成时,Electron 必须调用:
app.handshake
参数:
type HandshakeParams = {
protocolVersion: 11
client: "electron-main"
clientVersion: string
locale: string // default "en"
}结果:
type HandshakeResult = {
protocolVersion: 11
host: "rust-host-core"
hostVersion: string
features: string[]
}规则:
如果协议主要版本不匹配→中止启动
如果握手失败,Electron 应退出并出现可操作错误 3.后续所有调用都需要握手成功
版本4引入了持久通知收件箱和 带有通知的
session.endTurn结果。 5.版本5需要主机拥有的session.fork快照操作;一个 在聊天变为交互式之前,必须拒绝版本 4 主机 (ADR 0023)。版本 6 添加了持久模型上下文检查点
session.appendCompaction;版本 5 主机必须先被拒绝 运行时声明自动上下文保护 (ADR 0030)。版本 9 是冻结的 ADR 0053/0054 合约:它涵盖了检查点 Plan artifact/queue,主动转向 Plan identity/CAS,明确批准 权限、shell 目录身份和方言 pin、流式命令输出、 以及来自
config_json的计划任务模式投影。 v7 或不兼容 在 UI 变为交互式之前,必须拒绝 v8 主机。版本 11 撤回 A2A 协议栈(ADR 0165 / D326)。
a2a.*方法和通知 已移除,握手不再声明a2a;v10 主机或客户端必须在 UI 交互前拒绝。
协议 v11 与 host-core 存储架构 v14 配对。v14 增加插件会话来源 sidecar 和软删除字段;架构版本是内部持久性不变量,而不是额外的 JSON-RPC 字段, 检查点架构仍然由主机拥有。
4. 方法目录(MVP)
应用程序
app.handshakeapp.healthapp.getVersionapp.getOnboarding— 内联引导清单状态(D031)
app.health 返回诊断 toolBudget 对象:
type ToolBudgetHealth = {
active: number
queued: number
total: number
shell: number
reads: number
mutations: number
plugins: number
}工作区
workspace.getworkspace.setworkspace.clear
查看快照 (ADR 0043)
review.rollback({sessionId, snapshotId})— 验证当前的后期工具 hash,恢复会话拥有的先前字节,并返回其中之一rolledBack、alreadyRolledBack、conflict或unavailable。
项目
projects.list— 返回先固定的持久项目记录,然后返回 按上次开放时间;包括通过会话导入具体化的记录projects.create({ path })— 创建或复用持久项目记录,不切换当前工作区, 并返回宿主生成的项目 idprojects.remove({ path })— 删除一条持久项目行,并连同附加到它的每个会话一起删除, 移除这些会话的转录本、scratch 和 review 文件以及该项目的持久记忆,且从不触碰磁盘上的 项目文件夹。幂等:未知路径返回{ removed: false, sessionsRemoved: 0 }。作为已存储 多文件夹项目组根目录的路径会被拒绝,以便该组保留有效的 Primary 根目录;而只要其中仍有会话 在运行,调用就会被拒绝(1008 /CONFLICT),因此运行中的轮次绝不会丢失它正在写入的转录本。
秘密
secrets.setsecrets.deletesecrets.hassecrets.getForRuntime—— 仅 main/host 可用,渲染器永远够不到- // 永远不会将
secrets.get写入渲染器日志
一个提供商行有两个相互独立的引用 —— secret:provider:<id>:api_key 与 secret:provider:<id>:oauth(D237)。上面的通用方法同时服务于两者,因此 厂商账户凭据不需要新的主机方法。ProviderPublic 因此报告 hasSecret (任一种凭据存在即为真)、hasOauth 与非敏感的 oauthAccountLabel; providers.create / providers.update 接受 oauthAccountLabel 与可选的 headers(写入 config_json.headers,{} 清除), providers.delete 清除两个引用。登录编排与令牌刷新留在 Electron 主进程, 永远不会进入本协议 —— 参见 14-secrets-storage §10。
设置
settings.getsettings.set
会议
session.listsession.create— 接受可选的thinkingLevel; missing/null 默认值 至offsession.fork— 接受sessionId,呼叫者提供的可选显示title,以及可选的throughMessageId;创造 来自源当前活动规范的一个独立会话 转录本,在提供时在选定的消息处被截断。 孩子继承 project/provider/model/mode/thinking 并且 权限配置,接收新的 message/tool-call id,并启动 无需轮流、修订、通知、工件、资助或临时数据。 缺少源返回NOT_FOUND; Electron 拒绝活动源AGENT_BUSY在转发之前并标准化主机的持久化 运行转向CONFLICT回退到AGENT_BUSY;来源不明或throughMessageId返回NOT_FOUNDsession.getsession.deletesession.getScratchPath— 会话的 scratch 目录(D114),按需创建session.renamesession.configure— 以原子方式持久保存mode、providerId、modelId, 以及可选的thinkingLevel用于下一个 pi 回合; omitting/nullthinkingLevel保留当前值;返回无效模式或级别INVALID_PARAMS;模式为plan | goal | agent并更改任何会话 仅在空闲且没有 pending/queued/running 时才允许配置 Plan 或 Goal 记录session.appendMessagesession.saveInflightMessage— 仅供 Electron 主进程使用的检查点,保存正在流式 输出的助手回复以及message_end的完成快照:{ sessionId, turnId?, message }原子替换sessions/<id>.inflight.json(D299、D327,规格 04 §2.1)。返回{ ok, saved }; 消息没有可见文本、或该 id 已被索引(最终行先落盘)时saved为 false,后一种 情况下还会移除残留检查点。非助手角色属于INVALID_PARAMS类失败。completed/error的session.endTurn仅在该 id 已索引时才删除该文件。session.recoverInflightMessages— 仅供 Electron 主进程在 outbox 排空后调用的扫描 (D327)。把最终行从未落盘的残留检查点提升写入转录,回合已completed的提升为complete。启动恢复会跳过已完成回合,以便 outbox 先追加。返回{ ok, count }。session.appendCompaction— 仅附加最新类型的 sidecar 模型上下文检查点。它需要非空 checkpoint/summary/boundary ids 和非负tokensBefore;它不会插入 message/search 行 或更改可见的转录本投影session.replaceMessages— 原子记录重写(临时文件重命名 + 一项索引事务,D119),用于删除消息和未得到答复的渲染器智能停止撤销; 仅当边界和可选的第一个保留 id 在重写前缀中仍然有效时才保留最新检查点, 并且跨重写携带每条幸存消息所属的turn_id。只有在呼叫持续时间内拥有 整份记录的调用者才安全。重新生成和重试改走session.truncateFrom, 因此保留前缀不再经过 JSON-RPC(ADR 0216)session.truncateFrom— 主机拥有的后缀截断,供重新生成 / 重试 / 编辑重发:{ sessionId, fromMessageId?, truncateBefore? }。身份优先;未知fromMessageId为NOT_FOUND。在状态锁下中止残留的 running 回合、 归档被丢弃的重新生成尾巴、重写保留前缀,并删除进行中检查点。返回{ ok, keptCount, discardedCount, abortedTurnId, revision }。请求和结果 都不携带转录本快照。协议 v11 增量方法(ADR 0216)session.saveRevision— 将重新生成分支归档到(sessionId, rootUserId)。带revisionIndex时,就地刷新该已有变体的 载荷(分支自归档后又生长了),而不是新建索引;DB 行保留身份和活动 标志,只更新message_countsession.saveActiveRevision— 归档最新的分支 将带有修订版的用户 root 作为其活动修订版并标记该 root 的寻呼机 元数据,全部位于 RPC 锁下。邮票重写了一行文字记录 而不是文件,因此并发的session.appendMessage仍然存在。 当会话不拥有重新生成历史记录时,返回{ saved: null }。 已归档的活动变体会被刷新,而不是跳过。 回合完成调用者使用它而不是session.get+session.replaceMessagessession.listRevisions— 列出根用户系列的线性变体session.activateRevision— 用prefix + branch替换实时转录 并标记根寻呼机元数据。切换前它先从持久转录本重新归档该系列的实时 分支(刷新实时根消息activeRevision标记所指的变体,或把已标记但从未 归档的分支存为新变体),因此上次归档之后追加的内容不会丢失。当该系列 存在于持久转录本中时,恢复分支之前的前缀取自转录本而非调用方。幸存 消息保留所属的turn_idsession.beginTurnsession.queuePush/session.queueList/session.queueRemove/session.queuePrioritize/session.queueReorder—— Host 拥有的回合队列 (D386 / ADR 0213 / ADR 0265,架构 v18);push 按主体与 key 幂等,每会话最多八条。queuePrioritize把条目的priority写为其会话优先区块的MAX + 1(追加到区块末尾), 对已经带优先级的条目返回CONFLICT;queueReorder让一个未优先条目与其相邻的未优先 条目互换并返回{ moved }。列出与投递顺序为:已优先条目按priority升序,其余按position升序session.endTurn— 以原子方式将正在运行的回合移动到其终止状态,并且 有条件地返回新创建的completed/error通知;它还会落定该会话的进行中回复 检查点(D299):completed/error移除它;recoverInflight: true(sidecar 已丢失、不会再有最终行时发送)把最终行从未落盘的检查点提升为aborted助手消息 写入转录并作为recovered返回;普通的aborted(用户停止)保留检查点,交给 即将到达的最终行取代; 当createNotification=false、aborted或 对于已经结束的回合session.import— 以原子方式导入一个转换后的会话;一个非空的 项目路径在会话之前进行规范化并更新插入到projects中 引用它;返回{ imported, skipped }
插件拥有的会话方法是协议 v11 的增量方法,仅由 Electron main 在完成插件 权限和 manifest 来源校验后调用:
plugin.session.import— 使用(pluginId, source, externalId)幂等键导入 一个由主机拥有的会话;主机生成 id,只有调用方显式传入由projects.create创建的projectId时才绑定项目;历史projectPath仍是元数据plugin.session.importBatch— 有界的skip或全有或全无fail批量导入plugin.session.list/plugin.session.get/plugin.session.listMessages— 只读取调用插件自己导入且仍处于活动状态的会话plugin.session.rename— 重命名自己拥有的活动导入会话plugin.session.delete—trash隐藏并保留转录本;purge删除并允许重新导入plugin.usage.listTurns— 未删除会话的已完成 turn 事实页(标识符与 token 计数,绝不含消息正文)。由 Electron main 用usage.read鉴权。增量方法, 不升协议版本。- 插件会话变更成功后,Electron main 发送一次
sessionsChanged渲染器事件, 渲染器刷新会话列表;插件不发送此 UI 同步事件
主机会拒绝未知角色、非 RFC3339 或非单调时间戳,以及超大或过深的 payload; 工具值会清理主机保留键。每个插件每 60 秒最多 10 次单条导入、5 次批量导入和 20 次删除。P2/P3 方法不在协议 v11 中。
Plan 和 Goal 状态和批准
两种合约类型共享这些方法;可选的 kind (plan | goal,默认 plan,因此 D198 之前的 sidecar 仍然有效)选择哪个 合同正在洽谈中。
plans.enter— 仅接受活动 Agent 回合的sessionId、turnId, 和toolCallId加上kind; host-core 执行模式转换至 具有比较和交换更新的那种模式并发出plans.changed携带kind。无法识别的kind失败并显示INVALID_PARAMSplans.submit— 将主机拥有的工件写入该种类的目录下,并 创建一个待处理的提案,其kind保留在该行上plans.pending— 仅返回待批准行、会话计划 状态,以及正在协商的合约的kind(待处理行的类型, 回到会话自己的合约模式);渲染器重新加载不会 在主机还活着并且不恢复的情况下延长绝对期限 终端卡plans.resolve— 验证一个匹配的 approve/reject 响应,并且 批准,提交所选权限模式和execution_state = queuedplans.queuedExecutions/plans.claimExecution/plans.finishExecution— 消耗并转换执行字段 同一审批行;声明的执行报告其kind,因此 sidecar 可以 选择匹配的执行指令plans.abort— 标记待审批工作已中断;它永远不会重播或 将已批准的会话更改回其合同模式
计划任务
scheduled.list/scheduled.create/scheduled.update/scheduled.deletescheduled.import— 导入任务记录并标准化其持久模式scheduled.run/scheduled.finishRun/scheduled.listRuns
线 ScheduledTask.mode 是耐用的标准化投影 config_json.mode;创建、更新和导入映射旧版 chat 到 plan 以及 默认缺失值为 agent。 scheduled.run 读取所选任务的 持久模式; plan 或 goal 任务失败并显示 创建会话或运行之前的 PLAN_REQUIRES_INTERACTIVE_SESSION。它从来没有 使用 settings.defaultMode 作为任务模式。
宿主边界的规范思维水平是:
off | minimal | low | medium | high | xhigh | max会话 summaries/details 始终返回 thinkingLevel。助理消息 可能会返回 thinking;主机存储将其映射到规范内容块,而不是 而不是将其附加到答案 content 中。
工具
tools.listtools.executetools.abort- 有序
stdout/stderr块的tools.output通知
贝壳
commandShells.listsettings.set带有部分设置对象;保留省略的字段, 并且仅当每个 会话没有活动轮次并且没有 pending/queued/running Plan/Goal 工作
工具执行仅在准入后开始。 Shell 生成重试瞬态 资源耗尽(EAGAIN / WouldBlock),具有有限的退避,从不 在命令启动后重试命令,并在之前获取超时的子命令 释放执行槽。
session.appendMessage 通过消息 ID 是幂等的。若该 id 已属于另一会话,则在写 JSONL 之前改写为 {sessionId}:{id},之后重放原始 id 为无操作(D444)。Electron 主进程可以在 host-core 重启时把消息留在应用自有 outbox 里;握手成功后按顺序冲洗,并把 UNIQUE constraint failed: messages.id 当作确认而不是停整队。带 PERMISSION_DENIED: 前缀的追加同样丢弃以免毒消息卡住 FIFO(D597)。进行中检查点从不经过发件箱:检查点只对存活的主机有意义,在最终行之后重放它是错误的。
权限
permissions.evaluatepermissions.resolvepermissions.pending(D374:待处理请求作为 Host 状态)permissions.listSessionGrantspermissions.clearSessionGrants
插件
plugins.listplugins.loadDevplugins.installFromPathplugins.installFromPackage— 在校验和验证后安装.piplug归档plugins.enableplugins.disableplugins.uninstallplugins.getPermissionsplugins.grantPermissions/plugins.revokePermissions— 更改已授予集合; 运行时强制执行「已声明 ∩ 已授予」的交集plugins.setAutoUpdateplugins.setScope— 激活作用域(ADR 0056)plugins.resolveExecution— 在回合开始前解析某会话所属项目激活了哪些 插件工具/技能/MCP 服务器
市场
market.refresh— 从配置的 URL 拉取并缓存目录market.search/market.getDetailmarket.install— 下载、验证(PLUGIN_INTEGRITY、PLUGIN_MARKET_*)并安装 目录中的一个发布版本market.checkUpdates/market.applyUpdates
提供商与模型
providers.list/providers.get/providers.create/providers.update/providers.delete拒绝插件自有的行 (ownerPluginId):该行每次加载都由 manifest 刷新,因此只由其所属插件的 生命周期改动或删除,错误信息以PROVIDER_OWNED_BY_PLUGIN开头(ADR 0259)providers.setSecret({ id, secretValue })— 写入或清除某一行 provider 的 API key(secret:provider:<id>:api_key与行的secret_ref)。这是插件自有行 接受的写入:只改声明要求的凭据,绝不改 manifest 拥有的字段。secretValue为空或省略即删除已存 key。返回{ provider },未知 id 返回nullproviders.getSecret— 仅限 main/host,渲染器永远无法触达providers.listModels/providers.cacheModels— 已发现的模型行及其 宿主侧缓存(ADR 0027 / ADR 0134)providers.testConnection
Agent 能力(技能、子代理、MCP 服务器)
skills.list/skills.active/skills.read/skills.create/skills.update/skills.remove/skills.import/skills.setEnabled/skills.setScope— 用户技能文档(校验失败返回SKILL_INVALID)agents.list/agents.active/agents.read/agents.create/agents.update/agents.remove/agents.setEnabled/agents.setScope— 用户子代理文档(SUBAGENT_INVALID)mcp.list/mcp.active/mcp.upsert/mcp.remove/mcp.setEnabled/mcp.setScope— 用户 MCP 服务器定义(MCP_INVALID)
*.active 返回经激活作用域过滤后适用于给定项目的条目(未知作用域返回 CAPABILITY_INVALID)。
搜索、工件、键盘
search.query— 跨会话、项目和设置目的地的全局搜索(ADR 0034)artifacts.list— 某会话的 Plan/Goal 检查点工件keyboard.setGlobalShortcut— 在 Electron 无法注册插件启动器快捷键时, 由宿主持有的原生回退
审计
audit.append
通知 (D117)
notification.listnotification.markReadnotification.markAllReadnotification.clear
4a。通知合约(协议 v4)
type AppNotification = {
id: string;
kind: "task.completed" | "task.failed";
sessionId: string;
sessionTitle: string;
turnId: string;
errorCode?: string;
createdAt: string; // ISO-8601 UTC
readAt?: string | null;
};
type SessionEndTurnParams = {
turnId: string;
status: "completed" | "error" | "aborted";
errorCode?: string;
usage?: unknown;
createNotification?: boolean; // default true; Electron supplies visibility decision
};
type SessionEndTurnResult = {
ok: boolean; // false when the turn was missing/already terminal
notification?: AppNotification; // omitted when no row was inserted
};
type NotificationListParams = {
unreadOnly?: boolean; // default false
limit?: number; // default/max 200
};
type NotificationListResult = {
notifications: AppNotification[]; // newest first
unreadCount: number; // global count, independent of filter
};notification.markRead({ id }) -> { ok }是幂等的。ok=false意味着 该id不存在;已读取的行仍然成功。notification.markAllRead({}) -> { ok: true }更新中的每个未读行 一笔交易。notification.clear({}) -> { ok: true }仅删除收件箱行。- 不发出
notification.createdJSON-RPC 服务器通知。 Electron 直接从session.endTurn接收插入的记录,避免了 终端转持久化和UI刷新之间的第二个点餐通道。 createNotification=false仅抑制收件箱插入;跑步回合 仍然在同一事务中达到其请求的最终状态。失踪 或非布尔值默认为 true,因此 unknown/stale UI 状态不会丢失 通知。sessionTitle是与行一起存储的稳定会话名称快照。 本地化事件 title/body 散文是由 Electron/renderer 衍生而来,从未 跨越主机 RPC。
5. 工具执行合约
tools.execute 参数
type ToolsExecuteParams = {
sessionId: string
turnId?: string
toolCallId: string
toolName: string
args: unknown
/** Diagnostic/request context only; never used for authorization. */
requestedMode?: "plan" | "goal" | "agent"
expectedCommandShellId?: CommandShellId
/** Bash only: dialect pinned by the same runtime turn. */
expectedCommandShellDialect?: "powershell" | "cmd" | "posix"
/** Bash only: host default 60000; accepted override 1000..21600000. */
timeoutMs?: number
}权威模式和工作区解析是会话范围的:
- 主机加载
sessionId并解析其持久保存的 Electron/renderer/path。 - 主机读取持久保存的
sessions.mode并将其验证为plan | agent。 冲突的requestedMode会被忽略以进行授权并仅记录 作为诊断数据。 3、该路径成为工具沙箱根目录,用于权限预览、执行、 工件路径和审核上下文。工具的显式path可能会命名 仅在主机应用外部路径权限后才位于外部位置 规则;成功的外部结果保留了绝对的规范路径。 - 不会参考可变的
workspace.get选择来获取有效的持久性 会话,因此切换保留的项目选项卡无法重定向背景 打电话。 5.持久无路径会话解析无根并接收WORKSPACE_REQUIRED工具需要一个。选定的项目不是 继承的。 - 会话不存在的旧调用可能会暂时回退到会话 选定的工作空间;新的渲染器流必须始终提供有效的
sessionId。 7、database/session-resolution错误返回INTERNAL,关闭失败; 只有已确认的丢失会话才可以使用旧后备。
对于Read/Glob/Grep/Write/Edit,主机分类显式路径 在工作区之外并在低风险自动允许规则之前从头开始。 auto 执行它,而 ask 和 accept-edits 发出 permissions.request;拒绝、超时或取消返回 TOOL_DENIED 而不执行该操作。相对 .. 和符号链接转义使用 相同的分类。 Bash 的工作目录和隐式递归遍历 不继承这个异常。
在通用权限评估之前,host-core 应用模式策略:
- Plan 和 Goal 允许
Read、Glob、Grep、BrowserPreview、Bash和 适用于实时的种类提交工具(SubmitPlan/SubmitGoal) 规划状态。 - Plan 和 Goal 拒绝
Write、Edit、每个插件工具以及以下未知工具 所有权限模式和授予。主机读取会话的持久模式 对于此检查,因此在tools.execute中声明agent的 sidecar 无法扩大 它和*_IN_PLAN错误代码是两种类型共享的。 - Plan 和 Goal
Bash遵循已解析的权限模式:ask和accept-edits发出permissions.request;auto无需确认即可执行,并且可能 变异。主机重新解析有效 shell ID/dialect 并要求 精确之前的expectedCommandShellId和expectedCommandShellDialect权限评估并在生成前再次评估;它流式传输 stdout/stderr 分别。配置好的 shell 可能会回退到第一个可用平台 创建转销之前的 shell,但执行不会改变 shell 在引脚之后。 - Agent 应用正常的注册工具和权限策略。
可见的工具列表不是安全边界;伪造的 RPC 调用是 由该主机端矩阵授权。
结果
type ToolsExecuteResult = {
toolCallId: string
ok: boolean
isError?: boolean
content: unknown
durationMs: number
denied?: boolean
errorCode?: string
// Workspace Write/Edit results may include content.details.review. The
// record is persisted with the tool message and is independent of Git.
// Bash command failures preserve content.exitCode/stdout/stderr while
// setting ok=false, isError=true, and errorCode=TOOL_FAILED.
// The agent runtime forwards isError into the tool transcript without
// dropping the structured content/details needed for recovery.
}5. 1 Plan 和 Goal 提交和批准合约
SubmitPlan 和 SubmitGoal 在通用之前作为主机转换进行处理 工具执行。主机将准确的 Markdown 字节保留在新的唯一的 在发布提案之前,先将工件放在种类的目录下。
// Identical shape for both kinds; the tool name selects the kind.
type SubmitPlanParams = {
title: string;
markdown: string;
question: string;
};
type ProposalKind = "plan" | "goal";
type PlanningState = "inactive" | "planning" | "awaiting_approval";
type GlobalPermissionMode = "ask" | "accept-edits" | "auto";
type PlanApprovalAction = "approve" | "reject";
type PlanProposalStatus =
| "pending" | "approved" | "rejected"
| "expired" | "interrupted";
type PlanExecutionState =
| "queued" | "running" | "completed" | "interrupted";
type PlanArtifact = {
relativePath: string; // `.pi/plan/<unique-name>.md` or `.pi/goal/<unique-name>.md`
sha256: string;
sizeBytes: number;
};
type PlanProposal = {
id: string;
sessionId: string;
turnId: string;
toolCallId: string;
// Which contract this approval carries; rows written before the
// discriminator existed read back as `plan`.
kind: ProposalKind;
plan: string;
markdown: string;
title: string;
question: string;
status: PlanProposalStatus;
createdAt: string;
updatedAt: string;
expiresAt?: string;
resolvedAt?: string;
action?: PlanApprovalAction;
targetPermissionMode?: GlobalPermissionMode;
errorCode?: string;
artifact?: PlanArtifact;
version: number;
executionId?: string;
executionState?: PlanExecutionState;
};
type PlanExecution = {
id: string;
proposalId: string;
sessionId: string;
// Which contract was approved; selects the sidecar's execution instruction.
kind: ProposalKind;
plan: string;
title: string;
question: string;
artifact: PlanArtifact;
targetPermissionMode: GlobalPermissionMode;
state: PlanExecutionState;
};
type PlansPendingResult = {
plans: PlanProposal[];
state?: PlanningState;
// The contract being negotiated: the pending row's kind, else the session's
// own contract mode. Absent when nothing is being negotiated.
kind?: ProposalKind;
};
type PlanResolveIdentity = {
proposalId: string;
sessionId: string;
turnId: string;
toolCallId: string;
version?: number;
};
type PlanResolveRequest =
| (PlanResolveIdentity & {
action: "approve";
targetPermissionMode: GlobalPermissionMode;
})
| (PlanResolveIdentity & { action: "reject" });
type PlanResolutionResult = {
ok: boolean;
proposal: PlanProposal;
state: PlanningState;
action?: PlanApprovalAction;
targetPermissionMode?: GlobalPermissionMode;
execution?: PlanExecution;
};主持人通知:
method: "plans.changed"
params: {
sessionId: string
state: PlanningState
kind?: ProposalKind
proposalId?: string
proposal?: PlanProposal
action?: PlanApprovalAction
targetPermissionMode?: GlobalPermissionMode | null
execution?: PlanExecution | null
}
type ToolsOutputParams = {
sessionId: string
toolCallId: string
commandShellId: CommandShellId
stream: CommandShellOutputStream
chunk: string
}
method: "tools.output"
params: ToolsOutputParamsplans.changed 是针对 Plan 或 Goal 条目、提交、解决而发出的, 执行 claim/finish,然后中止。它的顶级参数正是字段 显示;不适用于转换的字段被省略,并且 kind 命名 合同,以便渲染器可以选择正确的模式芯片和批准副本,而无需 检查投影状态。对于 plans.resolve, 当没有值时,主机发出 targetPermissionMode 和 execution 作为 JSON null 存在。 Electron 通过以下方式转发此通知: 共享 IPC.event.plansChanged 渲染器通道。
plans.resolve 仅接受经过身份验证、仍待处理的请求,其 提案、会话、轮次、工具调用和版本匹配。 approve 需要 显式权限模式并原子地将 plan_approvals 行提交到 status = approved,分配 execution_id,设置 execution_state = queued, 设置 sessions.mode = agent,并存储所选的 sessions.permission_mode;该选择未写入应用程序设置中 作为下一次默认批准。询问仍然是产品默认设置。然后,同一个 Agent 会收到一个新的提供商 使用 Agent 工具请求。
reject 记录 rejected 并使会话处于合同模式(Plan 或 Goal)。绝对的 30 分钟截止时间记录 expired 和 PLAN_APPROVAL_TIMEOUT。中止,主机 重新启动、sidecar 重新启动或持久性失败记录 interrupted。之前 启动后提供 RPC 服务,主机以事务方式中断之前的挂起 批准和 queued/running 执行状态。待处理、排队和运行 工作永远不会重播; queued/running 批准后中断离开 Agent 中的会话。进程纪元是内部的,不是线路或数据库 场。
5. 2 Shell 目录
type CommandShellId =
| "windows-powershell"
| "windows-pwsh"
| "cmd"
| "git-bash"
| "bash";
type CommandShellOption = {
id: CommandShellId;
label: string;
dialect: "powershell" | "cmd" | "posix";
available: boolean;
isDefault: boolean;
};
type CommandShellCatalog = {
configuredId: CommandShellId | null;
effective: CommandShellOption | null;
fallback: boolean;
choices: CommandShellOption[];
};
type CommandShellOutputStream = "stdout" | "stderr";commandShells.list 返回主机发现结果。设置写入存储 仅使用目录 ID,并拒绝未知、不可用或错误的平台 ID COMMAND_SHELL_INVALID。如果持久化 ID 稍后变得不可用,则 Catalog 选择第一个可用的平台 shell 并设置 fallback: true。 Bash 请求包含同一轮中固定的有效 ID 和方言; host-core 拒绝之前使用 COMMAND_SHELL_CHANGED 更改的 ID 或方言 权限评估和生成前。身份不是可执行路径 哈希。
6. 权限请求通知
主机可能会发出:
method: "permissions.request"
params: {
requestId: string
sessionId: string
toolCallId: string
toolName: string
risk: "low" | "medium" | "high"
argsPreview: unknown
reason: string
timeoutMs: 120000
}Electron/UI 通过以下方式解决:
method: "permissions.resolve"
params: {
requestId: string
decision: "allow-once" | "allow-session" | "deny"
}超时行为 (D005):120 秒后未解决 → 拒绝。
permissions.pending 把待处理请求作为 Host 状态返回(D374/D375): { requests: PendingPermission[] },最早的在前,可按 sessionId 过滤。每一项包含与 permissions.request 通知相同的字段,外加 createdAt、expiresAt 和 remainingMs; 已超时的请求不会出现。在通知发出之后才接入的客户端读取此列表,并通过不变的 permissions.resolve 作答;通知路径本身不变。
7. 错误代码
JSON-RPC 错误携带一个数字 code 以及 data.errorCode,后者是来自 08-错误代码 的稳定字符串。多个字符串码 共用同一个数字槽位;字符串才是契约,数字只是传输细节。
| 代码 | 错误代码 | 意义 |
|---|---|---|
| 1000 | INTERNAL | 意外主机故障 |
| 1001 | UNAUTHORIZED | missing/invalid 握手或功能 |
| 1001 | HOST_SHUTTING_DOWN | 主机在 EOF 后正在排空,拒绝了该调用 |
| 1002 | INVALID_PARAMS | 架构验证失败 |
| 1002 | MODEL_ALIAS_TOO_LONG | 提供商行别名超过 60 个码点 |
| 1002 | MODEL_BINDINGS_DEGRADED | 存储模型绑定不可读;拒绝显式替换模型数组 |
| 1003 | NOT_FOUND | 实体缺失(遗留槽位,为旧调用方保留) |
| 1006 | RATE_LIMITED | 某个按调用方计的预算窗口已耗尽 |
| 1007 | NOT_FOUND | 实体缺失 |
| 1007 | SESSION_NOT_FOUND | 点名的会话不存在;工具请求永远不会回退到全局工作区 |
| 1008 | CONFLICT | busy/conflict 状态 |
| 1008 | AGENT_BUSY | 该会话有一个正在运行的回合 |
| 1009 | PLUGIN_INVALID | manifest/validation 失败 |
| 1010 | PLUGIN_LOAD_FAILED | enable/load 失败 |
| 1011 | PROTOCOL_MISMATCH | app.handshake 协议版本不匹配 |
| 1012 | PLUGIN_INTEGRITY | 包 checksum/signature 不匹配 |
| 1013 | PLUGIN_PERMISSION_DENIED | 插件缺少该调用所需的权限 |
| 1014 | PLUGIN_NETWORK | 市场 download/catalog 拉取失败 |
| 1015 | MCP_INVALID | 用户 MCP 服务器定义校验失败 |
| 1015 | PLAN_* | 所有 Plan/Goal 检查点失败(PLAN_APPROVAL_TIMEOUT、PLAN_APPROVAL_STALE、PLAN_APPROVAL_INTERRUPTED、PLAN_SESSION_NOT_FOUND、PLAN_WORKSPACE_REQUIRED……)共用此槽位;由字符串码区分 |
| 1016 | SKILL_INVALID | 用户技能文档校验失败 |
| 1017 | SUBAGENT_INVALID | 用户子代理文档校验失败 |
| 1018 | CAPABILITY_INVALID | Agent 能力 root/scope 设置校验失败 |
| 1019 | PLUGIN_CANCELLED | 用户在下载过程中取消了市场安装 |
| 1020 | PLUGIN_MARKET_NOT_PUBLISHED | 平台有该版本但尚未对外提供 |
| 1021 | PLUGIN_MARKET_ARCHIVED | 插件已被平台下架 |
| 1022 | PLUGIN_MARKET_NOT_FOUND | 平台没有该插件或该版本 |
| 1023 | PLUGIN_MARKET_RATE_LIMITED | 下载接口要求客户端等待后重试 |
| 1024 | PLUGIN_MARKET_NO_SOURCE | 没有任何分发目标能提供该包 |
| -32029 | HOST_OVERLOADED | RPC 调度程序容量已耗尽 |
| -32601 | — | 未知方法 |
| -32700 | — | 无法解析的请求行 |
| 1002 | LIMIT_EXCEEDED | 超过 64 MiB 的 NDJSON 请求行;Electron 在写入管道前拒绝;若主机仍读到该行,则读完余下部分、尽量从截断前缀取出请求 id 再应答,stdin 读取器继续运行 |
工具结果(TOOL_DENIED、TOOL_TIMEOUT、PATH_OUTSIDE_WORKSPACE、 WORKSPACE_PATH_DENIED、WRITE_DISABLED_IN_PLAN、SHELL_NOT_FOUND、 COMMAND_SHELL_CHANGED……)不是 JSON-RPC 错误:tools.execute 在结果中返回 ok: false 并附带 errorCode(§5)。
8. 并发/排序
- 请求可以在调度程序上限内并发。 Read/search 工具可能 并行运行;每个会话的 Read/search/
Write都是有界的并且按 FIFO 顺序排列, 一次会话中最多有一个突变。 - 不同的会话可以在保留的项目选项卡上同时继续; 每个都解析自己的项目根并授予
- 握手后随时可能收到通知
tools.output保留 stdout/stderr 分离和通知顺序; 它的作用域为 session/tool 调用,并且没有回合或排序字段; 最终结果仍然有限- Abort是幂等的,关闭整个Bash进程树
- Plan 和 Goal 批准请求为 proposal/session/turn/tool-call/version 范围; 每个项目仅存在一项待批准和一项 queued/running 执行 会话,并且分辨率由 host-core 序列化
- 启动事务性地中断待批准和 queued/running RPC 服务之前的执行状态。延迟渲染器响应无法关闭; 挂起的中断保持会话的合同模式和 已批准的 queued/running 中断保留 Agent。
- 会话分叉是一种主机拥有的快照操作。源转录本 永远不会被重写,并且处理的子 write/index 失败不会留下任何结果 可见的会话或孤立的转录文件。 D119 之后发生进程崩溃 现有的孤儿成绩单恢复政策。
- 消息范围的分叉除了规范快照结束之外是相同的 包括在
throughMessageId。它仍然重新映射 message/tool-call id 和 不创建运行时或修订状态,因此稍后的子 reseed/cache 状态为 由新的会话 ID 隔离。
9. 日志记录规则
- 从不记录 API keys/secrets
- 工具参数可能会在审核预览中进行编辑
- 每个tools.execute都会获得trace id =
toolCallId
10. 验收
- Electron 生成主机并完成握手 2.health方法返回ok
- 拒绝刀具路径返回
TOOL_DENIED4.超时路径120s后返回拒绝决策 5.将选定的工作空间从A切换到B不会改变工具根 会话 A 发出的呼叫的 - 协议 v4
session.endTurncreates/returns 恰好有一个通知 未见 completed/failed 轮次,并且没有可见电流、中止或 重复终端更新 7.通知list/unread/read-all/clear通过host-core往返 仍受最新 200 个持久行的限制 - 分叉一个空闲会话会产生一个独立可变的子进程 离开时具有相同的活动转录本和持久执行配置 源及其重新生成的修订版保持不变
- 分叉消息会排除后面的所有源行并拒绝 未创建子项的未知消息
- 伪造的
requestedMode无法授权工具进入持久模式; Plan 和 Goal 拒绝 Write/Edit/plugin/unknown 工具并申请权限 根据requestedMode/Write/Edit/plugin/unknown/Plan 提示 Bash - SubmitPlan 和 SubmitGoal 将精确的 Markdown 字节写入唯一的
.pi/plan/*.md或.pi/goal/*.md文件 hash/size 和结构化 title/question 字段;仅匹配 approve/reject 响应可以解析实时plan_approvals行,并且 针对其他类型运行的提交工具失败并显示PLAN_KIND_MISMATCH无需编写工件 - Plan 和 Goal 过期、中止、崩溃、计划拒绝和陈旧响应 产生记录的持久状态和事件
- Bash 验证固定 shell ID/dialect,传输 stdout/stderr,强制执行 60s default/bounded 覆盖,并关闭整个进程树
定时任务工具
Agent 模式按需提供 ScheduledTaskList、ScheduledTaskCreate、ScheduledTaskUpdate、 ScheduledTaskDelete。通过 tools.execute 复用现有授权、审计和定时任务领域处理器。 查询为低风险;Ask/Accept Edits 下修改需授权。Plan/Goal 即使在 Auto 下也拒绝。
Host 重新检查会话的持久化模式,按调用会话的项目限制访问,不使用前台项目或模型传入路径。 创建时绑定该项目,查询过滤项目,修改/删除要求项目匹配。未知字段、非法周期、空标题或 提示词、非法时间和星期在写入前拒绝;不能删除运行中的任务。创建需 title、prompt、cadence; 每天/每周自动任务需 schedule。修改使用已存在的 ID 并保留未指定字段。界面虽只提供四个 时段,工具仍支持具体本地时间。不新增数据库 schema 或传输协议。
定时任务:任务级执行设置
桌面端 create/update 可为单个任务保存 workspacePath、permissionMode 和成对的 providerId/modelId。立即运行与自动运行在字段存在时均使用这些值;字段缺失时保留旧版 项目捕获、应用默认模型和权限行为。非法权限与不完整模型组合在写入前拒绝。对话工具不暴露 这些字段,仍限制在调用会话所属项目。见 ADR 0305。
任务还可独立保存 thinkingLevel,取值与会话相同(包括 off 和 omit)。 模型和推理等级直接复用主对话框的完整选择器及交互逻辑,仅将保存回调接到任务草稿。 未配置此字段的旧任务仍以 off 运行;清空字段恢复旧行为,不需要数据库迁移。
定时任务:独立分发到期任务
Electron runner 无需等待其他任务的提示词准备完成,即可准入彼此独立的到期任务。 本地执行中所有权按任务 ID 和 Host 实例记录,直到准备结束;enabled、due 和重叠 检查仍由 Host 决定。旧 Host 的完成不会清除替代 Host 的所有权。停止 runner 只阻止新轮询,已准入任务继续使用现有执行和失败生命周期;错误仍可观察,迟到 90 秒的规则保持不变。
定时任务:删除项目与自动任务
删除项目时暂停与其绑定的定时任务,但保留任务定义、schedule、工作区绑定和运行历史。 删除项目会话后,历史中的会话引用可能变为 null。已经准入的任务即使尚未开始会话 轮次,也会阻止项目删除。其他项目的任务和未绑定的旧任务不受影响。用户显式恢复 任务或点击 Run now 时可以从保留路径重新创建项目;暂停状态下的自动轮询不会这样做。
定时任务:旧任务维护
Agent 工具允许对缺少 schedule 的旧版自动任务修改标题、提示词或暂停,也允许回传 未变化的 cadence。这些维护操作不会启用任务,也不会捕获前台工作区。显式启用、 改变 cadence 或提供 schedule 时仍执行 schedule 校验;恢复任务需要明确的合法 schedule,Manual 转 Hourly 继续使用现有默认间隔行为。
定时任务:日历配置意图
可选的 config_json.calendarConfigured 布尔值用于区分用户明确设置的每日/每周 日历时间与 Hourly 的内部占位 schedule。缺少该字段时,旧版 Daily/Weekly 任务视为已设置日历;旧版 Hourly 保留现有值,但转换为 Daily/Weekly 时必须明确 提供 schedule。已确认的日历配置在切换为 Hourly 和重启后仍会保留,包括午夜。 清空日历或改为不同的非日历占位值会清除该意图。该扩展不修改表结构;旧版本会 忽略该字段,无法执行新的转换保护。仅修改元数据以及 Manual 转 Hourly 的行为不变。
定时任务:工作区身份
保存和读取工作区绑定时统一使用现有项目路径规范化规则。在 Windows 上, 斜杠方向、大小写、末尾分隔符和扩展路径前缀的差异不会再让同项目会话看不到任务。 缺失的旧版绑定与显式 null 仍保持不同语义;其他项目的工具不能查询或修改绑定任务。