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:
rootandClaude Opus 5.5 committed 2026-09-27 19:15:49 +08:00
1 parent 8f59bf3772
commit db5352eecd
18 files changed
+553 -293

No files matched your search

+44 -7
View File
@@ -1,10 +1,10 @@
import { spawn } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { AgentRunEvent } from "@umt/contracts";
import { afterEach, describe, expect, it } from "vitest";
import {
type AgentEvent,
type AgentRun,
type AgentRunHandle,
type AgentSettings,
checkToolCall,
createFakeRuntime,
@@ -13,7 +13,7 @@ import {
import { crashScript, RUN_ID } from "./fixtures/crash-script.ts";
import { type Fixture, makeFixture, settings, tempDir, types } from "./helpers.ts";
const opened: AgentRun[] = [];
const opened: AgentRunHandle[] = [];
afterEach(async () => {
await Promise.all(opened.splice(0).map((r) => r.close()));
});
@@ -42,9 +42,20 @@ const dune: FakeScript = {
async function start(
fx: Fixture,
scripts: FakeScript[],
opts: { capabilities?: string[]; settings?: Partial<AgentSettings>; runId?: string; sessionDir?: string } = {},
opts: {
capabilities?: string[];
settings?: Partial<AgentSettings>;
runId?: string;
sessionDir?: string;
firstSeq?: number;
onEvent?: (e: AgentRunEvent) => void | Promise<void>;
} = {},
) {
const { runtime, fake } = createFakeRuntime({ registry: fx.registry, scripts });
const { runtime, fake } = createFakeRuntime({
registry: fx.registry,
scripts,
...(opts.onEvent ? { onEvent: opts.onEvent } : {}),
});
const sessionDir = opts.sessionDir ?? (await tempDir());
const run = await runtime.createRun({
runId: opts.runId ?? "run-1",
@@ -54,6 +65,7 @@ async function start(
settings: settings(opts.settings),
sessionDir,
actor: { actorId: "user:1", taskId: "task:1", scopeIds: ["media:dune"] },
...(opts.firstSeq !== undefined ? { firstSeq: opts.firstSeq } : {}),
});
opened.push(run);
return { run, fake, sessionDir };
@@ -106,12 +118,12 @@ describe("PiAgentRuntime + FakeModelProvider", () => {
it("events() 可按 seq 回放并在 close 后结束", async () => {
const fx = makeFixture();
const { run } = await start(fx, [{ match: "Dune", steps: [], finalText: "库里已有。" }]);
const collected: AgentEvent[] = [];
const collected: AgentRunEvent[] = [];
const consumer = (async () => {
for await (const e of run.events()) collected.push(e);
})();
await run.prompt();
const tail: AgentEvent[] = [];
const tail: AgentRunEvent[] = [];
const replay = (async () => {
for await (const e of run.events(1)) tail.push(e);
})();
@@ -122,6 +134,31 @@ describe("PiAgentRuntime + FakeModelProvider", () => {
expect(collected.at(-1)?.type).toBe("run_completed");
});
it("onEvent 按 seq 串行投递 contracts AgentRunEvent;firstSeq 续接序号,close 等待投递完成", async () => {
const fx = makeFixture();
const delivered: AgentRunEvent[] = [];
const { run } = await start(fx, [{ match: "Dune", steps: [], finalText: "库里已有。" }], {
firstSeq: 10,
// 故意让前面的事件落库更慢,验证不会乱序
onEvent: async (e) => {
await new Promise((r) => setTimeout(r, e.seq === 10 ? 20 : 0));
delivered.push(e);
},
});
await run.prompt();
const tail: AgentRunEvent[] = [];
const replay = (async () => {
for await (const e of run.events(11)) tail.push(e);
})();
await run.close();
await replay;
const seqs = run.history().map((e) => e.seq);
expect(seqs[0]).toBe(10);
expect(delivered.map((e) => e.seq)).toEqual(seqs);
expect(tail[0]?.seq).toBe(11);
for (const e of delivered) expect(AgentRunEvent.safeParse(e).success, e.type).toBe(true);
});
it("白名单:未注册工具(bash)与未列入白名单的已注册能力都被拒绝并产生事件,不执行", async () => {
const fx = makeFixture();
fx.grants.set("user:1", new Set(["download.submit"]));