fix(pi): load subagent extension in child sessions #39

Merged
alexion merged 1 commits from fix-subagent-nested-tools into main 2026-08-01 19:35:50 -04:00
2 changed files with 71 additions and 1 deletions

View File

@@ -0,0 +1,65 @@
import assert from "node:assert/strict";
import childProcess from "node:child_process";
import { EventEmitter } from "node:events";
import { fileURLToPath } from "node:url";
import test from "node:test";
import type { RunnerEvents } from "./types.ts";
class FakeStream extends EventEmitter {
setEncoding(_encoding: BufferEncoding): void {}
write(_chunk: string, callback?: (error?: Error | null) => void): boolean {
callback?.();
return true;
}
end(): void {}
}
function events(): RunnerEvents {
return {
accepted: () => {},
running: () => {},
settling: () => {},
completed: () => {},
failed: () => {},
};
}
test("child RPC process disables discovery while explicitly loading subagents extension", async (t) => {
const calls: Array<{ command: string; args: string[] }> = [];
const fakeChild = new EventEmitter() as EventEmitter & {
stdout: FakeStream;
stderr: FakeStream;
stdin: FakeStream;
killed: boolean;
pid?: number;
kill(signal?: NodeJS.Signals): boolean;
};
fakeChild.stdout = new FakeStream();
fakeChild.stderr = new FakeStream();
fakeChild.stdin = new FakeStream();
fakeChild.killed = false;
fakeChild.kill = () => {
fakeChild.killed = true;
return true;
};
const spawn = t.mock.method(childProcess, "spawn", (command, args) => {
calls.push({ command: String(command), args: Array.isArray(args) ? args.map(String) : [] });
return fakeChild as unknown as childProcess.ChildProcessWithoutNullStreams;
});
const { SubprocessRpcRunner } = await import("./runner.ts");
const runner = new SubprocessRpcRunner();
await runner.start("child-1", { prompt: "work" }, "/tmp", events());
assert.equal(spawn.mock.callCount(), 1);
const args = calls[0].args;
const noExtensionsIndex = args.indexOf("--no-extensions");
const extensionIndex = args.indexOf("--extension");
assert.notEqual(noExtensionsIndex, -1, "child args keep automatic extension discovery disabled");
assert.notEqual(extensionIndex, -1, "child args explicitly load the subagents extension entry");
assert.equal(args[extensionIndex + 1], fileURLToPath(new URL("./index.ts", import.meta.url)));
assert.ok(noExtensionsIndex < extensionIndex);
});

View File

@@ -1,4 +1,5 @@
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { fileURLToPath } from "node:url";
import type { ChildHandle, ChildRunner, RunnerEvents, SpawnRequest } from "./types.ts";
interface PendingResponse {
@@ -156,7 +157,7 @@ class RpcChildHandle implements ChildHandle {
export class SubprocessRpcRunner implements ChildRunner {
async start(id: string, request: SpawnRequest, cwd: string, events: RunnerEvents): Promise<ChildHandle> {
const args = [process.argv[1], "--mode", "rpc", "--no-extensions", "--name", `subagent ${id}`, ...contextArgs(request), ...toolArgs(request), ...modelArgs(request)];
const args = [process.argv[1], "--mode", "rpc", "--no-extensions", "--extension", subagentsExtensionPath(), "--name", `subagent ${id}`, ...contextArgs(request), ...toolArgs(request), ...modelArgs(request)];
const child = spawn(process.execPath, args, {
cwd,
env: childEnvironment(),
@@ -174,6 +175,10 @@ function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function subagentsExtensionPath(): string {
return fileURLToPath(new URL("./index.ts", import.meta.url));
}
function contextArgs(request: SpawnRequest): string[] {
if (request.context !== "fork" || !request.parentSessionFile) return [];
return ["--fork", request.parentSessionFile];