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:
1 parent
d931e75310
commit
74c2ed45aa
12 files changed
+379
-12
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user