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
@@ -17,6 +17,7 @@ import {
|
||||
import { buildGoalReport } from "./report.ts";
|
||||
import { type AgentEnv, type DriveResult, drive, goalSettled } from "./runner.ts";
|
||||
import { finalize } from "./service.ts";
|
||||
import { catchUpChildren, claimSubagentSlot } from "./subagents.ts";
|
||||
|
||||
// 持久任务:runAgent(驱动一次有界 Agent 会话 → 等目标关联的下载/入库落定 → 以业务表汇报)
|
||||
// 与 resolveDecision(decision.created → 自动调查;decision.updated(resolved)→ 执行所选处理)。
|
||||
@@ -52,7 +53,26 @@ export function registerAgentTasks(registry: TaskRegistry, env: () => AgentEnv)
|
||||
.execute();
|
||||
return null;
|
||||
});
|
||||
const res: DriveResult = await t.step.run("drive", () => drive(e, e.ctx.capabilities, runId, t.abortSignal));
|
||||
// 子代理:先占并发名额(父会话下同时 running 的子会话 < maxConcurrent);没名额就挂起等待,不占 worker、不占模型
|
||||
const parentId = await t.step.run("subagent-parent", async () => {
|
||||
const r = await e.ctx.db
|
||||
.selectFrom("agent_run")
|
||||
.select("parent_run_id")
|
||||
.where("id", "=", runId)
|
||||
.executeTakeFirst();
|
||||
return r?.parent_run_id ?? null;
|
||||
});
|
||||
let res: DriveResult;
|
||||
const slot = parentId
|
||||
? await t.step.waitForFact<"claimed" | "cancelled">("subagent-slot", {
|
||||
check: () => claimSubagentSlot(e.ctx, runId),
|
||||
timeout: "1s",
|
||||
maxRounds: 3600,
|
||||
})
|
||||
: "claimed";
|
||||
if (slot === null)
|
||||
res = { status: "failed", reason: "error", budget: null, message: "等待子会话名额超时", finalText: "" };
|
||||
else res = await t.step.run("drive", () => drive(e, e.ctx.capabilities, runId, t.abortSignal));
|
||||
let settled: AgentGoalReport | null = null;
|
||||
const run = await e.ctx.db
|
||||
.selectFrom("agent_run")
|
||||
@@ -102,6 +122,8 @@ export function registerAgentTasks(registry: TaskRegistry, env: () => AgentEnv)
|
||||
const out = await t.step.run("finalize", () =>
|
||||
finalize(e, runId, res, settled, recovery ? { summaryPrefix: recovery.summary } : {}),
|
||||
);
|
||||
// 父会话收尾后才结束的子会话:补记进父事件流
|
||||
await t.step.run("subagent-catchup", () => catchUpChildren(e.ctx, runId));
|
||||
if (recovery) {
|
||||
// 已在 finalize 内带上核实结论;保留该步骤兼容旧 run 的步骤历史(已带前缀时幂等跳过)
|
||||
const rec = recovery;
|
||||
|
||||
Reference in new issue
Block a user