Skip to content

Commit f39cebc

Browse files
feat(anthropic): support bash tool
1 parent f74479a commit f39cebc

File tree

6 files changed

+365
-0
lines changed

6 files changed

+365
-0
lines changed

libs/providers/langchain-anthropic/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,67 @@ const response2 = await llm.invoke(
506506

507507
For more information, see [Anthropic's Code Execution Tool documentation](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/code-execution-tool).
508508

509+
### Bash Tool
510+
511+
The bash tool (`bash_20250124`) enables shell command execution in a persistent bash session. Unlike the sandboxed code execution tool, this tool requires you to provide your own execution environment.
512+
513+
> **⚠️ Security Warning:** The bash tool provides direct system access. Implement safety measures such as running in isolated environments (Docker/VM), command filtering, and resource limits.
514+
515+
The bash tool provides:
516+
517+
- **Persistent bash session** - Maintains state between commands
518+
- **Shell command execution** - Run any shell command
519+
- **Environment access** - Access to environment variables and working directory
520+
- **Command chaining** - Support for pipes, redirects, and scripting
521+
522+
Available commands:
523+
524+
- Execute a command: `{ command: "ls -la" }`
525+
- Restart the session: `{ restart: true }`
526+
527+
```typescript
528+
import {
529+
ChatAnthropic,
530+
tools,
531+
type Bash20250124Command,
532+
} from "@langchain/anthropic";
533+
import { execSync } from "child_process";
534+
535+
const llm = new ChatAnthropic({
536+
model: "claude-sonnet-4-5-20250929",
537+
});
538+
539+
const bash = tools.bash_20250124({
540+
execute: async (args: Bash20250124Command) => {
541+
if (args.restart) {
542+
// Reset session state
543+
return "Bash session restarted";
544+
}
545+
try {
546+
const output = execSync(args.command!, {
547+
encoding: "utf-8",
548+
timeout: 30000,
549+
});
550+
return output;
551+
} catch (error) {
552+
return `Error: ${(error as Error).message}`;
553+
}
554+
},
555+
});
556+
557+
const llmWithBash = llm.bindTools([bash]);
558+
559+
const response = await llmWithBash.invoke(
560+
"List all Python files in the current directory"
561+
);
562+
563+
// Process tool calls and execute commands
564+
console.log(response.tool_calls?.[0].name); // "bash"
565+
console.log(response.tool_calls?.[0].args.command); // "ls -la *.py"
566+
```
567+
568+
For more information, see [Anthropic's Bash Tool documentation](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/bash-tool).
569+
509570
## Development
510571

511572
To develop the Anthropic package, you'll need to follow these instructions:
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { tool } from "@langchain/core/tools";
2+
import type { DynamicStructuredTool, ToolRuntime } from "@langchain/core/tools";
3+
4+
import type { Bash20250124Command } from "./types.js";
5+
6+
/**
7+
* Options for the bash tool.
8+
*/
9+
export interface Bash20250124Options {
10+
/**
11+
* Optional execute function that handles bash command execution.
12+
* This function receives the command input and should return the result
13+
* (stdout and stderr combined, or an error message).
14+
*/
15+
execute?: (args: Bash20250124Command) => string | Promise<string>;
16+
}
17+
18+
/**
19+
* Creates an Anthropic bash tool for Claude 4 models and Claude 3.7 that enables
20+
* shell command execution in a persistent bash session.
21+
*
22+
* The bash tool provides Claude with:
23+
* - **Persistent bash session**: Maintains state between commands
24+
* - **Shell command execution**: Run any shell command
25+
* - **Environment access**: Access to environment variables and working directory
26+
* - **Command chaining**: Support for pipes, redirects, and scripting
27+
*
28+
* Available commands:
29+
* - Execute a command: `{ command: "ls -la" }`
30+
* - Restart the session: `{ restart: true }`
31+
*
32+
* @warning The bash tool provides direct system access. Implement safety measures
33+
* such as running in isolated environments (Docker/VM), command filtering,
34+
* and resource limits.
35+
*
36+
* @example
37+
* ```typescript
38+
* import { ChatAnthropic, tools } from "@langchain/anthropic";
39+
* import { execSync } from "child_process";
40+
*
41+
* const llm = new ChatAnthropic({
42+
* model: "claude-sonnet-4-5-20250929",
43+
* });
44+
*
45+
* const bash = tools.bash_20250124({
46+
* execute: async (args) => {
47+
* if (args.restart) {
48+
* // Reset session state
49+
* return "Bash session restarted";
50+
* }
51+
* try {
52+
* const output = execSync(args.command!, {
53+
* encoding: "utf-8",
54+
* timeout: 30000,
55+
* });
56+
* return output;
57+
* } catch (error) {
58+
* return `Error: ${error.message}`;
59+
* }
60+
* },
61+
* });
62+
*
63+
* const llmWithBash = llm.bindTools([bash]);
64+
* const response = await llmWithBash.invoke(
65+
* "List all Python files in the current directory"
66+
* );
67+
*
68+
* // Outputs: "bash"
69+
* console.log(response.tool_calls[0].name);
70+
* // Outputs: "ls -la *.py 2>/dev/null || echo \"No Python files found in the current directory\"
71+
* console.log(response.tool_calls[0].args.command);
72+
* ```
73+
*
74+
* @example Multi-step automation
75+
* ```typescript
76+
* // Claude can chain commands in a persistent session:
77+
* // 1. cd /tmp
78+
* // 2. echo "Hello" > test.txt
79+
* // 3. cat test.txt // Works because we're still in /tmp
80+
* ```
81+
*
82+
* @param options - Configuration options for the bash tool
83+
* @param options.execute - Function that handles bash command execution
84+
* @returns The bash tool object that can be passed to `bindTools`
85+
*
86+
* @see https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/bash-tool
87+
*/
88+
export function bash_20250124(
89+
options?: Bash20250124Options
90+
): DynamicStructuredTool {
91+
const name = "bash";
92+
const bashTool = tool(
93+
options?.execute as (
94+
input: unknown,
95+
runtime: ToolRuntime<unknown, unknown>
96+
) => string | Promise<string>,
97+
{
98+
name,
99+
schema: {
100+
type: "object",
101+
properties: {
102+
command: {
103+
type: "string",
104+
description: "The bash command to run",
105+
},
106+
restart: {
107+
type: "boolean",
108+
description: "Set to true to restart the bash session",
109+
},
110+
},
111+
},
112+
}
113+
);
114+
115+
bashTool.extras = {
116+
...(bashTool.extras ?? {}),
117+
providerToolDefinition: {
118+
type: "bash_20250124",
119+
name,
120+
},
121+
};
122+
123+
return bashTool;
124+
}

libs/providers/langchain-anthropic/src/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { textEditor_20250728 } from "./textEditor.js";
99
import { computer_20251124, computer_20250124 } from "./computer.js";
1010
import { codeExecution_20250825 } from "./codeExecution.js";
11+
import { bash_20250124 } from "./bash.js";
1112

1213
export const tools = {
1314
memory_20250818,
@@ -19,6 +20,7 @@ export const tools = {
1920
computer_20251124,
2021
computer_20250124,
2122
codeExecution_20250825,
23+
bash_20250124,
2224
};
2325

2426
export type * from "./types.js";
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { expect, it, describe } from "vitest";
2+
import { AIMessage, HumanMessage } from "@langchain/core/messages";
3+
import { execSync } from "child_process";
4+
5+
import { ChatAnthropic } from "../../chat_models.js";
6+
import { bash_20250124 } from "../bash.js";
7+
8+
const createModel = () =>
9+
new ChatAnthropic({
10+
model: "claude-sonnet-4-5",
11+
temperature: 0,
12+
});
13+
14+
describe("Anthropic Bash Tool Integration Tests", () => {
15+
it("bash tool can be bound to ChatAnthropic and triggers tool use", async () => {
16+
const llm = createModel();
17+
18+
const bash = bash_20250124({
19+
execute: async (args) => {
20+
if (args.restart) {
21+
return "Bash session restarted";
22+
}
23+
try {
24+
const output = execSync(args.command!, {
25+
encoding: "utf-8",
26+
timeout: 10000,
27+
});
28+
return output;
29+
} catch (error) {
30+
return `Error: ${(error as Error).message}`;
31+
}
32+
},
33+
});
34+
35+
const llmWithBash = llm.bindTools([bash]);
36+
37+
const response = await llmWithBash.invoke([
38+
new HumanMessage("What is 2+2? Use bash to calculate it with echo."),
39+
]);
40+
41+
expect(AIMessage.isInstance(response)).toBe(true);
42+
expect(response.tool_calls?.[0]).toEqual(
43+
expect.objectContaining({
44+
name: "bash",
45+
type: "tool_call",
46+
args: expect.objectContaining({
47+
command: "echo $((2+2))",
48+
}),
49+
})
50+
);
51+
52+
const result = await bash.invoke({
53+
command: response.tool_calls?.[0]?.args?.command,
54+
});
55+
expect(result).toBe("4\n");
56+
}, 60000);
57+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { expect, it, describe } from "vitest";
2+
import { bash_20250124 } from "../bash.js";
3+
4+
describe("Anthropic Bash Tool Unit Tests", () => {
5+
describe("bash_20250124", () => {
6+
it("creates a valid bash tool with no options", () => {
7+
const bash = bash_20250124();
8+
9+
expect(bash.name).toBe("bash");
10+
expect(bash.extras?.providerToolDefinition).toMatchInlineSnapshot(`
11+
{
12+
"name": "bash",
13+
"type": "bash_20250124",
14+
}
15+
`);
16+
});
17+
18+
it("creates a valid bash tool with execute function", async () => {
19+
const mockExecute = async (args: {
20+
command?: string;
21+
restart?: boolean;
22+
}) => {
23+
if (args.restart) {
24+
return "Session restarted";
25+
}
26+
return `Executed: ${args.command}`;
27+
};
28+
29+
const bash = bash_20250124({
30+
execute: mockExecute,
31+
});
32+
33+
expect(bash.name).toBe("bash");
34+
expect(bash.func).toBeDefined();
35+
});
36+
37+
it("has correct schema for bash commands", () => {
38+
const bash = bash_20250124();
39+
40+
expect(bash.schema).toMatchInlineSnapshot(`
41+
{
42+
"properties": {
43+
"command": {
44+
"description": "The bash command to run",
45+
"type": "string",
46+
},
47+
"restart": {
48+
"description": "Set to true to restart the bash session",
49+
"type": "boolean",
50+
},
51+
},
52+
"type": "object",
53+
}
54+
`);
55+
});
56+
57+
it("can execute a command", async () => {
58+
let executedCommand: string | undefined;
59+
60+
const bash = bash_20250124({
61+
execute: async (args) => {
62+
if (args.command) {
63+
executedCommand = args.command;
64+
return "command output";
65+
}
66+
return "no command";
67+
},
68+
});
69+
70+
const result = await bash.invoke({ command: "ls -la" });
71+
72+
expect(result).toBe("command output");
73+
expect(executedCommand).toBe("ls -la");
74+
});
75+
76+
it("can restart the session", async () => {
77+
let wasRestarted = false;
78+
79+
const bash = bash_20250124({
80+
execute: async (args) => {
81+
if (args.restart) {
82+
wasRestarted = true;
83+
return "Bash session restarted";
84+
}
85+
return "command executed";
86+
},
87+
});
88+
89+
const result = await bash.invoke({ restart: true });
90+
91+
expect(result).toBe("Bash session restarted");
92+
expect(wasRestarted).toBe(true);
93+
});
94+
});
95+
});

libs/providers/langchain-anthropic/src/tools/types.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,29 @@ export interface ComputerZoomAction {
196196
/** Coordinates [x1, y1, x2, y2] defining top-left and bottom-right corners */
197197
region: [number, number, number, number];
198198
}
199+
200+
/**
201+
* Bash tool command types for Claude 4 models and Claude 3.7.
202+
* @see https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/bash-tool
203+
*/
204+
export type Bash20250124Command =
205+
| Bash20250124ExecuteCommand
206+
| Bash20250124RestartCommand;
207+
208+
/**
209+
* Execute a bash command.
210+
*/
211+
export interface Bash20250124ExecuteCommand {
212+
/** The bash command to run */
213+
command: string;
214+
restart?: never;
215+
}
216+
217+
/**
218+
* Restart the bash session to reset state.
219+
*/
220+
export interface Bash20250124RestartCommand {
221+
command?: never;
222+
/** Set to true to restart the bash session */
223+
restart: true;
224+
}

0 commit comments

Comments
 (0)