- agent_run gains depth / spawn_reason (0029); agent_run_event types
subagent_spawned / subagent_event / subagent_completed
- agent.spawn (propose, medium, replay never): allowlist ⊆ parent, mode not
above parent, budget carved from parent's remainder, idempotent per run+input;
settings.pi.subagents {enabled, maxConcurrent, maxDepth} (subagentsEnabled kept as alias)
- child runs claim a concurrency slot under an advisory lock (waitForFact while
queued); agent.await polls business tables without calling the model
- parent abort cascades to all descendants; parent runtime budget counts
descendant usage; child events mirrored into the parent stream by parent seq
- authorize caps every child call by all ancestors' allowlists/kinds
- session page shows parent link, inherited info and the subagent tree
- e2e E11; runbook section on subagents
Co-Authored-By: Claude Opus 5.5 <[email protected]>
243 lines
8.7 KiB
TypeScript
243 lines
8.7 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",
|
|
]);
|
|
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(),
|
|
});
|
|
|
|
// 子代理(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,
|
|
});
|
|
|
|
/** 事件主体(不含 runId/seq/at) */
|
|
export const AgentRunEventBody = z.discriminatedUnion("type", [
|
|
RunStarted,
|
|
TextDelta,
|
|
ThinkingDelta,
|
|
ToolCallStart,
|
|
ToolCallEnd,
|
|
ToolCallRejected,
|
|
Compaction,
|
|
RunCompleted,
|
|
RunFailed,
|
|
SubagentSpawned,
|
|
SubagentEvent,
|
|
SubagentCompleted,
|
|
]);
|
|
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),
|
|
]);
|
|
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) },
|
|
};
|
|
}
|
|
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;
|
|
}
|