11 KiB
11 KiB
job
Wait for or cancel background jobs managed by the session async runtime.
Source
- Entry:
packages/coding-agent/src/tools/job.ts - Model-facing prompt:
packages/coding-agent/src/prompts/tools/job.md - Key collaborators:
packages/coding-agent/src/async/job-manager.ts— job registry, cancellation, delivery suppression.packages/coding-agent/src/async/support.ts— feature gating for background jobs.packages/coding-agent/src/internal-urls/jobs-protocol.ts—jobs://listing and per-job detail.packages/coding-agent/src/tools/bash.ts— explicit async bash and auto-backgrounded bash jobs.packages/coding-agent/src/task/index.ts— async task-job scheduling.packages/coding-agent/src/sdk.ts— automatic follow-up delivery for unsuppressed completions.packages/coding-agent/src/config/settings-schema.ts—async.pollWaitDurationoptions.
Inputs
| Field | Type | Required | Description |
|---|---|---|---|
poll |
string[] |
No | Job ids to watch. If omitted and cancel is also omitted, the tool watches all running jobs. If provided, missing ids are silently filtered out before waiting. |
cancel |
string[] |
No | Job ids to cancel before any polling. Missing ids are reported as not_found; non-running ids as already_completed. |
Outputs
The tool returns one text block plus details.
content[0].text: markdown-like plain text sections assembled by#buildResult(...):## Cancelled (N)for cancel outcomes.## Completed (N)for non-running jobs, including storedresultTextanderrorText.## Still Running (N)for jobs still inrunning.
details.jobs: array of snapshots:id: stringtype: "bash" | "task"status: "running" | "completed" | "failed" | "cancelled"label: stringdurationMs: number- optional
resultText,errorText
details.cancelledappears only whencancelwas passed; each item is{ id, status }where status is"cancelled" | "not_found" | "already_completed".
Streaming behavior:
- During a polling wait,
execute(...)emitsonUpdate(...)every 500 ms with an empty text block and freshdetails.jobssnapshots. - Final return is single-shot after a completion, timeout, abort, or immediate fast path.
Related read path:
read jobs://lists all current jobs.read jobs://<id>renders one job with status, label, start time, duration, and stored result/error text.
Flow
JobTool.createIf(...)inpackages/coding-agent/src/tools/job.tsonly exposes the tool whenisBackgroundJobSupportEnabled(...)returns true for eitherasync.enabledorbash.autoBackground.enabled.execute(...)fetchessession.asyncJobManager. If absent, it returnsAsync execution is disabled; no background jobs are available.cancelids are processed first:manager.getJob(id)missing →not_found.- existing job with
status !== "running"→already_completed. - running job →
manager.cancel(id), which setsjob.status = "cancelled", aborts the controller, and schedules eviction.
- Polling mode is chosen with
const shouldPoll = requestedPollIds !== undefined || cancelIds.length === 0:- only
cancelpresent → return immediately, no wait. - explicit
poll, or no args at all → proceed to watch jobs.
- only
- Watch set resolution:
- explicit
poll→ map ids throughmanager.getJob(...)and drop missing ones. - no
polland nocancel→manager.getRunningJobs().
- explicit
- Empty watch set returns immediately:
- if cancellations happened, return snapshots for the cancelled ids that still exist.
- else return either
No matching jobs found for IDs: ...orNo running background jobs to wait for.
- If every watched job is already non-running,
#buildResult(...)returns immediately without waiting. - Otherwise the tool waits on
Promise.race(...)across:- every watched running job's
job.promise, - a timeout promise for
async.pollWaitDuration, - the tool-call abort signal when present.
- every watched running job's
- Before waiting, it calls
manager.watchJobs(watchedJobIds). This suppresses automatic completion delivery for those ids while they are being watched. - If
onUpdateexists, a 500 ms interval sends progress snapshots from#snapshotJobs(...); one snapshot is emitted immediately before entering the race. - In
finally, the tool always callsmanager.unwatchJobs(...), clears the timeout, and stops the progress interval. #buildResult(...)deduplicates jobs, snapshots current manager state, then callsmanager.acknowledgeDeliveries(...)for every non-running job in the result. That suppresses later automatic follow-up delivery for the same completions and removes queued deliveries for those ids.- The final text groups jobs by non-running vs still-running state. A timeout is not an error path; it simply returns the current snapshot.
Modes / Variants
- Poll all running jobs: call with neither
pollnorcancel. - Poll explicit ids: call with
pollonly. - Cancel only: call with
cancelonly; cancellations happen and the tool returns immediately. - Cancel then poll: call with both. Cancellations are applied first, then the tool watches the remaining resolved
pollids. - Read-only inspection outside the tool:
jobs://andjobs://<id>expose the same manager state without waiting.
Spawn paths that produce jobs:
packages/coding-agent/src/tools/bash.tsasync: truealways registers atype: "bash"job withAsyncJobManager.register(...)and returns a start message.- auto-background mode (
bash.autoBackground.enabled) starts the same managed job path for non-PTY commands, waits up tomin(bash.autoBackground.thresholdMs, timeoutMs - 1000), and if the command is still running returns a background-job start result instead of inline command output.
packages/coding-agent/src/task/index.ts- when
async.enabledis on, the chosen agent is not blocking, andtasks.length > 0, each task item is registered as atype: "task"job.
- when
Lifecycle and exact state names:
- Conceptual scheduling path:
pending(only task-progress bookkeeping before work starts) →running→completed/failed; cancellation changes a running async job tocancelled. - Exact
AsyncJob.statusvalues inpackages/coding-agent/src/async/job-manager.ts:"running" | "completed" | "failed" | "cancelled". - Exact per-task progress values in
packages/coding-agent/src/task/types.ts:"pending" | "running" | "completed" | "failed" | "aborted".
Side Effects
- Filesystem
- None in
job.tsitself. - Jobs being observed may already have written artifacts/results through their own tool runtimes.
- None in
- Session state (transcript, memory, jobs, checkpoints, registries)
- Reads and mutates
session.asyncJobManagerstate. watchJobs(...)/unwatchJobs(...)toggle delivery suppression for the watched ids.acknowledgeDeliveries(...)marks completed ids as suppressed and removes queued deliveries for them.cancel(...)aborts running jobs through each job'sAbortController.
- Reads and mutates
- User-visible prompts / interactive UI
- Polling emits periodic
onUpdatesnapshots every 500 ms. - Automatic job completion follow-ups are generated by
packages/coding-agent/src/sdk.tsonly for unsuppressed deliveries.
- Polling emits periodic
- Background work / cancellation
- Waiting uses a timeout plus optional tool-call abort signal.
- Cancelling a job does not synchronously await teardown; it flips state, aborts, and returns control to the manager/job promise.
Limits & Caps
- Poll wait duration comes from
async.pollWaitDurationinpackages/coding-agent/src/config/settings-schema.ts:- allowed values:
5s,10s,30s,1m,5m - default:
30s
- allowed values:
- Progress update cadence while polling:
PROGRESS_INTERVAL_MS = 500inpackages/coding-agent/src/tools/job.ts. - Async job retention default:
DEFAULT_RETENTION_MS = 5 * 60 * 1000inpackages/coding-agent/src/async/job-manager.ts. - Manager fallback max-running limit:
DEFAULT_MAX_RUNNING_JOBS = 15inpackages/coding-agent/src/async/job-manager.ts. - Session wiring clamps
async.maxJobsto1..100before constructing the manager inpackages/coding-agent/src/sdk.ts; settings default is100inpackages/coding-agent/src/config/settings-schema.ts. - Async completion delivery retry backoff in
packages/coding-agent/src/async/job-manager.ts:- base
500ms - max
30_000ms - jitter
< 200ms - exponent capped at 8 doublings
- base
Errors
- Tool-disabled path is returned as normal text, not thrown:
Async execution is disabled; no background jobs are available. - Polling a nonexistent id is not an exception:
- with
pollonly, missing ids are dropped; if none remain the tool returnsNo matching jobs found for IDs: .... - with
cancel, each missing id is reported asnot_foundindetails.cancelledand text.
- with
- Cancelling a non-running job is not an exception; it reports
already_completedeven if the actual status iscompleted,failed, orcancelled. - Tool-call abort during polling stops waiting and returns a final snapshot through
#buildResult(...); it does not cancel watched jobs. - Failures inside the underlying async work are stored on the job (
status: "failed",errorText) and reported in normal tool output, not rethrown byjob. read jobs://<id>missing job returns markdown content headed# Job Not Foundrather than throwing.
Notes
jobwaits for the first watched running job to settle, not for all watched jobs. If others remainrunning, they are reported under## Still Running; the caller must invokejobagain to continue waiting.- Delivery suppression is the key difference between snapshot and automatic delivery:
- snapshots (
job,read jobs://) read current manager state; - follow-up delivery comes from
AsyncJobManager.#enqueueDelivery(...)andsdk.tsonJobComplete; - watched or acknowledged ids are suppressed via
isDeliverySuppressed(...).
- snapshots (
manager.cancel(id)setsstatus = "cancelled"before the underlying promise settles. The job function may later populateresultTextorerrorText;job-manager.tspreserves that text but does not transition the status away fromcancelled.jobs://is implemented byJobsProtocolHandlerwithimmutable = true, but each resolve call reads live manager state at access time.jobs://<id>shows a cancellation section only when a cancelled job haserrorText; cancelled jobs withresultTextare not rendered with a result section there.- Retention eviction removes the job record, suppression flags, and watch flag together. After eviction, both
jobandread jobs://<id>behave as if the id never existed.