diff --git a/apps/server/test/sources-identity/site-capabilities.test.ts b/apps/server/test/sources-identity/site-capabilities.test.ts index cceffd2f..cad50241 100644 --- a/apps/server/test/sources-identity/site-capabilities.test.ts +++ b/apps/server/test/sources-identity/site-capabilities.test.ts @@ -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" }); diff --git a/packages/contracts/src/agent-events.ts b/packages/contracts/src/agent-events.ts index 23ae6ca1..fed94112 100644 --- a/packages/contracts/src/agent-events.ts +++ b/packages/contracts/src/agent-events.ts @@ -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; @@ -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; +/** 步骤清单的一步;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; + +/** 给用户选的一个方案 */ +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; + +/** 超时未选时的处理:recommended = 自动选推荐项;stop = 不选,模型收尾 */ +export const AgentOptionsTimeoutAction = z.enum(["recommended", "stop"]); +export type AgentOptionsTimeoutAction = z.infer; + +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; @@ -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; + } +} diff --git a/packages/contracts/src/agent.ts b/packages/contracts/src/agent.ts index cda44d80..801382dc 100644 --- a/packages/contracts/src/agent.ts +++ b/packages/contracts/src/agent.ts @@ -17,7 +17,11 @@ export const AgentRunStatus = z.enum([ "budget_exceeded", ]); export type AgentRunStatus = z.infer; -/** 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; @@ -65,7 +69,6 @@ export const AgentRun = EntityBase.extend({ startedAt: Timestamp.nullable(), finishedAt: Timestamp.nullable(), createdBy: z.string().nullable(), - mode: AgentRunMode, /** 后台触发按根因去重(如 investigate:);用户委托为 null */ dedupeKey: z.string().nullable(), /** 派生深度:用户/后台发起的会话为 0,agent.spawn 派生的子会话 = 父 + 1 */ @@ -321,8 +324,14 @@ export type AutomationGrantUpdate = z.infer; /** 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; @@ -330,6 +339,17 @@ export type AgentRunStartInput = z.infer; export const AgentRunMessageInput = z.object({ text: z.string().trim().min(1).max(8000) }); export type AgentRunMessageInput = z.infer; +/** + * 回答 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; + export const AgentRunAbortInput = z.object({ reason: z.string().max(500).optional() }); export type AgentRunAbortInput = z.infer; @@ -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, diff --git a/packages/contracts/src/api.ts b/packages/contracts/src/api.ts index e016ec78..f16e6476 100644 --- a/packages/contracts/src/api.ts +++ b/packages/contracts/src/api.ts @@ -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", diff --git a/packages/contracts/src/capabilities-io.ts b/packages/contracts/src/capabilities-io.ts index 09611b76..f4f1742e 100644 --- a/packages/contracts/src/capabilities-io.ts +++ b/packages/contracts/src/capabilities-io.ts @@ -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; type CapabilityIo = typeof CAPABILITY_IO; @@ -487,6 +526,10 @@ export type AgentSpawnOutput = z.infer; export type AgentAwaitInput = z.infer; export type AgentAwaitOutput = z.infer; export type SubagentResult = z.infer; +export type AgentPlanInput = z.infer; +export type AgentPlanOutput = z.infer; +export type AgentProposeOptionsInput = z.infer; +export type AgentProposeOptionsOutput = z.infer; export type SiteListInput = z.infer; export type SiteListOutput = z.infer; export type SiteHealthCheckInput = z.infer; diff --git a/packages/contracts/src/capability.ts b/packages/contracts/src/capability.ts index fa2a48ed..f8004299 100644 --- a/packages/contracts/src/capability.ts +++ b/packages/contracts/src/capability.ts @@ -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; @@ -398,6 +400,31 @@ export const CAPABILITY_CATALOG: Record 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( diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 843d3fe9..19caef7b 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -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", ], diff --git a/packages/contracts/test/agent-events.test.ts b/packages/contracts/test/agent-events.test.ts index b94a06d3..64fa40e2 100644 --- a/packages/contracts/test/agent-events.test.ts +++ b/packages/contracts/test/agent-events.test.ts @@ -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(); }); diff --git a/packages/contracts/test/db-sync.test.ts b/packages/contracts/test/db-sync.test.ts index f6b439b0..324fb3a4 100644 --- a/packages/contracts/test/db-sync.test.ts +++ b/packages/contracts/test/db-sync.test.ts @@ -39,8 +39,9 @@ type Obj = z.ZodObject; const ROWS: Record = { 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 }, diff --git a/packages/contracts/test/schemas.test.ts b/packages/contracts/test/schemas.test.ts index c80b776f..85329146 100644 --- a/packages/contracts/test/schemas.test.ts +++ b/packages/contracts/test/schemas.test.ts @@ -103,6 +103,11 @@ const OVERRIDES: Record) => Record ({ ...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[])[0]!; + return { ...v, options: [o, { ...o, id: "b" }] }; + }, EngineStats: (v) => ({ ...v, byStatus: { pending: 1 } }), TypedEventEnvelope: () => ({ seq: 1, diff --git a/packages/db/migrations/0040_agent_interaction.ts b/packages/db/migrations/0040_agent_interaction.ts new file mode 100644 index 00000000..ebcca009 --- /dev/null +++ b/packages/db/migrations/0040_agent_interaction.ts @@ -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): Promise { + await execDdl(db, swap([...OLD_TYPES, ...ADDED])); +} + +// biome-ignore lint/suspicious/noExplicitAny: 同上 +export async function down(db: Kysely): Promise { + await execDdl( + db, + ` +delete from agent_run_event where type in (${ADDED.map((t) => `'${t}'`).join(", ")}); +${swap(OLD_TYPES)}`, + ); +} diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts index cb1ac803..440499e6 100644 --- a/packages/db/src/types.ts +++ b/packages/db/src/types.ts @@ -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";