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:
can1357
2026-07-15 05:45:59 +02:00
parent b31eccbc21
commit c32ea55ba3
9 changed files with 106 additions and 16 deletions
@@ -24,16 +24,19 @@ import type { AgentDefinition, SingleResult } from "@oh-my-pi/pi-coding-agent/ta
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
import { EventBus } from "@oh-my-pi/pi-coding-agent/utils/event-bus";
function yieldEmittingSession(): AgentSession {
function yieldEmittingSession(initialTools: string[] = ["read", "yield"]): AgentSession {
const listeners: Array<(event: AgentSessionEvent) => void> = [];
let activeTools = initialTools;
const session = {
state: { messages: [] },
agent: { state: { systemPrompt: ["test"] } },
model: undefined,
extensionRunner: undefined,
sessionManager: { appendSessionInit: () => {} },
getActiveToolNames: () => ["read", "yield"],
setActiveToolsByName: async (_toolNames: string[]) => {},
getActiveToolNames: () => activeTools,
setActiveToolsByName: async (toolNames: string[]) => {
activeTools = toolNames;
},
subscribe: (listener: (event: AgentSessionEvent) => void) => {
listeners.push(listener);
return () => {
@@ -206,6 +209,36 @@ describe("runSubprocess per-agent prewalk", () => {
expect(result.exitCode).toBe(0);
expect(spy.mock.calls[0]?.[0]?.prewalk).toBeUndefined();
});
it("keeps the todo tool active for a prewalk-armed subagent (the todo gate needs it)", async () => {
const session = yieldEmittingSession(["read", "todo", "yield"]);
vi.spyOn(sdkModule, "createAgentSession").mockResolvedValue(createSessionResult(session));
const result = await runSubprocess({
...baseOptions("subagent-prewalk-todo-kept", Settings.isolated()),
agent: {
...baseAgent,
model: [`${primary.provider}/${primary.id}`],
prewalk: `${target.provider}/${target.id}`,
},
});
expect(result.exitCode).toBe(0);
expect(session.getActiveToolNames()).toContain("todo");
});
it("strips the parent-owned todo tool from non-prewalk subagents", async () => {
const session = yieldEmittingSession(["read", "todo", "yield"]);
vi.spyOn(sdkModule, "createAgentSession").mockResolvedValue(createSessionResult(session));
const result = await runSubprocess({
...baseOptions("subagent-no-prewalk-todo-stripped", Settings.isolated()),
agent: { ...baseAgent, model: [`${primary.provider}/${primary.id}`] },
});
expect(result.exitCode).toBe(0);
expect(session.getActiveToolNames()).not.toContain("todo");
expect(session.getActiveToolNames()).toContain("read");
});
});
// Plan-mode spawns are read-only exploration: the task tool must strip a
// prewalk-enabled agent definition before spawning so the hidden