a8dde34531
* pin all CLI installations to explicit versions and use pro models by default - codex: pin @openai/codex to 0.101.0 (was "latest") - opencode: pin opencode-ai to 1.1.56 (was "latest") - gemini: pin gemini-cli to v0.28.2 via new tag param on installFromGithub - cursor: pin to 2026.01.28-fd13201 via direct tarball download (replaces curl install script) - add installFromDirectTarball to install.ts for versioned tarball URLs - gemini auto effort now uses pro-preview instead of flash-preview - test runner model overrides updated to use pro-preview consistently Co-authored-by: Cursor <cursoragent@cursor.com> * increase activity timeout * fix delegation timeout * fix delegate timeouts --------- Co-authored-by: Cursor <cursoragent@cursor.com>
129 lines
4.8 KiB
TypeScript
129 lines
4.8 KiB
TypeScript
import { type } from "arktype";
|
|
import { Effort } from "../external.ts";
|
|
import type { Mode } from "../modes.ts";
|
|
import { markActivity } from "../utils/activity.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" : ""}`
|
|
);
|
|
|
|
// keep the process-level activity timeout alive while the subagent runs.
|
|
// agent CLIs can have long silent thinking phases (>60s) with no stdout,
|
|
// which would trigger the activity timeout. the overall run timeout (default 1h)
|
|
// is the real safety net for stalled agents.
|
|
const keepAliveInterval = setInterval(markActivity, 30_000);
|
|
|
|
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 {
|
|
clearInterval(keepAliveInterval);
|
|
// always release the lock so the orchestrator can delegate again
|
|
ctx.toolState.delegationActive = false;
|
|
}
|
|
}),
|
|
});
|
|
}
|