From e64ca7c583161674ac041589121dc0923895f2ea Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 12 Dec 2025 17:46:37 +0000 Subject: [PATCH] fix(agent): send tau rpc prompt as string --- src/process/tau-rpc.test.ts | 81 +++++++++++++++++++++++++++++++++++++ src/process/tau-rpc.ts | 9 ++--- 2 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 src/process/tau-rpc.test.ts diff --git a/src/process/tau-rpc.test.ts b/src/process/tau-rpc.test.ts new file mode 100644 index 000000000..e0c3cbdd5 --- /dev/null +++ b/src/process/tau-rpc.test.ts @@ -0,0 +1,81 @@ +import { EventEmitter } from "node:events"; +import { PassThrough } from "node:stream"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { resetPiRpc, runPiRpc } from "./tau-rpc.js"; + +vi.mock("node:child_process", () => { + const spawn = vi.fn(); + return { spawn }; +}); + +type MockChild = EventEmitter & { + stdin: EventEmitter & { + write: (chunk: string, cb?: (err?: Error | null) => void) => boolean; + once: (event: "drain", listener: () => void) => unknown; + }; + stdout: PassThrough; + stderr: PassThrough; + killed: boolean; + kill: (signal?: NodeJS.Signals) => boolean; +}; + +function makeChild(): MockChild { + const child = new EventEmitter() as MockChild; + const stdin = new EventEmitter() as MockChild["stdin"]; + stdin.write = (_chunk: string, cb?: (err?: Error | null) => void) => { + cb?.(null); + return true; + }; + child.stdin = stdin; + child.stdout = new PassThrough(); + child.stderr = new PassThrough(); + child.killed = false; + child.kill = () => { + child.killed = true; + return true; + }; + return child; +} + +describe("tau-rpc", () => { + afterEach(() => { + resetPiRpc(); + vi.resetAllMocks(); + }); + + it("sends prompt with string message", async () => { + const { spawn } = await import("node:child_process"); + const child = makeChild(); + vi.mocked(spawn).mockReturnValue(child as never); + + const writes: string[] = []; + child.stdin.write = (chunk: string, cb?: (err?: Error | null) => void) => { + writes.push(String(chunk)); + cb?.(null); + return true; + }; + + const run = runPiRpc({ + argv: ["tau", "--mode", "rpc"], + cwd: "/tmp", + timeoutMs: 500, + prompt: "hello", + }); + + // Allow the async `prompt()` to install the pending resolver before exiting. + await Promise.resolve(); + + expect(writes.length).toBeGreaterThan(0); + child.emit("exit", 0, null); + const res = await run; + + expect(res.code).toBe(0); + expect(writes.length).toBeGreaterThan(0); + const first = writes[0]?.trim(); + expect(first?.endsWith("\n")).toBe(false); + const obj = JSON.parse(first ?? "{}") as { type?: string; message?: unknown }; + expect(obj.type).toBe("prompt"); + expect(obj.message).toBe("hello"); + }); +}); diff --git a/src/process/tau-rpc.ts b/src/process/tau-rpc.ts index 22c37f033..1e11ed7c0 100644 --- a/src/process/tau-rpc.ts +++ b/src/process/tau-rpc.ts @@ -221,11 +221,10 @@ class TauRpcClient { const ok = child.stdin.write( `${JSON.stringify({ type: "prompt", - // Send structured content to match tau RPC expectations and avoid - // empty-text bugs on older builds. - message: { - content: [{ type: "text", text: prompt }], - }, + // Pi/Tau RPC expects a plain string prompt. + // (The structured { content: [{type:"text", text}] } shape is used by some + // model APIs, but is not the RPC wire format here.) + message: prompt, })}\n`, (err) => (err ? reject(err) : resolve()), );