Skip to content

06. 主机 RPC 协议 ​

翻译说明: 本页是与 英文源规格 一一对应的机器辅助翻译。代码、协议字段和标识符保持原文;如翻译与英文源事实有歧义,以英文版本为准。

Bounded existing-history operations (#1868) ​

The Desktop uses additive Host methods for existing history: session.deleteMessage takes sessionId/messageId and returns ok/deleted/deletedCount; session.retractPendingPrompt takes sessionId/userMessageId and optional turnId, returning ok/retracted/removedCount and a refusal reason when needed. Host verifies canonical message/turn ownership and meaningful reply activity; a refused retract leaves history intact. Repeating an already completed deletion or retraction does not delete another prompt. No history array is required.

session.activateRevision accepts optional requireCanonicalPrefix: true to require a durable Host prefix; a missing family/anchor fails explicitly. Existing explicit-prefix activation, session.replaceMessages, session.saveRevision and their original request/response forms remain supported. Existing edit and regenerate continue using session.truncateFrom; branch archival and fork remain Host-owned. Oversized local requests report the method and UTF-8 byte count without payload content; the caller sees a local-transport explanation and the existing session Fork recovery path. No identical oversized payload is retried. The unchanged 64 MiB limit still applies to arbitrary incoming requests, including a genuinely oversized single tool result.

ID mutations use the same session operation lock, rewrite fence and runtime disposal boundary. Their outbox drain must confirm all target-session entries were written; failure preserves the queue and rejects the mutation. Smart-stop restores a draft only after Host confirmation, and exposes failures.

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 调用在等待时不会保留全局容量 对于同一会话中的较早突变。

请求 ​

json
{
  "jsonrpc": "2.0",
  "id": "req_01H...",
  "method": "tools.execute",
  "params": {}
}

回应 ​

json
{
  "jsonrpc": "2.0",
  "id": "req_01H...",
  "result": {}
}

错误 ​

json
{
  "jsonrpc": "2.0",
  "id": "req_01H...",
  "error": {
    "code": 1003,
    "message": "PATH_OUTSIDE_WORKSPACE",
    "data": {
      "errorCode": "PATH_OUTSIDE_WORKSPACE",
      "details": {}
    }
  }
}

通知(服务器 → 客户端,无 id) ​

json
{
  "jsonrpc": "2.0",
  "method": "permissions.request",
  "params": {}
}

3. 握手 ​

在生成时,Electron 必须调用:

app.handshake ​

参数:

ts
type HandshakeParams = {
  protocolVersion: 11
  client: "electron-main"
  clientVersion: string
  locale: string // default "en"
}

结果:

ts
type HandshakeResult = {
  protocolVersion: 11
  host: "rust-host-core"
  hostVersion: string
  features: string[]
}

规则:

  1. 如果协议主要版本不匹配→中止启动

  2. 如果握手失败,Electron 应退出并出现可操作错误 3.后续所有调用都需要握手成功

  3. 版本4引入了持久通知收件箱和 带有通知的 session.endTurn 结果。 5.版本5需要主机拥有的session.fork快照操作;一个 在聊天变为交互式之前,必须拒绝版本 4 主机 (ADR 0023)。

  4. 版本 6 添加了持久模型上下文检查点 session.appendCompaction;版本 5 主机必须先被拒绝 运行时声明自动上下文保护 (ADR 0030)。

  5. 版本 9 是冻结的 ADR 0053/0054 合约:它涵盖了检查点 Plan artifact/queue,主动转向 Plan identity/CAS,明确批准 权限、shell 目录身份和方言 pin、流式命令输出、 以及来自 config_json 的计划任务模式投影。 v7 或不兼容 在 UI 变为交互式之前,必须拒绝 v8 主机。

  6. 版本 11 撤回 A2A 协议栈(ADR 0165 / D326)。a2a.* 方法和通知 已移除,握手不再声明 a2a;v10 主机或客户端必须在 UI 交互前拒绝。

协议 v11 与 host-core 存储架构 v23 配对。v14 增加插件会话来源 sidecar 和软删除字段;v23 增加 sessions.title_source,持久记录默认、手动和插件生成标题的归属。 架构版本是内部持久性不变量,而不是额外的 JSON-RPC 字段, 检查点架构仍然由主机拥有。

4. 方法目录(MVP) ​

应用程序 ​

  • app.handshake
  • app.health
  • app.getVersion
  • app.getOnboarding — 内联引导清单状态(D031)

app.health 返回诊断 toolBudget 对象:

ts
type ToolBudgetHealth = {
  active: number
  queued: number
  total: number
  shell: number
  reads: number
  mutations: number
  plugins: number
}

工作区 ​

  • workspace.get
  • workspace.set
  • workspace.clear

查看快照 (ADR 0043) ​

  • review.rollback({sessionId, snapshotId}) — 验证当前的后期工具 hash,恢复会话拥有的先前字节,并返回其中之一 rolledBack、alreadyRolledBack、conflict 或 unavailable。

项目 ​

  • projects.list — 返回先固定的持久项目记录,然后返回 按上次开放时间;包括通过会话导入具体化的记录
  • projects.create({ path }) — 创建或复用持久项目记录,不切换当前工作区, 并返回宿主生成的项目 id
  • projects.remove({ path }) — 删除一条持久项目行,并连同附加到它的每个会话一起删除, 移除这些会话的转录本、scratch 和 review 文件以及该项目的持久记忆,且从不触碰磁盘上的 项目文件夹。幂等:未知路径返回 { removed: false, sessionsRemoved: 0 }。作为已存储 多文件夹项目组根目录的路径会被拒绝,以便该组保留有效的 Primary 根目录;而只要其中仍有会话 在运行,调用就会被拒绝(1008 / CONFLICT),因此运行中的轮次绝不会丢失它正在写入的转录本。

秘密 ​

  • secrets.set
  • secrets.delete
  • secrets.has
  • secrets.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.get
  • settings.set

会议 ​

  • session.list

  • session.create — 接受可选的 thinkingLevel; missing/null 默认值 至 off。可选布尔值 fastMode(ADR 0334)保存会话的 /fast 开关;缺省/null 表示 false,非布尔值返回 INVALID_PARAMS。可选的 isolated: true(ADR 0329)同时要求 projectPath,并在返回前为新会话 创建专属 git worktree;无法创建时会话会被删除,调用以 WORKTREE_* 错误(§7)失败。 Optional modeSnapshot is validated for the selected mode and committed with the session atomically. A rejected snapshot or failed insert leaves no partial session or orphan snapshot; omitting it retains legacy creation semantics.

  • session.fork — accepts sessionId, an optional caller-provided display title, and optional throughMessageId; creates one independent session from the source's current active canonical transcript, truncated inclusively at the selected message when supplied. The child inherits project/provider/model/mode/thinking and permission configuration, receives new message/tool-call ids, and starts without turns, revisions, notifications, artifacts, grants, or scratch data. Missing sources or anchors return NOT_FOUND. While a Desktop source runs, only a completed assistant prefix containing no indexed messages owned by a running turn is allowed. This check and publication share the host RPC lock. Whole-session, non-assistant, streaming/error, or live-turn anchors return CONFLICT, normalized by Electron to AGENT_BUSY. The source turn continues without sharing runtime state with the child.

  • session.get

  • session.delete — 对隔离会话还会删除其 worktree 和分支(丢弃未合并的工作)

  • session.getScratchPath — 会话的 scratch 目录(D114),按需创建

  • session.usageCost({ sessionId }) — 会话 turns.usage_json 账本的只读费用汇总,按 providerId 分组: { sessionId, turnCount, requestCount, providers: [{ providerId?, total, pricedRequests, reportedRequests, unpricedRequests }] }。无表结构变更;缺少 sessionId 返回 INVALID_PARAMS。

  • session.rename — 在主机边界裁剪并校验标题,接受 1–80 个 Unicode 码点;成功后将标题来源 标记为 manual,但不更新 updated_at、转录内容、消息数或历史通知标题快照。

  • session.deriveTitle — { id, title }:应用确定性的首条提示兜底标题,返回 { updated: boolean }。标题按同样的 1–80 码点规则校验,且只在存储标题仍是可识别占位 标题、标题来源为 default 时写入;写入后仍保留该来源,因此标题插件还能替换派生文本, 并且不更新 updated_at、转录内容或消息数。

  • session.configure — 以原子方式持久保存 mode、providerId、modelId, 以及可选的 thinkingLevel 用于下一个 pi 回合; omitting/null thinkingLevel 保留当前值;可选布尔值 fastMode(ADR 0334)遵循同一规则, 非布尔值返回 INVALID_PARAMS;返回无效模式或级别 INVALID_PARAMS;模式为 plan | goal | agent 并更改任何会话 仅在空闲且没有 pending/queued/running 时才允许配置 Plan 或 Goal 记录

  • session.appendMessage

  • session.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_count

  • session.saveActiveRevision — 归档最新的分支 将带有修订版的用户 root 作为其活动修订版并标记该 root 的寻呼机 元数据,全部位于 RPC 锁下。邮票重写了一行文字记录 而不是文件,因此并发的 session.appendMessage 仍然存在。 当会话不拥有重新生成历史记录时,返回 { saved: null }。 已归档的活动变体会被刷新,而不是跳过。 回合完成调用者使用它而不是 session.get + session.replaceMessages

  • session.listRevisions — 列出根用户系列的线性变体

  • session.activateRevision — 用 prefix + branch 替换实时转录 并标记根寻呼机元数据。切换前它先从持久转录本重新归档该系列的实时 分支(刷新实时根消息 activeRevision 标记所指的变体,或把已标记但从未 归档的分支存为新变体),因此上次归档之后追加的内容不会丢失。当该系列 存在于持久转录本中时,恢复分支之前的前缀取自转录本而非调用方。幸存 消息保留所属的 turn_id

  • session.beginTurn

  • session.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.autoTitleContext — 通过 Electron 的 session.autoTitle 权限校验后, 仅返回活动且标题来源为 default 的会话首条用户提示(≤1,000 字符)和首条助手回复(≤500 字符)
  • plugin.session.setAutoTitle — 仅在预期标题和 default 来源仍匹配时更新标题;返回 { updated }
  • 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 中。

会话 Todo 清单

  • todos.get({ sessionId }) 返回活动 Desktop 会话已提交的完整清单快照: { sessionId, todos, revision, updatedAt }。未知或软删除会话返回 NOT_FOUND; 原生 Pi 会话和空 id 返回 INVALID_ARGUMENT。
  • tools.execute 的 toolName: "TodoWrite" 只接受 { todos },并替换整个有序清单。 主机会裁剪内容、将 priority 默认设为 medium,把超长 Unicode 内容限制在 500 个字符并 返回警告,把后续 in_progress 降级为 pending,并拒绝超过 50 项或格式错误的值。 所属 session 和运行中的 turn 来自可信传输字段,不来自工具参数。
  • TodoWrite 只允许 Agent 会话自己的运行 turn。Plan/Goal、委托、插件和 MCP 调用会返回工具错误, 不会修改存储。成功替换即使 todos 为空也会推进 revision,并且只在事务提交后发出 todos.changed;事件负载与 todos.get 返回的完整快照一致。
  • SQLite 只由 host-core 拥有。渲染器通过 Electron Main IPC 接收快照,按 session id 保存并忽略 更旧或相同 revision。远程 RACP 会话在这条垂直切片中保持 local-only,因为 RACP v1 尚无 Todo 快照操作。

Plan 和 Goal 状态和批准 ​

两种合约类型共享这些方法;可选的 kind (plan | goal,默认 plan,因此 D198 之前的 sidecar 仍然有效)选择哪个 合同正在洽谈中。

  • plans.enter — 仅接受活动 Agent 回合的 sessionId、turnId, 和 toolCallId 加上 kind; host-core 执行模式转换至 具有比较和交换更新的那种模式并发出 plans.changed 携带 kind。无法识别的 kind 失败并显示 INVALID_PARAMS
  • plans.submit — 将主机拥有的工件写入该种类的目录下,并 创建一个待处理的提案,其 kind 保留在该行上
  • plans.pending — 仅返回待批准行、会话计划 状态,以及正在协商的合约的 kind(待处理行的类型, 回到会话自己的合约模式);渲染器重新加载不会 在主机还活着并且不恢复的情况下延长绝对期限 终端卡
  • plans.resolve — 验证一个匹配的 approve/reject 响应,并且 批准,提交所选权限模式和 execution_state = queued
  • plans.queuedExecutions / plans.claimExecution / plans.finishExecution — 消耗并转换执行字段 同一审批行;声明的执行报告其 kind,因此 sidecar 可以 选择匹配的执行指令
  • plans.abort — 标记待审批工作已中断;它永远不会重播或 将已批准的会话更改回其合同模式

计划任务 ​

  • scheduled.list / scheduled.create / scheduled.update / scheduled.delete
  • scheduled.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 作为任务模式。

宿主边界的规范思维水平是:

text
off | minimal | low | medium | high | xhigh | max

会话 summaries/details 始终返回 thinkingLevel。助理消息 可能会返回 thinking;主机存储将其映射到规范内容块,而不是 而不是将其附加到答案 content 中。

会话 worktree(ADR 0329) ​

新增的 worktree.* 方法为增量 RPC,不升协议版本。失败使用代码 1025 及 WORKTREE_* 字符串代码。

  • worktree.probe({ projectPath }) → { available, uncommittedChanges };项目无法承载 隔离会话时(非 git、无提交、分离 HEAD、缺少 git 或低于 2.29、远程主机项目—— WORKTREE_REMOTE_UNSUPPORTED 等)返回 { available: false, errorCode, message }。session.create 对远程主机项目的 isolated: true 以同一代码拒绝, 不会创建任何会话。
  • worktree.preview({ sessionId }) → { preview },包含将要合入的提交、文件及 previewToken。令牌覆盖会话 HEAD、项目分支最新提交、冲突状态,以及所有未提交和未跟踪文件的完整内容。
  • worktree.merge({ sessionId, previewToken }) → { outcome }。令牌不再匹配(会话或项目分支有变化)为 WORKTREE_CHANGED_SINCE_PREVIEW;项目有未提交改动为 WORKTREE_PROJECT_DIRTY;冲突为 WORKTREE_CONFLICT,worktree 保持冲突状态,项目不受影响。项目检出必须仍在会话的所属分支上、worktree 必须仍在自己的分支上,每次写入前都会重新检查(WORKTREE_OWNER_BRANCH_CHANGED、 WORKTREE_SESSION_BRANCH_CHANGED);快进绑定到检查过的那个提交,完成后再核对。执行期间宿主持有会话的工具: 最多等待正在运行的工具五秒,超时返回 WORKTREE_BUSY,新的工具调用排队等到结束。
  • worktree.behind({ sessionId }) → { commits },即会话尚未包含的项目分支提交数。
  • worktree.behindAll() → { counts, statusesChanged }:以会话 id 为键给出每个隔离会话的同一计数 (读不到的会话省略),每次并发检查少量会话。处于冲突状态的会话会先检查是否已解决; statusesChanged 提示渲染层刷新会话列表。渲染层在后台读取,用于侧边栏标记。
  • worktree.sync({ sessionId }) → { outcome }(synced 及 commits,或 upToDate): “从项目同步”先提交会话中待提交的工作,再把项目分支合入会话 worktree,与合并一样持有会话工具并检查自身分支。 不改动项目检出;冲突为 WORKTREE_CONFLICT,留在 worktree 中,并像合并冲突一样标记会话。
  • worktree.pendingWork({ sessionId }) → { pending },即删除会丢弃的未合并工作。
  • worktree.setupLog({ sessionId }) → { setupStatus, log }。
  • worktree.location({ sessionId }) → { path };仅供 Electron Main 的“在文件夹中显示” 使用,绝不转发给渲染器。
  • worktree.setupCommand.get({ projectPath }) → { command }; worktree.setupCommand.set({ projectPath, command }) → { ok }(未知项目为 NOT_FOUND)。该命令在每个新 worktree 中运行一次。
  • worktree.delegate.create({ sessionId, delegationId }) → { worktree } 和 worktree.delegate.settle({ sessionId, delegationId, merge }) → { settlement }——仅供 agent-runtime 为隔离会话中的 Write/Edit 子代理使用(03-tools-and-permissions §10.2)。两者使用 10 分钟的传输期限(WORKTREE_DELEGATE_RPC_TIMEOUT_MS),因为提交和合并前可能要等待会话的工具。

工具 ​

  • tools.list
  • tools.execute
  • tools.abort
  • 有序 stdout/stderr 块的 tools.output 通知

后台任务 ​

  • backgroundTasks.list
  • backgroundTasks.output
  • backgroundTasks.stop
  • backgroundTasks.changed 通知,携带 { version };任务启动、退出或被停止时发出

tools.execute 带 runInBackground: true 时启动的是归宿主所有的 Bash 命令, 而不是本轮的命令:调用立即以 backgroundTaskId 返回,任务不进入按工具调用注册的 取消表,因此 tools.abort / agent.abort 都无法触及它,输出按流保存在有界环形缓冲 中。注册表位于宿主进程内,宿主关闭时会停止全部任务,因此应用退出不会残留孤儿进程。

输出块不会触发 backgroundTasks.changed;该事件仍用于生命周期状态失效通知, 不承担高频日志传输。客户端只读取当前展开的输出。最终状态包含排空后的尾部输出, 即使并发读取占用输出锁,关闭宿主仍会向每个任务发送停止信号。

贝壳 ​

  • commandShells.list
  • settings.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)。无法归类的追加会在同一 Host 代内按有界退避重试,连续失败后隔离到应用自有的死信文件而不是长期占用队头,因此单条被拒追加不会拖垮所有会话的写入(D656)。进行中检查点从不经过发件箱:检查点只对存活的主机有意义,在最终行之后重放它是错误的。

权限 ​

  • permissions.evaluate
  • permissions.resolve
  • permissions.pending(D374:待处理请求作为 Host 状态)
  • permissions.listSessionGrants
  • permissions.clearSessionGrants

插件 ​

  • plugins.list
  • plugins.loadDev
  • plugins.installFromPath
  • plugins.installFromPackage — 在校验和验证后安装 .piplug 归档
  • plugins.enable
  • plugins.disable
  • plugins.uninstall
  • plugins.getPermissions
  • plugins.grantPermissions / plugins.revokePermissions — 更改已授予集合; 运行时强制执行「已声明 ∩ 已授予」的交集
  • plugins.setAutoUpdate
  • plugins.setScope — 激活作用域(ADR 0056)
  • plugins.resolveExecution — 在回合开始前解析某会话所属项目激活了哪些 插件工具/技能/MCP 服务器

市场 ​

  • market.refresh — 从配置的 URL 拉取并缓存目录
  • market.search / market.getDetail
  • market.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 返回 null
  • providers.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.list
  • notification.markRead
  • notification.markAllRead
  • notification.clear

4a。通知合约(协议 v4) ​

ts
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 } 仅删除收件箱行。
  • id 是 Renderer 和本机投递的稳定一次性键。客户端必须丢弃已经确认/清除 的 id 的重复或延迟记录;清空收件箱不会让旧终端回合再次具备插入资格。 后续真正的终端回合会获得新的 id。
  • 不发出 notification.created JSON-RPC 服务器通知。 Electron 直接从 session.endTurn 接收插入的记录,避免了 终端转持久化和UI刷新之间的第二个点餐通道。
  • createNotification=false 仅抑制收件箱插入;跑步回合 仍然在同一事务中达到其请求的最终状态。失踪 或非布尔值默认为 true,因此 unknown/stale UI 状态不会丢失 通知。
  • sessionTitle 是与行一起存储的稳定会话名称快照。 本地化事件 title/body 散文是由 Electron/renderer 衍生而来,从未 跨越主机 RPC。

5. 工具执行合约 ​

tools.execute 参数 ​

ts
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
  /** Isolated `Write`/`Edit` delegate chain id; re-roots the call to that
   * delegate's worktree (ADR 0329). Omitted for every other call. */
  delegationId?: string
}

权威模式和工作区解析是会话范围的:

  1. 主机加载 sessionId 并解析其持久保存的 Electron/renderer/path。
  2. 主机读取持久保存的 sessions.mode 并将其验证为 plan | agent。 冲突的 requestedMode 会被忽略以进行授权并仅记录 作为诊断数据。 3、该路径成为工具沙箱根目录,用于权限预览、执行、 工件路径和审核上下文。工具的显式 path 可能会命名 仅在主机应用外部路径权限后才位于外部位置 规则;成功的外部结果保留了绝对的规范路径。
  3. 不会参考可变的 workspace.get 选择来获取有效的持久性 会话,因此切换保留的项目选项卡无法重定向背景 打电话。 5.持久无路径会话解析无根并接收 WORKSPACE_REQUIRED 工具需要一个。选定的项目不是 继承的。
  4. 会话不存在的旧调用可能会暂时回退到会话 选定的工作空间;新的渲染器流必须始终提供有效的 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 调用是 由该主机端矩阵授权。

结果 ​

ts
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 字节保留在新的唯一的 在发布提案之前,先将工件放在种类的目录下。

ts
// 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;
};

主持人通知:

text
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: ToolsOutputParams

plans.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 目录 ​

ts
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. 权限请求通知 ​

主机可能会发出:

ts
method: "permissions.request"
params: {
  requestId: string
  sessionId: string
  toolCallId: string
  toolName: string
  risk: "low" | "medium" | "high"
  argsPreview: unknown
  reason: string
}

Electron/UI 通过以下方式解决:

ts
method: "permissions.resolve"
params: {
  requestId: string
  decision: "allow-once" | "allow-session" | "deny"
}

本地权限行为(D636 / ADR 0310):未解决的请求会一直保持待处理,直到明确决定、取消或主机/进程关闭。tools.execute 的传输不设置截止时间;批准后仍执行工具自身的超时限制。

permissions.pending 把待处理请求作为 Host 状态返回(D374/D375): { requests: PendingPermission[] },最早的在前,可按 sessionId 过滤。每一项包含与 permissions.request 通知相同的字段,外加 createdAt;请求解决前会一直列出。在通知发出之后才接入的客户端读取此列表,并通过不变的 permissions.resolve 作答;通知路径本身不变。

7. 错误代码 ​

JSON-RPC 错误携带一个数字 code 以及 data.errorCode,后者是来自 08-错误代码 的稳定字符串。多个字符串码 共用同一个数字槽位;字符串才是契约,数字只是传输细节。

代码错误代码意义
1000INTERNAL意外主机故障
1001UNAUTHORIZEDmissing/invalid 握手或功能
1001HOST_SHUTTING_DOWN主机在 EOF 后正在排空,拒绝了该调用
1002INVALID_PARAMS架构验证失败
1002MODEL_ALIAS_TOO_LONG提供商行别名超过 60 个码点
1002MODEL_BINDINGS_DEGRADED存储模型绑定不可读;拒绝显式替换模型数组
1003NOT_FOUND实体缺失(遗留槽位,为旧调用方保留)
1006RATE_LIMITED某个按调用方计的预算窗口已耗尽
1007NOT_FOUND实体缺失
1007SESSION_NOT_FOUND点名的会话不存在;工具请求永远不会回退到全局工作区
1008CONFLICTbusy/conflict 状态
1008AGENT_BUSY该会话有一个正在运行的回合
1009PLUGIN_INVALIDmanifest/validation 失败
1010PLUGIN_LOAD_FAILEDenable/load 失败
1011PROTOCOL_MISMATCHapp.handshake 协议版本不匹配
1012PLUGIN_INTEGRITY包 checksum/signature 不匹配
1013PLUGIN_PERMISSION_DENIED插件缺少该调用所需的权限
1014PLUGIN_NETWORK市场 download/catalog 拉取失败
1015MCP_INVALID用户 MCP 服务器定义校验失败
1015PLAN_*所有 Plan/Goal 检查点失败(PLAN_APPROVAL_TIMEOUT、PLAN_APPROVAL_STALE、PLAN_APPROVAL_INTERRUPTED、PLAN_SESSION_NOT_FOUND、PLAN_WORKSPACE_REQUIRED……)共用此槽位;由字符串码区分
1016SKILL_INVALID用户技能文档校验失败
1017SUBAGENT_INVALID用户子代理文档校验失败
1018CAPABILITY_INVALIDAgent 能力 root/scope 设置校验失败
1019PLUGIN_CANCELLED用户在下载过程中取消了市场安装
1020PLUGIN_MARKET_NOT_PUBLISHED平台有该版本但尚未对外提供
1021PLUGIN_MARKET_ARCHIVED插件已被平台下架
1022PLUGIN_MARKET_NOT_FOUND平台没有该插件或该版本
1023PLUGIN_MARKET_RATE_LIMITED下载接口要求客户端等待后重试
1024PLUGIN_MARKET_NO_SOURCE没有任何分发目标能提供该包
1025WORKTREE_*所有隔离会话 worktree 失败(WORKTREE_NOT_GIT、WORKTREE_CONFLICT、WORKTREE_PROJECT_DIRTY、WORKTREE_MISSING 等)共用此槽位,以字符串代码区分
-32029HOST_OVERLOADEDRPC 调度程序容量已耗尽
-32601—未知方法
-32700—无法解析的请求行
1002LIMIT_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. 并发/排序 ​

  1. 请求可以在调度程序上限内并发。 Read/search 工具可能 并行运行;每个会话的 Read/search/Write 都是有界的并且按 FIFO 顺序排列, 一次会话中最多有一个突变。
  2. 不同的会话可以在保留的项目选项卡上同时继续; 每个都解析自己的项目根并授予
  3. 握手后随时可能收到通知
  4. tools.output 保留 stdout/stderr 分离和通知顺序; 它的作用域为 session/tool 调用,并且没有回合或排序字段; 最终结果仍然有限
  5. Abort是幂等的,关闭整个Bash进程树
  6. Plan 和 Goal 批准请求为 proposal/session/turn/tool-call/version 范围; 每个项目仅存在一项待批准和一项 queued/running 执行 会话,并且分辨率由 host-core 序列化
  7. 启动事务性地中断待批准和 queued/running RPC 服务之前的执行状态。延迟渲染器响应无法关闭; 挂起的中断保持会话的合同模式和 已批准的 queued/running 中断保留 Agent。
  8. 会话分叉是一种主机拥有的快照操作。源转录本 永远不会被重写,并且处理的子 write/index 失败不会留下任何结果 可见的会话或孤立的转录文件。 D119 之后发生进程崩溃 现有的孤儿成绩单恢复政策。
  9. 消息范围的分叉除了规范快照结束之外是相同的 包括在 throughMessageId。它仍然重新映射 message/tool-call id 和 不创建运行时或修订状态,因此稍后的子 reseed/cache 状态为 由新的会话 ID 隔离。

9. 日志记录规则 ​

  • 从不记录 API keys/secrets
  • 工具参数可能会在审核预览中进行编辑
  • 每个tools.execute都会获得trace id = toolCallId

10. 验收 ​

  1. Electron 生成主机并完成握手 2.health方法返回ok
  2. 拒绝刀具路径返回 TOOL_DENIED
  3. 未解决的权限请求会一直保持待处理,直到明确决定或取消 5.将选定的工作空间从A切换到B不会改变工具根 会话 A 发出的呼叫的
  4. 协议 v4 session.endTurn creates/returns 恰好有一个通知 未见 completed/failed 轮次,并且没有可见电流、中止或 重复终端更新 7.通知list/unread/read-all/clear通过host-core往返 仍受最新 200 个持久行的限制
  5. 分叉一个空闲会话会产生一个独立可变的子进程 离开时具有相同的活动转录本和持久执行配置 源及其重新生成的修订版保持不变
  6. 分叉消息会排除后面的所有源行并拒绝 未创建子项的未知消息
  7. 伪造的 requestedMode 无法授权工具进入持久模式; Plan 和 Goal 拒绝 Write/Edit/plugin/unknown 工具并申请权限 根据 requestedMode/Write/Edit/plugin/unknown/Plan 提示 Bash
  8. SubmitPlan 和 SubmitGoal 将精确的 Markdown 字节写入唯一的 .pi/plan/*.md 或 .pi/goal/*.md 文件 hash/size 和结构化 title/question 字段;仅匹配 approve/reject 响应可以解析实时 plan_approvals 行,并且 针对其他类型运行的提交工具失败并显示 PLAN_KIND_MISMATCH 无需编写工件
  9. Plan 和 Goal 过期、中止、崩溃、计划拒绝和陈旧响应 产生记录的持久状态和事件
  10. 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 仍保持不同语义;其他项目的工具不能查询或修改绑定任务。

Host 文件批次准入(版本 1) ​

sidecar 代理明确允许 tools.fileBatchCapabilities、tools.fileBatchRegister、 tools.fileBatchSeal、tools.fileBatchAbandon、tools.fileBatchClose,不添加通配权限。 Capabilities 接收 sessionId,返回 {version:1,supported:boolean};远程工作区返回 false。 Register 接收 sessionId、可选 delegationId、batchId、ordinal、toolCallId、toolName、args, 返回 {ticket},只接受内建文件工具名。Seal/Close 接收 sessionId/batchId,Abandon 接收 sessionId/ticket,返回 {ok:true}。登记为进程内有界元数据:全局 512 调用、每批 128 调用、 参数总计 32 MiB,不授予文件权限。

tools.execute 增加可选 fileBatchTicket,绑定封存后的单次调用。会话/委派、调用/工具名、 参数必须匹配;重放、改参、封存前执行均失败。无 ticket 的旧调用仍获得 Host 资源互斥, 但不具有批内源序信息。runtime 只在能力握手成功后开放写并行。权限、根、租约、文件身份 由 Host 决定,客户端不提供权威身份。工作区绑定变化返回 TOOL_CONTEXT_CHANGED;文件身份 或地址变化在写入前失败。Close/过期不能释放运行中的磁盘任务,不自动重放失败或结果不明 的写。生命周期和限制见 Host 文件准入 ADR。

会话范围的后台作业 RPC ​

模型代理仅放行下列接口,桌面的全局 backgroundTasks 接口不在代理白名单内。

方法参数结果
backgroundJobs.capabilitiessessionId
backgroundJobs.listsessionId
backgroundJobs.waitsessionId, id, timeoutMs?BackgroundJobSnapshot
backgroundJobs.stopsessionId, id, timeoutMs?BackgroundJobSnapshot

接口要求已存在的本地 Agent 会话。未知与其他会话作业均返回 NOT_FOUND;非法参数返回 INVALID_PARAMS;合约模式返回 PERMISSION_DENIED。作业归属不变,会话身份取自 runtime,模型工具参数不提供会话选择。

timeoutMs 为 0–1000 的整数,默认 0。wait 观察单作业完成信号,释放全局锁;stop 先向已有主管发停止信号,再观察结果。返回 state 为实际状态,stopRequested 仅表示请求已发出。摘要含 job、最多 1024 UTF-8 字节的 command、elapsedMs、state、可空 exitCode 和 stopRequested。wait/stop 另含分别最多 8192 UTF-8 字节的 stdout/stderr,以及表示 Host 缓冲丢失或本次裁剪的 truncated。 命令保留最多 1024 字节的完整 UTF-8 前缀;commandTruncated 表示命令是否裁剪,旧 Host 可省略该字段。

终态和最终输出在同一作业锁下读取。读操作不消耗输出,进行中的观察持有作业句柄直至有界请求结束,历史淘汰不会拆散这次读取。等待后重新校验会话。放弃请求不会停止进程。旧 Host 能力缺失会明确报告,其他错误不会转为空列表。桌面原 RPC 结构保持兼容。

监管任务优先处理已观察到的进程退出,并在排空管道时保留退出结果。终态和最终输出发布时固定运行耗时。

User-defined mode run RPC ​

ADR mode-definitions adds the following Host methods. Parameters use camelCase.

MethodParametersResult
modes.getsessionId{ run }, null when no open run exists
modes.startsessionId, input, turnId{ claimed, run, enteredStage, definition }
modes.startGoalsessionId, input, turnId, modeSnapshotclaim result for an explicit builtin Goal, independent of the input mode; the Host validates the Goal snapshot and rejects a second open run
modes.resetGoalInputsessionId{ ok: true }; compare-and-reset only a Goal input preference to Agent, preserving the run, permission/model fields and any newer non-Goal mode
modes.claimTurnsessionId, runId, expectedRevision, turnId, originclaim result; origins: user, approval, host
modes.completeStagesessionId, runId, turnId, summary, artifacts?, input?, report?failed, run, awaiting_approval, advanced, goto, blocked or finished outcome; MODE_INPUT_CHANGED when input (the input the turn was claimed with) differs from the run's current input
modes.gateResultsessionId, runId, turnId, gateIndex, toolCallIdnext stage outcome
modes.settleTurnsessionId, runId, turnId, tokens, outcome, error?{ run, continue }
modes.controlsessionId, runId, expectedRevision, action{ run, continue }; pause, resume, stop or remove. Stop and remove both end the run; Electron main interrupts the active turn only for stop
modes.updatesessionId, runId, expectedRevision, input?, budget?{ run, continue: false }; edits an open run's input and { maxTurns, maxTokens } budget (both fields required, null is unlimited), effective from the next claim
modes.resolvesessionId, runId, expectedRevision, action{ run, continue }; approve or reject
modes.settleReturnsessionId, runId, mode?, modeSnapshot?{ pending, applied }; after a run closed, reports the custom returnTo the session waits on (pending). Called again with that mode, it stores modeSnapshot for it, or moves the session to agent when the snapshot is absent because the mode no longer exists

session.configure accepts an optional validated modeSnapshot. Custom mode ids require a snapshot. Agent and Plan retain their builtin policies. Host notifications use modes.changed { sessionId, run }. Renderer catalog editing uses the pi-desktop/modes/* IPC channels and never writes SQLite directly.

tools.execute accepts modeGate { runId, gateIndex } only for the exact pending Bash gate command. The Host records the exit code; a model-supplied verdict cannot pass a command gate. Stage completion may write only declared artifacts within the workspace. Staged runs reject remote workspaces.

Of the mode RPCs, only modes.completeStage and modes.gateResult traverse the model-directed sidecar proxy. Start, settlement, controls and budget/input edits remain host/user operations. Both completion methods retain Host ownership and live-claim checks; a report never authorizes a paused or stopped claim.

Optional report is { status: "completed" | "blocked", outcomes: [{ criterion, outcome: "met" | "unmet" | "unverified", evidence }], remainingWork: string[], blockerReason?: string }. Unknown report/outcome fields are rejected. Outcomes number 1–64, with unique trimmed nonblank criteria (4 KiB each) and nonblank evidence (16 KiB each). Remaining work has at most 64 nonblank 4 KiB items; a supplied blocker reason is nonblank and at most 16 KiB. Total serialized report size is at most 256 KiB. All sizes count UTF-8 bytes. completed requires all outcomes met, no remaining work and no blocker reason; the existing exit gates must still pass. blocked requires a blocker reason and unfinished/unverified work, persists completionReport, sets waiting/blocked and returns a blocked outcome without evaluating success gates. Settlement does not continue it. The report validates model evidence structure, not its truth. Legacy calls without a report and saved definition snapshots remain valid. A call with a report must include the claim's frozen input; a missing or changed input is rejected with MODE_INPUT_CHANGED before artifact writes.

An unavailable completion RPC, broken transport or malformed Host outcome ends the run turn with MODE_COMPLETE_STAGE_FAILED, not a normal completed turn. There is no automatic transport retry; error settlement waits for user recovery. Argument/artifact validation and ordinary failed exit checks remain correctable.

Run snapshots add elapsedMs (checkpointed active milliseconds) and optional activeSinceMs (epoch milliseconds of the Running interval). Absent legacy fields display zero, not now - createdAt. Other states and startup recovery have no active interval.

Desktop prompt/queue requests accept optional boolean startGoal. Main validates explicit input; the existing durable queue retains that choice through dispatch and restoration. A replacement is accepted before stopping the old Goal; mode settlement completes before queue release. This field never authorizes Host continuation. Only the in-process trusted origin and Host claim can do so.

session.get adds display-only message turnId from the authoritative message index, including bounded windows. Root turns remain separate without synthetic user messages; this metadata is not an execution credential.

本地优先 · 模型可替换 · 插件可扩展。 AIUO.NET