BackgroundShells
Rust · crate toolnexus · SPEC §4A · rust/src/builtins/bash.rs
impl BackgroundShells { // toolnexus::builtins pub fn new() -> Self pub fn list(&self) -> Vec<BackgroundShellInfo> // oldest first pub async fn kill(&self, id: &str) -> Result<(), String> pub async fn close_all(&self)}
pub struct BackgroundShellInfo { pub id: String, pub command: String, pub started: SystemTime, pub running: bool, pub exit_code: i64, // valid when !running; -1 if killed by a signal pub killed: bool,}
// BuiltinsConfig.background: Option<Arc<BackgroundShells>> — None ⇒ none of this exists.Pass a registry as BuiltinsConfig.background and the builtins change in three ways:
bashgainsrun_in_background: true: it returns at once withStarted background shell <id>. …(metadata["backgroundId"]). No timeout applies.bash_output {id}returns the output produced since the last read, then[status: running|killed|exited with code N]. N is-1when the shell was killed by a signal it did not get fromkill_shell.kill_shell {id}stops the whole process group (SIGTERM,kill_grace_ms, then SIGKILL).
A background command is not tied to the turn that started it. The host owns the registry: call
close_all on shutdown, or the commands outlive the process that started them.
When to use it
Section titled “When to use it”A coding agent that starts a dev server, a watcher or a long test run and keeps working while it runs — then checks the output, or stops it.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — start, wait, read
Section titled “1. The smallest useful call — start, wait, read”use std::sync::Arc;use std::time::Duration;use serde_json::json;use toolnexus::builtins::BackgroundShells;use toolnexus::{BuiltinsConfig, ToolContext, ToolkitOptions, create_toolkit};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let bg = Arc::new(BackgroundShells::new()); let tk = create_toolkit(ToolkitOptions { builtins: Some(BuiltinsConfig { background: Some(bg.clone()), ..Default::default() }), ..Default::default() }) .await?;
let started = tk .execute("bash", json!({"command": "echo hello", "run_in_background": true}), ToolContext::default()) .await; assert!(!started.is_error, "{}", started.output); let id = started.metadata.as_ref().unwrap()["backgroundId"].as_str().unwrap().to_owned();
for _ in 0..100 { if !bg.list()[0].running { break; } tokio::time::sleep(Duration::from_millis(20)).await; } let out = tk.execute("bash_output", json!({"id": id}), ToolContext::default()).await; assert!(out.output.contains("hello"), "{}", out.output); assert!(out.output.contains("[status: exited with code 0]"), "{}", out.output);
bg.close_all().await; tk.close().await; println!("ok: {id} -> {:?}", out.output); Ok(())}2. The realistic case — a long-running command, stopped with kill_shell
Section titled “2. The realistic case — a long-running command, stopped with kill_shell”use std::sync::Arc;use serde_json::json;use toolnexus::builtins::BackgroundShells;use toolnexus::{BuiltinsConfig, ToolContext, ToolkitOptions, create_toolkit};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let bg = Arc::new(BackgroundShells::new()); let tk = create_toolkit(ToolkitOptions { builtins: Some(BuiltinsConfig { background: Some(bg.clone()), kill_grace_ms: 100, ..Default::default() }), ..Default::default() }) .await?;
let started = tk .execute("bash", json!({"command": "sleep 30", "run_in_background": true}), ToolContext::default()) .await; let id = started.metadata.as_ref().unwrap()["backgroundId"].as_str().unwrap().to_owned();
let running = tk.execute("bash_output", json!({"id": id}), ToolContext::default()).await; assert!(running.output.contains("[status: running]"), "{}", running.output);
let killed = tk.execute("kill_shell", json!({"id": id}), ToolContext::default()).await; assert_eq!(killed.output, format!("Killed background shell {id}"));
bg.close_all().await; tk.close().await; println!("ok: {}", killed.output); Ok(())}3. The full surface — the host’s view with list, kill and close_all
Section titled “3. The full surface — the host’s view with list, kill and close_all”use std::sync::Arc;use serde_json::json;use toolnexus::builtins::{BackgroundShells, create_builtin_tools};use toolnexus::{BuiltinsConfig, ToolContext};
#[tokio::main]async fn main() { let bg = Arc::new(BackgroundShells::new()); let tools = create_builtin_tools(&BuiltinsConfig { background: Some(bg.clone()), kill_grace_ms: 100, ..Default::default() });
let names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect(); assert!(names.contains(&"bash_output") && names.contains(&"kill_shell")); let bash = tools.iter().find(|t| t.name == "bash").unwrap();
for cmd in ["sleep 30", "sleep 31"] { let r = bash.call(json!({"command": cmd, "run_in_background": true}), ToolContext::default()).await; assert!(!r.is_error, "{}", r.output); } let list = bg.list(); assert_eq!(list.len(), 2); assert!(list[0].running);
bg.kill(&list[0].id).await.unwrap(); // the host's own stop button assert!(bg.kill("bg-nope").await.is_err()); bg.close_all().await; // on shutdown: stops everything still running assert!(bg.list().iter().all(|s| !s.running && s.killed));
println!("ok: {} shells started, all stopped", list.len());}See also
Section titled “See also”create_builtin_tools— the ten default builtins and the other opt-ins.select_builtins— choose which builtins a toolkit gets.