121 lines
4.3 KiB
TypeScript
121 lines
4.3 KiB
TypeScript
import { type } from "arktype";
|
|
import { Effort } from "../external.ts";
|
|
import type { Mode } from "../modes.ts";
|
|
import { log } from "../utils/cli.ts";
|
|
import { resolveSubagentInstructions } from "../utils/instructions.ts";
|
|
import type { ToolContext } from "./server.ts";
|
|
import { execute, tool } from "./shared.ts";
|
|
|
|
export const DelegateParams = type({
|
|
mode: type.string.describe(
|
|
"the name of the mode to delegate to (e.g., 'Build', 'Plan', 'Review', 'Fix', 'AddressReviews')"
|
|
),
|
|
"effort?": Effort.describe(
|
|
'effort level for the subagent: "mini" (fast), "auto" (default, highly capable), or "max" (maximum capability)'
|
|
),
|
|
"instructions?": type.string.describe(
|
|
"optional additional context or instructions for the subagent — use this to pass results from earlier delegations or narrow the subagent's focus"
|
|
),
|
|
});
|
|
|
|
// exported for unit testing
|
|
export function resolveMode(modes: Mode[], modeName: string): Mode | null {
|
|
return modes.find((m) => m.name.toLowerCase() === modeName.toLowerCase()) ?? null;
|
|
}
|
|
|
|
// cap subagent output to avoid bloating the orchestrator's context window.
|
|
// the orchestrator needs enough to understand what happened, not the full NDJSON stream.
|
|
const MAX_OUTPUT_CHARS = 20_000;
|
|
|
|
// exported for unit testing
|
|
export function truncateOutput(output: string | undefined): string | undefined {
|
|
if (!output || output.length <= MAX_OUTPUT_CHARS) return output;
|
|
const truncated = output.slice(-MAX_OUTPUT_CHARS);
|
|
return `[truncated — showing last ${MAX_OUTPUT_CHARS} chars]\n${truncated}`;
|
|
}
|
|
|
|
export function DelegateTool(ctx: ToolContext) {
|
|
return tool({
|
|
name: "delegate",
|
|
description:
|
|
"Delegate a task to a subagent with a specific mode and effort level. The subagent runs as a separate process with the mode's step-by-step instructions.",
|
|
parameters: DelegateParams,
|
|
execute: execute(async (params) => {
|
|
// guard: prevent subagent recursion
|
|
if (ctx.toolState.delegationActive) {
|
|
return {
|
|
error: "delegation is not available inside a delegated subagent",
|
|
};
|
|
}
|
|
|
|
// resolve mode
|
|
const selectedMode = resolveMode(ctx.modes, params.mode);
|
|
if (!selectedMode) {
|
|
const availableModes = ctx.modes.map((m) => m.name).join(", ");
|
|
return {
|
|
error: `mode "${params.mode}" not found. available modes: ${availableModes}`,
|
|
availableModes: ctx.modes.map((m) => ({
|
|
name: m.name,
|
|
description: m.description,
|
|
})),
|
|
};
|
|
}
|
|
|
|
const effort = params.effort ?? "auto";
|
|
|
|
// track state
|
|
ctx.toolState.selectedMode = selectedMode.name;
|
|
ctx.toolState.delegationActive = true;
|
|
|
|
log.info(
|
|
`» delegating to ${selectedMode.name} mode (effort=${effort})${params.instructions ? " with orchestrator instructions" : ""}`
|
|
);
|
|
|
|
try {
|
|
// build subagent payload with effort override
|
|
const subagentPayload = { ...ctx.payload, effort };
|
|
|
|
// build subagent instructions with mode prompt baked in
|
|
const subagentInstructions = resolveSubagentInstructions({
|
|
payload: subagentPayload,
|
|
repo: ctx.repo,
|
|
modes: ctx.modes,
|
|
mode: selectedMode,
|
|
orchestratorInstructions: params.instructions,
|
|
});
|
|
|
|
// spawn subagent — reuses same MCP server, same toolState
|
|
const result = await ctx.agent.run({
|
|
payload: subagentPayload,
|
|
mcpServerUrl: ctx.mcpServerUrl,
|
|
tmpdir: ctx.tmpdir,
|
|
instructions: subagentInstructions,
|
|
});
|
|
|
|
log.info(`» delegation to ${selectedMode.name} completed (success=${result.success})`);
|
|
|
|
return {
|
|
success: result.success,
|
|
mode: selectedMode.name,
|
|
effort,
|
|
output: truncateOutput(result.output),
|
|
error: result.error,
|
|
};
|
|
} catch (err) {
|
|
// normalize agent crashes into the same return shape as clean failures
|
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
log.error(`» delegation to ${selectedMode.name} crashed: ${errorMessage}`);
|
|
return {
|
|
success: false,
|
|
mode: selectedMode.name,
|
|
effort,
|
|
error: errorMessage,
|
|
};
|
|
} finally {
|
|
// always release the lock so the orchestrator can delegate again
|
|
ctx.toolState.delegationActive = false;
|
|
}
|
|
}),
|
|
});
|
|
}
|