Problem
I'm using bash-tool with Workflow DevKit's DurableAgent, where:
- Tool definitions (descriptions, schemas) are declared in a "use workflow" orchestrator, a deterministic and restricted context with no Node.js native module access.
- Tool execution happens in separate "use step" functions which are isolated serverless invocations with full Node.js access
createBashTool() requires a resolved Sandbox instance and performs tool discovery at construction time. This makes it unusable in my orchestrator context.
The persistent sandbox pattern solves reconnecting across stateless serverless invocations where you rebuild everything from scratch each time. It doesn't help when tool construction and tool execution are in fundamentally different execution contexts.
The custom sandbox implementation pattern is a great direction, but it still requires going through createBashTool(), which I can't call in my restricted context.
I've had to reimplement the tool logic manually, losing access to the rich generateDescription() output, the cwd handling, etc.
Solution
The individual tool creators (createBashExecuteTool, createReadFileTool, createWriteFileTool) already exist internally, they just build an AI SDK tool() object from a Sandbox interface and a cwd. Exporting them would let me combine them with a custom sandbox proxy that reconnects in durable steps.
Here's how this would work in a Workflow DevKit DurableAgent setup:
workflow.ts
// workflow.ts — runs in "use workflow" (deterministic, no Node.js I/O)
import { DurableAgent } from '@workflow/ai/agent';
import { createBashExecuteTool, createReadFileTool, createWriteFileTool } from 'bash-tool';
import type { Sandbox } from 'bash-tool';
export async function chatWorkflow(payload) {
"use workflow";
const { sandboxId } = await prepareContext(payload); // "use step" — returns serializable IDs
// Custom sandbox proxy — each method delegates to a "use step" function
// that reconnects via Sandbox.get({ sandboxId }) inside the step.
// No I/O happens here; the proxy is just a plain object with functions.
const sandbox: Sandbox = {
executeCommand: (cmd) => bashStep({ command: cmd, sandboxId }),
readFile: (path) => readFileStep({ path, sandboxId }),
writeFiles: (files) => writeFilesStep({ files, sandboxId }),
};
// Tool creation is synchronous — builds descriptions and schemas only.
// Safe in "use workflow" because no I/O, no Node.js native modules.
const tools = {
bash: createBashExecuteTool({ sandbox, cwd: '/workspace' }),
readFile: createReadFileTool({ sandbox, cwd: '/workspace' }),
writeFile: createWriteFileTool({ sandbox, cwd: '/workspace' }),
};
const agent = new DurableAgent({ model, tools });
await agent.stream({ messages, writable: getWritable(), maxSteps: 50 });
}
steps.ts
// steps.ts — runs in "use step" (full Node.js access, retryable, isolated)
import { Sandbox } from '@vercel/sandbox';
export async function bashStep({ command, sandboxId }) {
"use step";
// Reconnects to the existing sandbox — near-instant on a warm VM.
const sandbox = await Sandbox.get({ sandboxId });
const result = await sandbox.runCommand('bash', ['-c', command]);
return { stdout: await result.stdout(), stderr: await result.stderr(), exitCode: result.exitCode };
}
export async function readFileStep({ path, sandboxId }) {
"use step";
const sandbox = await Sandbox.get({ sandboxId });
const content = await sandbox.readFile({ path });
return content;
}
export async function writeFilesStep({ files, sandboxId }) {
"use step";
const sandbox = await Sandbox.get({ sandboxId });
await sandbox.writeFiles(files);
}
Each tool call becomes its own short-lived, retryable step and, without much added latency. This same pattern applies to any durable execution framework where only serializable data crosses invocation boundaries like Inngest (step.run), Temporal (activities), Trigger.dev (tasks), AWS Step Functions (Lambda invocations).
Proposed change
Re-export the three existing functions from index.ts to the Public API:
export { createBashExecuteTool } from "./tools/bash.js";
export { createReadFileTool } from "./tools/read-file.js";
export { createWriteFileTool } from "./tools/write-file.js";
Today, the only entry point is createBashTool(), which couples three concerns into one call:
- Sandbox resolution: requires a live Sandbox instance (Node.js I/O)
- File uploading & tool discovery: async operations against the sandbox
- Tool creation: building AI SDK tool() objects with descriptions and schemas
In durable execution frameworks, 3 must happen in a restricted orchestrator context (no I/O), while 1 and 2 must happen in isolated worker steps (full I/O). Today there's no way to split createBashTool() across these boundaries.
Note
createReadFileTool and createWriteFileTool import node:path at the module level. In restricted environments like Workflow DevKit's "use workflow", Node.js core modules including path are blocked. Maybe replacing the single nodePath.posix.resolve() call with an inline implementation would make all three creators usable in these contexts?
Problem
I'm using bash-tool with Workflow DevKit's DurableAgent, where:
createBashTool() requires a resolved Sandbox instance and performs tool discovery at construction time. This makes it unusable in my orchestrator context.
The persistent sandbox pattern solves reconnecting across stateless serverless invocations where you rebuild everything from scratch each time. It doesn't help when tool construction and tool execution are in fundamentally different execution contexts.
The custom sandbox implementation pattern is a great direction, but it still requires going through createBashTool(), which I can't call in my restricted context.
I've had to reimplement the tool logic manually, losing access to the rich generateDescription() output, the cwd handling, etc.
Solution
The individual tool creators (createBashExecuteTool, createReadFileTool, createWriteFileTool) already exist internally, they just build an AI SDK tool() object from a Sandbox interface and a cwd. Exporting them would let me combine them with a custom sandbox proxy that reconnects in durable steps.
Here's how this would work in a Workflow DevKit DurableAgent setup:
workflow.ts
steps.ts
Each tool call becomes its own short-lived, retryable step and, without much added latency. This same pattern applies to any durable execution framework where only serializable data crosses invocation boundaries like Inngest (step.run), Temporal (activities), Trigger.dev (tasks), AWS Step Functions (Lambda invocations).
Proposed change
Re-export the three existing functions from index.ts to the Public API:
Today, the only entry point is createBashTool(), which couples three concerns into one call:
In durable execution frameworks, 3 must happen in a restricted orchestrator context (no I/O), while 1 and 2 must happen in isolated worker steps (full I/O). Today there's no way to split createBashTool() across these boundaries.
Note
createReadFileTool and createWriteFileTool import node:path at the module level. In restricted environments like Workflow DevKit's "use workflow", Node.js core modules including path are blocked. Maybe replacing the single nodePath.posix.resolve() call with an inline implementation would make all three creators usable in these contexts?