- 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]>
140 lines
4.5 KiB
TypeScript
140 lines
4.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import * as C from "../src/index.ts";
|
|
|
|
const RUN = "7f0f5d64-6a39-4a4e-9a86-0d3a2f1e8b11";
|
|
const AT = "2026-09-27T00:00:00.000Z";
|
|
const usage = { inputTokens: 1, outputTokens: 2, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 3, costUsd: 0 };
|
|
|
|
const samples: C.AgentRunEventBody[] = [
|
|
{ type: "run_started", goal: "g", resumed: false },
|
|
{ type: "text_delta", delta: "hi" },
|
|
{ type: "thinking_delta", delta: "..." },
|
|
{ type: "tool_call_start", toolCallId: "t1", capabilityId: "inventory.query", inputSummary: "{}", recovery: false },
|
|
{
|
|
type: "tool_call_end",
|
|
toolCallId: "t1",
|
|
capabilityId: "inventory.query",
|
|
isError: true,
|
|
outputSummary: "x",
|
|
recovery: false,
|
|
},
|
|
{ type: "tool_call_rejected", toolCallId: "t2", toolName: "bash", reason: "不在白名单" },
|
|
{ type: "compaction", phase: "end", reason: "threshold", status: "completed" },
|
|
{ type: "run_completed", finalText: "done", usage },
|
|
{ type: "run_failed", reason: "budget", message: "超预算", usage, budget: "tokens" },
|
|
{
|
|
type: "subagent_spawned",
|
|
childRunId: RUN,
|
|
goal: "子目标",
|
|
mode: "plan_only",
|
|
depth: 1,
|
|
capabilities: ["inventory.query"],
|
|
budget: { maxToolCalls: 5 },
|
|
},
|
|
{
|
|
type: "subagent_event",
|
|
childRunId: RUN,
|
|
childSeq: 3,
|
|
childType: "tool_call_end",
|
|
capabilityId: "inventory.query",
|
|
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("样例覆盖全部 17 种事件类型", () => {
|
|
expect(samples.map((s) => s.type).sort()).toEqual([...C.AgentRunEventType.options].sort());
|
|
});
|
|
|
|
it("行往返无损,且行 schema 合法", () => {
|
|
samples.forEach((body, seq) => {
|
|
const e = { ...body, runId: RUN, seq, at: AT } as C.AgentRunEvent;
|
|
expect(C.AgentRunEvent.safeParse(e).success, body.type).toBe(true);
|
|
const row = C.agentRunEventToRow(e);
|
|
expect(C.AgentRunEventRow.safeParse(row).success, body.type).toBe(true);
|
|
expect(row.payload).not.toHaveProperty("type");
|
|
expect(C.agentRunEventFromRow(row)).toEqual(e);
|
|
});
|
|
});
|
|
|
|
it("映射到全局 SSE 事件:tool phase / run status,且 payload 合法", () => {
|
|
const bus = samples.map((body, seq) =>
|
|
C.agentRunEventToBusEvent({ ...body, runId: RUN, seq, at: AT } as C.AgentRunEvent),
|
|
);
|
|
expect(
|
|
bus.map((b) =>
|
|
b
|
|
? `${b.type}:${"phase" in b.payload ? b.payload.phase : "status" in b.payload ? b.payload.status : ""}`
|
|
: null,
|
|
),
|
|
).toEqual([
|
|
"agent.run_status:running",
|
|
"agent.text_delta:",
|
|
null,
|
|
"agent.tool_call:started",
|
|
"agent.tool_call:failed",
|
|
"agent.tool_call:denied",
|
|
null,
|
|
"agent.run_status:succeeded",
|
|
"agent.run_status:budget_exceeded",
|
|
"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();
|
|
});
|
|
});
|