feat(agent): Pi subagents via agent.spawn / agent.await
- 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]>
This commit is contained in:
1 parent
daa85a8bb4
commit
459991421e
27 files changed
+1720
-32
No files matched your search
@@ -16,6 +16,9 @@ export const AgentRunEventType = z.enum([
|
||||
"compaction",
|
||||
"run_completed",
|
||||
"run_failed",
|
||||
"subagent_spawned",
|
||||
"subagent_event",
|
||||
"subagent_completed",
|
||||
]);
|
||||
export type AgentRunEventType = z.infer<typeof AgentRunEventType>;
|
||||
|
||||
@@ -78,6 +81,36 @@ const RunFailed = z.object({
|
||||
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,
|
||||
@@ -89,6 +122,9 @@ export const AgentRunEventBody = z.discriminatedUnion("type", [
|
||||
Compaction,
|
||||
RunCompleted,
|
||||
RunFailed,
|
||||
SubagentSpawned,
|
||||
SubagentEvent,
|
||||
SubagentCompleted,
|
||||
]);
|
||||
export type AgentRunEventBody = z.infer<typeof AgentRunEventBody>;
|
||||
|
||||
@@ -111,6 +147,9 @@ export const AgentRunEvent = z.discriminatedUnion("type", [
|
||||
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 };
|
||||
|
||||
@@ -176,6 +215,21 @@ export function agentRunEventToBusEvent(e: AgentRunEvent): TypedEvent | null {
|
||||
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 } };
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user