diff --git a/crates/bashkit-js/__test__/ai-adapters.spec.ts b/crates/bashkit-js/__test__/ai-adapters.spec.ts new file mode 100644 index 00000000..304a5c8c --- /dev/null +++ b/crates/bashkit-js/__test__/ai-adapters.spec.ts @@ -0,0 +1,110 @@ +import test from "ava"; +import { bashTool as aiSdkBashTool } from "../ai.js"; +import { bashTool as anthropicBashTool } from "../anthropic.js"; +import { bashTool as openAiBashTool } from "../openai.js"; + +// ============================================================================ +// Issue #990: AI adapters must use a single interpreter instance. +// Files written via the exposed `bash` handle must be visible to tool +// execution, and vice versa — no state divergence. +// ============================================================================ + +// --- Vercel AI SDK adapter --------------------------------------------------- + +test("ai: files option is readable via execute", async (t) => { + const adapter = aiSdkBashTool({ files: { "/data.txt": "hello" } }); + const result = await adapter.tools.bash.execute({ commands: "cat /data.txt" }); + t.is(result, "hello"); +}); + +test("ai: files written via bash.writeFile are visible in execute", async (t) => { + const adapter = aiSdkBashTool(); + adapter.bash.writeFile("/x.txt", "from-api"); + const result = await adapter.tools.bash.execute({ commands: "cat /x.txt" }); + t.is(result, "from-api"); +}); + +test("ai: files created via execute are readable via bash.readFile", async (t) => { + const adapter = aiSdkBashTool(); + await adapter.tools.bash.execute({ commands: "echo -n created > /y.txt" }); + t.is(adapter.bash.readFile("/y.txt"), "created"); +}); + +// --- Anthropic SDK adapter --------------------------------------------------- + +test("anthropic: files option is readable via handler", async (t) => { + const adapter = anthropicBashTool({ files: { "/data.txt": "hello" } }); + const result = await adapter.handler({ + type: "tool_use", + id: "t1", + name: "bash", + input: { commands: "cat /data.txt" }, + }); + t.is(result.content, "hello"); + t.false(result.is_error); +}); + +test("anthropic: files written via bash.writeFile are visible in handler", async (t) => { + const adapter = anthropicBashTool(); + adapter.bash.writeFile("/x.txt", "from-api"); + const result = await adapter.handler({ + type: "tool_use", + id: "t2", + name: "bash", + input: { commands: "cat /x.txt" }, + }); + t.is(result.content, "from-api"); +}); + +test("anthropic: files created via handler are readable via bash.readFile", async (t) => { + const adapter = anthropicBashTool(); + await adapter.handler({ + type: "tool_use", + id: "t3", + name: "bash", + input: { commands: "echo -n created > /y.txt" }, + }); + t.is(adapter.bash.readFile("/y.txt"), "created"); +}); + +// --- OpenAI SDK adapter ------------------------------------------------------ + +test("openai: files option is readable via handler", async (t) => { + const adapter = openAiBashTool({ files: { "/data.txt": "hello" } }); + const result = await adapter.handler({ + id: "c1", + type: "function", + function: { + name: "bash", + arguments: JSON.stringify({ commands: "cat /data.txt" }), + }, + }); + t.is(result.content, "hello"); +}); + +test("openai: files written via bash.writeFile are visible in handler", async (t) => { + const adapter = openAiBashTool(); + adapter.bash.writeFile("/x.txt", "from-api"); + const result = await adapter.handler({ + id: "c2", + type: "function", + function: { + name: "bash", + arguments: JSON.stringify({ commands: "cat /x.txt" }), + }, + }); + t.is(result.content, "from-api"); +}); + +test("openai: files created via handler are readable via bash.readFile", async (t) => { + const adapter = openAiBashTool(); + await adapter.handler({ + id: "c3", + type: "function", + function: { + name: "bash", + arguments: JSON.stringify({ commands: "echo -n created > /y.txt" }), + }, + }); + t.is(adapter.bash.readFile("/y.txt"), "created"); +}); diff --git a/crates/bashkit-js/ai.ts b/crates/bashkit-js/ai.ts index 38191cfb..b2ef4923 100644 --- a/crates/bashkit-js/ai.ts +++ b/crates/bashkit-js/ai.ts @@ -27,7 +27,7 @@ * @packageDocumentation */ -import { Bash, BashTool } from "./wrapper.js"; +import { BashTool } from "./wrapper.js"; import type { BashOptions, ExecResult } from "./wrapper.js"; // Vercel AI SDK tool types — we define them inline to avoid requiring @@ -60,8 +60,8 @@ export interface BashToolAdapter { system: string; /** Tool definitions for Vercel AI SDK's generateText/streamText. */ tools: Record; - /** The underlying Bash instance for direct access. */ - bash: Bash; + /** The underlying BashTool instance for direct access. */ + bash: BashTool; } function formatOutput(result: ExecResult): string { @@ -104,8 +104,7 @@ function formatOutput(result: ExecResult): string { export function bashTool(options?: BashToolOptions): BashToolAdapter { const { files, ...bashOptions } = options ?? {}; - const bashToolInstance = new BashTool(bashOptions); - const bash = new Bash(bashOptions); + const bash = new BashTool(bashOptions); if (files) { for (const [path, content] of Object.entries(files)) { @@ -113,11 +112,11 @@ export function bashTool(options?: BashToolOptions): BashToolAdapter { } } - const system = bashToolInstance.systemPrompt(); + const system = bash.systemPrompt(); const tools: Record = { bash: { - description: bashToolInstance.description(), + description: bash.description(), parameters: { type: "object", properties: { diff --git a/crates/bashkit-js/anthropic.ts b/crates/bashkit-js/anthropic.ts index ab5d3f7b..cbde1294 100644 --- a/crates/bashkit-js/anthropic.ts +++ b/crates/bashkit-js/anthropic.ts @@ -31,7 +31,7 @@ * @packageDocumentation */ -import { Bash, BashTool } from "./wrapper.js"; +import { BashTool } from "./wrapper.js"; import type { BashOptions, ExecResult } from "./wrapper.js"; /** Options for configuring the bash tool adapter. */ @@ -75,8 +75,8 @@ export interface BashToolAdapter { tools: AnthropicTool[]; /** Handler that executes a tool_use block and returns a tool_result. */ handler: (toolUse: ToolUseBlock) => Promise; - /** The underlying Bash instance for direct access. */ - bash: Bash; + /** The underlying BashTool instance for direct access. */ + bash: BashTool; } function formatOutput(result: ExecResult): string { @@ -118,8 +118,7 @@ function formatOutput(result: ExecResult): string { export function bashTool(options?: BashToolOptions): BashToolAdapter { const { files, ...bashOptions } = options ?? {}; - const bashToolInstance = new BashTool(bashOptions); - const bash = new Bash(bashOptions); + const bash = new BashTool(bashOptions); // Pre-populate VFS files if (files) { @@ -128,12 +127,12 @@ export function bashTool(options?: BashToolOptions): BashToolAdapter { } } - const system = bashToolInstance.systemPrompt(); + const system = bash.systemPrompt(); const tools: AnthropicTool[] = [ { name: "bash", - description: bashToolInstance.description(), + description: bash.description(), input_schema: { type: "object", properties: { diff --git a/crates/bashkit-js/openai.ts b/crates/bashkit-js/openai.ts index 02741045..ef375451 100644 --- a/crates/bashkit-js/openai.ts +++ b/crates/bashkit-js/openai.ts @@ -30,7 +30,7 @@ * @packageDocumentation */ -import { Bash, BashTool } from "./wrapper.js"; +import { BashTool } from "./wrapper.js"; import type { BashOptions, ExecResult } from "./wrapper.js"; /** Options for configuring the bash tool adapter. */ @@ -78,8 +78,8 @@ export interface BashToolAdapter { tools: OpenAITool[]; /** Handler that executes a tool_call and returns a tool message. */ handler: (toolCall: OpenAIToolCall) => Promise; - /** The underlying Bash instance for direct access. */ - bash: Bash; + /** The underlying BashTool instance for direct access. */ + bash: BashTool; } function formatOutput(result: ExecResult): string { @@ -122,8 +122,7 @@ function formatOutput(result: ExecResult): string { export function bashTool(options?: BashToolOptions): BashToolAdapter { const { files, ...bashOptions } = options ?? {}; - const bashToolInstance = new BashTool(bashOptions); - const bash = new Bash(bashOptions); + const bash = new BashTool(bashOptions); if (files) { for (const [path, content] of Object.entries(files)) { @@ -131,14 +130,14 @@ export function bashTool(options?: BashToolOptions): BashToolAdapter { } } - const system = bashToolInstance.systemPrompt(); + const system = bash.systemPrompt(); const tools: OpenAITool[] = [ { type: "function", function: { name: "bash", - description: bashToolInstance.description(), + description: bash.description(), parameters: { type: "object", properties: {