feat(contracts,db): agent.plan / agent.propose_options, interaction events, proposeFirst replaces run mode

- agent_run_event gains plan_updated, options_proposed/resolved, approval_requested/resolved (migration 0040)
- AgentRunStartInput.proposeFirst; mode deprecated (plan_only maps to proposeFirst); mode dropped from AgentRun API
- POST /agent/runs/{id}/choice; agent.optionsTimeoutSec/optionsTimeoutAction/approvalTimeoutSec settings

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 21:07:01 +08:00
1 parent d931e75310
commit 74c2ed45aa
12 files changed
+379 -12

No files matched your search

+142
View File
@@ -19,6 +19,11 @@ export const AgentRunEventType = z.enum([
"subagent_spawned",
"subagent_event",
"subagent_completed",
"plan_updated",
"options_proposed",
"options_resolved",
"approval_requested",
"approval_resolved",
]);
export type AgentRunEventType = z.infer<typeof AgentRunEventType>;
@@ -111,6 +116,90 @@ const SubagentCompleted = z.object({
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>;
/** 给用户选的一个方案 */
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),
});
export type AgentOptionItem = z.infer<typeof AgentOptionItem>;
/** 超时未选时的处理: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(AgentOptionItem),
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,
@@ -125,6 +214,11 @@ export const AgentRunEventBody = z.discriminatedUnion("type", [
SubagentSpawned,
SubagentEvent,
SubagentCompleted,
PlanUpdated,
OptionsProposed,
OptionsResolved,
ApprovalRequested,
ApprovalResolved,
]);
export type AgentRunEventBody = z.infer<typeof AgentRunEventBody>;
@@ -150,6 +244,11 @@ export const AgentRunEvent = z.discriminatedUnion("type", [
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 };
@@ -230,6 +329,20 @@ export function agentRunEventToBusEvent(e: AgentRunEvent): TypedEvent | null {
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 } };
}
@@ -240,3 +353,32 @@ export function agentRunEventToBusEvent(e: AgentRunEvent): TypedEvent | null {
}
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;
}
}