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

+188
View File
@@ -0,0 +1,188 @@
import { z } from "zod";
import type { AgentRunStatus } from "./agent.ts";
import { Id, JsonObject, NonNegInt, Timestamp } from "./common.ts";
import type { TypedEvent } from "./events.ts";
// Agent 运行事件(REQ §10.6):packages/agent 产生,server 先写 agent_run_event(带 seq)再推 SSE。
// 这里是唯一定义;agent 包的 onEvent 回调直接收 AgentRunEvent。
export const AgentRunEventType = z.enum([
"run_started",
"text_delta",
"thinking_delta",
"tool_call_start",
"tool_call_end",
"tool_call_rejected",
"compaction",
"run_completed",
"run_failed",
]);
export type AgentRunEventType = z.infer<typeof AgentRunEventType>;
/** 一次运行累计用量(数值都齐全;写 agent_run.usage 时可直接用,是 AgentUsage 的子集形状) */
export const AgentUsageTotals = z.object({
inputTokens: NonNegInt,
outputTokens: NonNegInt,
cacheReadTokens: NonNegInt,
cacheWriteTokens: NonNegInt,
totalTokens: NonNegInt,
costUsd: z.number().nonnegative(),
});
export type AgentUsageTotals = z.infer<typeof AgentUsageTotals>;
export const AgentFailureReason = z.enum(["budget", "aborted", "error"]);
export type AgentFailureReason = z.infer<typeof AgentFailureReason>;
export const AgentBudgetKind = z.enum(["tool_calls", "duration", "tokens", "cost"]);
export type AgentBudgetKind = z.infer<typeof AgentBudgetKind>;
const RunStarted = z.object({ type: z.literal("run_started"), goal: z.string(), resumed: z.boolean() });
const TextDelta = z.object({ type: z.literal("text_delta"), delta: z.string() });
const ThinkingDelta = z.object({ type: z.literal("thinking_delta"), delta: z.string() });
const ToolCallStart = z.object({
type: z.literal("tool_call_start"),
toolCallId: z.string(),
capabilityId: z.string(),
/** 已脱敏、截断的输入摘要 */
inputSummary: z.string(),
/** 崩溃恢复后重放/补记的调用 */
recovery: z.boolean(),
});
const ToolCallEnd = z.object({
type: z.literal("tool_call_end"),
toolCallId: z.string(),
capabilityId: z.string(),
isError: z.boolean(),
/** 已脱敏、截断的输出摘要 */
outputSummary: z.string(),
recovery: z.boolean(),
});
const ToolCallRejected = z.object({
type: z.literal("tool_call_rejected"),
toolCallId: z.string(),
toolName: z.string(),
capabilityId: z.string().optional(),
reason: z.string(),
});
const Compaction = z.object({
type: z.literal("compaction"),
phase: z.enum(["start", "end"]),
reason: z.enum(["manual", "threshold", "overflow"]),
status: z.enum(["completed", "declined", "aborted", "failed"]).optional(),
});
const RunCompleted = z.object({ type: z.literal("run_completed"), finalText: z.string(), usage: AgentUsageTotals });
const RunFailed = z.object({
type: z.literal("run_failed"),
reason: AgentFailureReason,
message: z.string(),
usage: AgentUsageTotals,
budget: AgentBudgetKind.optional(),
});
/** 事件主体(不含 runId/seq/at) */
export const AgentRunEventBody = z.discriminatedUnion("type", [
RunStarted,
TextDelta,
ThinkingDelta,
ToolCallStart,
ToolCallEnd,
ToolCallRejected,
Compaction,
RunCompleted,
RunFailed,
]);
export type AgentRunEventBody = z.infer<typeof AgentRunEventBody>;
const EventMeta = {
/** = agent_run.id */
runId: z.string().min(1),
/** 该 run 内从 0 起单调递增;续跑时从 agent_run.last_event_seq + 1 继续 */
seq: NonNegInt,
at: Timestamp,
};
/** 完整事件:主体 + runId/seq/at */
export const AgentRunEvent = z.discriminatedUnion("type", [
RunStarted.extend(EventMeta),
TextDelta.extend(EventMeta),
ThinkingDelta.extend(EventMeta),
ToolCallStart.extend(EventMeta),
ToolCallEnd.extend(EventMeta),
ToolCallRejected.extend(EventMeta),
Compaction.extend(EventMeta),
RunCompleted.extend(EventMeta),
RunFailed.extend(EventMeta),
]);
export type AgentRunEvent = AgentRunEventBody & { runId: string; seq: number; at: string };
/** agent_run_event 行:(run_id, seq) 唯一;payload = 事件主体去掉 type */
export const AgentRunEventRow = z.object({
runId: Id,
seq: NonNegInt,
type: AgentRunEventType,
at: Timestamp,
payload: JsonObject,
});
export type AgentRunEventRow = z.infer<typeof AgentRunEventRow>;
export function agentRunEventToRow(e: AgentRunEvent): AgentRunEventRow {
const { runId, seq, at, type, ...payload } = e;
return { runId, seq, at, type, payload };
}
/** 行 → 事件;payload 不合法时抛错(表里只应有 agentRunEventToRow 写入的内容) */
export function agentRunEventFromRow(row: AgentRunEventRow): AgentRunEvent {
return AgentRunEvent.parse({ ...row.payload, type: row.type, runId: row.runId, seq: row.seq, at: row.at });
}
/** 工具调用事件 → SSE agent.tool_call 的 phase */
export function toolCallPhase(e: AgentRunEventBody): "started" | "succeeded" | "failed" | "denied" | null {
switch (e.type) {
case "tool_call_start":
return "started";
case "tool_call_end":
return e.isError ? "failed" : "succeeded";
case "tool_call_rejected":
return "denied";
default:
return null;
}
}
/** run_completed / run_failed / run_started → agent_run.status */
export function agentRunStatusOf(e: AgentRunEventBody): AgentRunStatus | null {
switch (e.type) {
case "run_started":
return "running";
case "run_completed":
return "succeeded";
case "run_failed":
return e.reason === "budget" ? "budget_exceeded" : e.reason === "aborted" ? "cancelled" : "failed";
default:
return null;
}
}
/**
* Agent 运行事件 → 全局 SSE 事件(agent.run_status / agent.text_delta / agent.tool_call)。
* thinking_delta 与 compaction 不上全局总线(只在 run 详情里按 agent_run_event 回放),返回 null。
*/
export function agentRunEventToBusEvent(e: AgentRunEvent): TypedEvent | null {
const phase = toolCallPhase(e);
if (phase) {
let capabilityId = "";
let summary = "";
if (e.type === "tool_call_start") [capabilityId, summary] = [e.capabilityId, e.inputSummary];
else if (e.type === "tool_call_end") [capabilityId, summary] = [e.capabilityId, e.outputSummary];
else if (e.type === "tool_call_rejected") [capabilityId, summary] = [e.capabilityId ?? e.toolName, e.reason];
return { type: "agent.tool_call", payload: { runId: e.runId, runSeq: e.seq, capabilityId, phase, summary } };
}
if (e.type === "text_delta") {
return { type: "agent.text_delta", payload: { runId: e.runId, runSeq: e.seq, delta: e.delta } };
}
const status = agentRunStatusOf(e);
if (status) {
const summary = e.type === "run_completed" ? e.finalText : e.type === "run_failed" ? e.message : null;
return { type: "agent.run_status", payload: { runId: e.runId, status, summary } };
}
return null;
}
+1
View File
@@ -30,6 +30,7 @@ export const AgentUsage = z.object({
outputTokens: NonNegInt.optional(),
cacheReadTokens: NonNegInt.optional(),
cacheWriteTokens: NonNegInt.optional(),
totalTokens: NonNegInt.optional(),
costUsd: z.number().nonnegative().optional(),
durationMs: NonNegInt.optional(),
});
+18 -1
View File
@@ -1,6 +1,6 @@
import { z } from "zod";
import { MediaAsset } from "./assets.ts";
import type { KnownCapabilityId } from "./capability.ts";
import { CAPABILITY_CATALOG, type CapabilitySpec, type KnownCapabilityId } from "./capability.ts";
import { Id, JsonObject, NonNegInt } from "./common.ts";
import { MediaEntity, MediaKind } from "./content.ts";
import { AcquisitionPlan, DownloadTask, Task, TaskStatus } from "./execution.ts";
@@ -188,6 +188,23 @@ export const CAPABILITY_IO = {
"inventory.query": { input: InventoryQueryInput, output: InventoryQueryOutput },
} as const satisfies Record<KnownCapabilityId, { input: z.ZodType; output: z.ZodType }>;
type CapabilityIo = typeof CAPABILITY_IO;
/** 首批能力 id 对应的运行时规格类型(input/output 为该能力的具体 schema) */
export type KnownCapabilitySpec<K extends KnownCapabilityId> = CapabilitySpec<
CapabilityIo[K]["input"],
CapabilityIo[K]["output"]
>;
/**
* 首批能力的运行时规格:CAPABILITY_CATALOG 的元数据 + CAPABILITY_IO 的 schema。
* server 各模块注册处理器时一律用它,不要手写描述:
* `registry.register(capabilitySpec("release.search"), handler)`
*/
export function capabilitySpec<K extends KnownCapabilityId>(id: K): KnownCapabilitySpec<K> {
const { inputSchema: _i, outputSchema: _o, ...meta } = CAPABILITY_CATALOG[id];
return { ...meta, inputSchema: CAPABILITY_IO[id].input, outputSchema: CAPABILITY_IO[id].output };
}
export type CatalogLookupInput = z.infer<typeof CatalogLookupInput>;
export type CatalogLookupOutput = z.infer<typeof CatalogLookupOutput>;
export type ReleaseSearchInput = z.infer<typeof ReleaseSearchInput>;
+30 -20
View File
@@ -24,37 +24,31 @@ export const MutationPlan = z.object({
});
export type MutationPlan = z.infer<typeof MutationPlan>;
/** 能力风险级别 */
export const CapabilityRisk = z.enum(["read", "preview", "write", "destructive"]);
export type CapabilityRisk = z.infer<typeof CapabilityRisk>;
/** 能力 ID:`域.动作`,如 download.submit */
export const CapabilityId = z.string().regex(/^[a-z][a-z0-9]*\.[a-z][a-z0-9_]*$/);
export type CapabilityId = z.infer<typeof CapabilityId>;
/** 能力定义(注册中心元数据;处理器在 server 侧挂载) */
export interface CapabilityDefinition<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType> {
id: CapabilityId;
version: number;
description: string;
input: I;
output: O;
scopes: string[];
risk: CapabilityRisk;
idempotent: boolean;
supportsPreview: boolean;
}
// ── 能力描述符与首批能力(任务书 §3.1-9;REQ §10.3) ──
/** 风险级别:low 只读;medium 预览/可撤回写入;high 触发下载、文件变更或策略发布 */
export const RiskLevel = z.enum(["low", "medium", "high"]);
export type RiskLevel = z.infer<typeof RiskLevel>;
/** 接口类别(REQ §12.3):read / preview / propose / approve / execute */
/**
* 接口类别(REQ §12.3),由弱到强:
* - read:只读查询
* - preview:生成预览/方案,不落任何变更
* - propose:生成并保存变更计划(MutationPlan),等待授权
* - approve:对计划作出授权决定
* - execute:产生外部副作用(下载提交、Copy/Move、策略发布…)
*/
export const CapabilityKind = z.enum(["read", "preview", "propose", "approve", "execute"]);
export type CapabilityKind = z.infer<typeof CapabilityKind>;
/** 崩溃恢复时 harness 能否用持久化参数重跑:有副作用的一律 never,只读用 safe */
export const CapabilityReplay = z.enum(["safe", "never"]);
export type CapabilityReplay = z.infer<typeof CapabilityReplay>;
/** 首批能力 ID */
export const CAPABILITY_IDS = [
"catalog.lookup",
@@ -78,7 +72,11 @@ export type KnownCapabilityId = z.infer<typeof KnownCapabilityId>;
/** schema 引用:指向 contracts 中导出的 schema 名(如 "ReleaseSearchInput"),便于序列化给 UI/MCP 列表 */
export const SchemaRef = z.string().regex(/^[A-Z][A-Za-z0-9]*$/);
/** 能力注册中心的可序列化描述(处理器在 server 侧挂载;模型只能看到白名单内的描述) */
/**
* 能力的可序列化描述(唯一定义;处理器在 server 侧挂载,模型只能看到白名单内的描述)。
* 运行时注册用 `CapabilitySpec`(同字段,inputSchema/outputSchema 换成真实 zod schema),
* 首批能力的 spec 用 `capabilitySpec(id)` 从 CAPABILITY_CATALOG + CAPABILITY_IO 组装。
*/
export const CapabilityDescriptor = z.object({
id: CapabilityId,
version: z.number().int().min(1),
@@ -87,14 +85,24 @@ export const CapabilityDescriptor = z.object({
kind: CapabilityKind,
inputSchema: SchemaRef,
outputSchema: SchemaRef,
/** 调用所需 API scope */
/** 经 HTTP/MCP 调用时所需的 API scope */
scope: ApiScope,
riskLevel: RiskLevel,
idempotent: z.boolean(),
supportsPreview: z.boolean(),
replay: CapabilityReplay,
});
export type CapabilityDescriptor = z.infer<typeof CapabilityDescriptor>;
/** 运行时能力规格:描述符字段 + 真实的输入/输出 zod schema(注册中心、Pi 工具、HTTP 共用) */
export type CapabilitySpec<I extends z.ZodType = z.ZodType, O extends z.ZodType = z.ZodType> = Omit<
CapabilityDescriptor,
"inputSchema" | "outputSchema"
> & { inputSchema: I; outputSchema: O };
/** 需要走授权回调(automation_grant / approval)的类别 */
export const AUTHORIZED_KINDS: readonly CapabilityKind[] = ["propose", "approve", "execute"];
/** 首批能力的描述(inputSchema/outputSchema 名对应 capabilities-io.ts 中的导出) */
export const CAPABILITY_CATALOG: Record<KnownCapabilityId, CapabilityDescriptor> = {
"catalog.lookup": d(
@@ -276,5 +284,7 @@ function d(
riskLevel,
idempotent,
supportsPreview,
// 有副作用的类别一律不可重放
replay: kind === "read" || kind === "preview" ? "safe" : "never",
};
}
+1
View File
@@ -1,4 +1,5 @@
export * from "./agent.ts";
export * from "./agent-events.ts";
export * from "./api.ts";
export * from "./assets.ts";
export * from "./capabilities-io.ts";