Skip to content

BackgroundShells

JavaScript · package toolnexus · SPEC §4A · js/src/builtin_extras.ts

class BackgroundShells {
list(): BackgroundShellInfo[] // oldest first
killShell(id: string): Promise<void>
closeAll(): Promise<void> // call on shutdown
}
createToolkit({ builtins: { background: new BackgroundShells(), killGraceMs?: number, … } })
// bash { command, run_in_background: true } → metadata.backgroundId
// bash_output { id } → new output since the last read + "[status: running|killed|exited with code N]"
// kill_shell { id } → SIGTERM the process group, killGraceMs, then SIGKILL

Opt-in. With background set, bash gains run_in_background (returns at once with an id, no timeout applies) and two tools appear: bash_output (incremental output plus status; metadata has status and exitCode) and kill_shell (stops the whole process group). The host owns the registry, so it can list and kill shells from its own UI, and must call closeAll() on shutdown. A shell killed by a signal it did not get from kill_shell reports exited with code -1. POSIX process groups; on Windows only the direct child is stopped.

The same config takes the other opt-in builtins: webSearch (a WebSearchFunc the host supplies; adds websearch) and notebookEdit: true (adds notebook_edit) — see createBuiltinTools.

A coding agent that starts a dev server, a watcher or a long build and keeps working while it runs.

1. The smallest useful call — start, read, finish

Section titled “1. The smallest useful call — start, read, finish”
import assert from "node:assert"
import { BackgroundShells, createBuiltinTools } from "toolnexus"
const bg = new BackgroundShells()
const tools = createBuiltinTools({ background: bg })
const bash = tools.find((t) => t.name === "bash")!, out = tools.find((t) => t.name === "bash_output")!
try {
const r = await bash.execute({ command: "echo hello", run_in_background: true })
const id = r.metadata?.backgroundId as string
let o
for (let i = 0; i < 100; i++) {
o = await out.execute({ id })
if (o.metadata?.status === "exited") break
await new Promise((res) => setTimeout(res, 50))
}
assert.equal(o!.metadata?.exitCode, 0)
assert.match(o!.output, /hello/)
console.log("ok:", id, o!.metadata?.status)
} finally {
await bg.closeAll()
}

2. The realistic case — kill a server that ignores SIGTERM

Section titled “2. The realistic case — kill a server that ignores SIGTERM”
import assert from "node:assert"
import { BackgroundShells, createBuiltinTools } from "toolnexus"
const bg = new BackgroundShells()
const tools = createBuiltinTools({ background: bg, killGraceMs: 100 })
const pick = (n: string) => tools.find((t) => t.name === n)!
try {
const r = await pick("bash").execute({ command: "trap '' TERM; sleep 30 & wait", run_in_background: true })
const id = r.metadata?.backgroundId as string
await new Promise((res) => setTimeout(res, 100))
const k = await pick("kill_shell").execute({ id })
assert.equal(k.metadata?.killed, true)
assert.equal((await pick("bash_output").execute({ id })).metadata?.status, "killed")
console.log("ok: killed", id)
} finally {
await bg.closeAll()
}

3. The full surface — the host’s view, and what stays off by default

Section titled “3. The full surface — the host’s view, and what stays off by default”
import assert from "node:assert"
import { BackgroundShells, createBuiltinTools } from "toolnexus"
// Default builtins: no background tools, no run_in_background.
const plain = createBuiltinTools().map((t) => t.name)
assert.ok(!plain.includes("bash_output") && !plain.includes("kill_shell"))
const bg = new BackgroundShells()
const bash = createBuiltinTools({ background: bg }).find((t) => t.name === "bash")!
try {
await bash.execute({ command: "sleep 5", run_in_background: true })
const [info] = bg.list()
assert.equal(info.running, true)
await bg.killShell(info.id) // the host can stop it from its own UI
assert.equal(bg.list()[0].running, false)
console.log("ok:", info.id, info.command)
} finally {
await bg.closeAll()
}
  • selectBuiltins — Pick which built-in file/shell/web tools are in play, by name or by source toggle.
  • createBuiltinTools — Construct every built-in tool directly, outside a toolkit.