refactor(contracts,agent): single capability spec and agent run event definitions in contracts
B3: CapabilityDescriptor gains replay; CapabilitySpec + capabilitySpec(id) assemble CAPABILITY_CATALOG + CAPABILITY_IO; drop CapabilityDefinition/CapabilityRisk. agent registry registers contracts CapabilitySpec (kind/allowedKinds instead of its own scope enum; approve also requires authorize). AgentRun port renamed AgentRunHandle. B9: contracts AgentRunEvent/AgentRunEventBody/AgentRunEventRow/AgentUsageTotals with row and SSE mapping helpers; agent onEvent receives AgentRunEvent, delivered serially in seq order, firstSeq continues numbering across processes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
8f59bf3772
commit
db5352eecd
18 files changed
+553
-293
No files matched your search
@@ -0,0 +1,66 @@
|
||||
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" },
|
||||
];
|
||||
|
||||
describe("agent 运行事件", () => {
|
||||
it("样例覆盖全部 9 种事件类型", () => {
|
||||
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",
|
||||
]);
|
||||
for (const b of bus) if (b) expect(C.parseEventPayload(b.type, b.payload), b.type).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -40,6 +40,7 @@ const ROWS: Record<string, { row: Obj; dbOnly?: string[]; apiOnly?: string[] }>
|
||||
AcquisitionPlan: { row: C.AcquisitionPlan },
|
||||
AcquisitionSlot: { row: C.AcquisitionSlot },
|
||||
AgentRun: { row: C.AgentRun },
|
||||
AgentRunEvent: { row: C.AgentRunEventRow },
|
||||
ApiKey: { row: C.ApiKey, dbOnly: ["keyHash"] },
|
||||
Approval: { row: C.Approval },
|
||||
Artifact: { row: C.Artifact },
|
||||
@@ -80,6 +81,10 @@ const ROWS: Record<string, { row: Obj; dbOnly?: string[]; apiOnly?: string[] }>
|
||||
TitleAlias: { row: C.TitleAlias },
|
||||
};
|
||||
|
||||
/** 引擎自有表(@umt/tasks 内部使用,不经 API 暴露),不要求 Row schema */
|
||||
const INTERNAL_TABLES = ["TasksRunMeta", "TasksFact", "TasksOpLedger"];
|
||||
const INTERNAL_ENUMS = ["TasksOpLedgerStatus"];
|
||||
|
||||
/** DB 枚举类型名 → contracts 枚举 */
|
||||
const ENUMS: Record<string, z.ZodEnum> = {
|
||||
AcquisitionPlanPurpose: C.PlanPurpose,
|
||||
@@ -88,6 +93,7 @@ const ENUMS: Record<string, z.ZodEnum> = {
|
||||
AcquisitionSlotStatus: C.SlotStatus,
|
||||
AgentRunTrigger: C.AgentTrigger,
|
||||
AgentRunStatus: C.AgentRunStatus,
|
||||
AgentRunEventType: C.AgentRunEventType,
|
||||
ApprovalStatus: C.ApprovalStatus,
|
||||
ArtifactKind: C.ArtifactKind,
|
||||
AssetLocationLocationKind: C.LocationKind,
|
||||
@@ -177,7 +183,7 @@ describe("contracts ⇔ db 表结构同步", () => {
|
||||
const tables = tableColumns();
|
||||
|
||||
it("每张表都有 Row schema", () => {
|
||||
expect([...tables.keys()].sort()).toEqual(Object.keys(ROWS).sort());
|
||||
expect([...tables.keys()].filter((t) => !INTERNAL_TABLES.includes(t)).sort()).toEqual(Object.keys(ROWS).sort());
|
||||
});
|
||||
|
||||
for (const [table, spec] of Object.entries(ROWS)) {
|
||||
@@ -190,6 +196,7 @@ describe("contracts ⇔ db 表结构同步", () => {
|
||||
|
||||
it("每个 DB 枚举都有对应 contracts 枚举且取值一致", () => {
|
||||
const aliases = enumAliases();
|
||||
for (const internal of INTERNAL_ENUMS) aliases.delete(internal);
|
||||
expect([...aliases.keys()].sort()).toEqual(Object.keys(ENUMS).sort());
|
||||
for (const [name, values] of aliases) {
|
||||
expect(ENUMS[name]!.options, name).toEqual(values);
|
||||
|
||||
@@ -184,6 +184,18 @@ describe("关键合同行为", () => {
|
||||
expect(isSchema(exported[cap.inputSchema]), cap.inputSchema).toBe(true);
|
||||
expect(isSchema(exported[cap.outputSchema]), cap.outputSchema).toBe(true);
|
||||
expect(exported[cap.inputSchema]).toBe(C.CAPABILITY_IO[cap.id as C.KnownCapabilityId].input);
|
||||
expect(exported[cap.outputSchema]).toBe(C.CAPABILITY_IO[cap.id as C.KnownCapabilityId].output);
|
||||
}
|
||||
});
|
||||
|
||||
it("capabilitySpec 组装目录元数据与真实 schema;有副作用的类别不可重放", () => {
|
||||
for (const id of C.CAPABILITY_IDS) {
|
||||
const spec = C.capabilitySpec(id);
|
||||
const { inputSchema: _i, outputSchema: _o, ...meta } = C.CAPABILITY_CATALOG[id];
|
||||
expect(spec).toMatchObject(meta);
|
||||
expect(spec.inputSchema).toBe(C.CAPABILITY_IO[id].input);
|
||||
expect(spec.outputSchema).toBe(C.CAPABILITY_IO[id].output);
|
||||
if (spec.kind !== "read" && spec.kind !== "preview") expect(spec.replay, id).toBe("never");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user