From cd70a6950806b30dd0d198629a077efe0e1e82c8 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 28 Sep 2026 21:07:01 +0800 Subject: [PATCH] docs(runbook): automation-first agent interaction, plan/options/approval events and E12 Co-Authored-By: Claude Opus 5.5 --- docs/design/agent-ui-reference.md | 6 +++--- docs/design/glossary.md | 3 +++ docs/runbook/agent.md | 33 +++++++++++++++++++++++++------ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/docs/design/agent-ui-reference.md b/docs/design/agent-ui-reference.md index 7a820b30..eefbf469 100644 --- a/docs/design/agent-ui-reference.md +++ b/docs/design/agent-ui-reference.md @@ -105,7 +105,7 @@ | `Approval`(`agent.ts` ~222,`status: pending\|approved\|rejected\|revoked\|expired\|consumed`) | `ApprovalCard`/`ToolApproval` | 状态集不对齐:我们多出 `revoked/expired/consumed`,库多出 `submitting/changes-requested/answered`,需要做一层状态映射并砍掉/新增对应视觉 | | `AgentGoalReport`(`modelText` vs `buildGoalReport()` 系统汇总)、UI `FinalReport()`(`agent-conversation.tsx`,"模型说的"/"系统核实的结果"双栏) | 无对应组件 | **该库完全没有"声明 vs 核实"双轨展示**,`StreamingResponse` 只有单一正文 + `Citations`;需要新写一张"系统核实结果"卡片,可能借 `ToolResult` 或 `Citations` 的折叠壳改造 | | `AgentBudget`/`AgentUsage`、`BudgetBar()`+`Meter()`(`agent-conversation.tsx`,四项:工具调用/token/时长/费用) | `UsageDashboard` | 该组件是历史用量仪表盘(按天图表 + 汇总表),**没有"当前额度 vs 已用"的进度条**,量级和展示逻辑都不同,预算条要另写 | -| `AgentRunMode`(`execute`/`plan_only`) | 无对应组件 | 库里没有模式指示器,需要自建(如顶部条幅或输入框旁的模式开关) | +| ~~`AgentRunMode`(`execute`/`plan_only`)~~ | — | AG-2 已取消运行模式;"先看方案再执行"是输入区的开关(`scope.proposeFirst`) | ## 4. 设计纪律冲突 @@ -135,7 +135,7 @@ 1. `Message` + `MessageBubble` + `MessageScroller` —— 会话主干,先落地,材质改造集中在气泡外壳。 2. `StreamingResponse` —— 承载 `text_delta`,需先验证"整段替换"模型能否套我们的增量拼接。 3. `ToolResult` + `ToolApproval` —— 覆盖 `tool_call_*` 与 approval 对象;`tool_call_rejected` 需新增比 `denied` 更醒目的拒绝态。 -4. `TodoList` —— `plan_only` 模式下的步骤展示。 +4. `TodoList` —— `agent.plan` 步骤清单(会话页顶部;抽屉压缩为一行进度)。 5. `ApprovalCard` —— 覆盖非工具类通用审批。 6. `CodeBlock`/`FileDiff` —— 系统核实结果与 diff 类工具输出的展示载体。 7. `Citations` —— 改造成本最低,可较早排期。 @@ -158,7 +158,7 @@ | `StreamingResponse` | `streaming-response.tsx` | 流式文本、"模型说的"栏 | 新增流式光标;去点赞/点踩;来源接 `CitationList` | | `ToolResult` | `tool-result.tsx` | 工具调用卡片 | 新增 `badges`(类别/scope/风险/幂等/恢复补记)与收起摘要;输入输出用 `CodeBlock` | | `ToolApproval` | `tool-approval.tsx` | **拒绝态**(白名单拦截 `tool_call_rejected` 与授权校验失败的 `unauthorized`) | 新增 `rejected` 醒目态:destructive 材质 + `role=alert` + 原因 + 授权说明;去掉 "Always allow" | -| `TodoList` | `todo-list.tsx` | 只要方案模式的"调查步骤" | SVG 状态图标保留,弹簧动画换 CSS;新增 `error` 态 | +| `TodoList` | `todo-list.tsx` | `agent.plan` 步骤清单(新增 `skipped` 态) | SVG 状态图标保留,弹簧动画换 CSS;新增 `error` 态 | | `ApprovalCard` | `approval-card.tsx` | Approval 对象(计划、payload hash、版本、到期、风险) | 状态集对齐 `pending/approved/rejected/revoked/expired/consumed`(+`submitting`);去掉问答模式;非管理员会话按钮禁用并说明 | | `CodeBlock`(+`agent-code`) | `code-block.tsx` | 工具输入/输出 | 不做语法高亮;JSON 自动缩进 | | `FileDiff` | `file-diff.tsx` | 系统核实栏的"入库目标" | +/− 符号保留,底色换 success/destructive 洗层 | diff --git a/docs/design/glossary.md b/docs/design/glossary.md index c7146256..38fb39a1 100644 --- a/docs/design/glossary.md +++ b/docs/design/glossary.md @@ -38,6 +38,9 @@ | AgentRun(parent_run_id 非空) | 子会话;派生它的叫"父会话" | 子代理、子 Agent、subagent | | AgentRun 被中止 | 停止;状态"已停止" | 中止、取消 | | AutomationGrant | 持续授权 | 自动化授权、automation_grant | +| agent.plan 的步骤清单 | 步骤;进度写"步骤 2/5" | 计划(计划只指下载计划 / 整理计划)、TODO、任务列表 | +| agent.propose_options | 方案;动作叫"选定",都不要叫"退回" | 选项、提案 | +| scope.proposeFirst | 先看方案再执行 | 只要方案、只给方案、plan_only 模式 | | 工作流引擎的一次执行 | 运行 | run | ## 文案规则 diff --git a/docs/runbook/agent.md b/docs/runbook/agent.md index f617f079..9c534f50 100644 --- a/docs/runbook/agent.md +++ b/docs/runbook/agent.md @@ -22,12 +22,13 @@ | 端点 | 说明 | |---|---| -| `POST /agent/runs {goal, mode: execute\|plan_only, capabilityAllowlist?}` | 建 `agent_run` 和业务任务行(kind=agentRun),并入队持久任务 `runAgent`。**HTTP 请求里不跑模型**,返回时状态为 `queued`。`plan_only` 只允许 read / preview / propose 类能力,白名单里放了 execute 类会返回 422 | +| `POST /agent/runs {goal, proposeFirst?, capabilityAllowlist?}` | 建 `agent_run` 和业务任务行(kind=agentRun),并入队持久任务 `runAgent`。**HTTP 请求里不跑模型**,返回时状态为 `queued`。运行模式已取消(AG-2):会话默认在授权范围内执行;`proposeFirst=true` = 先看方案再执行(见「会话交互」)。旧字段 `mode` 已弃用:`plan_only` 按 `proposeFirst=true` 处理,响应带 `Deprecation: true`;响应里不再有 `mode` | +| `POST /agent/runs/{id}/choice {decisionId, optionId \| null, reason?}` | 回答 `agent.propose_options`:选一个方案,或 `optionId=null` 都不要(必须写原因)。方案不存在 422,已处理 409;作答后挂起的会话随即继续 | | `GET /agent/runs` / `GET /agent/runs/{id}?afterSeq&limit` | 详情包括:`agent_run_event` 按 seq 分页、追加的消息、服务端核查过的目标汇报(`report`) | | `POST /agent/runs/{id}/messages {text}` | 追加用户消息。运行中时,当前会话在本轮结束后消费;已结束或等待中时,会唤醒一次新的 runAgent 续跑 | | `POST /agent/runs/{id}/abort {reason?}` | 写取消标志,会话监视器会中止模型并收尾为 cancelled。只停止后续步骤,不撤销已经完成的外部动作 | | `GET/POST /automation-grants`、`PATCH /automation-grants/{id}` | 持续授权。PATCH 只能撤销或缩短有效期,扩大范围要新建一条 | -| `POST /approvals {planType, planId}`、`PATCH /approvals/{id} {status, payloadHash}` | 单次审批。**只接受控制台管理员会话**(API Key 一律 403,持 `organize.execute` 的 Key 不能给自己的计划开审批再自批)。批准时要回传 payloadHash;计划 payload hash / revision 变化、过期、撤销后旧审批失效,执行成功一次即 consumed,不可重放(`test/agent/approvals.test.ts`) | +| `POST /approvals {planType, planId}`、`PATCH /approvals/{id} {status, payloadHash, reason?}` | 单次审批。**只接受控制台管理员会话**(API Key 一律 403,持 `organize.execute` 的 Key 不能给自己的计划开审批再自批)。批准时要回传 payloadHash;计划 payload hash / revision 变化、过期、撤销后旧审批失效,执行成功一次即 consumed,不可重放(`test/agent/approvals.test.ts`)。会话内审批(见「会话交互」)决定后写关联待处理事项并唤醒挂起的会话 | | `GET /capabilities`、`POST /capabilities/{id}/invoke` | 能力目录与调用,HTTP scope 按能力描述符再校验一次 | | `GET /tasks[/{id}]`、`POST /tasks/{id}/cancel`、`POST /tasks/{id}/retry` | 业务任务;`?subjectId=` 按业务对象查(submitDownload 的 subject 是计划,verifyAndImport 的是下载任务) | | `POST /acquisition-plans {sourceReleaseId, slotId? \| entityId?, note?}` | 手动提交下载(工作台下载入口):依次调 `download.plan` → `download.submit`,和 Agent 同一条能力路径(授权 / 审计 / 幂等键 / 账本)。没给 slotId 时按作品的 Slot 创建顺序逐个尝试,第一个生成计划的胜出;都没生成时 `submitted=false` 并逐个 Slot 给出原因。控制台管理员算人工授权;API Key 需要持续授权或单次审批 | @@ -43,7 +44,7 @@ - 取上游(provider / baseUrl / 模型 / 思考档位来自 `pi_upstream`,凭证由上游的凭证存储在请求时解密注入,不落日志),从设置读上下文预算、压缩和预算。 - 检查今日次数和费用预算(`budget.agentMaxRunsPerDay` / `agentMaxCostUsdPerDay`)。 - 打开 `AGENT_SESSION_DIR/.jsonl`。`pendingOperations>0` 说明上次崩溃:先按幂等键对账,再 `resume()`;否则就 `prompt(goal)`。最后逐条消费追加的消息。 -2. `goal-settled` 步骤:只在 execute 模式且会话正常结束时执行。每隔 `agent.goalPollSec` 检查一次,直到本 run 触达的计划、下载和整理记录全部落定,最长等 `agent.goalWaitHours`。等待期间 run 状态为 `waiting`,汇报写的是"已提交≠已入库"。 +2. `goal-settled` 步骤:只在会话正常结束、且不是后台只读调查(内部 `agent_run.mode=plan_only`)时执行。每隔 `agent.goalPollSec` 检查一次,直到本 run 触达的计划、下载和整理记录全部落定,最长等 `agent.goalWaitHours`。等待期间 run 状态为 `waiting`,汇报写的是"已提交≠已入库"。 3. `finalize` 步骤:从业务表汇总计划、下载任务和整理记录,生成 `AgentGoalReport`(artifact kind=report),并据此决定终态: - 入库完成 → `succeeded`,汇报带整理记录 ID; - 仍在等待 → `waiting`; @@ -55,6 +56,23 @@ worker 崩溃后,新 worker 在引擎租约到期后(约 30s)接手,`drive` 步骤会从头重跑。已经完成的工具调用不会重放。副作用调用的结果模型会看到"结果未知",模型需要重新核对;同参数再调用一次,会直接拿到账本里的结果。 +## 会话交互(AG-2 自动化优先) + +不再分 plan_only / execute 模式:每个用户会话默认在授权范围内执行,模型自己规划并可视化,用户在需要时同意、选方案或审批。 +`agent_run.mode` 列保留为内部能力上限(后台只读调查、巡检、只读子会话用 `plan_only`),不出现在 API 与界面上;会话选项存在 `agent_run.scope`: + +| scope 字段 | 含义 | +|---|---| +| `proposeFirst` | 先看方案再执行(输入区开关,缺省关)。用户选定一个方案(`agent.propose_options` 的待处理事项 resolved)之前,本会话及其子会话的 execute 类调用一律被授权器拒绝(`本会话要求先看方案再执行…`);系统提示也要求先调查、再给方案 | +| `interactive` | 会话由控制台管理员会话(cookie)发起。只有这种会话出会话内审批卡;API Key / 开发密钥发起的会话缺少授权时照旧直接拒绝 | + +- **步骤清单** `agent.plan`(类别 read):`{steps:[{id,title?,status?,note?}], replace?}`,按 id 合并(新步骤缺省 pending,已有步骤只改传入的字段),最多 30 步。每次调用产生 `plan_updated {steps, revision}` 事件;会话页顶部是 TodoList 实时勾选,抽屉里压缩为「步骤 n/m · 当前:…」一行进度。系统提示要求先出计划再执行,执行中逐步更新。只在运行中的 Agent 会话里可用(HTTP / MCP 调用报 precondition)。 +- **方案选择** `agent.propose_options`(类别 propose,注册时 replay=safe):`{question, options:[{id,title,summary,risks,estimatedCost?}](2–4 个), recommendedId?}`。只在用户发起的顶层会话里提供(子会话 / 后台 / 巡检没人可问)。处理器登记待处理事项(kind=other,`dedupe_key=agent-options::`,待处理中心也能选),发 `options_proposed`,然后挂起会话(和 `agent.await` 同一机制:`await_wake` 登记后 `waitForFact`,不占 worker 槽位、不调用模型;run 状态 `waiting`、summary「等待选择方案」)。用户作答(`POST /agent/runs/{id}/choice` 或待处理中心)后唤醒,重放这次调用,返回 `{outcome: chosen|rejected|stop, choice, option, reason, by: user|timeout}` 并发 `options_resolved`。超时(`agent.optionsTimeoutSec`,缺省 1800)按 `agent.optionsTimeoutAction`:`recommended` 自动选推荐项(没有推荐项选第一个),`stop` 不选、模型收尾。 +- **内联审批**:`download.submit` / `organize.execute` / `policy.apply` 在 interactive 会话里没有持续授权、输入里也没有 `approvalId` 时,授权器在当前步骤建单次审批(绑定 plan + payload hash + revision,有效期 `agent.approvalTimeoutSec`,缺省 86400)与待处理事项(kind=approval_required,`agent-approval::`),发 `approval_requested {approvalId, capabilityId, planType, planId, payloadHash, expectedRevision, riskLevel, summary, stepId, expiresAt}`,挂起会话(summary「等待审批」)。管理员在审批卡 `PATCH /approvals/{id}` 批准 / 拒绝(可带理由)后唤醒:挂起的调用重放一次,批准 → 按审批执行(账本记 approval_id,成功后 consumed);拒绝 / 过期 / 计划已变化 → 调用以原因失败(`管理员拒绝了审批:<理由>`),模型按理由调整;关联待处理事项写 `resolution {approval, reason, suggestion}`。每种结果发一次 `approval_resolved`。 + - 重放机制:interactive 会话里 replay=never 的能力对 harness 声明 safe,首次执行写调用级 memo;恢复时只有本次恢复针对的那个调用(`resumedAwaits`)会重新走注册中心,其余中断的调用仍按"执行被中断、外部结果未知"返回,崩溃恢复语义不变。 +- 停止会话:还在等用户的方案选择 / 审批一并收起(事项 dismissed、审批 revoked),并唤醒挂起的会话按取消收尾。 +- 排查:挂起期间 `agent_run.status=waiting`、`await_wake` 非空;`decision` 里按 `dedupe_key like 'agent-options::%'` / `'agent-approval::%'` 查;`approval.scope` 带 `{agentRunId, invocationId, capabilityId, stepId, summary}`。 + ## 后台触发与 resolveDecision - `decision.created` 路由到 `resolveDecision(trigger=created)`,由 `agent/investigate.ts` 的 `autoInvestigate` 处理。满足以下全部条件时,会发起一次有界 run:`agent.autoInvestigate=true`、kind 为 `identity_ambiguous` / `download_failed` / `download_stalled`、事项仍为 open、事项不是由 Agent 自己引起的。`trigger=event`,工具调用上限为 `agent.investigateMaxToolCalls`,时长上限 `min(600, agent.maxDurationSec)` 秒,`dedupe_key=investigate:`。同一事项只会有一个 run,重复事件或重新打开都不会再建。模式见下一节。调查结束后,没有被系统核实恢复的事项转为 `awaiting_user`,`evidence.agentInvestigation` 里带上结论。 @@ -180,7 +198,9 @@ OpenRouter 换到的是永久 key,不刷新。 "autoInvestigate": false, "investigateMaxToolCalls": 8, "autoInvestigateMode": "investigate", // 或 "recover"(授权内恢复,见上一节) "investigateMaxAttempts": 2, "recoverVerifySec": 600, - "goalPollSec": 60, "goalWaitHours": 72 + "goalPollSec": 60, "goalWaitHours": 72, + "optionsTimeoutSec": 1800, "optionsTimeoutAction": "recommended", // 方案选择超时:自动选推荐项或 "stop" + "approvalTimeoutSec": 86400 // 会话内审批有效期 }, "budget": { "agentMaxRunsPerDay": 50, "agentMaxCostUsdPerDay": 5 } } @@ -195,7 +215,7 @@ OpenRouter 换到的是永久 key,不刷新。 - 开关:`pi.subagents.enabled`(设置页「允许子代理」,旧字段 `subagentsEnabled` 同步),关闭时模型拿不到 `agent.spawn`,服务端也拒绝派生。 - 用法:会话调用 `agent.spawn {goal, capabilityAllowlist?, budget?, mode?, upstreamId?, await?}` 派生子会话(独立 agent_run、同一 runAgent 任务、独立会话文件),再用 `agent.await` 取各子会话的状态与汇报摘要(已脱敏)。 -- 权限:子会话能力白名单必须是父会话的子集,只给方案的会话只能派生只给方案的子会话,子会话继承父会话发起人,授权校验再按全部祖先会话的白名单与类别取交集。 +- 权限:子会话能力白名单必须是父会话的子集,只读(内部 plan_only)的会话只能派生只读子会话,父会话要求先看方案时子会话同样等方案选定,子会话继承父会话发起人,授权校验再按全部祖先会话的白名单与类别取交集。 - 限制:同一父会话下同时运行的子会话不超过 `maxConcurrent`(多出的排队等名额,不占 worker 也不调用模型),派生层数不超过 `maxDepth`,一个会话最多派生 10 个子会话。 - 预算:子会话预算从父会话剩余预算划拨(缺省剩余的一半,请求超过剩余直接拒绝),子会话超预算只结束它自己;父会话的工具调用 / token / 费用预算把子孙会话的实际用量算在内。 - 取消:中止父会话会递归取消全部未结束的子孙会话(排队中的也一样)。 @@ -211,7 +231,7 @@ fake 上游也支持 authKind = oauth,用来联调登录流程:browser 模式粘 ```sh curl -X POST localhost:4380/api/v1/agent/runs -H "Authorization: Bearer umt-dev-admin-key" \ - -H 'content-type: application/json' -d '{"goal":"看看库里有什么","mode":"plan_only"}' + -H 'content-type: application/json' -d '{"goal":"看看库里有什么","proposeFirst":true}' ``` 没有配置 `agent.fakeScript` 时,使用内置演示脚本:查一次库存,然后如实汇报,不做写操作。自定义脚本用 JSON 写,可以是单个对象或数组: @@ -250,6 +270,7 @@ curl -X POST localhost:4380/api/v1/agent/runs -H "Authorization: Bearer umt-dev- | `e5-auto-investigate` | 识别歧义 → 有界调查,去重 | | `e6-auto-recover` | qB missingFiles → download_failed → 授权内恢复(task.inspect → download.recheck → 系统核实关闭事项);无授权时降级只调查;换源被拒;去重;同根因尝试上限;预算超限保持待处理 | | `e11-subagents` | 父会话派生两个只读子会话并等待后汇总;maxConcurrent=1 时串行;子会话超预算不影响父;合计用量;子会话事件进父事件流;越权 / 提权 / 深度 / 关闭被拒;父中止后子会话 cancelled | +| `e12-interaction` | 步骤清单先于业务调用、revision 递增;先看方案时执行被拒;方案选择挂起(waiting + await_wake)、选定 / 都不要 / 超时自动选推荐项后续跑;旧 `mode=plan_only` 按先看方案处理;管理员会话发起的会话内审批:批准后重放执行、审批 consumed,拒绝后以理由失败并写待处理事项 | ## 已知限制