Skip to content

BackgroundShells

Java · package io.github.muthuishere:toolnexus · SPEC §4A

public final class BackgroundShells implements AutoCloseable {
public BackgroundShells()
public List<Info> list()
public void kill(String id)
public void closeAll() // also close()
public record Info(String id, String command, Instant started,
boolean running, int exitCode, boolean killed) {}
}
// pass it as the `background` key of the builtins config
Map.of("background", shells, "killGraceMs", 2000)

A host-owned registry of commands started by bash with run_in_background: true. With it set:

  • bash gains run_in_background and returns at once with a background id (also in metadata.backgroundId).
  • bash_output {id} returns the output produced since the last read plus [status: running|killed|exited with code N]. N is -1 when the shell was killed by a signal not sent by kill_shell (on the JVM a command that itself exits 129..159 also reads as -1).
  • kill_shell {id} stops the whole process group: SIGTERM, killGraceMs, SIGKILL.

Without it, the default ten builtins are unchanged. A background command is NOT tied to the turn that started it: call closeAll() (or use try-with-resources) when the host shuts down. POSIX only.

A coding agent that starts a dev server, a watcher or a long test run and keeps working while it runs, checking its output later.

1. The smallest useful call — the tools appear

Section titled “1. The smallest useful call — the tools appear”
import io.github.muthuishere.toolnexus.*;
import java.util.List;
import java.util.Map;
public class Example {
public static void main(String[] args) {
try (BackgroundShells bg = new BackgroundShells()) {
List<String> names = BuiltinTools.create(Map.of("background", bg)).stream().map(Tool::name).toList();
if (!names.contains("bash_output") || !names.contains("kill_shell")) throw new AssertionError(names);
List<String> plain = BuiltinTools.create(Map.of()).stream().map(Tool::name).toList();
if (plain.contains("bash_output")) throw new AssertionError("must be opt-in");
System.out.println("ok: " + names.size() + " builtins with background shells");
}
}
}

2. The realistic case — start, poll, finish

Section titled “2. The realistic case — start, poll, finish”
import io.github.muthuishere.toolnexus.*;
import java.util.List;
import java.util.Map;
public class Example {
static Tool find(List<Tool> ts, String n) { return ts.stream().filter(t -> t.name().equals(n)).findFirst().orElseThrow(); }
public static void main(String[] args) throws Exception {
try (BackgroundShells bg = new BackgroundShells()) {
List<Tool> tools = BuiltinTools.create(Map.of("background", bg));
ToolResult started = find(tools, "bash").execute(
Map.of("command", "echo building; sleep 0.2; echo built", "run_in_background", true), null);
String id = (String) started.metadata().get("backgroundId");
String out = "";
for (int i = 0; i < 100 && !out.contains("exited"); i++) {
Thread.sleep(50);
out += find(tools, "bash_output").execute(Map.of("id", id), null).output();
}
if (!out.contains("built") || !out.contains("[status: exited with code 0]")) throw new AssertionError(out);
System.out.println("ok: " + id + " exited 0");
}
}
}

3. The full surface — kill_shell, list and closeAll

Section titled “3. The full surface — kill_shell, list and closeAll”
import io.github.muthuishere.toolnexus.*;
import java.util.List;
import java.util.Map;
public class Example {
static Tool find(List<Tool> ts, String n) { return ts.stream().filter(t -> t.name().equals(n)).findFirst().orElseThrow(); }
public static void main(String[] args) throws Exception {
BackgroundShells bg = new BackgroundShells();
try {
List<Tool> tools = BuiltinTools.create(Map.of("background", bg, "killGraceMs", 100));
String id = (String) find(tools, "bash").execute(
Map.of("command", "sleep 30", "run_in_background", true), null).metadata().get("backgroundId");
if (!bg.list().get(0).running()) throw new AssertionError("should be running");
find(tools, "kill_shell").execute(Map.of("id", id), null);
String status = find(tools, "bash_output").execute(Map.of("id", id), null).output();
if (!status.contains("killed")) throw new AssertionError(status);
System.out.println("ok: " + status.trim());
} finally {
bg.closeAll(); // the host owns the lifetime of every background command
}
}
}
  • 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.