Files
UHD_Media_Tool/packages/contracts/src/agent-events.ts
T
rootandClaude Opus 5.5 003815591c feat(agent): 运行时从 pi-agent-core 0.99.2 迁到 pi-durable 1.1.0 + pi-ai 1.1.0
- PiAgentRuntime 改用 pi-durable Harness:每个 run 一个 JSONL 存储目录 sessionDir/<runId>/ + root 对话;
  系统提示 / 能力工具 / 白名单与预算钩子装在每次运行自己的扩展里;watchEvents 翻译成 AgentRunEvent
- 挂起(工具内 close)立即封口,调用保持未完成,重开后 resume 重放;replayGate 用调用级 memo
- 旧格式 <runId>.jsonl 打开时抛 LegacySessionError;skills 加载与 <skill> 块格式内置(源自 pi-agent-core,MIT)
- tool_call_end 新增可选 durationMs;用量取 pi.usage 合计(整段会话总量)
- Azure provider 改名 azure,迁移 0093 改存量;移除 pi-agent-core,锁 pi-durable/pi-ai/chord 1.1.0
- 文档:ADR-003、pi-providers、runbook、agent README、CHANGELOG

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-08 13:42:36 +08:00

413 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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",
"subagent_spawned",
"subagent_event",
"subagent_completed",
"plan_updated",
"options_proposed",
"options_resolved",
"approval_requested",
"approval_resolved",
]);
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(),
/** 工具执行耗时(毫秒,单调时钟;没真正执行的调用没有) */
durationMs: z.number().int().nonnegative().optional(),
});
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(),
});
// 子代理(agent.spawn):子会话的事件镜像进父会话事件流,由父会话的运行进程按父 seq 写入(单写者,seq 不冲突)。
const SubagentSpawned = z.object({
type: z.literal("subagent_spawned"),
childRunId: z.string(),
goal: z.string(),
mode: z.enum(["execute", "plan_only"]),
depth: NonNegInt,
capabilities: z.array(z.string()),
/** 从父会话划拨给子会话的预算 */
budget: z.record(z.string(), z.number()),
});
/** 子会话的一条事件(工具调用 / 开始 / 结束);childSeq 是子会话内的 seq,正文已脱敏截断 */
const SubagentEvent = z.object({
type: z.literal("subagent_event"),
childRunId: z.string(),
childSeq: NonNegInt,
childType: z.string(),
capabilityId: z.string().optional(),
summary: z.string(),
});
const SubagentCompleted = z.object({
type: z.literal("subagent_completed"),
childRunId: z.string(),
status: z.string(),
/** 子会话汇报摘要(业务核查结论 + 未经核查的模型文本),已脱敏截断 */
summary: z.string(),
usage: AgentUsageTotals,
toolCalls: NonNegInt,
});
// 自动化优先的会话交互(AG-2):模型用 agent.plan 维护步骤清单,用 agent.propose_options 让用户在方案里选;
// 需审批的能力在对应步骤处出审批卡。这些事件都由会话的运行进程写入(单写者):用户在挂起期间作的选择 / 审批,
// 在会话恢复、重放挂起的调用时才记为 *_resolved。
export const AgentPlanStepStatus = z.enum(["pending", "running", "done", "skipped", "failed"]);
export type AgentPlanStepStatus = z.infer<typeof AgentPlanStepStatus>;
/** 步骤清单的一步;id 由模型给,同一会话内稳定(更新时按 id 合并) */
export const AgentPlanStep = z.object({
id: z.string().trim().min(1).max(40),
title: z.string().trim().min(1).max(200),
status: AgentPlanStepStatus,
note: z.string().max(500).nullable().default(null),
});
export type AgentPlanStep = z.infer<typeof AgentPlanStep>;
/**
* 方案绑定的待审批动作(AGUI-3):模型只给能力与计划 ID;服务端在提出方案时按计划当前的 payload hash + revision
* 绑定,并用计划本身生成动作要点(对象、大小)。管理员会话选定这个方案 = 批准这一份计划,执行时服务端再核对绑定。
*/
export const AgentOptionActionCapability = z.enum(["download.submit", "organize.execute", "policy.apply"]);
export const AgentOptionAction = z.object({
capabilityId: AgentOptionActionCapability,
/** download.submit / organize.execute 的 planId;policy.apply 的 policyRevisionId */
planId: Id,
});
export type AgentOptionAction = z.infer<typeof AgentOptionAction>;
/** 给用户选的一个方案 */
export const AgentOptionItem = z.object({
id: z.string().trim().min(1).max(40),
title: z.string().trim().min(1).max(200),
summary: z.string().trim().min(1).max(1000),
risks: z.array(z.string().max(300)).max(8).default([]),
/** 估算代价(体积 / 时长 / 费用),模型给出的文本,未经核查 */
estimatedCost: z.string().max(200).nullable().default(null),
/** 选定后要执行的需审批动作(可选) */
action: AgentOptionAction.nullable().optional(),
});
export type AgentOptionItem = z.infer<typeof AgentOptionItem>;
/** 事件里的方案动作:服务端从计划生成的要点;approves = 选定即批准(删除、覆盖类动作为 false,仍单独确认) */
export const AgentOptionActionView = AgentOptionAction.extend({
/** 动作名,如「提交下载」 */
label: z.string(),
/** 动作要点:对象与大小 / 数量,如「《沙丘:第二部》· 12.0 GB」 */
detail: z.string().nullable(),
approves: z.boolean(),
});
export type AgentOptionActionView = z.infer<typeof AgentOptionActionView>;
export const AgentOptionView = AgentOptionItem.extend({ action: AgentOptionActionView.nullable().optional() });
export type AgentOptionView = z.infer<typeof AgentOptionView>;
/** 超时未选时的处理:recommended = 自动选推荐项;stop = 不选,模型收尾 */
export const AgentOptionsTimeoutAction = z.enum(["recommended", "stop"]);
export type AgentOptionsTimeoutAction = z.infer<typeof AgentOptionsTimeoutAction>;
const PlanUpdated = z.object({
type: z.literal("plan_updated"),
steps: z.array(AgentPlanStep),
/** 本会话第几版计划(从 1 起) */
revision: z.number().int().min(1),
});
const OptionsProposed = z.object({
type: z.literal("options_proposed"),
/** 挂起的工具调用(恢复时重放) */
invocationId: z.string(),
/** 对应的待处理事项(用户也可以在待处理中心选) */
decisionId: Id,
question: z.string(),
options: z.array(AgentOptionView),
recommendedId: z.string().nullable(),
/** 出现在哪一步(提出时正在进行的步骤) */
stepId: z.string().nullable(),
expiresAt: Timestamp,
timeoutAction: AgentOptionsTimeoutAction,
});
const OptionsResolved = z.object({
type: z.literal("options_resolved"),
invocationId: z.string(),
decisionId: Id,
/** chosen = 选了 choice;rejected = 都不要(reason 是用户说明);stop = 超时按设置停止 */
outcome: z.enum(["chosen", "rejected", "stop"]),
choice: z.string().nullable(),
reason: z.string().nullable(),
by: z.enum(["user", "timeout"]),
});
const ApprovalRequested = z.object({
type: z.literal("approval_requested"),
approvalId: Id,
decisionId: Id.nullable(),
invocationId: z.string(),
capabilityId: z.string(),
planType: z.string(),
planId: Id,
payloadHash: z.string(),
expectedRevision: z.number().int().min(0),
riskLevel: z.enum(["low", "medium", "high"]),
/** 计划摘要(已脱敏截断) */
summary: z.string(),
stepId: z.string().nullable(),
expiresAt: Timestamp,
});
const ApprovalResolved = z.object({
type: z.literal("approval_resolved"),
approvalId: Id,
status: z.enum(["approved", "rejected", "expired", "revoked", "invalid"]),
reason: z.string().nullable(),
decidedBy: z.string().nullable(),
});
/** 事件主体(不含 runId/seq/at) */
export const AgentRunEventBody = z.discriminatedUnion("type", [
RunStarted,
TextDelta,
ThinkingDelta,
ToolCallStart,
ToolCallEnd,
ToolCallRejected,
Compaction,
RunCompleted,
RunFailed,
SubagentSpawned,
SubagentEvent,
SubagentCompleted,
PlanUpdated,
OptionsProposed,
OptionsResolved,
ApprovalRequested,
ApprovalResolved,
]);
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),
SubagentSpawned.extend(EventMeta),
SubagentEvent.extend(EventMeta),
SubagentCompleted.extend(EventMeta),
PlanUpdated.extend(EventMeta),
OptionsProposed.extend(EventMeta),
OptionsResolved.extend(EventMeta),
ApprovalRequested.extend(EventMeta),
ApprovalResolved.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 } };
}
// 子会话事件:借 agent.tool_call(能力 agent.spawn)提示父会话页"有新事件",页面按 seq 补读
if (e.type === "subagent_spawned" || e.type === "subagent_event" || e.type === "subagent_completed") {
const phase =
e.type === "subagent_completed" ? (e.status === "succeeded" ? "succeeded" : "failed") : ("started" as const);
const summary =
e.type === "subagent_spawned"
? `派生子会话 ${e.childRunId}:${e.goal}`
: e.type === "subagent_event"
? `子会话 ${e.childRunId}:${e.childType} ${e.capabilityId ?? ""} ${e.summary}`.trim()
: `子会话 ${e.childRunId} ${e.status}:${e.summary}`;
return {
type: "agent.tool_call",
payload: { runId: e.runId, runSeq: e.seq, capabilityId: "agent.spawn", phase, summary: summary.slice(0, 600) },
};
}
// 计划 / 方案 / 审批:借 agent.tool_call 提示会话页"有新事件"(页面按 seq 补读),摘要给全局通知用
const hint = interactionHint(e);
if (hint) {
return {
type: "agent.tool_call",
payload: {
runId: e.runId,
runSeq: e.seq,
capabilityId: hint.capabilityId,
phase: hint.phase,
summary: hint.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;
}
function interactionHint(
e: AgentRunEventBody,
): { capabilityId: string; phase: "started" | "succeeded" | "failed" | "denied"; summary: string } | null {
switch (e.type) {
case "plan_updated": {
const done = e.steps.filter((s) => s.status === "done").length;
return { capabilityId: "agent.plan", phase: "succeeded", summary: `步骤 ${done}/${e.steps.length}` };
}
case "options_proposed":
return { capabilityId: "agent.propose_options", phase: "started", summary: e.question.slice(0, 600) };
case "options_resolved":
return {
capabilityId: "agent.propose_options",
phase: e.outcome === "chosen" ? "succeeded" : "failed",
summary: e.outcome === "chosen" ? `已选 ${e.choice}` : e.outcome === "rejected" ? "都不要" : "超时未选",
};
case "approval_requested":
return { capabilityId: e.capabilityId, phase: "started", summary: `等待审批:${e.summary}`.slice(0, 600) };
case "approval_resolved":
return {
capabilityId: "approval",
phase: e.status === "approved" ? "succeeded" : "denied",
summary: `审批${e.status}${e.reason ? `:${e.reason}` : ""}`.slice(0, 600),
};
default:
return null;
}
}