2026-07-09 21:22:54 +08:00
# Background Task Runtime
2026-08-13 00:36:22 +08:00
English | [中文 ](jobs.zh.md )
2026-07-26 02:33:29 +08:00
2026-08-13 00:36:22 +08:00
Types shared by long-running producers, `ctx.jobs` , and job controls. The [runtime Agent Note ](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md ) owns the design; this page records the exact fields and variants from [`packages/jobs/jobs/src/types.ts` ](../../packages/jobs/jobs/src/types.ts ).
2026-07-09 21:22:54 +08:00
## Ids and status
2026-08-13 00:36:22 +08:00
`JobId` is a [branded id ](core.md#branded-ids ) generated as `<kind>-N` . Access control relies on owner authorization, not id secrecy. `JobKind` derives from a merge-extensible map; the registry treats kinds as opaque id namespaces.
2026-07-15 21:45:30 +08:00
```ts type-equiv
2026-07-19 12:25:40 +08:00
/**
2026-08-13 00:36:22 +08:00
* Producer-defined job kinds. Plugins extend this map by declaration merging;
2026-07-19 12:25:40 +08:00
* the registry treats every value as an opaque id namespace.
*/
2026-08-13 00:36:22 +08:00
interface JobKindMap {
2026-07-15 21:45:30 +08:00
bash: 'bash'
subagent: 'subagent'
}
` ``
2026-08-13 00:36:22 +08:00
` JobStatus` is ` 'running' | 'stopping' | 'completed' | 'killed' | 'failed'`; producer-specific facts belong in ` JobSnapshot.detail`.
2026-07-09 21:22:54 +08:00
2026-07-15 21:08:58 +08:00
## Producer contract
2026-07-09 21:22:54 +08:00
2026-08-13 00:36:22 +08:00
` JobStart` declares identity and a starter. The runtime finishes preflight before calling ` run()` and commits without a later failable step. Producers own execution resources; the runtime owns identity, access, and lifecycle state.
2026-07-09 21:22:54 +08:00
` ``ts type-equiv
2026-07-19 12:25:40 +08:00
/**
2026-08-13 00:36:22 +08:00
* Producer declaration passed to {@link JobRegistry.start}. The runtime
2026-07-19 12:25:40 +08:00
* preflights access and cleanup before invoking {@link run}; the producer owns
* execution resources while the runtime owns identity and lifecycle state.
*/
2026-08-13 00:36:22 +08:00
interface JobStart {
2026-07-15 21:45:30 +08:00
/** Producer kind — also the id prefix (` bash`, ` subagent`, …). */
2026-08-13 00:36:22 +08:00
kind: JobKind
2026-07-09 21:22:54 +08:00
/** One-line model-facing label (the command; the delegation description). */
label: string
2026-07-22 22:37:20 +08:00
/**
* Optional UTF-8 byte cap for each complete model-facing completion notice or
2026-07-24 19:54:25 +08:00
* output read, including controller status metadata.
2026-07-22 22:37:20 +08:00
*/
outputLimitBytes?: number
2026-07-09 21:22:54 +08:00
/**
2026-07-15 21:08:58 +08:00
* Owning live agent. Access is fenced by its session id, and agent disposal
2026-08-13 00:36:22 +08:00
* cancels and awaits the job. The instance must be the one currently
* registered under its agent id. Omitting the owner creates an unowned job,
2026-07-15 21:45:30 +08:00
* open to any caller until service disposal.
2026-07-09 21:22:54 +08:00
*/
2026-07-15 21:45:30 +08:00
owner?: Agent
2026-07-09 21:53:48 +08:00
/**
2026-07-15 21:08:58 +08:00
* Start the work after preflight and synchronously return its hooks. Called
* once; a throw leaves nothing registered, and the producer must clean up any
* partially started resources.
2026-07-09 21:53:48 +08:00
*/
2026-08-13 00:36:22 +08:00
run(): JobHooks
2026-07-09 21:53:48 +08:00
}
` ``
2026-08-13 00:36:22 +08:00
` JobHooks.done` resolves after the producer releases its resources, not merely when work finishes. Optional ` readOutput` distinguishes consuming stream jobs from final-output-only jobs.
2026-07-15 21:08:58 +08:00
2026-07-09 21:53:48 +08:00
` ``ts type-equiv
2026-07-19 12:25:40 +08:00
/** Hooks through which the runtime controls and observes producer work. */
2026-08-13 00:36:22 +08:00
interface JobHooks {
2026-07-09 21:22:54 +08:00
/**
2026-07-15 21:08:58 +08:00
* Request termination. Must be synchronous, idempotent, and eventually settle
* {@link done}; throws propagate. The optional reason is forwarded verbatim.
2026-07-09 21:22:54 +08:00
*/
cancel(reason?: string): void
/**
2026-07-15 21:08:58 +08:00
* Resolves after the producer releases its resources, not merely when work
* finishes. Must not reject; the runtime converts a rejection to ` failed`.
* If teardown cancellation throws, the runtime may force-fail only the
* registry record without claiming that the work stopped.
2026-07-09 21:22:54 +08:00
*/
2026-08-13 00:36:22 +08:00
done: Promise<JobOutcome>
2026-07-09 21:22:54 +08:00
/**
2026-07-15 21:08:58 +08:00
* Consume output produced since the previous call. The producer formats
2026-08-13 00:36:22 +08:00
* truncation and spill notices. Absence marks a final-output-only job; each
* job has one consuming cursor.
2026-07-09 21:22:54 +08:00
*/
readOutput?(): string
}
` ``
` ``ts type-equiv
2026-08-13 00:36:22 +08:00
/** Terminal result supplied by a producer through {@link JobHooks.done}. */
interface JobOutcome {
/** How the job ended: finished (` completed`), cancelled (` killed`), or broke (` failed`). */
2026-07-09 21:22:54 +08:00
status: 'completed' | 'killed' | 'failed'
/** Kind-specific detail rendered into status lines ('exit code: 3', 'max-tokens'). */
detail?: string
2026-08-13 00:36:22 +08:00
/** Final output for jobs without ` readOutput`; stream jobs leave it unset. */
2026-07-09 21:22:54 +08:00
output?: string
}
` ``
2026-07-15 21:08:58 +08:00
## Consumer views
2026-07-09 21:22:54 +08:00
2026-08-11 19:36:49 +08:00
Snapshots are fresh read-only projections. ` ownerSession` carries the shared ` SessionId` used for authorization; completion listeners separately receive the exact owner object used for lifecycle cleanup. ` reported` suppresses a completion notice after another reporter has delivered or committed to deliver the terminal state, including the teardown cancel that drains an owner or the service.
2026-07-09 21:22:54 +08:00
` ``ts type-equiv
2026-07-19 12:25:40 +08:00
/**
2026-08-13 00:36:22 +08:00
* A read-only projection of one job, safe to hand to listeners and tools —
2026-07-19 12:25:40 +08:00
* a fresh object per call, never live registry state.
*/
2026-08-13 00:36:22 +08:00
interface JobSnapshot {
2026-07-09 21:22:54 +08:00
/** The registry-issued id (` <kind>-N`). */
2026-08-13 00:36:22 +08:00
id: JobId
/** The producer kind the job was registered with. */
kind: JobKind
2026-07-09 21:22:54 +08:00
/** The producer-supplied one-line label. */
label: string
2026-07-22 22:37:20 +08:00
/** Producer-owned cap for complete model-facing notices and output reads. */
outputLimitBytes?: number
2026-07-09 21:22:54 +08:00
/**
2026-07-15 21:08:58 +08:00
* Owner session id used for authorization and correlation; absent for
2026-08-13 00:36:22 +08:00
* unowned jobs. Completion listeners receive the exact {@link Agent}
* separately through {@link JobDoneListener}.
2026-07-09 21:22:54 +08:00
*/
2026-07-12 17:10:02 +08:00
ownerSession?: SessionId
2026-07-09 21:22:54 +08:00
/** Current lifecycle state. */
2026-08-13 00:36:22 +08:00
status: JobStatus
2026-07-09 21:22:54 +08:00
/** Kind-specific status detail, present once the producer supplied one (usually terminal). */
detail?: string
2026-08-13 00:36:22 +08:00
/** Epoch ms when the job was registered. */
2026-07-09 21:22:54 +08:00
startedAt: number
2026-08-13 00:36:22 +08:00
/** Epoch ms when the job settled; absent while ` running`/` stopping`. */
2026-07-09 21:22:54 +08:00
finishedAt?: number
/**
2026-08-11 19:36:49 +08:00
* True when a kill, read, wait, or teardown cancel has reported or committed
* to report the terminal state. Completion reporters suppress redundant
* notices when set. Teardown claims it because the owner or service being
* destroyed leaves no reader: a reporter that opens a turn on notice would
* otherwise spend a model request per teardown layer.
2026-07-09 21:22:54 +08:00
*/
reported: boolean
}
` ``
` ``ts type-equiv
2026-08-13 00:36:22 +08:00
/** Output and post-read state returned by {@link JobRegistry.read}. */
interface JobRead {
2026-07-09 21:22:54 +08:00
/**
* Stream kinds: the consuming delta since the previous read. Final-output
2026-08-13 00:36:22 +08:00
* kinds: empty while live, the terminal {@link JobOutcome.output} (or
2026-07-09 21:22:54 +08:00
* empty) once settled — idempotent, never consumed.
*/
text: string
2026-08-13 00:36:22 +08:00
/** The job's state at read time. */
snapshot: JobSnapshot
2026-07-09 21:22:54 +08:00
}
` ``
2026-07-15 21:08:58 +08:00
## Service behavior
2026-07-09 21:22:54 +08:00
2026-08-13 00:36:22 +08:00
The abstract [` JobRegistry`](../../packages/jobs/jobs/src/index.ts) Service Definition specifies atomic ` start`, caller-scoped ` get` and ` list`, ` read`, ` kill`, bounded ` wait`, failure-isolated ` onJobDone` and ` onJobsChanged` listeners, and when ` attachController` becomes available; [` LocalJobRegistry`](../../packages/jobs/jobs-local/src/index.ts) is the process-local Service provider. Authorization compares owner sessions; owner cleanup and admission use the exact registered ` Agent` instance. The local provider's positive-safe-integer ` maxConcurrentJobsPerOwner` config defaults to ` 10` and counts ` running` plus ` stopping` records per exact owner, with one shared bucket for unowned jobs; terminal producer settlement releases capacity. See [` dsh-jobs`](../../packages/jobs/jobs/README.md) for the Service Definition contract, [` dsh-jobs-local`](../../packages/jobs/jobs-local/README.md) for the registry lifecycle and admission policy, and [` dsh-tool-jobs`](../../packages/jobs/tool-jobs/README.md) for the model-facing Consumer.
2026-07-30 21:40:58 +08:00
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
2026-07-24 19:54:25 +08:00
## Cordis API
2026-07-30 21:40:58 +08:00
2026-07-24 19:54:25 +08:00
Generated from source by ` scripts/gen-cordis-catalog.ts` (verified fresh by ` pnpm run verify-cordis-catalog` in doc-sync; regenerate with ` pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a ` ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited ` ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
2026-07-30 21:40:58 +08:00
2026-08-13 00:36:22 +08:00
<a id="ctxjobs--jobregistry-abstract-seam"></a>
2026-07-30 21:40:58 +08:00
2026-08-13 00:36:22 +08:00
### ` ctx.jobs` — ` JobRegistry` (abstract seam)
2026-07-30 21:40:58 +08:00
2026-08-13 00:36:22 +08:00
Abstract background job registry. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as ` ctx.jobs` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).
2026-07-30 21:40:58 +08:00
Implementations must honor these semantics:
2026-08-11 19:36:49 +08:00
- Registrations outlive producer and controller fibers. Owner and service disposal cancel live work and await compliant producers; a throwing teardown cancel force-fails only the record. Teardown cancellation also marks the record reported, because a record its owner is being destroyed for has no reader left.
2026-08-13 00:36:22 +08:00
- Owned-job access is fenced by the owner's session id. Ids are predictable, so authorization — not secrecy — is the boundary.
2026-08-11 19:36:49 +08:00
- Settlement is first-wins: one terminal record, released waiters, and one round of contained listener notification, even against a late producer outcome. Completion is announced last, after the record is committed and every other observer of the settlement has seen it, because a reporter may open a model turn synchronously.
2026-08-13 00:36:22 +08:00
- start refuses work while no attached job controller serves the spec's owner, so a producer cannot start work that owner cannot collect or stop. One registry serves every composition in the process, so this question — and completion-listener delivery — is owner-relative rather than process-wide: registrations made from an unscoped context serve every owner, and registrations made under an agent composition's scope serve exactly the agents composed under it.
2026-07-30 21:40:58 +08:00
` ``ts cordis-catalog
/**
2026-08-11 16:23:05 +08:00
* Preflight access, validation, owner cleanup, and implementation-owned
* admission before starting and atomically registering work. Any preflight
2026-08-13 00:36:22 +08:00
* rejection leaves no job id or execution resource. A throwing starter
2026-08-11 16:23:05 +08:00
* leaves nothing registered; after it returns, registration cannot fail.
* Settlement records the outcome, notifies listeners, and releases waiters.
2026-08-13 00:36:22 +08:00
* @param spec - job identity, owner, and synchronous starter.
2026-07-30 21:40:58 +08:00
* @returns the registry-issued ` <kind>-N` id.
*/
2026-08-13 00:36:22 +08:00
abstract start(spec: JobStart): JobId
2026-07-30 21:40:58 +08:00
/**
2026-08-13 00:36:22 +08:00
* List caller-owned and unowned jobs in registration order without exposing
2026-07-30 21:40:58 +08:00
* another session's labels.
2026-08-13 00:36:22 +08:00
* @param caller - reading agent; a non-agent caller sees only unowned jobs.
2026-07-30 21:40:58 +08:00
* @returns fresh snapshots.
*/
2026-08-13 00:36:22 +08:00
abstract list(caller?: Agent): JobSnapshot[]
2026-07-30 21:40:58 +08:00
/**
* Return a non-consuming snapshot without changing its read cursor or notice
2026-08-13 00:36:22 +08:00
* state. Throws for an unknown or foreign job.
* @param id - job to look up.
2026-07-30 21:40:58 +08:00
* @param caller - reading agent checked against the owner.
* @returns a fresh snapshot.
*/
2026-08-13 00:36:22 +08:00
abstract get(id: JobId, caller?: Agent): JobSnapshot
2026-07-30 21:40:58 +08:00
/**
* Read the next stream delta, or the idempotent final output after settlement.
2026-08-13 00:36:22 +08:00
* A terminal read marks the job reported. Throws for an unknown or foreign
* job.
* @param id - job to read.
2026-07-30 21:40:58 +08:00
* @param caller - reading agent checked against the owner.
* @returns output text and the post-read snapshot.
*/
2026-08-13 00:36:22 +08:00
abstract read(id: JobId, caller?: Agent): JobRead
2026-07-30 21:40:58 +08:00
/**
2026-08-13 00:36:22 +08:00
* Request cancellation, then mark the job stopping and reported. A producer
* throw propagates without changing job state. Throws for an unknown or
* foreign job.
* @param id - job to cancel.
2026-07-30 21:40:58 +08:00
* @param caller - killing agent checked against the owner.
* @param reason - logged reason forwarded to the producer.
* @returns ` requested` for live work, otherwise ` already-finished`.
*/
2026-08-13 00:36:22 +08:00
abstract kill(id: JobId, caller?: Agent, reason?: string): 'requested' | 'already-finished'
2026-07-30 21:40:58 +08:00
/**
2026-08-13 00:36:22 +08:00
* Wait for settlement or timeout without cancelling the job. Caller abort
* rejects only while the job is live; after settlement the terminal
2026-07-30 21:40:58 +08:00
* snapshot wins so a notice suppressed for this waiter is still delivered.
* Throws for invalid, unknown, or foreign input.
2026-08-13 00:36:22 +08:00
* @param id - job to wait for.
2026-07-30 21:40:58 +08:00
* @param timeoutMs - positive finite wait bound in milliseconds.
* @param caller - waiting agent checked against the owner.
* @param signal - optional cancellation of the wait itself.
* @returns snapshot at settlement or timeout.
*/
2026-08-13 00:36:22 +08:00
abstract wait(id: JobId, timeoutMs: number, caller?: Agent, signal?: AbortSignal): Promise<JobSnapshot>
2026-07-30 21:40:58 +08:00
/**
2026-08-10 16:12:41 +08:00
* Register an effect-scoped completion listener. It receives the settlements
* of the owners its registering context's scope covers; each listener is
* contained; returned promises are observed but not awaited. No listener runs
* after service disposal.
2026-07-30 21:40:58 +08:00
* @param listener - receives each terminal snapshot and its exact owner.
* @returns disposer that unregisters the listener.
*/
2026-08-13 00:36:22 +08:00
abstract onJobDone(listener: JobDoneListener): () => void
2026-07-30 21:40:58 +08:00
2026-08-11 11:57:33 +08:00
/**
2026-08-09 13:49:34 +08:00
/**
* Register an effect-scoped observer of visible-set changes. It fires after
* every commit that changes what {@link list} returns for that owner —
2026-08-10 13:37:18 +08:00
* registration, every stopping transition (including the one teardown
* performs before it awaits a slow producer), settlement, owner-disposal
* removal, and the emptying that service disposal commits — so an observer
* re-reads rather than accumulating deltas.
*
2026-08-13 00:36:22 +08:00
* Delivery is owner-relative on the same terms as {@link onJobDone}: an
2026-08-11 11:57:33 +08:00
* observer registered from an unscoped context — a host composition's own
* carrier — sees every owner, while one registered under an agent
* composition's scope sees exactly the agents composed under it.
2026-08-09 13:49:34 +08:00
*
2026-08-13 00:36:22 +08:00
* This is not a superset of {@link onJobDone}: that one delivers the terminal
* record under first-wins semantics a job controller couples to notice
2026-08-09 13:49:34 +08:00
* delivery, while this one carries no delivery meaning and marks nothing
* reported. Listeners are contained and never awaited.
* @param listener - receives the owner whose visible set changed, or
2026-08-13 00:36:22 +08:00
* ` undefined` when an unowned job changed and every caller's set did.
2026-08-09 13:49:34 +08:00
* @returns disposer that unregisters the listener.
*/
2026-08-13 00:36:22 +08:00
abstract onJobsChanged(listener: JobsChangedListener): () => void
2026-08-09 13:49:34 +08:00
2026-07-30 21:40:58 +08:00
/**
2026-08-13 00:36:22 +08:00
* Attach an effect-scoped controller that can read and stop jobs. It serves the
2026-08-10 16:12:41 +08:00
* owners its registering context's scope covers, and {@link start} refuses an
2026-07-24 19:54:25 +08:00
* owner no attached controller serves.
2026-07-30 21:40:58 +08:00
* @param name - diagnostic label; duplicate names remain independent.
2026-07-24 19:54:25 +08:00
* @returns disposer that detaches this controller.
2026-07-30 21:40:58 +08:00
*/
2026-07-24 19:54:25 +08:00
abstract attachController(name: string): () => void
2026-07-30 21:40:58 +08:00
` ``
Types: [Agent](core.md)
2026-08-13 00:36:22 +08:00
Source: [` packages/jobs/jobs/src/index.ts:62`](../../packages/jobs/jobs/src/index.ts)
2026-07-30 21:40:58 +08:00
<!-- END GENERATED cordis-surface -->