BackgroundShells
C# · package Toolnexus · SPEC §4A · BuiltinExtras.cs
public sealed class BackgroundShells{ public List<BackgroundShellInfo> List(); public Task KillAsync(string id); public Task CloseAllAsync(); public void CloseAll();}
public sealed record BackgroundShellInfo(string Id, string Command, DateTimeOffset Started, bool Running, int ExitCode, bool Killed);
// Turned on through the builtins config:new Dictionary<string, object?> { ["background"] = new BackgroundShells() }Opt-in. With a BackgroundShells under the builtins config key background:
bashgainsrun_in_background: true, which returns at once withMetadata["backgroundId"].bash_output {id}returns the output produced since the last read, then[status: running|killed|exited with code N](Metadata["status"],Metadata["exitCode"]). N is-1for a shell killed by a signal it did not get fromkill_shell.kill_shell {id}stops the whole process group: SIGTERM,killGraceMs(default 2000), SIGKILL.
A background command is not tied to the turn that started it, so the host owns the registry and
calls CloseAll on shutdown. POSIX only. The other opt-in builtins, webSearch and
notebookEdit, are on BuiltinTools.Create.
When to use it
Section titled “When to use it”A coding agent that starts a dev server, a watcher or a long build and keeps working while it runs, checking the output later.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. Off by default, two more tools when on
Section titled “1. Off by default, two more tools when on”using Toolnexus;
var plainNames = BuiltinTools.Create().Select(t => t.Name).ToList();if (plainNames.Contains("bash_output")) throw new Exception("background tools must be opt-in");
var bg = new BackgroundShells();var names = BuiltinTools.Create(new Dictionary<string, object?> { ["background"] = bg }).Select(t => t.Name).ToList();if (!names.Contains("bash_output") || !names.Contains("kill_shell")) throw new Exception(string.Join(",", names));bg.CloseAll();Console.WriteLine($"ok: {plainNames.Count} default builtins, {names.Count} with background shells");2. Start a command, read its output as it arrives
Section titled “2. Start a command, read its output as it arrives”using Toolnexus;
var bg = new BackgroundShells();try{ var tools = BuiltinTools.Create(new Dictionary<string, object?> { ["background"] = bg }); var bash = tools.Single(t => t.Name == "bash"); var output = tools.Single(t => t.Name == "bash_output");
var started = await bash.ExecuteAsync(new Dictionary<string, object?> { ["command"] = "echo one; sleep 0.2; echo two", ["run_in_background"] = true }); var id = (string)started.Metadata!["backgroundId"]!;
var seen = ""; var deadline = DateTime.UtcNow.AddSeconds(5); while (true) { var o = await output.ExecuteAsync(new Dictionary<string, object?> { ["id"] = id }); seen += o.Output.Split("\n[status:")[0]; if ((string)o.Metadata!["status"]! == "exited") break; if (DateTime.UtcNow > deadline) throw new Exception("never exited"); await Task.Delay(50); } if (!seen.Contains("one") || !seen.Contains("two")) throw new Exception(seen); Console.WriteLine($"ok: {id} produced {seen.Replace("\n", " ").Trim()}");}finally { bg.CloseAll(); }3. Kill a stubborn job, and close everything on shutdown
Section titled “3. Kill a stubborn job, and close everything on shutdown”using Toolnexus;
var bg = new BackgroundShells();var tools = BuiltinTools.Create(new Dictionary<string, object?> { ["background"] = bg, ["killGraceMs"] = 100L });var bash = tools.Single(t => t.Name == "bash");var kill = tools.Single(t => t.Name == "kill_shell");
// Ignores SIGTERM and has a child: kill_shell still stops the whole group after killGraceMs.var r = await bash.ExecuteAsync(new Dictionary<string, object?> { ["command"] = "trap '' TERM; sleep 30 & wait", ["run_in_background"] = true });var id = (string)r.Metadata!["backgroundId"]!;await Task.Delay(100);var k = await kill.ExecuteAsync(new Dictionary<string, object?> { ["id"] = id });if (k.Metadata!["killed"] is not true) throw new Exception(k.Output);
await bash.ExecuteAsync(new Dictionary<string, object?> { ["command"] = "sleep 30", ["run_in_background"] = true });await bg.CloseAllAsync(); // what a host calls on shutdownif (bg.List().Any(j => j.Running)) throw new Exception("still running after CloseAll");Console.WriteLine($"ok: {bg.List().Count} jobs, none running");See also
Section titled “See also”BuiltinTools.Select— Pick which built-in file/shell/web tools are in play, by name or by source toggle.BuiltinTools.Create— Construct every built-in tool directly, outside a toolkit.