- 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
413 lines
16 KiB
TypeScript
413 lines
16 KiB
TypeScript
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;
|
||
}
|
||
}
|