fix(coding-agent): kept todo active for prewalk subagents and fixed prewalk gate deadlock
- Updated prewalk gating in `AgentSession` to key the todo gate on active tools instead of registry presence, so deactivated todo tools no longer block prewalk handoff. - Changed subprocess tool filtering so `todo` is stripped for normal subagents but retained when prewalk is armed, and propagated the prewalk state through tool-session setup. - Added regression tests for restricted active-tool slates and prewalk/non-prewalk subagent tool propagation to verify todo is handled correctly in each case.
This commit is contained in:
@@ -1750,7 +1750,7 @@ export class AgentSession {
|
||||
#prewalkPlanInjected = false;
|
||||
/** True once any successful `todo` call landed — opens the prewalk
|
||||
* trigger gate: the switch fires at the first edit/write AFTER the todo
|
||||
* list exists (sessions without a todo tool skip the gate). */
|
||||
* list exists (sessions without an ACTIVE todo tool skip the gate). */
|
||||
#prewalkTodoSeen = false;
|
||||
#planYolo: PlanYolo | undefined;
|
||||
#planYoloPreviousTools: string[] | undefined;
|
||||
@@ -2270,12 +2270,14 @@ export class AgentSession {
|
||||
// todo list from it and start" — so the switch waits until a todo list
|
||||
// exists AND the model has actually started implementing (first
|
||||
// edit/write). The todo call itself never triggers: firing there handed
|
||||
// the fast model the whole implementation cold. Sessions without a todo
|
||||
// tool skip the gate.
|
||||
// the fast model the whole implementation cold. The gate keys on the
|
||||
// ACTIVE tool set, not the registry: a registered-but-deactivated todo
|
||||
// (e.g. a restricted active-tool slate) is uncallable and would
|
||||
// deadlock the switch.
|
||||
if (context.toolResults.some(result => result.toolName === "todo")) {
|
||||
this.#prewalkTodoSeen = true;
|
||||
}
|
||||
const todoGateOpen = this.#prewalkTodoSeen || !this.#toolRegistry.has("todo");
|
||||
const todoGateOpen = this.#prewalkTodoSeen || !this.getActiveToolNames().includes("todo");
|
||||
const action = todoGateOpen
|
||||
? context.toolResults.find(result => PREWALK_ACTION_TOOLS[result.toolName])
|
||||
: undefined;
|
||||
|
||||
@@ -2575,9 +2575,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
});
|
||||
}
|
||||
|
||||
// Todos are parent-owned bookkeeping and stripped from subagents —
|
||||
// except under prewalk, whose plan nudge + todo gate require the
|
||||
// subagent to commit its own todo list before the hand-off.
|
||||
const isParentOwnedTool = (name: string): boolean => !prewalk && name === "todo";
|
||||
const subagentToolNames = session.getActiveToolNames();
|
||||
const parentOwnedToolNames = new Set(["todo"]);
|
||||
const filteredSubagentTools = subagentToolNames.filter(name => !parentOwnedToolNames.has(name));
|
||||
const filteredSubagentTools = subagentToolNames.filter(name => !isParentOwnedTool(name));
|
||||
if (filteredSubagentTools.length !== subagentToolNames.length) {
|
||||
await awaitAbortable(session.setActiveToolsByName(filteredSubagentTools));
|
||||
}
|
||||
@@ -2635,7 +2638,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
getActiveTools: () => session.getActiveToolNames(),
|
||||
getAllTools: () => session.getAllToolNames(),
|
||||
setActiveTools: (toolNames: string[]) =>
|
||||
session.setActiveToolsByName(toolNames.filter(name => !parentOwnedToolNames.has(name))),
|
||||
session.setActiveToolsByName(toolNames.filter(name => !isParentOwnedTool(name))),
|
||||
getCommands: () => getSessionSlashCommands(session),
|
||||
setModel: model => runExtensionSetModel(session, model),
|
||||
getThinkingLevel: () => session.thinkingLevel,
|
||||
|
||||
@@ -206,7 +206,6 @@ function renderDescription(
|
||||
agents: renderedAgents,
|
||||
spawningDisabled,
|
||||
defaultAgent: spawnPolicy.defaultAgent,
|
||||
allowedAgentsText: spawnPolicy.allowedPromptText,
|
||||
isolationEnabled,
|
||||
batchEnabled,
|
||||
asyncEnabled,
|
||||
|
||||
@@ -205,6 +205,9 @@ export interface ToolSession {
|
||||
outputSchema?: unknown;
|
||||
/** Whether to include the yield tool by default */
|
||||
requireYieldTool?: boolean;
|
||||
/** Session starts with a prewalk hand-off armed. Keeps `todo` in yield-gated
|
||||
* (subagent) registries: the prewalk plan nudge + todo gate need it. */
|
||||
prewalkArmed?: boolean;
|
||||
/** Task recursion depth (0 = top-level, 1 = first child, etc.) */
|
||||
taskDepth?: number;
|
||||
/** Get shared eval executor session ID. Subagents inherit this to share JS/Python/Ruby/Julia state. */
|
||||
@@ -612,7 +615,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
||||
if (name === "launch") return session.settings.get("launch.enabled");
|
||||
if (name === "eval") return allowEval;
|
||||
if (name === "debug") return session.settings.get("debug.enabled");
|
||||
if (name === "todo") return !includeYield && session.settings.get("todo.enabled");
|
||||
if (name === "todo")
|
||||
return (!includeYield || session.prewalkArmed === true) && session.settings.get("todo.enabled");
|
||||
if (name === "glob") return session.settings.get("glob.enabled");
|
||||
if (name === "grep") return session.settings.get("grep.enabled");
|
||||
if (name === "github") return session.settings.get("github.enabled");
|
||||
|
||||
Reference in New Issue
Block a user