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:
rootandClaude Opus 5.5 committed 2026-09-28 02:49:37 +08:00
1 parent daa85a8bb4
commit 459991421e
27 files changed
+1720 -32

No files matched your search

+40
View File
@@ -68,6 +68,10 @@ export const AgentRun = EntityBase.extend({
mode: AgentRunMode,
/** 后台触发按根因去重(如 investigate:<decisionId>);用户委托为 null */
dedupeKey: z.string().nullable(),
/** 派生深度:用户/后台发起的会话为 0,agent.spawn 派生的子会话 = 父 + 1 */
depth: NonNegInt,
/** 父会话派生时给的理由 */
spawnReason: z.string().nullable(),
});
export type AgentRun = z.infer<typeof AgentRun>;
export const AgentRunCreate = AgentRun.pick({
@@ -314,6 +318,8 @@ export const AgentRunListQuery = z.object({
limit: z.coerce.number().int().min(1).max(200).default(50),
status: AgentRunStatus.optional(),
trigger: AgentTrigger.optional(),
/** 只列某个会话直接派生的子会话 */
parentRunId: Id.optional(),
});
export type AgentRunListQuery = z.infer<typeof AgentRunListQuery>;
@@ -366,8 +372,42 @@ export const AgentRunMessage = EntityBase.extend({
});
export type AgentRunMessage = z.infer<typeof AgentRunMessage>;
/** 会话树节点(父会话 / 子会话,子会话页与父会话页互相跳转) */
export const AgentRunNode = z.object({
id: Id,
parentRunId: Id.nullable(),
goal: z.string(),
status: AgentRunStatus,
mode: AgentRunMode,
depth: NonNegInt,
spawnReason: z.string().nullable(),
toolCallCount: NonNegInt,
usage: AgentUsage,
summary: z.string().nullable(),
/** 能力白名单(继承信息展示) */
capabilities: z.array(z.string()),
budget: AgentBudget,
createdAt: Timestamp,
finishedAt: Timestamp.nullable(),
});
export type AgentRunNode = z.infer<typeof AgentRunNode>;
/** 本会话 + 全部子孙会话的用量合计(父会话总预算含子会话用量) */
export const AgentFamilyUsage = z.object({
runs: NonNegInt,
toolCalls: NonNegInt,
totalTokens: NonNegInt,
costUsd: z.number().nonnegative(),
});
export type AgentFamilyUsage = z.infer<typeof AgentFamilyUsage>;
export const AgentRunDetail = z.object({
run: AgentRun,
/** 父会话(子会话才有) */
parent: AgentRunNode.nullable().optional(),
/** 全部子孙会话(扁平,按 parentRunId 组树) */
children: z.array(AgentRunNode).optional(),
familyUsage: AgentFamilyUsage.optional(),
events: z.array(AgentRunEventRow),
/** 下一页起点(传给 afterSeq);null 表示已到最新 */
nextAfterSeq: z.number().int().nullable(),