Resolve tools before building subagent system prompt so the "## Tooling" section is included, matching OpenClaw's pattern. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
149 lines
4.8 KiB
TypeScript
149 lines
4.8 KiB
TypeScript
/**
|
|
* sessions_spawn tool — allows a parent agent to spawn subagent runs.
|
|
*
|
|
* Subagents run in isolated sessions with restricted tools.
|
|
* Results are announced back to the parent when the child completes.
|
|
*/
|
|
|
|
import { v7 as uuidv7 } from "uuid";
|
|
import { Type } from "@sinclair/typebox";
|
|
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
|
import { getHub } from "../../hub/hub-singleton.js";
|
|
import { buildSubagentSystemPrompt } from "../subagent/announce.js";
|
|
import { registerSubagentRun } from "../subagent/registry.js";
|
|
import { resolveTools } from "../tools.js";
|
|
|
|
const SessionsSpawnSchema = Type.Object({
|
|
task: Type.String({ description: "The task for the subagent to perform.", minLength: 1 }),
|
|
label: Type.Optional(
|
|
Type.String({ description: "Human-readable label for this background task." }),
|
|
),
|
|
model: Type.Optional(
|
|
Type.String({ description: "Override the LLM model for the subagent (e.g. 'gpt-4o', 'claude-sonnet')." }),
|
|
),
|
|
cleanup: Type.Optional(
|
|
Type.Union([Type.Literal("delete"), Type.Literal("keep")], {
|
|
description: "Session cleanup after completion. 'delete' removes session files, 'keep' preserves for audit. Default: 'delete'.",
|
|
}),
|
|
),
|
|
timeoutSeconds: Type.Optional(
|
|
Type.Number({
|
|
description: "Execution timeout in seconds. The subagent will be terminated if it exceeds this.",
|
|
minimum: 1,
|
|
}),
|
|
),
|
|
});
|
|
|
|
type SessionsSpawnArgs = {
|
|
task: string;
|
|
label?: string;
|
|
model?: string;
|
|
cleanup?: "delete" | "keep";
|
|
timeoutSeconds?: number;
|
|
};
|
|
|
|
export type SessionsSpawnResult = {
|
|
status: "accepted" | "error";
|
|
childSessionId?: string;
|
|
runId?: string;
|
|
error?: string;
|
|
};
|
|
|
|
export interface CreateSessionsSpawnToolOptions {
|
|
/** Whether the current agent is itself a subagent */
|
|
isSubagent?: boolean;
|
|
/** Session ID of the current (requester) agent */
|
|
sessionId?: string;
|
|
}
|
|
|
|
export function createSessionsSpawnTool(
|
|
options: CreateSessionsSpawnToolOptions,
|
|
): AgentTool<typeof SessionsSpawnSchema, SessionsSpawnResult> {
|
|
return {
|
|
name: "sessions_spawn",
|
|
label: "Spawn Subagent",
|
|
description:
|
|
"Spawn a background subagent to handle a specific task. The subagent runs in an isolated session with its own tool set. " +
|
|
"When it completes, its findings are announced back to you automatically. " +
|
|
"Use this for parallelizable work, long-running analysis, or tasks that benefit from isolation.",
|
|
parameters: SessionsSpawnSchema,
|
|
execute: async (_toolCallId, args) => {
|
|
const { task, label, model, cleanup = "delete", timeoutSeconds } = args as SessionsSpawnArgs;
|
|
|
|
// Guard: subagents cannot spawn subagents
|
|
if (options.isSubagent) {
|
|
return {
|
|
content: [{ type: "text", text: "Error: sessions_spawn is not allowed from sub-agent sessions." }],
|
|
details: {
|
|
status: "error",
|
|
error: "sessions_spawn is not allowed from sub-agent sessions",
|
|
},
|
|
};
|
|
}
|
|
|
|
const requesterSessionId = options.sessionId ?? "unknown";
|
|
const runId = uuidv7();
|
|
const childSessionId = uuidv7();
|
|
|
|
// Resolve tools for the subagent (with isSubagent=true for policy filtering)
|
|
const subagentTools = resolveTools({ isSubagent: true });
|
|
const toolNames = subagentTools.map((t) => t.name);
|
|
|
|
// Build system prompt for the child
|
|
const systemPrompt = buildSubagentSystemPrompt({
|
|
requesterSessionId,
|
|
childSessionId,
|
|
label,
|
|
task,
|
|
tools: toolNames,
|
|
});
|
|
|
|
// Spawn child agent via Hub
|
|
try {
|
|
const hub = getHub();
|
|
const childAgent = hub.createSubagent(childSessionId, {
|
|
systemPrompt,
|
|
model,
|
|
});
|
|
|
|
// Write the task to the child (non-blocking) before registering,
|
|
// so waitForIdle() observes the queued work.
|
|
childAgent.write(task);
|
|
|
|
// Register the run for lifecycle tracking
|
|
registerSubagentRun({
|
|
runId,
|
|
childSessionId,
|
|
requesterSessionId,
|
|
task,
|
|
label,
|
|
cleanup,
|
|
timeoutSeconds,
|
|
});
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: `Subagent spawned successfully.\n\nRun ID: ${runId}\nSession: ${childSessionId}\nTask: ${label || task.slice(0, 80)}\n\nThe subagent is now working in the background. You will receive its findings when it completes.`,
|
|
},
|
|
],
|
|
details: {
|
|
status: "accepted",
|
|
childSessionId,
|
|
runId,
|
|
},
|
|
};
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
return {
|
|
content: [{ type: "text", text: `Error spawning subagent: ${message}` }],
|
|
details: {
|
|
status: "error",
|
|
error: message,
|
|
},
|
|
};
|
|
}
|
|
},
|
|
};
|
|
}
|