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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user