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 long_running: true or background: true.
Supported stdlib operations:
walk_dir(path, {long_running: true, ...})glob(pattern, base?, {long_running: true})glob(pattern, {base: "...", long_running: true})find_text(root, pattern, {long_running: true, ...})
Supported host tools:
tools.run_commandtools.run_testtools.run_build_command
std/command is the script-facing foreground command-step layer. It can pass
background-related request fields through to the hostlib command runner and will
surface the returned handle_id, planned output_path, and live-log artifact
when the host returns a running snapshot. It does not wait on background handles
itself; completion feedback and cancellation remain hostlib/agent-loop
responsibilities. A dedicated Harn-facing background wait primitive can be added
later if scripts need that lifecycle without involving an agent loop.
Handle envelope
A long-running call returns immediately with a handle envelope:
let handle = walk_dir(".", {long_running: 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
long_running: trueorbackground: 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() {
let handle = walk_dir(".", {long_running: true})
defer {
host_tool_call("cancel_handle", {handle_id: handle.handle_id})
}
agent_loop("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() {
let handle = walk_dir(".", {long_running: true})
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.