Long-running tools
Long-running tool handles let a script start slow work, continue the agent
loop, and receive the final result through the pending feedback queue on a
later turn. The idiom is the same for host command tools and stdlib operations
that support background: true.
Supported stdlib operations:
harness.fs.walk(path, {background: true, ...})harness.fs.glob(pattern, base?, {background: true})harness.fs.glob(pattern, {base: "...", background: true})harness.fs.find_text(root, pattern, {background: true, ...})harness.fs.find_evidence(roots, patterns, {background: true, ...})
Supported host tools:
tools.run_commandtools.run_testtools.run_build_command
std/command is also the script-facing command lifecycle. command_run passes
background request fields through to the host and returns the handle and planned
output artifacts. command_wait joins completion,
command_wait_for_output parks until output matches, the process exits, or a
deadline expires, and command_cancel tears down the owned process group. The
output wait subscribes to capture publication; it does not poll artifact files.
import {
command_cancel, command_run, command_wait_for_output,
} from "std/command"
fn run_service(tools: HarnessTools) {
const service = command_run(tools, ["my-service"], {background: true})
defer { command_cancel(tools, service, {wait_result_ms: 5000}) }
const ready = command_wait_for_output(tools, service, "listening", {
source: "stderr",
timeout_ms: 10000,
})
if !ready.matched {
throw "service did not become ready: " + ready.status
}
}
Process lifecycle#
Foreground command tools (run_command without background, plus
run_test, run_build_command, manage_packages, and the VM-side
harness.process.exec / shell / exec_opts methods) tie their subprocess to
the invoking scope. The child runs in its own process group; when the
invoking scope is cancelled, a deadline expires, or the VM is dropped,
the whole group — grandchildren included — receives SIGTERM and, after a
2-second grace period, SIGKILL. The tool response reports
status: "killed", and the pipeline observes the usual cancellation or
Deadline exceeded error. Group semantics are Unix-first; on Windows the
runtime kills the direct child (best effort) instead.
Work that must outlive the invoking scope belongs in a background handle:
background: true children are exempt
from scope cancellation and deadlines. They are owned by the handle store
and die only through tools.cancel_handle or agent-session-end cleanup,
as described below.
Handle envelope#
A long-running call returns immediately with a handle envelope:
const handle = harness.fs.walk(".", {background: true})
The returned dict includes:
{
handle_id: string,
started_at: string,
status: "running",
command_or_op_descriptor: string
}
Command tools also include command-specific fields such as command_id, pid,
planned output paths, and sandbox metadata.
Lifecycle#
- Spawn the operation with
background: true. - Save
handle_id. - Let the agent loop poll normally. Background workers push a
tool_resultitem to the pending feedback queue when they complete. - Cancel abandoned work with
tools.cancel_handle. - Rely on session-end cleanup only as a backstop. When an agent-loop session ends, registered resource managers cancel remaining handles for that session.
Correct cleanup#
Use defer or finally so early returns and thrown errors still release the
handle when the script no longer needs the result.
pipeline main(harness: Harness) {
const handle = harness.fs.walk(".", {background: true})
defer {
harness.tools.invoke("cancel_handle", {handle_id: handle.handle_id})
}
agent_loop(
harness, "Summarize the repository while the file walk runs.", nil, {
tools: ["read_file"],
})
}
When the operation finishes before the cleanup path runs, cancellation returns
cancelled: false; that is expected because the handle has already left the
in-flight store and its result has been queued.
Incorrect lifecycle#
This starts background work but has no cleanup path if the pipeline exits early:
pipeline main(harness: Harness) {
const handle = harness.fs.walk(".", {background: true})
harness.stdio.log(handle.handle_id)
}
harn lint warns for this shape with long-running-without-cleanup. Add a
defer or finally block that calls tools.cancel_handle.
Feedback shape#
Completed stdlib operations enqueue a tool_result payload like:
{
"handle_id": "hso-...",
"status": "completed",
"operation": "walk_dir",
"command_or_op_descriptor": "walk_dir /repo",
"started_at": "2026-04-30T12:00:00Z",
"ended_at": "2026-04-30T12:00:01Z",
"duration_ms": 1000,
"result": []
}
Failed operations use status: "failed" and include error.