Skip to content
Merged
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
110 changes: 110 additions & 0 deletions crates/bashkit-js/__test__/ai-adapters.spec.ts
Original file line number Diff line number Diff line change
@@ -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");
});
13 changes: 6 additions & 7 deletions crates/bashkit-js/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -60,8 +60,8 @@ export interface BashToolAdapter {
system: string;
/** Tool definitions for Vercel AI SDK's generateText/streamText. */
tools: Record<string, AiTool>;
/** The underlying Bash instance for direct access. */
bash: Bash;
/** The underlying BashTool instance for direct access. */
bash: BashTool;
}

function formatOutput(result: ExecResult): string {
Expand Down Expand Up @@ -104,20 +104,19 @@ 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)) {
bash.writeFile(path, content);
}
}

const system = bashToolInstance.systemPrompt();
const system = bash.systemPrompt();

const tools: Record<string, AiTool> = {
bash: {
description: bashToolInstance.description(),
description: bash.description(),
parameters: {
type: "object",
properties: {
Expand Down
13 changes: 6 additions & 7 deletions crates/bashkit-js/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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<ToolResult>;
/** The underlying Bash instance for direct access. */
bash: Bash;
/** The underlying BashTool instance for direct access. */
bash: BashTool;
}

function formatOutput(result: ExecResult): string {
Expand Down Expand Up @@ -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) {
Expand All @@ -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: {
Expand Down
13 changes: 6 additions & 7 deletions crates/bashkit-js/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -78,8 +78,8 @@ export interface BashToolAdapter {
tools: OpenAITool[];
/** Handler that executes a tool_call and returns a tool message. */
handler: (toolCall: OpenAIToolCall) => Promise<ToolResult>;
/** The underlying Bash instance for direct access. */
bash: Bash;
/** The underlying BashTool instance for direct access. */
bash: BashTool;
}

function formatOutput(result: ExecResult): string {
Expand Down Expand Up @@ -122,23 +122,22 @@ 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)) {
bash.writeFile(path, content);
}
}

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: {
Expand Down
Loading