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
@@ -15,8 +15,8 @@ const TiB = 1024 ** 4;
|
||||
const SITE_IDS = ["site.list", "site.health_check", "site.refresh_login", "site.user_data"] as const;
|
||||
|
||||
describe("站点能力规格", () => {
|
||||
it("进入首批能力目录(17 + 4 站点 + 4 浏览器 = 25);类别 / 风险 / 重放按约定", () => {
|
||||
expect(CAPABILITY_IDS).toHaveLength(25);
|
||||
it("进入首批能力目录(17 + 4 站点 + 4 浏览器 + 2 会话交互 = 27);类别 / 风险 / 重放按约定", () => {
|
||||
expect(CAPABILITY_IDS).toHaveLength(27);
|
||||
for (const id of SITE_IDS) expect(CAPABILITY_IDS).toContain(id);
|
||||
expect(capabilitySpec("site.list")).toMatchObject({ kind: "read", riskLevel: "low", replay: "safe" });
|
||||
expect(capabilitySpec("site.user_data")).toMatchObject({ kind: "read", riskLevel: "low", replay: "safe" });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,11 @@ export const AgentRunStatus = z.enum([
|
||||
"budget_exceeded",
|
||||
]);
|
||||
export type AgentRunStatus = z.infer<typeof AgentRunStatus>;
|
||||
/** execute:授权范围内边执行边汇报;plan_only:"先给方案",只允许 read/preview/propose 类能力 */
|
||||
/**
|
||||
* 内部能力上限(agent_run.mode 列,不再出现在 API 与界面上):execute = 按白名单与授权执行;
|
||||
* plan_only = 只允许 read/preview/propose 类能力(后台只读调查、只读子会话用)。
|
||||
* 用户会话一律 execute;"先看方案再执行"是会话选项(scope.proposeFirst),见 AgentRunStartInput。
|
||||
*/
|
||||
export const AgentRunMode = z.enum(["execute", "plan_only"]);
|
||||
export type AgentRunMode = z.infer<typeof AgentRunMode>;
|
||||
|
||||
@@ -65,7 +69,6 @@ export const AgentRun = EntityBase.extend({
|
||||
startedAt: Timestamp.nullable(),
|
||||
finishedAt: Timestamp.nullable(),
|
||||
createdBy: z.string().nullable(),
|
||||
mode: AgentRunMode,
|
||||
/** 后台触发按根因去重(如 investigate:<decisionId>);用户委托为 null */
|
||||
dedupeKey: z.string().nullable(),
|
||||
/** 派生深度:用户/后台发起的会话为 0,agent.spawn 派生的子会话 = 父 + 1 */
|
||||
@@ -321,8 +324,14 @@ export type AutomationGrantUpdate = z.infer<typeof AutomationGrantUpdate>;
|
||||
/** POST /agent/runs:建 agent_run + 入队持久任务 runAgent(不在 HTTP 请求里跑模型) */
|
||||
export const AgentRunStartInput = z.object({
|
||||
goal: z.string().trim().min(1).max(8000),
|
||||
mode: AgentRunMode.default("execute"),
|
||||
/** 能力白名单(缺省按模式给全部可用能力);plan_only 下超出 read/preview/propose 的会被拒绝 */
|
||||
/**
|
||||
* 先看方案再执行:模型先调查,用 agent.propose_options 给方案,用户选定前服务端拒绝有副作用的执行类能力。
|
||||
* 缺省关(自动化优先:按步骤清单直接执行,需授权的动作在步骤处审批)。
|
||||
*/
|
||||
proposeFirst: z.boolean().default(false),
|
||||
/** @deprecated 已取消运行模式;plan_only 按 proposeFirst=true 处理,execute 忽略 */
|
||||
mode: AgentRunMode.optional().describe("已弃用:plan_only 等同 proposeFirst=true"),
|
||||
/** 能力白名单(缺省全部可用能力) */
|
||||
capabilityAllowlist: z.array(KnownCapabilityId).min(1).max(50).optional(),
|
||||
});
|
||||
export type AgentRunStartInput = z.infer<typeof AgentRunStartInput>;
|
||||
@@ -330,6 +339,17 @@ export type AgentRunStartInput = z.infer<typeof AgentRunStartInput>;
|
||||
export const AgentRunMessageInput = z.object({ text: z.string().trim().min(1).max(8000) });
|
||||
export type AgentRunMessageInput = z.infer<typeof AgentRunMessageInput>;
|
||||
|
||||
/**
|
||||
* 回答 agent.propose_options:选一个方案(optionId),或都不要(optionId=null,reason 写原因)。
|
||||
* 会话挂起在这个问题上;回答后会话从这里继续。
|
||||
*/
|
||||
export const AgentRunChoiceInput = z.object({
|
||||
decisionId: Id,
|
||||
optionId: z.string().min(1).max(40).nullable(),
|
||||
reason: z.string().trim().max(1000).optional(),
|
||||
});
|
||||
export type AgentRunChoiceInput = z.infer<typeof AgentRunChoiceInput>;
|
||||
|
||||
export const AgentRunAbortInput = z.object({ reason: z.string().max(500).optional() });
|
||||
export type AgentRunAbortInput = z.infer<typeof AgentRunAbortInput>;
|
||||
|
||||
@@ -398,7 +418,6 @@ export const AgentRunNode = z.object({
|
||||
parentRunId: Id.nullable(),
|
||||
goal: z.string(),
|
||||
status: AgentRunStatus,
|
||||
mode: AgentRunMode,
|
||||
depth: NonNegInt,
|
||||
spawnReason: z.string().nullable(),
|
||||
toolCallCount: NonNegInt,
|
||||
|
||||
@@ -1206,7 +1206,7 @@ export const Api = {
|
||||
method: "post",
|
||||
path: "/agent/runs",
|
||||
scope: "organize.execute",
|
||||
summary: "委托目标:建 agent_run 并入队 runAgent(execute | plan_only)",
|
||||
summary: "委托目标:建 agent_run 并入队 runAgent(proposeFirst = 先看方案再执行;mode 已弃用)",
|
||||
body: agent.AgentRunStartInput,
|
||||
response: agent.AgentRun,
|
||||
}),
|
||||
@@ -1219,6 +1219,16 @@ export const Api = {
|
||||
body: agent.AgentRunMessageInput,
|
||||
response: agent.AgentRun,
|
||||
}),
|
||||
// 回答 agent.propose_options(会话挂起等待中):选一个方案或都不要,会话随即继续
|
||||
choose: ep({
|
||||
method: "post",
|
||||
path: "/agent/runs/{id}/choice",
|
||||
scope: "organize.execute",
|
||||
summary: "回答方案选择:选一个方案或都不要(说明原因),挂起的会话随即继续",
|
||||
params: IdParam,
|
||||
body: agent.AgentRunChoiceInput,
|
||||
response: agent.AgentRun,
|
||||
}),
|
||||
abort: ep({
|
||||
method: "post",
|
||||
path: "/agent/runs/{id}/abort",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { 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";
|
||||
@@ -252,6 +253,42 @@ export const AgentAwaitInput = z.object({
|
||||
});
|
||||
export const AgentAwaitOutput = z.object({ results: z.array(SubagentResult), allDone: z.boolean() });
|
||||
|
||||
// ── 会话交互(AG-2):步骤清单 / 方案选择 ──
|
||||
|
||||
export const AgentPlanInput = z.object({
|
||||
/** 新增或更新的步骤(按 id 合并;title 缺省沿用已有步骤的标题) */
|
||||
steps: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().trim().min(1).max(40),
|
||||
title: z.string().trim().min(1).max(200).optional(),
|
||||
/** 缺省:新步骤 pending,已有步骤不变 */
|
||||
status: AgentPlanStepStatus.optional(),
|
||||
note: z.string().max(500).nullable().optional(),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
.max(20),
|
||||
/** true:用这批步骤整体替换原计划 */
|
||||
replace: z.boolean().default(false),
|
||||
});
|
||||
export const AgentPlanOutput = z.object({ steps: z.array(AgentPlanStep), revision: z.number().int().min(1) });
|
||||
|
||||
export const AgentProposeOptionsInput = z.object({
|
||||
question: z.string().trim().min(1).max(500),
|
||||
options: z.array(AgentOptionItem).min(2).max(4),
|
||||
/** 推荐项 id(超时自动选择时用它) */
|
||||
recommendedId: z.string().max(40).optional(),
|
||||
});
|
||||
export const AgentProposeOptionsOutput = z.object({
|
||||
/** chosen = 选了 choice;rejected = 都不要(看 reason);stop = 超时按设置停止,简要汇报后结束 */
|
||||
outcome: z.enum(["chosen", "rejected", "stop"]),
|
||||
choice: z.string().nullable(),
|
||||
option: AgentOptionItem.nullable(),
|
||||
reason: z.string().nullable(),
|
||||
by: z.enum(["user", "timeout"]),
|
||||
});
|
||||
|
||||
// ── 站点能力(W10-pre2):输出只给状态与统计,不含凭证、Cookie、secret 引用、登录名与站点 config ──
|
||||
|
||||
/** 账号摘要:凭证只给"有没有" */
|
||||
@@ -433,6 +470,8 @@ export const CAPABILITY_IO = {
|
||||
"browser.navigate": { input: BrowserNavigateInput, output: BrowserNavigateOutput },
|
||||
"browser.interact": { input: BrowserInteractInput, output: BrowserInteractOutput },
|
||||
"browser.download": { input: BrowserDownloadInput, output: BrowserDownloadOutput },
|
||||
"agent.plan": { input: AgentPlanInput, output: AgentPlanOutput },
|
||||
"agent.propose_options": { input: AgentProposeOptionsInput, output: AgentProposeOptionsOutput },
|
||||
} as const satisfies Record<KnownCapabilityId, { input: z.ZodType; output: z.ZodType }>;
|
||||
|
||||
type CapabilityIo = typeof CAPABILITY_IO;
|
||||
@@ -487,6 +526,10 @@ export type AgentSpawnOutput = z.infer<typeof AgentSpawnOutput>;
|
||||
export type AgentAwaitInput = z.infer<typeof AgentAwaitInput>;
|
||||
export type AgentAwaitOutput = z.infer<typeof AgentAwaitOutput>;
|
||||
export type SubagentResult = z.infer<typeof SubagentResult>;
|
||||
export type AgentPlanInput = z.infer<typeof AgentPlanInput>;
|
||||
export type AgentPlanOutput = z.infer<typeof AgentPlanOutput>;
|
||||
export type AgentProposeOptionsInput = z.infer<typeof AgentProposeOptionsInput>;
|
||||
export type AgentProposeOptionsOutput = z.infer<typeof AgentProposeOptionsOutput>;
|
||||
export type SiteListInput = z.infer<typeof SiteListInput>;
|
||||
export type SiteListOutput = z.infer<typeof SiteListOutput>;
|
||||
export type SiteHealthCheckInput = z.infer<typeof SiteHealthCheckInput>;
|
||||
|
||||
@@ -76,6 +76,8 @@ export const CAPABILITY_IDS = [
|
||||
"browser.navigate",
|
||||
"browser.interact",
|
||||
"browser.download",
|
||||
"agent.plan",
|
||||
"agent.propose_options",
|
||||
] as const;
|
||||
export const KnownCapabilityId = z.enum(CAPABILITY_IDS);
|
||||
export type KnownCapabilityId = z.infer<typeof KnownCapabilityId>;
|
||||
@@ -398,6 +400,31 @@ export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor>
|
||||
false,
|
||||
false,
|
||||
),
|
||||
// AG-2 自动化优先的会话交互:步骤清单与方案选择。只在 Agent 会话里可用(HTTP / MCP 调用直接报错)
|
||||
"agent.plan": d(
|
||||
"agent.plan",
|
||||
"更新步骤清单",
|
||||
"开始时列出 3–7 个步骤,之后每开始、完成、跳过或失败一步就更新(只传变化的步骤,按 id 合并;replace=true 整体替换)。步骤清单实时显示在会话页顶部",
|
||||
"read",
|
||||
"AgentPlan",
|
||||
"read",
|
||||
"low",
|
||||
true,
|
||||
false,
|
||||
),
|
||||
// 类别 propose:只登记一个待用户选择的待处理事项,不产生外部副作用;等待期间会话挂起(不占 worker、不调用模型)。
|
||||
// 与 agent.spawn 一样在注册时声明 replay=safe(恢复后重放本调用取用户的选择)
|
||||
"agent.propose_options": d(
|
||||
"agent.propose_options",
|
||||
"给出方案",
|
||||
"有明显取舍时给用户 2–3 个方案(各写摘要、风险、估算代价,标出推荐项),等用户选定后返回所选方案;用户也可以都不要并说明原因。超时按设置自动选推荐项或停止",
|
||||
"propose",
|
||||
"AgentProposeOptions",
|
||||
"organize.execute",
|
||||
"low",
|
||||
true,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
function d(
|
||||
|
||||
@@ -137,6 +137,20 @@ export const AgentRuntimeSettings = z.object({
|
||||
.number()
|
||||
.min(0)
|
||||
.max(24 * 30),
|
||||
/** agent.propose_options 等用户选择的最长时间(秒);超时按 optionsTimeoutAction 处理 */
|
||||
optionsTimeoutSec: z
|
||||
.number()
|
||||
.int()
|
||||
.min(5)
|
||||
.max(7 * 86400),
|
||||
/** 方案选择超时:recommended = 自动选推荐项(没有推荐项时选第一个);stop = 不选,模型收尾 */
|
||||
optionsTimeoutAction: z.enum(["recommended", "stop"]),
|
||||
/** 会话内审批卡的有效期(秒);过期未批准的调用按拒绝处理 */
|
||||
approvalTimeoutSec: z
|
||||
.number()
|
||||
.int()
|
||||
.min(60)
|
||||
.max(30 * 86400),
|
||||
/** fake 模式脚本(JSON,FakeScript 的可序列化子集);null 用内置演示脚本 */
|
||||
fakeScript: z.string().max(100_000).nullable(),
|
||||
/**
|
||||
@@ -350,6 +364,9 @@ export const DEFAULT_SETTINGS: GlobalSettings = {
|
||||
investigateMaxToolCalls: 8,
|
||||
goalPollSec: 60,
|
||||
goalWaitHours: 72,
|
||||
optionsTimeoutSec: 1800,
|
||||
optionsTimeoutAction: "recommended",
|
||||
approvalTimeoutSec: 86400,
|
||||
fakeScript: null,
|
||||
sessionDir: null,
|
||||
},
|
||||
@@ -607,6 +624,9 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
|
||||
"agent.investigateMaxToolCalls",
|
||||
"agent.goalPollSec",
|
||||
"agent.goalWaitHours",
|
||||
"agent.optionsTimeoutSec",
|
||||
"agent.optionsTimeoutAction",
|
||||
"agent.approvalTimeoutSec",
|
||||
"agent.fakeScript",
|
||||
"agent.sessionDir",
|
||||
],
|
||||
|
||||
@@ -40,10 +40,57 @@ const samples: C.AgentRunEventBody[] = [
|
||||
summary: "{}",
|
||||
},
|
||||
{ type: "subagent_completed", childRunId: RUN, status: "succeeded", summary: "ok", usage, toolCalls: 1 },
|
||||
{
|
||||
type: "plan_updated",
|
||||
steps: [
|
||||
{ id: "s1", title: "查库存", status: "done", note: null },
|
||||
{ id: "s2", title: "搜资源", status: "running", note: "只查已开启站点" },
|
||||
],
|
||||
revision: 2,
|
||||
},
|
||||
{
|
||||
type: "options_proposed",
|
||||
invocationId: "inv-1",
|
||||
decisionId: RUN,
|
||||
question: "收哪一个版本?",
|
||||
options: [
|
||||
{ id: "a", title: "REMUX", summary: "画质最好", risks: ["体积大"], estimatedCost: "78 GB" },
|
||||
{ id: "b", title: "WEB-DL", summary: "体积小", risks: [], estimatedCost: null },
|
||||
],
|
||||
recommendedId: "a",
|
||||
stepId: "s2",
|
||||
expiresAt: AT,
|
||||
timeoutAction: "recommended",
|
||||
},
|
||||
{
|
||||
type: "options_resolved",
|
||||
invocationId: "inv-1",
|
||||
decisionId: RUN,
|
||||
outcome: "chosen",
|
||||
choice: "a",
|
||||
reason: null,
|
||||
by: "user",
|
||||
},
|
||||
{
|
||||
type: "approval_requested",
|
||||
approvalId: RUN,
|
||||
decisionId: null,
|
||||
invocationId: "inv-2",
|
||||
capabilityId: "download.submit",
|
||||
planType: "acquisition_plan",
|
||||
planId: RUN,
|
||||
payloadHash: "h",
|
||||
expectedRevision: 1,
|
||||
riskLevel: "high",
|
||||
summary: "下载《沙丘2》",
|
||||
stepId: "s3",
|
||||
expiresAt: AT,
|
||||
},
|
||||
{ type: "approval_resolved", approvalId: RUN, status: "rejected", reason: "太大", decidedBy: "user:1" },
|
||||
];
|
||||
|
||||
describe("agent 运行事件", () => {
|
||||
it("样例覆盖全部 12 种事件类型", () => {
|
||||
it("样例覆盖全部 17 种事件类型", () => {
|
||||
expect(samples.map((s) => s.type).sort()).toEqual([...C.AgentRunEventType.options].sort());
|
||||
});
|
||||
|
||||
@@ -81,6 +128,11 @@ describe("agent 运行事件", () => {
|
||||
"agent.tool_call:started",
|
||||
"agent.tool_call:started",
|
||||
"agent.tool_call:succeeded",
|
||||
"agent.tool_call:succeeded",
|
||||
"agent.tool_call:started",
|
||||
"agent.tool_call:succeeded",
|
||||
"agent.tool_call:started",
|
||||
"agent.tool_call:denied",
|
||||
]);
|
||||
for (const b of bus) if (b) expect(C.parseEventPayload(b.type, b.payload), b.type).not.toBeNull();
|
||||
});
|
||||
|
||||
@@ -39,8 +39,9 @@ type Obj = z.ZodObject;
|
||||
const ROWS: Record<string, { row: Obj; dbOnly?: string[]; apiOnly?: string[] }> = {
|
||||
AcquisitionPlan: { row: C.AcquisitionPlan },
|
||||
AcquisitionSlot: { row: C.AcquisitionSlot, apiOnly: ["currentAsset"] },
|
||||
// upstream_id 只在服务端使用;await_wake 是挂起等待子会话时的唤醒信号(C1-B),都不进 AgentRun 视图
|
||||
AgentRun: { row: C.AgentRun, dbOnly: ["upstreamId", "awaitWake"] },
|
||||
// upstream_id 只在服务端使用;await_wake 是挂起等待子会话 / 用户选择 / 审批时的唤醒信号(C1-B);
|
||||
// mode 是内部能力上限(AG-2 取消运行模式后不再出 API):都不进 AgentRun 视图
|
||||
AgentRun: { row: C.AgentRun, dbOnly: ["upstreamId", "awaitWake", "mode"] },
|
||||
AgentRunEvent: { row: C.AgentRunEventRow },
|
||||
AgentRunMessage: { row: C.AgentRunMessage },
|
||||
CapabilityInvocation: { row: C.CapabilityInvocation },
|
||||
|
||||
@@ -103,6 +103,11 @@ const OVERRIDES: Record<string, (v: Record<string, unknown>) => Record<string, u
|
||||
ReleaseIdentityAssign: (v) => ({ ...v, external: undefined, candidateIndex: undefined }),
|
||||
CacheSettings: (v) => ({ ...v, hardThresholdPercent: 90, resumePercent: 85, cleanupTargetPercent: 80 }),
|
||||
GlobalSettings: () => C.DEFAULT_SETTINGS,
|
||||
// 方案至少 2 个(id 不同)
|
||||
AgentProposeOptionsInput: (v) => {
|
||||
const o = (v.options as Record<string, unknown>[])[0]!;
|
||||
return { ...v, options: [o, { ...o, id: "b" }] };
|
||||
},
|
||||
EngineStats: (v) => ({ ...v, byStatus: { pending: 1 } }),
|
||||
TypedEventEnvelope: () => ({
|
||||
seq: 1,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { execDdl, oneOf } from "../src/migration-helpers.ts";
|
||||
|
||||
// AG-2 Agent 交互改为自动化优先:取消 plan_only / execute 运行模式(agent_run.mode 列保留为内部能力上限),
|
||||
// agent_run_event 增加会话交互事件:
|
||||
// plan_updated(agent.plan 步骤清单)、options_proposed / options_resolved(agent.propose_options 方案选择)、
|
||||
// approval_requested / approval_resolved(需审批的能力在步骤处的审批卡)。
|
||||
|
||||
const OLD_TYPES = [
|
||||
"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",
|
||||
];
|
||||
const ADDED = ["plan_updated", "options_proposed", "options_resolved", "approval_requested", "approval_resolved"];
|
||||
|
||||
const swap = (values: readonly string[]) => `
|
||||
alter table agent_run_event drop constraint agent_run_event_type_check;
|
||||
alter table agent_run_event add constraint agent_run_event_type_check ${oneOf("type", values)};
|
||||
`;
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(db, swap([...OLD_TYPES, ...ADDED]));
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await execDdl(
|
||||
db,
|
||||
`
|
||||
delete from agent_run_event where type in (${ADDED.map((t) => `'${t}'`).join(", ")});
|
||||
${swap(OLD_TYPES)}`,
|
||||
);
|
||||
}
|
||||
@@ -50,7 +50,12 @@ export type AgentRunEventType =
|
||||
| "run_failed"
|
||||
| "subagent_spawned"
|
||||
| "subagent_event"
|
||||
| "subagent_completed";
|
||||
| "subagent_completed"
|
||||
| "plan_updated"
|
||||
| "options_proposed"
|
||||
| "options_resolved"
|
||||
| "approval_requested"
|
||||
| "approval_resolved";
|
||||
export type AgentRunMessageStatus = "pending" | "consumed";
|
||||
export type ApprovalStatus = "pending" | "approved" | "rejected" | "revoked" | "expired" | "consumed";
|
||||
export type ArtifactKind = "evidence" | "plan" | "proposal" | "report" | "snapshot" | "log";
|
||||
|
||||
Reference in new issue
Block a user