- 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]>
411 lines
15 KiB
TypeScript
411 lines
15 KiB
TypeScript
import { z } from "zod";
|
|
import type { AgentRunStatus } from "./agent.ts";
|
|
import { Id, JsonObject, NonNegInt, Timestamp } from "./common.ts";
|
|
import type { TypedEvent } from "./events.ts";
|
|
|
|
// Agent 运行事件(REQ §10.6):packages/agent 产生,server 先写 agent_run_event(带 seq)再推 SSE。
|
|
// 这里是唯一定义;agent 包的 onEvent 回调直接收 AgentRunEvent。
|
|
|
|
export const AgentRunEventType = z.enum([
|
|
"run_started",
|
|
"text_delta",
|
|
"thinking_delta",
|
|
"tool_call_start",
|
|
"tool_call_end",
|
|
"tool_call_rejected",
|
|
"compaction",
|
|
"run_completed",
|
|
"run_failed",
|
|
"subagent_spawned",
|
|
"subagent_event",
|
|
"subagent_completed",
|
|
"plan_updated",
|
|
"options_proposed",
|
|
"options_resolved",
|
|
"approval_requested",
|
|
"approval_resolved",
|
|
]);
|
|
export type AgentRunEventType = z.infer<typeof AgentRunEventType>;
|
|
|
|
/** 一次运行累计用量(数值都齐全;写 agent_run.usage 时可直接用,是 AgentUsage 的子集形状) */
|
|
export const AgentUsageTotals = z.object({
|
|
inputTokens: NonNegInt,
|
|
outputTokens: NonNegInt,
|
|
cacheReadTokens: NonNegInt,
|
|
cacheWriteTokens: NonNegInt,
|
|
totalTokens: NonNegInt,
|
|
costUsd: z.number().nonnegative(),
|
|
});
|
|
export type AgentUsageTotals = z.infer<typeof AgentUsageTotals>;
|
|
|
|
export const AgentFailureReason = z.enum(["budget", "aborted", "error"]);
|
|
export type AgentFailureReason = z.infer<typeof AgentFailureReason>;
|
|
export const AgentBudgetKind = z.enum(["tool_calls", "duration", "tokens", "cost"]);
|
|
export type AgentBudgetKind = z.infer<typeof AgentBudgetKind>;
|
|
|
|
const RunStarted = z.object({ type: z.literal("run_started"), goal: z.string(), resumed: z.boolean() });
|
|
const TextDelta = z.object({ type: z.literal("text_delta"), delta: z.string() });
|
|
const ThinkingDelta = z.object({ type: z.literal("thinking_delta"), delta: z.string() });
|
|
const ToolCallStart = z.object({
|
|
type: z.literal("tool_call_start"),
|
|
toolCallId: z.string(),
|
|
capabilityId: z.string(),
|
|
/** 已脱敏、截断的输入摘要 */
|
|
inputSummary: z.string(),
|
|
/** 崩溃恢复后重放/补记的调用 */
|
|
recovery: z.boolean(),
|
|
});
|
|
const ToolCallEnd = z.object({
|
|
type: z.literal("tool_call_end"),
|
|
toolCallId: z.string(),
|
|
capabilityId: z.string(),
|
|
isError: z.boolean(),
|
|
/** 已脱敏、截断的输出摘要 */
|
|
outputSummary: z.string(),
|
|
recovery: z.boolean(),
|
|
});
|
|
const ToolCallRejected = z.object({
|
|
type: z.literal("tool_call_rejected"),
|
|
toolCallId: z.string(),
|
|
toolName: z.string(),
|
|
capabilityId: z.string().optional(),
|
|
reason: z.string(),
|
|
});
|
|
const Compaction = z.object({
|
|
type: z.literal("compaction"),
|
|
phase: z.enum(["start", "end"]),
|
|
reason: z.enum(["manual", "threshold", "overflow"]),
|
|
status: z.enum(["completed", "declined", "aborted", "failed"]).optional(),
|
|
});
|
|
const RunCompleted = z.object({ type: z.literal("run_completed"), finalText: z.string(), usage: AgentUsageTotals });
|
|
const RunFailed = z.object({
|
|
type: z.literal("run_failed"),
|
|
reason: AgentFailureReason,
|
|
message: z.string(),
|
|
usage: AgentUsageTotals,
|
|
budget: AgentBudgetKind.optional(),
|
|
});
|
|
|
|
// 子代理(agent.spawn):子会话的事件镜像进父会话事件流,由父会话的运行进程按父 seq 写入(单写者,seq 不冲突)。
|
|
const SubagentSpawned = z.object({
|
|
type: z.literal("subagent_spawned"),
|
|
childRunId: z.string(),
|
|
goal: z.string(),
|
|
mode: z.enum(["execute", "plan_only"]),
|
|
depth: NonNegInt,
|
|
capabilities: z.array(z.string()),
|
|
/** 从父会话划拨给子会话的预算 */
|
|
budget: z.record(z.string(), z.number()),
|
|
});
|
|
/** 子会话的一条事件(工具调用 / 开始 / 结束);childSeq 是子会话内的 seq,正文已脱敏截断 */
|
|
const SubagentEvent = z.object({
|
|
type: z.literal("subagent_event"),
|
|
childRunId: z.string(),
|
|
childSeq: NonNegInt,
|
|
childType: z.string(),
|
|
capabilityId: z.string().optional(),
|
|
summary: z.string(),
|
|
});
|
|
const SubagentCompleted = z.object({
|
|
type: z.literal("subagent_completed"),
|
|
childRunId: z.string(),
|
|
status: z.string(),
|
|
/** 子会话汇报摘要(业务核查结论 + 未经核查的模型文本),已脱敏截断 */
|
|
summary: z.string(),
|
|
usage: AgentUsageTotals,
|
|
toolCalls: NonNegInt,
|
|
});
|
|
|
|
// 自动化优先的会话交互(AG-2):模型用 agent.plan 维护步骤清单,用 agent.propose_options 让用户在方案里选;
|
|
// 需审批的能力在对应步骤处出审批卡。这些事件都由会话的运行进程写入(单写者):用户在挂起期间作的选择 / 审批,
|
|
// 在会话恢复、重放挂起的调用时才记为 *_resolved。
|
|
|
|
export const AgentPlanStepStatus = z.enum(["pending", "running", "done", "skipped", "failed"]);
|
|
export type AgentPlanStepStatus = z.infer<typeof AgentPlanStepStatus>;
|
|
/** 步骤清单的一步;id 由模型给,同一会话内稳定(更新时按 id 合并) */
|
|
export const AgentPlanStep = z.object({
|
|
id: z.string().trim().min(1).max(40),
|
|
title: z.string().trim().min(1).max(200),
|
|
status: AgentPlanStepStatus,
|
|
note: z.string().max(500).nullable().default(null),
|
|
});
|
|
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),
|
|
title: z.string().trim().min(1).max(200),
|
|
summary: z.string().trim().min(1).max(1000),
|
|
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>;
|
|
|
|
const PlanUpdated = z.object({
|
|
type: z.literal("plan_updated"),
|
|
steps: z.array(AgentPlanStep),
|
|
/** 本会话第几版计划(从 1 起) */
|
|
revision: z.number().int().min(1),
|
|
});
|
|
const OptionsProposed = z.object({
|
|
type: z.literal("options_proposed"),
|
|
/** 挂起的工具调用(恢复时重放) */
|
|
invocationId: z.string(),
|
|
/** 对应的待处理事项(用户也可以在待处理中心选) */
|
|
decisionId: Id,
|
|
question: z.string(),
|
|
options: z.array(AgentOptionView),
|
|
recommendedId: z.string().nullable(),
|
|
/** 出现在哪一步(提出时正在进行的步骤) */
|
|
stepId: z.string().nullable(),
|
|
expiresAt: Timestamp,
|
|
timeoutAction: AgentOptionsTimeoutAction,
|
|
});
|
|
const OptionsResolved = z.object({
|
|
type: z.literal("options_resolved"),
|
|
invocationId: z.string(),
|
|
decisionId: Id,
|
|
/** chosen = 选了 choice;rejected = 都不要(reason 是用户说明);stop = 超时按设置停止 */
|
|
outcome: z.enum(["chosen", "rejected", "stop"]),
|
|
choice: z.string().nullable(),
|
|
reason: z.string().nullable(),
|
|
by: z.enum(["user", "timeout"]),
|
|
});
|
|
const ApprovalRequested = z.object({
|
|
type: z.literal("approval_requested"),
|
|
approvalId: Id,
|
|
decisionId: Id.nullable(),
|
|
invocationId: z.string(),
|
|
capabilityId: z.string(),
|
|
planType: z.string(),
|
|
planId: Id,
|
|
payloadHash: z.string(),
|
|
expectedRevision: z.number().int().min(0),
|
|
riskLevel: z.enum(["low", "medium", "high"]),
|
|
/** 计划摘要(已脱敏截断) */
|
|
summary: z.string(),
|
|
stepId: z.string().nullable(),
|
|
expiresAt: Timestamp,
|
|
});
|
|
const ApprovalResolved = z.object({
|
|
type: z.literal("approval_resolved"),
|
|
approvalId: Id,
|
|
status: z.enum(["approved", "rejected", "expired", "revoked", "invalid"]),
|
|
reason: z.string().nullable(),
|
|
decidedBy: z.string().nullable(),
|
|
});
|
|
|
|
/** 事件主体(不含 runId/seq/at) */
|
|
export const AgentRunEventBody = z.discriminatedUnion("type", [
|
|
RunStarted,
|
|
TextDelta,
|
|
ThinkingDelta,
|
|
ToolCallStart,
|
|
ToolCallEnd,
|
|
ToolCallRejected,
|
|
Compaction,
|
|
RunCompleted,
|
|
RunFailed,
|
|
SubagentSpawned,
|
|
SubagentEvent,
|
|
SubagentCompleted,
|
|
PlanUpdated,
|
|
OptionsProposed,
|
|
OptionsResolved,
|
|
ApprovalRequested,
|
|
ApprovalResolved,
|
|
]);
|
|
export type AgentRunEventBody = z.infer<typeof AgentRunEventBody>;
|
|
|
|
const EventMeta = {
|
|
/** = agent_run.id */
|
|
runId: z.string().min(1),
|
|
/** 该 run 内从 0 起单调递增;续跑时从 agent_run.last_event_seq + 1 继续 */
|
|
seq: NonNegInt,
|
|
at: Timestamp,
|
|
};
|
|
|
|
/** 完整事件:主体 + runId/seq/at */
|
|
export const AgentRunEvent = z.discriminatedUnion("type", [
|
|
RunStarted.extend(EventMeta),
|
|
TextDelta.extend(EventMeta),
|
|
ThinkingDelta.extend(EventMeta),
|
|
ToolCallStart.extend(EventMeta),
|
|
ToolCallEnd.extend(EventMeta),
|
|
ToolCallRejected.extend(EventMeta),
|
|
Compaction.extend(EventMeta),
|
|
RunCompleted.extend(EventMeta),
|
|
RunFailed.extend(EventMeta),
|
|
SubagentSpawned.extend(EventMeta),
|
|
SubagentEvent.extend(EventMeta),
|
|
SubagentCompleted.extend(EventMeta),
|
|
PlanUpdated.extend(EventMeta),
|
|
OptionsProposed.extend(EventMeta),
|
|
OptionsResolved.extend(EventMeta),
|
|
ApprovalRequested.extend(EventMeta),
|
|
ApprovalResolved.extend(EventMeta),
|
|
]);
|
|
export type AgentRunEvent = AgentRunEventBody & { runId: string; seq: number; at: string };
|
|
|
|
/** agent_run_event 行:(run_id, seq) 唯一;payload = 事件主体去掉 type */
|
|
export const AgentRunEventRow = z.object({
|
|
runId: Id,
|
|
seq: NonNegInt,
|
|
type: AgentRunEventType,
|
|
at: Timestamp,
|
|
payload: JsonObject,
|
|
});
|
|
export type AgentRunEventRow = z.infer<typeof AgentRunEventRow>;
|
|
|
|
export function agentRunEventToRow(e: AgentRunEvent): AgentRunEventRow {
|
|
const { runId, seq, at, type, ...payload } = e;
|
|
return { runId, seq, at, type, payload };
|
|
}
|
|
|
|
/** 行 → 事件;payload 不合法时抛错(表里只应有 agentRunEventToRow 写入的内容) */
|
|
export function agentRunEventFromRow(row: AgentRunEventRow): AgentRunEvent {
|
|
return AgentRunEvent.parse({ ...row.payload, type: row.type, runId: row.runId, seq: row.seq, at: row.at });
|
|
}
|
|
|
|
/** 工具调用事件 → SSE agent.tool_call 的 phase */
|
|
export function toolCallPhase(e: AgentRunEventBody): "started" | "succeeded" | "failed" | "denied" | null {
|
|
switch (e.type) {
|
|
case "tool_call_start":
|
|
return "started";
|
|
case "tool_call_end":
|
|
return e.isError ? "failed" : "succeeded";
|
|
case "tool_call_rejected":
|
|
return "denied";
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** run_completed / run_failed / run_started → agent_run.status */
|
|
export function agentRunStatusOf(e: AgentRunEventBody): AgentRunStatus | null {
|
|
switch (e.type) {
|
|
case "run_started":
|
|
return "running";
|
|
case "run_completed":
|
|
return "succeeded";
|
|
case "run_failed":
|
|
return e.reason === "budget" ? "budget_exceeded" : e.reason === "aborted" ? "cancelled" : "failed";
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Agent 运行事件 → 全局 SSE 事件(agent.run_status / agent.text_delta / agent.tool_call)。
|
|
* thinking_delta 与 compaction 不上全局总线(只在 run 详情里按 agent_run_event 回放),返回 null。
|
|
*/
|
|
export function agentRunEventToBusEvent(e: AgentRunEvent): TypedEvent | null {
|
|
const phase = toolCallPhase(e);
|
|
if (phase) {
|
|
let capabilityId = "";
|
|
let summary = "";
|
|
if (e.type === "tool_call_start") [capabilityId, summary] = [e.capabilityId, e.inputSummary];
|
|
else if (e.type === "tool_call_end") [capabilityId, summary] = [e.capabilityId, e.outputSummary];
|
|
else if (e.type === "tool_call_rejected") [capabilityId, summary] = [e.capabilityId ?? e.toolName, e.reason];
|
|
return { type: "agent.tool_call", payload: { runId: e.runId, runSeq: e.seq, capabilityId, phase, summary } };
|
|
}
|
|
// 子会话事件:借 agent.tool_call(能力 agent.spawn)提示父会话页"有新事件",页面按 seq 补读
|
|
if (e.type === "subagent_spawned" || e.type === "subagent_event" || e.type === "subagent_completed") {
|
|
const phase =
|
|
e.type === "subagent_completed" ? (e.status === "succeeded" ? "succeeded" : "failed") : ("started" as const);
|
|
const summary =
|
|
e.type === "subagent_spawned"
|
|
? `派生子会话 ${e.childRunId}:${e.goal}`
|
|
: e.type === "subagent_event"
|
|
? `子会话 ${e.childRunId}:${e.childType} ${e.capabilityId ?? ""} ${e.summary}`.trim()
|
|
: `子会话 ${e.childRunId} ${e.status}:${e.summary}`;
|
|
return {
|
|
type: "agent.tool_call",
|
|
payload: { runId: e.runId, runSeq: e.seq, capabilityId: "agent.spawn", phase, summary: summary.slice(0, 600) },
|
|
};
|
|
}
|
|
// 计划 / 方案 / 审批:借 agent.tool_call 提示会话页"有新事件"(页面按 seq 补读),摘要给全局通知用
|
|
const hint = interactionHint(e);
|
|
if (hint) {
|
|
return {
|
|
type: "agent.tool_call",
|
|
payload: {
|
|
runId: e.runId,
|
|
runSeq: e.seq,
|
|
capabilityId: hint.capabilityId,
|
|
phase: hint.phase,
|
|
summary: hint.summary,
|
|
},
|
|
};
|
|
}
|
|
if (e.type === "text_delta") {
|
|
return { type: "agent.text_delta", payload: { runId: e.runId, runSeq: e.seq, delta: e.delta } };
|
|
}
|
|
const status = agentRunStatusOf(e);
|
|
if (status) {
|
|
const summary = e.type === "run_completed" ? e.finalText : e.type === "run_failed" ? e.message : null;
|
|
return { type: "agent.run_status", payload: { runId: e.runId, status, summary } };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function interactionHint(
|
|
e: AgentRunEventBody,
|
|
): { capabilityId: string; phase: "started" | "succeeded" | "failed" | "denied"; summary: string } | null {
|
|
switch (e.type) {
|
|
case "plan_updated": {
|
|
const done = e.steps.filter((s) => s.status === "done").length;
|
|
return { capabilityId: "agent.plan", phase: "succeeded", summary: `步骤 ${done}/${e.steps.length}` };
|
|
}
|
|
case "options_proposed":
|
|
return { capabilityId: "agent.propose_options", phase: "started", summary: e.question.slice(0, 600) };
|
|
case "options_resolved":
|
|
return {
|
|
capabilityId: "agent.propose_options",
|
|
phase: e.outcome === "chosen" ? "succeeded" : "failed",
|
|
summary: e.outcome === "chosen" ? `已选 ${e.choice}` : e.outcome === "rejected" ? "都不要" : "超时未选",
|
|
};
|
|
case "approval_requested":
|
|
return { capabilityId: e.capabilityId, phase: "started", summary: `等待审批:${e.summary}`.slice(0, 600) };
|
|
case "approval_resolved":
|
|
return {
|
|
capabilityId: "approval",
|
|
phase: e.status === "approved" ? "succeeded" : "denied",
|
|
summary: `审批${e.status}${e.reason ? `:${e.reason}` : ""}`.slice(0, 600),
|
|
};
|
|
default:
|
|
return null;
|
|
}
|
|
}
|