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

@@ -0,0 +1,50 @@
import type { Kysely } from "kysely";
import { execDdl, oneOf } from "../src/migration-helpers.ts";
// W7-B Pi 子代理(agent.spawn):agent_run 已有 parent_run_id(0010),这里补派生深度与派生理由;
// agent_run_event 增加 subagent_spawned / subagent_event / subagent_completed(子会话事件镜像进父会话事件流)。
const OLD_TYPES = [
"run_started",
"text_delta",
"thinking_delta",
"tool_call_start",
"tool_call_end",
"tool_call_rejected",
"compaction",
"run_completed",
"run_failed",
];
const NEW_TYPES = [...OLD_TYPES, "subagent_spawned", "subagent_event", "subagent_completed"];
const swap = (values: readonly string[]) => `
alter table agent_run_event drop constraint agent_run_event_type_check;
alter table agent_run_event add constraint agent_run_event_type_check ${oneOf("type", values)};
`;
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
export async function up(db: Kysely<any>): Promise<void> {
await execDdl(
db,
`
alter table agent_run add column depth integer not null default 0 check (depth >= 0);
alter table agent_run add column spawn_reason text;
create index agent_run_parent_idx on agent_run (parent_run_id, created_at) where parent_run_id is not null;
${swap(NEW_TYPES)}
`,
);
}
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function down(db: Kysely<any>): Promise<void> {
await execDdl(
db,
`
delete from agent_run_event where type in ('subagent_spawned', 'subagent_event', 'subagent_completed');
${swap(OLD_TYPES)}
drop index if exists agent_run_parent_idx;
alter table agent_run drop column spawn_reason;
alter table agent_run drop column depth;
`,
);
}
+6 -1
View File
@@ -47,7 +47,10 @@ export type AgentRunEventType =
| "tool_call_rejected"
| "compaction"
| "run_completed"
| "run_failed";
| "run_failed"
| "subagent_spawned"
| "subagent_event"
| "subagent_completed";
export type AgentRunMessageStatus = "pending" | "consumed";
export type ApprovalStatus = "pending" | "approved" | "rejected" | "revoked" | "expired" | "consumed";
export type ArtifactKind = "evidence" | "plan" | "proposal" | "report" | "snapshot" | "log";
@@ -344,6 +347,8 @@ export interface AgentRunTable {
mode: Generated<AgentRunMode>;
dedupe_key: string | null;
upstream_id: string | null;
depth: Generated<number>;
spawn_reason: string | null;
}
export interface AgentRunEventTable {