The public kill result used the awkward phrase already-terminal. Rename it to already-finished and keep the model-facing response aligned; not-alive would be inaccurate because a force-failed registry record can still correspond to orphaned producer work. Task kinds were open strings even though producer namespaces are an extension point. Add the merge-extensible TaskKindMap and derived TaskKind, cover consumer declarations in task and bundle tests, and retain the runtime non-empty check for untyped callers. With exactOptionalPropertyTypes, owner?: Agent | undefined allowed an explicit undefined value that no caller needs. Tighten the property to owner?: Agent so unowned work is expressed by omitting it. Record the requested task-service/backend split as a follow-up, using a systemd-backed runtime as a concrete candidate without guessing its durability and ownership contract in this PR. Regenerate the type and Cordis catalogs so public docs match the declarations.
130 lines
5.3 KiB
Markdown
130 lines
5.3 KiB
Markdown
# Background Task Runtime
|
|
|
|
Types shared by long-running producers, `ctx.tasks`, and task control surfaces. The [runtime RFC](../rfc/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md) owns the design; this page records the literal shapes from [`packages/tasks/tasks/src/types.ts`](../../packages/tasks/tasks/src/types.ts).
|
|
|
|
## Ids and status
|
|
|
|
`TaskId` is a [branded id](core.md#branded-ids) generated as `<kind>-N`. Access control relies on owner authorization, not id secrecy. `TaskKind` derives from a merge-extensible map; the registry treats kinds as opaque id namespaces.
|
|
|
|
```ts type-equiv
|
|
interface TaskKindMap {
|
|
bash: 'bash'
|
|
subagent: 'subagent'
|
|
}
|
|
```
|
|
|
|
`TaskStatus` is `'running' | 'stopping' | 'completed' | 'killed' | 'failed'`; producer-specific facts belong in `TaskSnapshot.detail`.
|
|
|
|
## Producer contract
|
|
|
|
`TaskStart` 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.
|
|
|
|
```ts type-equiv
|
|
interface TaskStart {
|
|
/** Producer kind — also the id prefix (`bash`, `subagent`, …). */
|
|
kind: TaskKind
|
|
/** One-line model-facing label (the command; the delegation description). */
|
|
label: string
|
|
/**
|
|
* Owning live agent. Access is fenced by its session id, and agent disposal
|
|
* cancels and awaits the task. The instance must be the one currently
|
|
* registered under its agent id. Omitting the owner creates an unowned task,
|
|
* open to any caller until service disposal.
|
|
*/
|
|
owner?: Agent
|
|
/**
|
|
* 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.
|
|
*/
|
|
run(): TaskHooks
|
|
}
|
|
```
|
|
|
|
`TaskHooks.done` is the quiescence boundary. Optional `readOutput` distinguishes consuming stream tasks from final-output-only tasks.
|
|
|
|
```ts type-equiv
|
|
interface TaskHooks {
|
|
/**
|
|
* Request termination. Must be synchronous, idempotent, and eventually settle
|
|
* {@link done}; throws propagate. The optional reason is forwarded verbatim.
|
|
*/
|
|
cancel(reason?: string): void
|
|
/**
|
|
* 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.
|
|
*/
|
|
done: Promise<TaskOutcome>
|
|
/**
|
|
* Consume output produced since the previous call. The producer formats
|
|
* truncation and spill notices. Absence marks a final-output-only task; each
|
|
* task has one consuming cursor.
|
|
*/
|
|
readOutput?(): string
|
|
}
|
|
```
|
|
|
|
```ts type-equiv
|
|
interface TaskOutcome {
|
|
/** How the task ended: finished (`completed`), cancelled (`killed`), or broke (`failed`). */
|
|
status: 'completed' | 'killed' | 'failed'
|
|
/** Kind-specific detail rendered into status lines ('exit code: 3', 'max-tokens'). */
|
|
detail?: string
|
|
/** Final output for tasks without `readOutput`; stream tasks leave it unset. */
|
|
output?: string
|
|
}
|
|
```
|
|
|
|
## Consumer views
|
|
|
|
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 surface has delivered or committed to deliver the terminal state.
|
|
|
|
```ts type-equiv
|
|
interface TaskSnapshot {
|
|
/** The registry-issued id (`<kind>-N`). */
|
|
id: TaskId
|
|
/** The producer kind the task was registered with. */
|
|
kind: TaskKind
|
|
/** The producer-supplied one-line label. */
|
|
label: string
|
|
/**
|
|
* Owner session id used for authorization and correlation; absent for
|
|
* unowned tasks. Completion listeners receive the exact {@link Agent}
|
|
* separately through {@link TaskDoneListener}.
|
|
*/
|
|
ownerSession?: SessionId
|
|
/** Current lifecycle state. */
|
|
status: TaskStatus
|
|
/** Kind-specific status detail, present once the producer supplied one (usually terminal). */
|
|
detail?: string
|
|
/** Epoch ms when the task was registered. */
|
|
startedAt: number
|
|
/** Epoch ms when the task settled; absent while `running`/`stopping`. */
|
|
finishedAt?: number
|
|
/**
|
|
* True when a kill, read, or wait has reported or committed to report the
|
|
* terminal state. Completion surfaces suppress redundant notices when set.
|
|
*/
|
|
reported: boolean
|
|
}
|
|
```
|
|
|
|
```ts type-equiv
|
|
interface TaskRead {
|
|
/**
|
|
* Stream kinds: the consuming delta since the previous read. Final-output
|
|
* kinds: empty while live, the terminal {@link TaskOutcome.output} (or
|
|
* empty) once settled — idempotent, never consumed.
|
|
*/
|
|
text: string
|
|
/** The task's state at read time. */
|
|
snapshot: TaskSnapshot
|
|
}
|
|
```
|
|
|
|
## Service behavior
|
|
|
|
[`TaskService`](../../packages/tasks/tasks/src/index.ts) provides atomic `start`, caller-scoped `get` and `list`, `read`, `kill`, bounded `wait`, contained `onTaskDone` listeners, and the `attachSurface` availability fence. Authorization compares owner sessions; owner cleanup selects the exact registered `Agent` instance. See [`dsh-tasks`](../../packages/tasks/tasks/README.md) for the package contract and [`dsh-tool-tasks`](../../packages/tasks/tool-tasks/README.md) for the model-facing surface.
|