# Comate CLI RPC stdio v3 接入指南

`comate --rpc-stdio` 是 `comate_cli` 宿主里的 headless 前端：它和终端 TUI 平级，共用同一套 settings / preflight / `_build_agent()` 配置治理，工具集恒为 comate 全量。它不是 facade 的传输适配器，也不暴露 QueueManager 管理面。

传输是 stdin/stdout 上的 JSON-RPC 2.0 NDJSON：stdin 每行一个 client frame，stdout 每行一个 server frame。stdout 同时承载方法响应、事件通知和 server->client 请求，接入方必须持续读取。

## 启动

```bash
comate --rpc-stdio
```

不支持以下启动组合：

| 组合 | 原因 |
| --- | --- |
| `comate resume <session_id> --rpc-stdio` | v3 的恢复由 `session.open.params.session_id` 显式声明，不走进程级 resume。 |
| `comate resume --rpc-stdio` | RPC stdio 不打开本地历史选择 UI。 |
| `comate --rpc-stdio --yolo` | 权限模式由每个 `session.open.params.permission_mode` 显式声明。 |
| `comate -p ... --rpc-stdio` | print mode 是另一条 one-shot 输出路径。 |

preflight 仍在启动时执行，但以非交互模式运行。外部宿主需要提前确保 provider/settings 已配置好；preflight 阻塞时进程会在进入 RPC server 前返回。

## 接入铁律

1. client MUST 持续 drain stdout。
   响应、事件和 `permission.request` 共用一条 stdout 通道；不读 stdout 会让 server 写入阻塞，进而让会话和审批自锁死。

2. client MUST 能处理 server->client 请求帧。
   当前唯一的 server request 是 `permission.request`。如果 client 不能弹审批 UI，必须在 `rpc.initialize.params.capabilities.supports_permission_requests` 声明 `false`；此时 `ask` 类工具审批一律 deny。

3. client MUST 明确处理进程退出。
   推荐优先发 `rpc.shutdown`，它有响应并会关闭所有 live sessions。stdin EOF 是无响应关闭。stdout 断开会被视为 transport failure，server 会关闭会话并 deny pending approval。

4. client MUST 按 JSON-RPC id 路由。
   client request 的 response 使用 client 传入的 id；server request 使用 `srv-N` id，client 需要把审批结果用同一个 id 回给 server。

## 握手

client 启动后第一帧必须是 `rpc.initialize`。

请求：

```json
{"jsonrpc":"2.0","id":"r-init","method":"rpc.initialize","params":{"capabilities":{"supports_permission_requests":true}}}
```

响应：

```json
{"jsonrpc":"2.0","id":"r-init","result":{"protocol_version":3,"capabilities":{"multi_session":true,"permission_requests":true,"set_permission_mode":true,"partial_messages":true,"sessions_read":true,"session_status":true,"custom_commands":true},"default_cwd":"/workspace/project"}}
```

client capability:

| 字段 | 含义 |
| --- | --- |
| `supports_permission_requests` | client 是否能响应 server 发起的 `permission.request`。缺省是 `false`。 |

server capabilities:

| 字段 | 含义 |
| --- | --- |
| `multi_session` | 单进程内可打开多个 live `ChatSession`。 |
| `permission_requests` | server 会通过 `permission.request` 请求逐工具审批。 |
| `set_permission_mode` | 支持运行时切换 session permission mode。 |
| `partial_messages` | `session.open.include_partial_messages=true` 时会发送 transient delta event。 |
| `sessions_read` | 支持历史会话读侧方法。 |
| `session_status` | 支持 live session 状态查询。 |
| `custom_commands` | 支持发现和展开自定义 slash command。 |

重复调用 `rpc.initialize` 会返回 `INVALID_STATE`，且不会改写第一次握手记录的 client capability。

## 错误码

| code | 名称 | 常见触发 |
| --- | --- | --- |
| `-32700` | `PARSE_ERROR` | JSON 无法解析。 |
| `-32600` | `INVALID_REQUEST` | payload 不是 JSON-RPC request/response、request id 非 string/int、method 非 string。 |
| `-32601` | `METHOD_NOT_FOUND` | 未知方法或 v1 legacy 方法。 |
| `-32602` | `INVALID_PARAMS` | params 不是 object、字段类型不对、枚举值非法、未知 `session.open` key。 |
| `-32603` | `INTERNAL_ERROR` | server 内部异常。 |
| `-32000` | `INVALID_STATE` | 未 initialize、server 正在关闭或当前 session 状态不满足。 |
| `-32001` | `SESSION_NOT_FOUND` | `session_handle` 不存在或已关闭。 |

## 方法参考

### `rpc.initialize`

参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `capabilities` | object | 否 | 目前只读取 `supports_permission_requests`。 |

结果：`protocol_version`、server `capabilities`、`default_cwd`。

错误：`INVALID_PARAMS`、`INVALID_STATE`。

### `rpc.shutdown`

参数：无。

结果：

```json
{"shutdown":true}
```

语义：关闭所有 live sessions，响应后退出 transport loop。幂等。

### `session.open`

参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `cwd` | string | 否 | session 工作目录，缺省为进程启动时的 project root。 |
| `session_id` | string | 否 | 恢复已有 session。缺省创建新 session。 |
| `permission_mode` | string | 否 | `default` / `acceptEdits` / `bypassPermissions`，缺省 `default`。 |
| `include_partial_messages` | boolean | 否 | 是否发送 transient delta/progress event，缺省 `false`。 |

结果：

```json
{"session_handle":"s-1","session_id":"...","permission_mode":"default"}
```

错误：`INVALID_PARAMS`、`INVALID_STATE`、`INTERNAL_ERROR`。恢复或创建 ChatSession 触发的 `ChatSessionError` 会映射为 `INVALID_STATE`。

### `session.query`

参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `session_handle` | string | 是 | `session.open` 返回的 handle。 |
| `prompt` | string | 是 | 用户输入。非空。自定义 slash command 会在 session cwd 下展开。 |

结果：`{"accepted":true}`。

错误：`SESSION_NOT_FOUND`、`INVALID_PARAMS`、`INVALID_STATE`。

### `session.interrupt`

参数：`session_handle`。

结果：`{"interrupted":true|false}`，表示调用时 session 是否处于 running 状态。

错误：`SESSION_NOT_FOUND`、`INVALID_PARAMS`。

### `session.setPermissionMode`

参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `session_handle` | string | 是 | session handle。 |
| `mode` | string | 是 | `default` / `acceptEdits` / `bypassPermissions`。 |

结果：`{"permission_mode":"...","previous":"..."}`。mode 变化时会发 `PermissionModeChanged` 事件。

错误：`SESSION_NOT_FOUND`、`INVALID_PARAMS`。

### `session.status`

参数：`session_handle`。

结果字段包括：

| 字段 | 说明 |
| --- | --- |
| `session_id` | SDK session id。 |
| `run_state` | `idle` / `running` / `waiting_for_input`。 |
| `permission_mode` | 当前权限模式。 |
| `model_level` | 当前模型档位。 |
| `usage` | SDK usage snapshot。 |
| `context` | SDK context info。 |

错误：`SESSION_NOT_FOUND`。

### `session.compact`

参数：`session_handle`。

结果：`{"compacted":true|false}`。

错误：`SESSION_NOT_FOUND`、`INVALID_STATE`。

### `session.clearHistory`

参数：`session_handle`。

结果：`{"cleared":true}`。

错误：`SESSION_NOT_FOUND`。

### `session.setModelLevel`

参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `session_handle` | string | 是 | session handle。 |
| `level` | string | 是 | `LOW` / `MID` / `HIGH`。 |

结果：`{"model_level":"HIGH","previous_level":"MID","new_model":"..."}`。

错误：`SESSION_NOT_FOUND`、`INVALID_PARAMS`。

### `session.close`

参数：`session_handle`。

结果：`{"closed":true}`。

语义：关闭 session，取消 pump，shutdown ChatSession，并 deny pending approval。

错误：`SESSION_NOT_FOUND`。

### `sessions.list`

参数：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `cwd` | string | 否 | 查询指定 cwd 下的 session。缺省 `default_cwd`。 |
| `limit` | integer | 否 | 返回数量上限。 |

结果：SDK `list_sessions()` 的 JSONable 结果。

错误：`INVALID_PARAMS`。

### `sessions.listAll`

参数：`limit` integer，选填。

结果：SDK `list_all_sessions()` 的 JSONable 结果。

错误：`INVALID_PARAMS`。

### `sessions.search`

参数：`query` string 必填，`cwd` string 选填，`limit` integer 选填。

结果：SDK `search_sessions()` 的 JSONable 结果。

错误：`INVALID_PARAMS`。

### `sessions.messages`

参数：`session_id` string 必填，`cwd` string 选填，`limit` integer 选填，`offset` integer 选填。

结果：SDK `get_session_messages()` 的 JSONable 结果。

错误：`INVALID_PARAMS`。

### `commands.list`

参数：无。

结果：

```json
{"commands":[{"name":"review","description":"...","argument_hint":"...","scope":"project"}],"warnings":[]}
```

语义：返回默认 slash command 与项目自定义 slash command。内置 slash command 不作为 raw text 暴露，需使用一等 RPC 方法。

## 事件信封

事件 notification：

```json
{"jsonrpc":"2.0","method":"event","params":{"schema_version":1,"scope":"session","session_handle":"s-1","sequence":1,"event":{"type":"TextEvent","payload":{"content":"hello"}}}}
```

信封字段：

| 字段 | 说明 |
| --- | --- |
| `schema_version` | 当前固定为 `1`。 |
| `scope` | `session` 表示某 live session 的事件；`server` 表示 server 合成事件。 |
| `session_handle` | session-scope 为 handle；server-scope 可以是相关 handle 或 null。 |
| `sequence` | 进程内全局单调递增，不按 session 重置。 |
| `event.type` | SDK event 类名或 RPC 合成事件名。 |
| `event.payload` | JSONable payload。Pydantic typed 字段会变成 `{ "type": "...", "payload": {...} }`。 |

transient event 包括 `TextDeltaEvent`、`ThinkingDeltaEvent`、`ToolCallStartEvent`、`ToolCallArgsProgressEvent`、`StreamDisplayResetEvent`。`session.open.include_partial_messages=false` 时不会发送这些事件。

客户端渲染规则：

- 不要同时把 delta 和 final 双渲染为两份内容。
- 如果开启 partial：用 delta/progress 做临时展示，收到 final event 后以 final 为准。
- 如果关闭 partial：只处理 final event，例如 `TextEvent`、`ThinkingEvent`、`ToolCallEvent`、`ToolResultEvent`、`StopEvent`。

合成事件：

| type | 触发 |
| --- | --- |
| `PermissionModeChanged` | `session.setPermissionMode` 后 mode 变化。 |
| `SessionClosed` | session pump 崩溃时 server 合成。 |

常见 SDK 事件包括 `TextEvent`、`StopEvent`、`UserQuestionEvent`、`ToolCallEvent`、`ToolResultEvent` 等。事件 payload 以 SDK 当前 dataclass 字段为准。

## 交互流时序

### 普通问答

```json
{"jsonrpc":"2.0","id":"r-init","method":"rpc.initialize","params":{"capabilities":{"supports_permission_requests":true}}}
{"jsonrpc":"2.0","id":"r-open","method":"session.open","params":{"permission_mode":"default"}}
{"jsonrpc":"2.0","id":"r-query","method":"session.query","params":{"session_handle":"s-1","prompt":"用一句话解释 RPC"}}
```

server 可能输出：

```json
{"jsonrpc":"2.0","id":"r-query","result":{"accepted":true}}
{"jsonrpc":"2.0","method":"event","params":{"schema_version":1,"scope":"session","session_handle":"s-1","sequence":1,"event":{"type":"TextEvent","payload":{"content":"RPC 是进程间按约定调用方法并返回结果的协议。"}}}}
{"jsonrpc":"2.0","method":"event","params":{"schema_version":1,"scope":"session","session_handle":"s-1","sequence":2,"event":{"type":"StopEvent","payload":{"reason":"completed"}}}}
```

### 逐工具审批

client 初始化时声明支持审批：

```json
{"jsonrpc":"2.0","id":"r-init","method":"rpc.initialize","params":{"capabilities":{"supports_permission_requests":true}}}
```

工具需要审批时 server 发 request：

```json
{"jsonrpc":"2.0","id":"srv-1","method":"permission.request","params":{"session_handle":"s-1","session_id":"sid-1","tool_name":"Bash","tool_input":{"command":"ls"},"tool_call_id":"tc-1","cwd":"/workspace/project","permission_mode":"default","reason":"ask","permission_suggestions":[],"policy_source":"rule","agent_id":null,"agent_type":null,"team_name":null}}
```

`params` 是 SDK tool approval payload 外加 RPC `session_handle`，因此会同时包含 SDK `session_id` 和 RPC handle。

client allow：

```json
{"jsonrpc":"2.0","id":"srv-1","result":{"decision":"allow"}}
```

client allow 并更新权限：

```json
{"jsonrpc":"2.0","id":"srv-1","result":{"decision":"allow","updatedPermissions":[{"type":"setMode","mode":"acceptEdits","destination":"session"}]}}
```

client deny：

```json
{"jsonrpc":"2.0","id":"srv-1","result":{"decision":"deny","reason":"用户拒绝执行"}}
```

注意：

- 审批结果没有 `updatedInput`，RPC v3 不支持由 client 改写工具入参。
- 审批没有 server 侧超时；client 不响应会让该工具调用等待。
- transport 失败、session close、client 未声明 `supports_permission_requests`、畸形响应都会 deny 兜底。

### 追问回答

server 反问用户并停在等待输入：

```json
{"jsonrpc":"2.0","method":"event","params":{"schema_version":1,"scope":"session","session_handle":"s-1","sequence":20,"event":{"type":"UserQuestionEvent","payload":{"questions":[{"question":"目标目录是哪一个？","suggestions":["packages/comate_cli"]}],"tool_call_id":"ask-1"}}}}
{"jsonrpc":"2.0","method":"event","params":{"schema_version":1,"scope":"session","session_handle":"s-1","sequence":21,"event":{"type":"StopEvent","payload":{"reason":"waiting_for_input"}}}}
```

client 用同一个 `session.query` 回答：

```json
{"jsonrpc":"2.0","id":"r-answer","method":"session.query","params":{"session_handle":"s-1","prompt":"目标目录是 packages/comate_cli"}}
```

## 安全默认

- `session.open` 缺省 `permission_mode` 是 `default`。
- `bypassPermissions` 必须由 client 在 `session.open` 或 `session.setPermissionMode` 显式传入。
- `--yolo` 不支持与 `--rpc-stdio` 混用。
- `updatedPermissions` 只能表达 SDK 支持的权限更新目的地，例如 `localSettings` 或 `session`；具体复评估由 SDK 完成。
- RPC 不裁剪工具集；工具集与终端 comate 一致。

## 多会话模型

- 每次 `session.open` 分配一个进程内 handle：`s-1`、`s-2`、`s-3`，单调递增，不复用。
- 每个 handle 对应一个独立 `ChatSession`、event pump、permission mode、approval bridge。
- `sequence` 是进程全局单调递增，跨 session 共享。
- 单个 session pump 崩溃时只移除该 session，并发 server-scope `SessionClosed`；其他 sessions 继续服务。
- `rpc.shutdown`、stdin EOF、stdout BrokenPipe 都会关闭所有 live sessions。

## legacy 迁移

| v1 方法 | v3 替代 |
| --- | --- |
| `initialize` | `rpc.initialize` + `session.open` |
| `prompt` | `session.query` |
| `cancel` | `session.interrupt` |
| `replay` | `sessions.messages` 或重新 `session.open` 指定 `session_id` |

v3 收到 `initialize`、`prompt`、`cancel`、`replay` 会返回 `METHOD_NOT_FOUND`，并在 error data 中给出迁移提示。
