feat: add assistant proposal worker runtime
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
`gori-agent` 是一个 Node.js 20+ / TypeScript 网关:从一个 IM 平台接收消息,通过官方 Agent Client Protocol(ACP)驱动一个 Coding Agent。
|
||||
|
||||
```text
|
||||
用户 → 单个平台 Adapter → Gateway → ACP Session Manager → 单个 ACP Agent
|
||||
用户 → 单个平台 Adapter → Gateway → AssistantManager(Assistant / Proposal / Worker)→ 单个 ACP Agent
|
||||
```
|
||||
|
||||
## Config v3 实例模型
|
||||
@@ -12,9 +12,11 @@ Config v3 只支持以下边界:
|
||||
|
||||
- 一份运行配置对应一个固定 Bot 身份。
|
||||
- 一个 Bot 只有一个 workspace、persona、ACP agent、skill 列表和 permission policy。
|
||||
- 每个 chat 一个无工具 Assistant 会话;它只对话、创建 Proposal 并解释 Worker 反馈,自己从不执行。
|
||||
- 同一时刻全实例只有一个 Worker 在 `bot.workspace` 执行一个已确认 Proposal;success 与 failure 都需要用户确认后才收尾,系统不会自动开始下一个 Proposal。
|
||||
- 一个实例只绑定一个平台;多平台使用多个实例。
|
||||
- 不再有 `roles[]`、`defaultRole`、`backends[]`、动态 `/role` 切换或 Config v1/v2 迁移。
|
||||
- `configVersion` 非 `3` 会明确失败,旧 state v1 也不会自动改写。
|
||||
- 不再有 `roles[]`、`defaultRole`、`backends[]`、动态 `/role` 切换、单主任务/近似 BTW 或 Config v1/v2 迁移。
|
||||
- `configVersion` 非 `3` 会明确失败,旧 state v1/v2 也不会自动改写。
|
||||
|
||||
一台机器上的实例统一位于:
|
||||
|
||||
@@ -23,7 +25,10 @@ ${GORI_AGENT_ROOT:-$HOME/.gori-agent}/instances/<bot-id>/
|
||||
├── config.json
|
||||
├── logs/gori-agent.log
|
||||
└── state/
|
||||
├── acp-sessions.json
|
||||
├── acp-sessions.json # state v3:Assistant binding
|
||||
├── proposals.json # Proposal 记录
|
||||
├── assistant-workspaces/ # 每 chat 私有 Assistant cwd
|
||||
├── kimi/assistant/ # Assistant 私有 KIMI_CODE_HOME
|
||||
└── gori-agent.pid
|
||||
```
|
||||
|
||||
@@ -168,53 +173,71 @@ QQ websocket 示例:
|
||||
|
||||
### ACP 生命周期
|
||||
|
||||
默认 `runtime.acp.promptTimeoutMs` 是 `7200000`(2 小时),适合长构建/部署任务。其他默认值:
|
||||
默认 `runtime.acp.promptTimeoutMs` 是 `14400000`(4 小时),适合长构建/部署任务。其他默认值:
|
||||
|
||||
- initialize:10 秒
|
||||
- cancel grace:5 秒
|
||||
- idle worker:30 分钟
|
||||
- idle assistant:30 分钟
|
||||
- sweep:60 秒
|
||||
- max processes:8
|
||||
- max processes:8(Assistant + Worker 总和)
|
||||
- max assistant sessions:4
|
||||
|
||||
`stateFile` 为空时使用当前实例的 `$GORI_AGENT_HOME/state/acp-sessions.json`。
|
||||
`stateFile` 为空时使用当前实例的 `$GORI_AGENT_HOME/state/acp-sessions.json`;Proposal 记录固定保存在同目录的 `proposals.json`。Kimi Code agent 的 `args` 必须严格为 `["acp"]`,避免额外启动参数绕过 Assistant 的 no-tool profile。
|
||||
|
||||
## Session 与 state v2
|
||||
## Assistant / Proposal / Worker 与 state v3
|
||||
|
||||
每个绑定由当前实例固定的 `platform + chatId` 唯一确定。state v2 header 保存 `botId` 与 platform type;打开 state 时身份不匹配会拒绝启动,避免错误复用另一个实例目录。
|
||||
运行链路是三层:
|
||||
|
||||
Binding 保存:
|
||||
- **Assistant**:每个 chat 一个无工具 ACP 会话,只与用户对话。它把用户意图整理成 Proposal(title、goal、steps),并解释 Worker 的反馈。Assistant 每条回复必须以隐藏 `GORI_ASSISTANT_ACTION_V1` envelope 结尾(`reply` + `actions`),action 只有 `create_proposal`、`confirm`、`start_next`、`cancel`、`stop`;格式错误只修复一次。
|
||||
- **Proposal**:一份待确认的工作单。状态流为 `proposed → queued → working → awaiting_user_confirmation → completed | failed | cancelled`。`proposed` 只有用户确认后才进入 `queued`;空闲时最早确认的 queued Proposal 才开始执行。
|
||||
- **Worker**:同一时刻全实例只有一个,在 `bot.workspace` 用 `bot.permissions` policy 执行一个已确认 Proposal。每轮必须以隐藏 `GORI_WORKER_RESULT_V1` envelope 收尾:`SUCCESS`、`FAILED` 或 `NEEDS_CONFIRMATION`(可带 `question`、`nextStep`、`dirty`)。
|
||||
|
||||
- chat key
|
||||
- agent ID
|
||||
- native ACP session ID
|
||||
- workspace
|
||||
- Bot fingerprint
|
||||
- 创建与更新时间
|
||||
确认语义是刻意的:
|
||||
|
||||
Bot fingerprint 包含 Bot ID、workspace、persona、agent、permissions、skill 路径/内容 hash 和 bootstrap schema version。任一语义变化后,下一条消息创建新 native session,不恢复旧身份上下文。
|
||||
- `SUCCESS` 和 `FAILED` 都进入 `awaiting_user_confirmation`,都需要用户确认后才分别落定为 `completed` / `failed`;对 failed Proposal 用 answer `retry` 确认可以重试。
|
||||
- 系统**不会**在一个 Proposal 完成后自动开始下一个;只有用户确认新 Proposal 或 Assistant 发出 `start_next` 才会推进队列。
|
||||
- Worker 执行中遇到 dirty 或意外目标时报告 `NEEDS_CONFIRMATION`,这是正常的执行中确认点,不是 blocked 终态;用户回答后同一 Worker 继续执行。
|
||||
- Gateway 不再有 15/60/180/480 秒的固定时间提醒;Worker 落定结果时通过 Assistant 生成一条事件说明,由平台 adapter 作为**新消息**发给 owner chat(QQ 同样是新消息,不引用原消息)。
|
||||
|
||||
首次 prompt 会创建 native session,发送隐藏 bootstrap,bootstrap 成功后才保存 binding。worker 空闲回收后优先 `session/resume`,否则回退 `session/load`。失败 prompt 不会自动重放,因为工具操作可能已有副作用。
|
||||
Assistant 隔离与旧 side Session 一致且更严格:
|
||||
|
||||
旧 state v1 会被原样保留并明确拒绝;使用新的实例目录开始 state v2,不要手工改写运行中的 state。
|
||||
- cwd 位于实例私有 `state/assistant-workspaces/`,目录名由进程随机 salt 和 chat key 计算 HMAC,不直接编码 chat 标识,并且不能与项目 workspace 重叠;重启后按 binding 恢复同一目录。
|
||||
- 项目级 `agent.md` override 显式设置 `tools: []` 与 `subagents: []`;ACP 传入空 `mcpServers`;Assistant 只接受 Kimi Code ACP,并强制 permission deny 作为附加层。
|
||||
- 除非 `bot.agent.env` 已显式设置,Assistant 进程的 `KIMI_CODE_HOME` 指向实例私有 `state/kimi/assistant/`(`0700`),与真实用户配置隔离。
|
||||
- 一旦出现 tool update 或 permission request,即视为隔离违约:当前 turn fail closed、终止整个 ACP 进程组并删除 binding。Worker 的 cancel、timeout、crash 同样按进程组清理工具后代;bootstrap 禁止 `setsid`、`nohup`、detached/daemon/background 遗留进程,主动脱离进程组仍属于无 Bubblewrap/cgroup 方案的信任边界。
|
||||
- 每个 ACP worker 以独立进程组运行并携带随机 `GORI_AGENT_WORKER_TOKEN`;runner 重启时会把仍处于 `working` 的 Proposal 校验 token 后清理旧进程组,并标记为 failed(`worker_lost`),交给用户确认或重试。
|
||||
|
||||
state v3(`acp-sessions.json`)只保存 header(version 3、`botId`、platform)和 Assistant binding:chat key、agent ID、native session ID、assistant workspace、Bot fingerprint 与时间戳。`proposals.json`(version 1)保存 Proposal 记录:title/goal/steps、owner chat、状态、pending(`step | success | failure`)、worker native session ID 与进程组 PGID/token、时间戳;不保存消息正文。两个 store 都做单 writer lock、串行持久化、临时文件 + fsync + 原子 rename;version 或 identity 不匹配(含旧 state v1/v2)一律拒绝启动并保留原文件,不清空、不迁移。
|
||||
|
||||
Bot fingerprint 包含 bootstrap schema version、Bot ID、workspace、persona、agent 定义、permission policy 和 skill 路径/内容 hash;fingerprint 或 agent 不匹配的 binding 会被丢弃并重建 Assistant 会话。失败 prompt 不会自动重放,因为工具操作可能已有副作用。
|
||||
|
||||
## Workspace 独占
|
||||
|
||||
同一 `GORI_AGENT_ROOT` 下,旧的 `/usr/bin/flock` workspace lease 与 `owner.json` 已退役。`doctor`、外层 `start` 与内部 runner 都对其他实例目录和完整 Config v3 扫描 fail closed,canonical workspace 相同或互为父子都拒绝;不再做配置级 workspace 共享。`setup/start` 还通过实例 lifecycle flock 互斥,父 CLI 把已验证配置摘要传给 runner,配置发生换挡时 runner 拒绝启动。
|
||||
|
||||
## IM 命令
|
||||
|
||||
```text
|
||||
/help
|
||||
/status
|
||||
/list
|
||||
/confirm
|
||||
/stop
|
||||
/cancel
|
||||
/new
|
||||
```
|
||||
|
||||
- `/status` 绕过 per-chat lock,显示固定 Bot、agent、workspace、队列数、Gateway 当前任务时长,以及 ACP `phase`、`runningSeconds`、`idleSeconds`。`phase` 为 `idle`、bootstrap 的 `initializing` 或普通 prompt 的 `processing`;`idleSeconds` 按当前 session 的最近一次 `session/update` 活动计算,不记录或输出 update 内容。
|
||||
- `/cancel` 绕过 per-chat lock,发送 ACP cancel。
|
||||
- `/help` 绕过 per-chat lock,可在长任务期间立即返回。
|
||||
- `/new` 保持串行,清除当前 chat binding;下一条普通消息创建新 native session。
|
||||
- `/roles`、`/role`、`/agents`、`/agent` 会返回固定 retired 提示,不会转发给 ACP。
|
||||
- `/help`:显示自然语言使用说明和兜底命令列表。
|
||||
- `/status`:显示固定 Bot、agent、workspace、Assistant 会话数、各状态 Proposal 计数、Worker 是否在执行及当前 chat 是否 owner。
|
||||
- `/list`:列出当前 chat 拥有的 Proposal(id、状态、title)。
|
||||
- `/confirm`:确认当前 chat 最近待确认的 Proposal(`proposed` 进入队列,`awaiting_user_confirmation` 落定完成/失败或带着回答继续)。
|
||||
- `/stop`:停止当前正在执行的 Worker 并将其 Proposal 标记为 cancelled;只有 owner chat 可用。
|
||||
- `/cancel`:取消当前 chat 最近未开始的 Proposal(`proposed` / `queued`)。
|
||||
|
||||
同一 chat 的普通消息和 `/new` 串行执行,不同 chat 可并发。异步平台的普通消息轮到后立即开始 ACP prompt,不等待状态提示发送:15 秒内完成只发送真实结果;15 秒仍未完成时按 running/queued 发送口语化提示,发送过 queued 提示的消息真正开始时再补充开始提示。主动提示从入站绝对时间按 15 秒、60 秒、180 秒、480 秒触发,480 秒后每 600 秒一次(18、28、38 分钟……);它们只描述用户可感知的处理或等待状态,不暴露 ACP、`phase`、`idleSeconds` 等内部技术指标,技术状态只保留在 `/status`。延迟执行的 timer 不补发历史提醒;同步 webhook 不发送这些提示。
|
||||
日常操作以自然语言为主,Assistant 会自己生成 confirm/cancel/stop 等 action;这些命令是旁路 Assistant 的兜底入口。未识别的 `/` 命令按普通消息交给 Assistant。
|
||||
|
||||
每条异步入站有独立、串行的回复流,`replySequence` 从 1 动态递增;因此短任务最终回复使用 `msg_seq=1`,已发送提示时后续消息使用下一序号。平台发送失败只记录不含消息正文或 provider 错误详情的安全日志,不影响 ACP 任务或回复流中的后续发送,也不会把成功的 ACP 结果误报为 `Agent error`。最终结果加入回复流后即释放 per-chat lock,平台发送延迟不会阻塞下一项任务;完成与 shutdown 都会清理状态提醒 timer。
|
||||
Gateway 不维护普通消息队列或 per-chat task lock,也不再有固定时间(15/60/180/480 秒)的处理中提醒;每条 allowlist 普通消息按 chat 串行交给 AssistantManager。Worker 落定结果(成功、失败或提问)时,AssistantManager 生成事件文案并经 `Gateway.sendEvent` 由平台 adapter 作为**新消息**发出(QQ 也是新消息,不引用原消息);发送失败只记录不含消息正文或 provider 错误详情的安全日志,不影响任务或后续发送。
|
||||
|
||||
每条异步入站使用独立、串行的回复流,`replySequence` 从 1 动态递增;同步 webhook 直接返回 JSON 结果。
|
||||
|
||||
## HTTP 端点
|
||||
|
||||
@@ -228,7 +251,8 @@ Bot fingerprint 包含 Bot ID、workspace、persona、agent、permissions、skil
|
||||
|
||||
- Config v3 与 example placeholder。
|
||||
- 配置权限 `0600`。
|
||||
- Bot ID、workspace、skills 与 permissions。
|
||||
- Bot ID、workspace、skills、permissions,以及 agent `args` 严格为 `["acp"]`。
|
||||
- 同一实例根下相同或 parent/child workspace 重叠。
|
||||
- ACP initialize 与 session restore capability。
|
||||
- state 目录可读写。
|
||||
- 单平台必需字段,但不打印 credential 值。
|
||||
@@ -245,6 +269,6 @@ git diff --check
|
||||
bash -n install.sh gori-agent.sh bin/gori-agent
|
||||
```
|
||||
|
||||
测试使用 Node `node:test` + `tsx` 和本地 fake ACP 子进程,覆盖 Config v3、permission、bootstrap、timeout/cancel、冷恢复、fingerprint mismatch、state v2 identity、原子配置/state 写入、Gateway retired commands、QQ normalization 和单平台 route。
|
||||
测试使用 Node `node:test` + `tsx` 和本地 fake ACP 子进程,覆盖 Config v3、permission、Assistant/Worker envelope 协议、Proposal 状态流与确认语义、timeout/cancel、worker_lost 恢复、state v3 identity、workspace 重叠扫描、Gateway 无队列语义、QQ normalization 和单平台 route。真实 Kimi ACP 的执行层兼容性可用 `node scripts/side-session-spike.mjs` 复验(assistant no-tool spike:私有 cwd 在项目外、`mcpServers: []`、toolUpdates/permissionRequests/fsRequests 全为 0、`GORI_ASSISTANT_ACTION_V1` envelope 可解析);该脚本使用临时 cwd/profile,不读取或输出真实配置、凭据、日志正文。
|
||||
|
||||
`src/agents/*` 与 `src/core/session-store.ts` 仅为 legacy compatibility/reference,不在当前运行链路。运行时没有单轮 CLI fallback。
|
||||
|
||||
Reference in New Issue
Block a user