BackgroundShells
Go · package github.com/muthuishere/toolnexus/golang · SPEC §4A · golang/builtin_extras.go
func NewBackgroundShells() *BackgroundShellsfunc (b *BackgroundShells) List() []BackgroundShellInfofunc (b *BackgroundShells) Kill(id string) errorfunc (b *BackgroundShells) CloseAll()
type BackgroundShellInfo struct { ID, Command string Started time.Time Running bool ExitCode int // valid when !Running; -1 if killed by a signal Killed bool}
// BuiltinsConfig.Background *BackgroundShells — nil ⇒ 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,KillGraceMs, then SIGKILL).
A background command is not tied to the turn that started it. The host owns the registry: call
CloseAll 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”package main
import ( "context" "fmt" "log" "strings" "time"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { bg := toolnexus.NewBackgroundShells() defer bg.CloseAll()
tk, err := toolnexus.CreateToolkit(context.Background(), toolnexus.Options{ Builtins: toolnexus.BuiltinsConfig{Background: bg}, }) if err != nil { log.Fatal(err) } defer tk.Close() ctx := context.Background()
started, err := tk.Execute(ctx, "bash", map[string]any{"command": "echo hello", "run_in_background": true}) if err != nil { log.Fatal(err) } id := started.Metadata["backgroundId"].(string)
for i := 0; i < 50 && bg.List()[0].Running; i++ { time.Sleep(20 * time.Millisecond) } out, err := tk.Execute(ctx, "bash_output", map[string]any{"id": id}) if err != nil || !strings.Contains(out.Output, "hello") || !strings.Contains(out.Output, "[status: exited with code 0]") { log.Fatalf("%v %q", err, out.Output) } fmt.Printf("ok: %s -> %q\n", id, out.Output)}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”package main
import ( "context" "fmt" "log" "strings"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { bg := toolnexus.NewBackgroundShells() defer bg.CloseAll()
tk, err := toolnexus.CreateToolkit(context.Background(), toolnexus.Options{ Builtins: toolnexus.BuiltinsConfig{Background: bg, KillGraceMs: 100}, }) if err != nil { log.Fatal(err) } defer tk.Close() ctx := context.Background()
started, _ := tk.Execute(ctx, "bash", map[string]any{"command": "sleep 30", "run_in_background": true}) id := started.Metadata["backgroundId"].(string)
running, _ := tk.Execute(ctx, "bash_output", map[string]any{"id": id}) if !strings.Contains(running.Output, "[status: running]") { log.Fatalf("expected running, got %q", running.Output) } killed, _ := tk.Execute(ctx, "kill_shell", map[string]any{"id": id}) if killed.Output != "Killed background shell "+id { log.Fatalf("unexpected kill output %q", killed.Output) } fmt.Println("ok:", killed.Output)}3. The full surface — the host’s view with List, Kill and CloseAll
Section titled “3. The full surface — the host’s view with List, Kill and CloseAll”package main
import ( "fmt" "log"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { bg := toolnexus.NewBackgroundShells() tools := toolnexus.CreateBuiltinToolsWith(toolnexus.BuiltinsConfig{Background: bg, KillGraceMs: 100})
names := map[string]bool{} var bash toolnexus.Tool for _, t := range tools { names[t.Name] = true if t.Name == "bash" { bash = t } } if !names["bash_output"] || !names["kill_shell"] { log.Fatal("bash_output and kill_shell should be added") }
for _, cmd := range []string{"sleep 30", "sleep 31"} { if _, err := bash.Execute(map[string]any{"command": cmd, "run_in_background": true}, nil); err != nil { log.Fatal(err) } } list := bg.List() if len(list) != 2 || !list[0].Running { log.Fatalf("expected two running shells: %+v", list) } if err := bg.Kill(list[0].ID); err != nil { // the host's own stop button log.Fatal(err) } bg.CloseAll() // on shutdown: stops everything still running for _, s := range bg.List() { if s.Running { log.Fatalf("%s still running after CloseAll", s.ID) } } fmt.Println("ok:", len(list), "shells started, all stopped")}See also
Section titled “See also”CreateBuiltinTools— the ten default builtins and the other opt-ins.SelectBuiltins— choose which builtins a toolkit gets.Hooks—bashis hinted destructive and open-world; gate it inBeforeTool.