Quick Start
SpadeBox provides a set of sandboxed tools for AI agents: file system access, HTTP fetching, and a JavaScript REPL. This page shows how to get up and running in TypeScript, Python, and Rust.
Installation
- TypeScript
- Python
- Rust
npm install @spadebox/spadebox
uv add spadebox
cargo add spadebox
Creating a SpadeBox Instance
Start by creating a SpadeBox and enabling the desired tools. All tools are disabled by default and require explicit opt-in.
- TypeScript
- Python
- Rust
const sb = new SpadeBox()
sb.enableFiles(sandboxPath)
.enableJs()
.enableHttp()
.allow('*', ['GET', 'HEAD'])
sb = SpadeBox()
sb.enable_files(sandbox_path).enable_js().enable_http().allow("*", ["GET", "HEAD"])
let sb = SpadeBox::new()
.enable_files(&sandbox_path)?
.enable_js()
.enable_http()
.allow("*", &["GET", "HEAD"])?;
Listing Available Tools
SpadeBox exposes tool metadata in the format expected by LLM tool-calling APIs (name, description, and a JSON Schema for parameters).
For instance, to create an OpenAI API-compatible tool list:
- TypeScript
- Python
- Rust
const tools = sb.tools().map((t) => ({
type: 'function' as const,
function: {
name: t.name,
description: t.description,
parameters: JSON.parse(t.inputSchema),
},
}))
tools = [
{
"type": "function",
"function": {
"name": t.name,
"description": t.description,
"parameters": json.loads(t.input_schema),
},
}
for t in sb.tools()
]
let tools: Value = sb
.tools()
.into_iter()
.map(|t| {
serde_json::json!({
"type": "function",
"function": {
"name": t.name,
"description": t.description,
"parameters": serde_json::from_str::<Value>(&t.input_schema).unwrap(),
},
})
})
.collect();
Dispatching Tool Calls
When the API returns a tool call, the tool name and JSON-encoded parameters can be passed directly to call_tool. The result tells you whether the call succeeded and contains the tool's output.
- TypeScript
- Python
- Rust
const result = await sb.callTool('read_file', JSON.stringify({ path: 'hello.txt' }))
assert(!result.isError)
assertEquals(result.output, 'hi from callTool')
result = sb.call_tool('read_file', json.dumps({'path': 'hello.txt'}))
assert not result.is_error
assert result.output == 'hi from call_tool'
let result = sb
.call_tool("read_file", r#"{"path":"hello.txt"}"#)
.await
.unwrap();
assert!(!result.is_error);
assert_eq!(result.output, "hi from call_tool");
Agent Loop
Here is a minimal agent loop that connects SpadeBox to an LLM and drives a multi-turn conversation with tool use:
- TypeScript
- Python
- Rust
async function runTurn(messages: Message[]): Promise<void> {
while (true) {
const response = await chat(messages)
messages.push({ role: 'assistant', content: response.content, tool_calls: response.tool_calls })
if (!response.tool_calls?.length) {
if (response.content) console.log(`\n${CYAN}Agent:${RESET} ${response.content}\n`)
return
}
for (const call of response.tool_calls) {
const { name, arguments: args } = call.function
console.log(`\n${BLUE}[call]${RESET} ${GRAY}${name}(${args})${RESET}`)
const result = await sb.callTool(name, args)
const tag = result.isError ? `${RED}[error]${RESET}` : `${GREEN}[ok]${RESET}`
console.log(`${tag} ${GRAY}${result.output}${RESET}`)
messages.push({ role: 'tool', tool_call_id: call.id, content: result.output })
}
}
}
def run_turn(messages: list[dict[str, Any]]) -> None:
while True:
response = chat(messages)
messages.append(
{
"role": "assistant",
"content": response.get("content"),
"tool_calls": response.get("tool_calls"),
}
)
tool_calls = response.get("tool_calls") or []
if not tool_calls:
content = response.get("content")
if content:
print(f"\n{CYAN}Agent:{RESET} {content}\n")
return
for call in tool_calls:
name = call["function"]["name"]
args = call["function"]["arguments"]
print(f"\n{BLUE}[call]{RESET} {GRAY}{name}({args}){RESET}")
result = sb.call_tool(name, args)
tag = f"{RED}[error]{RESET}" if result.is_error else f"{GREEN}[ok]{RESET}"
print(f"{tag} {GRAY}{result.output}{RESET}")
messages.append(
{"role": "tool", "tool_call_id": call["id"], "content": result.output}
)
async fn run_turn(
sb: &SpadeBox,
client: &reqwest::Client,
messages: &mut Vec<Message>,
tools: &Value,
) -> anyhow::Result<()> {
loop {
let response = chat(client, messages, tools).await?;
let tool_calls = match &response {
Message::Assistant { tool_calls, .. } => tool_calls.clone(),
_ => None,
};
messages.push(response.clone());
let calls = match tool_calls {
Some(calls) if !calls.is_empty() => calls,
_ => {
if let Message::Assistant {
content: Some(text),
..
} = response
{
println!("\n{CYAN}Agent:{RESET} {text}\n");
}
return Ok(());
}
};
for call in calls {
let name = &call.function.name;
let args = &call.function.arguments;
println!("\n{BLUE}[call]{RESET} {GRAY}{name}({args}){RESET}");
let result = sb.call_tool(name, args).await?;
let tag = if result.is_error {
format!("{RED}[error]{RESET}")
} else {
format!("{GREEN}[ok]{RESET}")
};
println!("{tag} {GRAY}{}{RESET}", result.output);
messages.push(Message::Tool {
tool_call_id: call.id.clone(),
content: result.output,
});
}
}
}