feat(agent): 选定方案即批准,服务端绑定计划(AGUI-3)
- agent.propose_options 的方案可带 action(capabilityId + planId)。提出方案时服务端按计划当前的 payload hash + revision 绑定,并用计划本身生成动作要点(对象、大小 / 文件数)发给界面,不用模型的说法。 - 控制台管理员会话选定绑定了计划的方案时,建一份 scope.via=option 的已批准单次审批(审计 approval.approved); 执行时授权器 optionApproval 核对能力、计划类型 + ID、payload hash、revision、有效期后放行,用后 consumed, 不再出第二张审批卡。API Key 作答、超时自动选、计划已变化都不算批准;移动 / 替换文件的整理与发布策略 (删除、覆盖类)选定后仍单独确认。会话停止时撤销没用掉的这类审批。 - contracts:CAPABILITY_ACTIVITY 给每个能力加 verb / noun(会话进度行的人话计数);agent.plan / agent.propose_options 标为 hidden。 - e12 补测试:管理员选定 → 无 approval_requested、审批绑定同一计划并 consumed、换 hash 不放行; API Key 选定 → 仍出审批卡。 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c9c6505d71
commit
4e0e6712da
8 files changed
+507
-33
No files matched your search
@@ -131,6 +131,18 @@ export const AgentPlanStep = z.object({
|
||||
});
|
||||
export type AgentPlanStep = z.infer<typeof AgentPlanStep>;
|
||||
|
||||
/**
|
||||
* 方案绑定的待审批动作(AGUI-3):模型只给能力与计划 ID;服务端在提出方案时按计划当前的 payload hash + revision
|
||||
* 绑定,并用计划本身生成动作要点(对象、大小)。管理员会话选定这个方案 = 批准这一份计划,执行时服务端再核对绑定。
|
||||
*/
|
||||
export const AgentOptionActionCapability = z.enum(["download.submit", "organize.execute", "policy.apply"]);
|
||||
export const AgentOptionAction = z.object({
|
||||
capabilityId: AgentOptionActionCapability,
|
||||
/** download.submit / organize.execute 的 planId;policy.apply 的 policyRevisionId */
|
||||
planId: Id,
|
||||
});
|
||||
export type AgentOptionAction = z.infer<typeof AgentOptionAction>;
|
||||
|
||||
/** 给用户选的一个方案 */
|
||||
export const AgentOptionItem = z.object({
|
||||
id: z.string().trim().min(1).max(40),
|
||||
@@ -139,9 +151,23 @@ export const AgentOptionItem = z.object({
|
||||
risks: z.array(z.string().max(300)).max(8).default([]),
|
||||
/** 估算代价(体积 / 时长 / 费用),模型给出的文本,未经核查 */
|
||||
estimatedCost: z.string().max(200).nullable().default(null),
|
||||
/** 选定后要执行的需审批动作(可选) */
|
||||
action: AgentOptionAction.nullable().optional(),
|
||||
});
|
||||
export type AgentOptionItem = z.infer<typeof AgentOptionItem>;
|
||||
|
||||
/** 事件里的方案动作:服务端从计划生成的要点;approves = 选定即批准(删除、覆盖类动作为 false,仍单独确认) */
|
||||
export const AgentOptionActionView = AgentOptionAction.extend({
|
||||
/** 动作名,如「提交下载」 */
|
||||
label: z.string(),
|
||||
/** 动作要点:对象与大小 / 数量,如「《沙丘:第二部》· 12.0 GB」 */
|
||||
detail: z.string().nullable(),
|
||||
approves: z.boolean(),
|
||||
});
|
||||
export type AgentOptionActionView = z.infer<typeof AgentOptionActionView>;
|
||||
export const AgentOptionView = AgentOptionItem.extend({ action: AgentOptionActionView.nullable().optional() });
|
||||
export type AgentOptionView = z.infer<typeof AgentOptionView>;
|
||||
|
||||
/** 超时未选时的处理:recommended = 自动选推荐项;stop = 不选,模型收尾 */
|
||||
export const AgentOptionsTimeoutAction = z.enum(["recommended", "stop"]);
|
||||
export type AgentOptionsTimeoutAction = z.infer<typeof AgentOptionsTimeoutAction>;
|
||||
@@ -159,7 +185,7 @@ const OptionsProposed = z.object({
|
||||
/** 对应的待处理事项(用户也可以在待处理中心选) */
|
||||
decisionId: Id,
|
||||
question: z.string(),
|
||||
options: z.array(AgentOptionItem),
|
||||
options: z.array(AgentOptionView),
|
||||
recommendedId: z.string().nullable(),
|
||||
/** 出现在哪一步(提出时正在进行的步骤) */
|
||||
stepId: z.string().nullable(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { DecisionKind, DecisionOption, DecisionStatus, Severity } from "./agent.ts";
|
||||
import { AgentOptionItem, AgentPlanStep, AgentPlanStepStatus } from "./agent-events.ts";
|
||||
import { AgentOptionAction, AgentOptionItem, AgentPlanStep, AgentPlanStepStatus } from "./agent-events.ts";
|
||||
import { MediaAsset } from "./assets.ts";
|
||||
import { BrowserController, BrowserDownloadVerification, BrowserSnapshot } from "./browser.ts";
|
||||
import { CAPABILITY_CATALOG, type CapabilitySpec, type KnownCapabilityId } from "./capability.ts";
|
||||
@@ -404,6 +404,8 @@ export const AgentProposeOptionsOutput = z.object({
|
||||
outcome: z.enum(["chosen", "rejected", "stop"]),
|
||||
choice: z.string().nullable(),
|
||||
option: AgentOptionItem.nullable(),
|
||||
/** 选定即批准(AGUI-3):用户在管理员会话里选定的方案绑定了这份计划,可直接执行;null = 没有随选定批准的动作 */
|
||||
approvedAction: AgentOptionAction.nullable().optional(),
|
||||
reason: z.string().nullable(),
|
||||
by: z.enum(["user", "timeout"]),
|
||||
});
|
||||
|
||||
@@ -532,7 +532,7 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
||||
"agent.propose_options": d(
|
||||
"agent.propose_options",
|
||||
"给出方案",
|
||||
"有明显取舍时给用户 2–3 个方案(各写摘要、风险、估算代价,标出推荐项),等用户选定后返回所选方案;用户也可以都不要并说明原因。超时按设置自动选推荐项或停止",
|
||||
"有明显取舍时给用户 2–3 个方案(各写摘要、风险、估算代价,标出推荐项),等用户选定后返回所选方案;用户也可以都不要并说明原因。超时按设置自动选推荐项或停止。方案要执行需审批的动作时,先生成计划,再在 action 里填 capabilityId 与 planId:界面按计划写明对象和大小,用户选定即批准这份计划(结果里的 approvedAction),不再单独出审批卡",
|
||||
"propose",
|
||||
"AgentProposeOptions",
|
||||
"organize.execute",
|
||||
@@ -579,6 +579,62 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* 能力的人话计数(AGUI-3):会话里的进度行把一回合的工具调用聚合成「查了 1 个站点、2 个下载任务和媒体库」。
|
||||
* verb 是动作(拼成「查了」),noun 是对象;unit 为 null 时不写次数(「查了媒体库」)。verb + noun 相同的调用合并计数。
|
||||
* hidden:步骤清单与方案选择在会话里有自己的呈现,不当作工具调用计数。
|
||||
*/
|
||||
export interface CapabilityActivity {
|
||||
verb: string;
|
||||
noun: string;
|
||||
unit: string | null;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
const act = (verb: string, noun: string, unit: string | null = null): CapabilityActivity => ({ verb, noun, unit });
|
||||
|
||||
export const CAPABILITY_ACTIVITY: Record<KnownCapabilityId, CapabilityActivity> = {
|
||||
"catalog.lookup": act("查", "作品信息"),
|
||||
"release.search": act("搜", "资源", "次"),
|
||||
"release.inspect": act("看", "资源", "个"),
|
||||
"identity.resolve": act("识别", "作品", "个"),
|
||||
"download.plan": act("生成", "下载计划", "份"),
|
||||
"download.submit": act("提交", "下载", "个"),
|
||||
"download.recheck": act("校验", "下载任务", "个"),
|
||||
"organize.plan": act("生成", "整理计划", "份"),
|
||||
"organize.execute": act("执行", "整理", "次"),
|
||||
"task.inspect": act("查", "任务", "个"),
|
||||
"task.retry": act("重试", "任务", "个"),
|
||||
"task.cancel": act("取消", "任务", "个"),
|
||||
"policy.preview": act("预览", "策略"),
|
||||
"policy.apply": act("发布", "策略"),
|
||||
"inventory.query": act("查", "媒体库"),
|
||||
"subscription.list": act("查", "订阅"),
|
||||
"decision.list": act("查", "待处理事项"),
|
||||
"download.inspect": act("查", "下载任务", "个"),
|
||||
"agent.spawn": act("派出", "子会话", "个"),
|
||||
"agent.await": act("等", "子会话"),
|
||||
"site.list": act("查", "站点"),
|
||||
"site.health_check": act("检测", "站点", "个"),
|
||||
"site.refresh_login": act("刷新", "站点登录"),
|
||||
"site.user_data": act("查", "站点数据"),
|
||||
"browser.snapshot": act("读", "原站页面", "次"),
|
||||
"browser.navigate": act("打开", "原站页面", "次"),
|
||||
"browser.interact": act("操作", "原站页面", "次"),
|
||||
"browser.download": act("下载", "种子文件", "个"),
|
||||
"browser.screenshot": act("截取", "原站页面", "次"),
|
||||
"browser.click_at": act("操作", "原站页面", "次"),
|
||||
"browser.type_at": act("操作", "原站页面", "次"),
|
||||
"browser.scroll": act("操作", "原站页面", "次"),
|
||||
"browser.wait": act("等", "页面加载"),
|
||||
"browser.solve_challenge": act("处理", "页面挑战", "次"),
|
||||
"agent.plan": { ...act("更新", "步骤"), hidden: true },
|
||||
"agent.propose_options": { ...act("给出", "方案"), hidden: true },
|
||||
"web.search": act("搜", "网页", "次"),
|
||||
"web.fetch": act("读", "网页", "个"),
|
||||
"metadata.enrich": act("补全", "元数据"),
|
||||
};
|
||||
|
||||
function d(
|
||||
id: KnownCapabilityId,
|
||||
title: string,
|
||||
|
||||
Reference in new issue
Block a user