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

+34 -3
View File
@@ -1,3 +1,4 @@
import { CAPABILITY_IDS, capabilitySpec } from "@umt/contracts";
import { describe, expect, it } from "vitest";
import { z } from "zod";
import {
@@ -32,6 +33,7 @@ describe("CapabilityRegistry.register", () => {
description: "说明",
inputSchema: z.object({}),
outputSchema: z.object({}),
kind: "read" as const,
scope: "read" as const,
riskLevel: "low" as const,
idempotent: true,
@@ -44,13 +46,41 @@ describe("CapabilityRegistry.register", () => {
expect(() => r.register({ ...base, id: "bad id" }, () => ({}))).toThrow(/域.动作/);
r.register({ ...base, id: "catalog.lookup" }, () => ({}));
expect(() => r.register({ ...base, id: "catalog.lookup" }, () => ({}))).toThrow(/已注册/);
expect(() => r.register({ ...base, id: "cache.cleanup", scope: "execute", idempotent: false }, () => ({}))).toThrow(
expect(() => r.register({ ...base, id: "cache.cleanup", kind: "execute", idempotent: false }, () => ({}))).toThrow(
/replay/,
);
expect(() => r.resolve(["catalog.lookup", "nope.nope"])).toThrow(/未注册/);
expect(r.get("catalog.lookup")?.toolName).toBe("catalog__lookup");
});
it("首批 14 个能力可直接用 contracts 的 capabilitySpec 注册", () => {
const r = new CapabilityRegistry();
for (const id of CAPABILITY_IDS) r.register(capabilitySpec(id), () => ({}) as never);
expect(r.list().map((c) => c.descriptor.id)).toEqual([...CAPABILITY_IDS]);
expect(r.inputJsonSchema("release.search").type).toBe("object");
});
it("approve 类能力同样必须经授权回调,且生成幂等键", async () => {
const seen: string[] = [];
const r = new CapabilityRegistry({
authorize: ({ descriptor }) => {
seen.push(descriptor.id);
return { allowed: true, approvalId: "ap-1" };
},
});
r.register({ ...base, id: "approval.decide", kind: "approve", replay: "never" }, (_i, c) => {
expect(c.idempotencyKey).toMatch(/^approval\.decide:v1:/);
return {};
});
await r.invoke("approval.decide", {}, ctx());
expect(seen).toEqual(["approval.decide"]);
await expect(
new CapabilityRegistry()
.register({ ...base, id: "approval.decide", kind: "approve" }, () => ({}))
.invoke("approval.decide", {}, ctx()),
).rejects.toMatchObject({ code: "unauthorized" });
});
it("输入 JSON Schema 可给模型使用(无 $schema)", () => {
const { registry } = makeFixture();
const schema = registry.inputJsonSchema("download.submit");
@@ -97,7 +127,7 @@ describe("CapabilityRegistry.invoke", () => {
expect(fx.audits.at(-1)).toMatchObject({ outcome: "ok", grantId: "grant:download.submit" });
});
it("未注入授权回调:read 放行、execute 一律拒绝;allowedScopes 限制作用域", async () => {
it("未注入授权回调:read 放行、execute 一律拒绝;allowedKinds 限制能力类别", async () => {
const r = new CapabilityRegistry();
const fx = makeFixture();
const inv = fx.registry.get("inventory.query")!;
@@ -113,7 +143,7 @@ describe("CapabilityRegistry.invoke", () => {
fx.registry.invoke(
"download.submit",
{ title: "x", quality: "720p" },
ctx({ allowedScopes: ["read", "preview"] }),
ctx({ allowedKinds: ["read", "preview"] }),
),
).rejects.toMatchObject({ code: "unauthorized" });
expect(fx.submitted).toHaveLength(0);
@@ -158,6 +188,7 @@ describe("CapabilityRegistry.invoke", () => {
description: "说明",
inputSchema: z.object({}),
outputSchema: z.object({ ok: z.boolean() }),
kind: "read" as const,
scope: "read" as const,
riskLevel: "low" as const,
idempotent: true,