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

+37 -28
View File
@@ -10,6 +10,7 @@ import {
type RunResult,
} from "@earendil-works/pi-agent-core";
import type { Usage } from "@earendil-works/pi-ai";
import type { AgentBudgetKind, AgentRunEvent, AgentRunEventBody, AgentUsageTotals } from "@umt/contracts";
import type { TSchema } from "typebox";
import { isCapabilityError } from "../capability/errors.ts";
import type { CapabilityRegistry, RegisteredCapability } from "../capability/registry.ts";
@@ -17,12 +18,8 @@ import { toolNameToCapabilityId } from "../capability/tool-name.ts";
import { redactForModel, redactString, summarizeForLog } from "../redact.ts";
import { EventChannel } from "../runtime/event-channel.ts";
import {
type AgentEvent,
type AgentEventBody,
type AgentRun,
type AgentRunHandle,
type AgentRuntimePort,
type AgentUsage,
type BudgetKind,
type CreateRunSpec,
emptyUsage,
type RunOutcome,
@@ -35,8 +32,13 @@ export interface PiAgentRuntimeOptions {
registry: CapabilityRegistry;
/** 默认按设置构造真实上游;测试用 FakeModelProvider 覆盖 */
resolveModel?: PiModelResolver;
/** 每个事件产生时回调(server 用它写 agent_run_event 并推 SSE) */
onEvent?: (event: AgentEvent) => void | Promise<void>;
/**
* 每个事件产生时回调(server 用它写 agent_run_event 再推 SSE)。
* 同一 run 内按 seq 顺序串行调用(前一个 Promise 结束才调下一个);回调抛错只记入 onEventError,不影响运行。
* close() 会等待已排队的回调全部完成。
*/
onEvent?: (event: AgentRunEvent) => void | Promise<void>;
onEventError?: (error: unknown, event: AgentRunEvent) => void;
now?: () => Date;
}
@@ -50,7 +52,7 @@ const SUMMARY_LIMIT = 600;
export class PiAgentRuntime implements AgentRuntimePort {
constructor(private readonly options: PiAgentRuntimeOptions) {}
async createRun(spec: CreateRunSpec): Promise<AgentRun> {
async createRun(spec: CreateRunSpec): Promise<AgentRunHandle> {
const ctx = BACKGROUND_CONTEXT;
const settings = resolveAgentSettings(spec.settings);
const capabilities = this.options.registry.resolve(spec.capabilities);
@@ -86,22 +88,24 @@ export class PiAgentRuntime implements AgentRuntimePort {
}
}
type BudgetState = { kind: BudgetKind; message: string };
type BudgetState = { kind: AgentBudgetKind; message: string };
class PiAgentRun implements AgentRun {
class PiAgentRun implements AgentRunHandle {
readonly runId: string;
readonly sessionPath: string;
pendingOperations = 0;
private readonly channel = new EventChannel<AgentEvent>();
private readonly channel = new EventChannel<AgentRunEvent>();
private readonly allowed = new Map<string, string>();
private readonly rejected = new Set<string>();
private readonly abortController = new AbortController();
private harness!: AgentHarnessInstance<object>;
private lane!: AgentLane;
private seq = 0;
private readonly firstSeq: number;
private seq: number;
private delivery: Promise<void> = Promise.resolve();
private toolCalls = 0;
private usage: AgentUsage = emptyUsage();
private usage: AgentUsageTotals = emptyUsage();
private finalText = "";
private budget: BudgetState | undefined;
private userAbort: string | undefined;
@@ -119,6 +123,8 @@ class PiAgentRun implements AgentRun {
) {
this.runId = spec.runId;
this.sessionPath = sessionPath;
this.firstSeq = spec.firstSeq ?? 0;
this.seq = this.firstSeq;
for (const c of capabilities) this.allowed.set(c.toolName, c.descriptor.id);
}
@@ -130,10 +136,10 @@ class PiAgentRun implements AgentRun {
return {
name: entry.toolName,
label: descriptor.title,
description: `${descriptor.description}\n[能力 ${descriptor.id} v${descriptor.version};作用域 ${descriptor.scope};风险 ${descriptor.riskLevel}]`,
description: `${descriptor.description}\n[能力 ${descriptor.id} v${descriptor.version};类别 ${descriptor.kind};风险 ${descriptor.riskLevel}]`,
parameters: registry.inputJsonSchema(descriptor.id) as unknown as TSchema,
replay: descriptor.replay,
...(descriptor.scope === "execute" ? { executionMode: "sequential" as const } : {}),
...(descriptor.kind === "execute" ? { executionMode: "sequential" as const } : {}),
execute: async (toolCallId, params, _onUpdate, _toolCtx, invocation, context) => {
const signal = context.abortSignal ?? this.abortController.signal;
try {
@@ -141,7 +147,7 @@ class PiAgentRun implements AgentRun {
actorId: actor.actorId,
taskId: actor.taskId,
scopeIds: actor.scopeIds,
...(actor.allowedScopes ? { allowedScopes: actor.allowedScopes } : {}),
...(actor.allowedKinds ? { allowedKinds: actor.allowedKinds } : {}),
signal,
source: "agent",
invocationId: invocation.invocationId ?? toolCallId,
@@ -251,11 +257,11 @@ class PiAgentRun implements AgentRun {
);
}
events(fromSeq = 0): AsyncIterable<AgentEvent> {
return this.channel.iterate(fromSeq);
events(fromSeq = this.firstSeq): AsyncIterable<AgentRunEvent> {
return this.channel.iterate(fromSeq - this.firstSeq);
}
history(): AgentEvent[] {
history(): AgentRunEvent[] {
return this.channel.snapshot();
}
@@ -291,6 +297,7 @@ class PiAgentRun implements AgentRun {
await this.harness?.close(ctx).catch(() => undefined);
await this.repo.close(ctx).catch(() => undefined);
this.channel.close();
await this.delivery;
}
// ---- internals ----
@@ -341,7 +348,7 @@ class PiAgentRun implements AgentRun {
return outcome;
}
private failed(reason: RunOutcome["reason"] & string, message: string, budget?: BudgetKind): RunOutcome {
private failed(reason: RunOutcome["reason"] & string, message: string, budget?: AgentBudgetKind): RunOutcome {
return {
status: "failed",
reason,
@@ -359,7 +366,7 @@ class PiAgentRun implements AgentRun {
this.emit({ type: "tool_call_rejected", toolCallId, toolName, ...(capabilityId ? { capabilityId } : {}), reason });
}
private exceed(kind: BudgetKind, message: string): void {
private exceed(kind: AgentBudgetKind, message: string): void {
if (this.budget) return;
this.budget = { kind, message };
this.scheduleAbort();
@@ -392,13 +399,15 @@ class PiAgentRun implements AgentRun {
this.deadlineTimer = undefined;
}
private emit(body: AgentEventBody): void {
const event = { ...body, runId: this.runId, seq: this.seq++, at: new Date().toISOString() } as AgentEvent;
private emit(body: AgentRunEventBody): void {
const event = { ...body, runId: this.runId, seq: this.seq++, at: new Date().toISOString() } as AgentRunEvent;
this.channel.push(event);
if (this.options.onEvent) {
Promise.resolve()
.then(() => this.options.onEvent?.(event))
.catch(() => undefined);
const onEvent = this.options.onEvent;
if (onEvent) {
// 串行投递:落库顺序与 seq 一致
this.delivery = this.delivery
.then(() => onEvent(event))
.catch((error) => this.options.onEventError?.(error, event));
}
}
}
@@ -427,7 +436,7 @@ function assistantText(message: AgentMessage): string | undefined {
.join("");
}
function toAgentUsage(u: Usage): AgentUsage {
function toAgentUsage(u: Usage): AgentUsageTotals {
return {
inputTokens: u.input,
outputTokens: u.output,