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,7 @@ import { cancelTask, retryTask, taskToApi } from "./capabilities.ts";
|
||||
import { route } from "./http.ts";
|
||||
import { type InvokeLimiter, sharedInvokeLimiter, UpstreamBusyError, UpstreamTimeoutError } from "./invoke-limits.ts";
|
||||
import { AgentRunError, abortRun, appendMessage, startRun } from "./service.ts";
|
||||
import { descendants, descendantUsage } from "./subagents.ts";
|
||||
|
||||
// Agent 会话 API、持续授权 / 单次审批、能力目录与调用、业务任务。
|
||||
|
||||
@@ -57,11 +58,34 @@ export function runToApi(r: AgentRunRow) {
|
||||
createdBy: r.created_by,
|
||||
mode: r.mode,
|
||||
dedupeKey: r.dedupe_key,
|
||||
depth: r.depth,
|
||||
spawnReason: r.spawn_reason,
|
||||
createdAt: r.created_at.toISOString(),
|
||||
updatedAt: r.updated_at.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/** 会话树节点(父会话 / 子孙会话) */
|
||||
export function runToNode(r: AgentRunRow) {
|
||||
const scope = (r.scope ?? {}) as { capabilities?: string[] };
|
||||
return {
|
||||
id: r.id,
|
||||
parentRunId: r.parent_run_id,
|
||||
goal: r.goal,
|
||||
status: r.status,
|
||||
mode: r.mode,
|
||||
depth: r.depth,
|
||||
spawnReason: r.spawn_reason,
|
||||
toolCallCount: r.tool_call_count,
|
||||
usage: r.usage as Record<string, number>,
|
||||
summary: r.summary,
|
||||
capabilities: scope.capabilities ?? [],
|
||||
budget: r.budget as Record<string, number>,
|
||||
createdAt: r.created_at.toISOString(),
|
||||
finishedAt: toIso(r.finished_at),
|
||||
};
|
||||
}
|
||||
|
||||
/** snake_case 行 → camelCase(Date → ISO) */
|
||||
function camel(row: object): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = {};
|
||||
@@ -98,6 +122,7 @@ export function registerAgentRoutes(
|
||||
let q = db().selectFrom("agent_run").selectAll();
|
||||
if (query.status) q = q.where("status", "=", query.status);
|
||||
if (query.trigger) q = q.where("trigger", "=", query.trigger);
|
||||
if (query.parentRunId) q = q.where("parent_run_id", "=", query.parentRunId);
|
||||
const cur = decode(query.cursor);
|
||||
if (cur) q = q.where(sql<boolean>`(created_at, id) < (${cur.at}::timestamptz, ${cur.id}::uuid)`);
|
||||
const rows = await q
|
||||
@@ -138,8 +163,23 @@ export function registerAgentRoutes(
|
||||
.where("kind", "=", "report")
|
||||
.orderBy("created_at", "desc")
|
||||
.executeTakeFirst();
|
||||
// 会话树:父会话 + 全部子孙会话;合计用量 = 本会话 + 子孙会话
|
||||
const parent = run.parent_run_id
|
||||
? await db().selectFrom("agent_run").selectAll().where("id", "=", run.parent_run_id).executeTakeFirst()
|
||||
: undefined;
|
||||
const kids = await descendants(ctx, run.id);
|
||||
const own = (run.usage ?? {}) as { totalTokens?: number; costUsd?: number };
|
||||
const ext = await descendantUsage(ctx, run.id);
|
||||
return {
|
||||
run: runToApi(run),
|
||||
parent: parent ? runToNode(parent) : null,
|
||||
children: kids.map(runToNode),
|
||||
familyUsage: {
|
||||
runs: kids.length + 1,
|
||||
toolCalls: run.tool_call_count + ext.toolCalls,
|
||||
totalTokens: (own.totalTokens ?? 0) + ext.totalTokens,
|
||||
costUsd: (own.costUsd ?? 0) + ext.costUsd,
|
||||
},
|
||||
events: page.map((e) => ({
|
||||
runId: e.run_id,
|
||||
seq: Number(e.seq),
|
||||
|
||||
Reference in new issue
Block a user