Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/agent-spawn/src/agent-spawn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,19 @@ describe("buildSpawnArgs", () => {
expect(result.args).not.toContain("test");
});

it("builds stdin args for codex when the prompt is too large for argv", () => {
const prompt = "a".repeat(64 * 1024 + 1);
const result = buildSpawnArgs("codex", { prompt });

expect(result.args).toEqual([
codexSpawnConfig.promptFlag,
...codexSpawnConfig.stdinMode!.extraArgs,
...codexSpawnConfig.defaultArgs,
...codexSpawnConfig.modes.yolo
]);
expect(result.args).not.toContain(prompt);
});

it("ignores useStdin for agents without stdinMode", () => {
const result = buildSpawnArgs("opencode", { prompt: "hello", useStdin: true });

Expand Down Expand Up @@ -743,6 +756,31 @@ describe("spawn", () => {
expect(child.__capturedStdin()).toBe("hi");
});

it("automatically writes large prompts to stdin when supported", async () => {
const prompt = "a".repeat(64 * 1024 + 1);
const spawnMock = vi
.mocked(spawnChildProcess)
.mockReturnValue(createMockChildProcess({ stdout: "ok\n", exitCode: 0 }));

await spawn("codex", { prompt });

expect(spawnMock).toHaveBeenCalledTimes(1);
const [command, args, spawnOptions] = spawnMock.mock.calls[0]!;
expect(command).toBe("codex");
expect(args).toEqual([
codexSpawnConfig.promptFlag,
...(codexSpawnConfig.stdinMode?.extraArgs ?? []),
...codexSpawnConfig.defaultArgs,
...codexSpawnConfig.modes.yolo
]);
expect(spawnOptions).toMatchObject({
stdio: ["pipe", "pipe", "pipe"]
});

const child = spawnMock.mock.results[0]?.value as any;
expect(child.__capturedStdin()).toBe(prompt);
});

it("forwards output to tee streams when provided", async () => {
vi.mocked(spawnChildProcess).mockReturnValue(
createMockChildProcess({ stdout: "agent output", stderr: "agent progress", exitCode: 0 })
Expand Down
21 changes: 19 additions & 2 deletions packages/agent-spawn/src/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
type StdinMode
} from "./types.js";

const PROMPT_STDIN_FALLBACK_BYTES = 64 * 1024;

function createAbortError(): Error {
const error = new Error("Agent spawn aborted");
error.name = "AbortError";
Expand Down Expand Up @@ -79,6 +81,21 @@ function getMcpArgsPosition(
return config.mcpArgsBeforeCommand ? "beforeCommand" : "afterCommand";
}

function resolveStdinMode(
config: CliSpawnConfig,
options: Pick<BuildSpawnArgsOptions, "prompt" | "useStdin">
): StdinMode | undefined {
if (!config.stdinMode) {
return undefined;
}
if (options.useStdin) {
return config.stdinMode;
}
return Buffer.byteLength(options.prompt, "utf8") > PROMPT_STDIN_FALLBACK_BYTES
? config.stdinMode
: undefined;
}

function buildCliArgs(
config: CliSpawnConfig,
options: BuildSpawnArgsOptions,
Expand Down Expand Up @@ -169,7 +186,7 @@ export function buildSpawnArgs(
options: BuildSpawnArgsOptions
): BuildSpawnArgsResult {
const { binaryName, spawnConfig } = resolveCliConfig(agentId);
const stdinMode = options.useStdin && spawnConfig.stdinMode ? spawnConfig.stdinMode : undefined;
const stdinMode = resolveStdinMode(spawnConfig, options);
const result = buildCliArgs(spawnConfig, options, stdinMode);
return { binaryName, args: result.args, env: result.env };
}
Expand All @@ -185,7 +202,7 @@ export async function spawn(

const { agentId: resolvedId, binaryName, spawnConfig } = resolveCliConfig(agentId);

const stdinMode = options.useStdin && spawnConfig.stdinMode ? spawnConfig.stdinMode : undefined;
const stdinMode = resolveStdinMode(spawnConfig, options);

const { args: spawnArgs, env: modeEnv } = buildCliArgs(spawnConfig, options, stdinMode);

Expand Down
Loading