mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor(tasks): drive wait() timing through dsh-timeout
ctx.tasks.wait arms a deadline() fusing the caller's abort with the wait timeout and classifies the outcome with timeoutOf scoped to the new TASK_WAIT_TIMEOUT code: a wait timeout resolves to the live snapshot (the task keeps running), a caller abort rejects the wait — same contract, no hand-rolled timer/listener plumbing, and a nested foreign deadline can no longer misread as a wait timeout. task_output deliberately declares NO ToolDefinition.timeoutMs: timeout-policy turns a timed-out call into a structured TOOL_TIMEOUT failure, but a timed-out wait is a SUCCESS that must still report [status: running] (decision recorded in the runtime RFC alternatives).
This commit is contained in:
@@ -224,7 +224,7 @@ attachSurface(name: string): () => void
|
||||
|
||||
Types: [Agent](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/tasks/tasks/src/index.ts:84`](../../packages/tasks/tasks/src/index.ts)
|
||||
Source: [`packages/tasks/tasks/src/index.ts:93`](../../packages/tasks/tasks/src/index.ts)
|
||||
|
||||
## `ctx.tools` — `ToolRegistry`
|
||||
|
||||
|
||||
@@ -142,6 +142,10 @@ The established bash habit is poll-between-work, and the guidance tells the mode
|
||||
|
||||
Waiting is never useful without reading the result afterwards; a separate tool doubles the calls and the schema surface for zero information. Folding it into `task_output` matches the only real usage pattern.
|
||||
|
||||
### Why not `ToolDefinition.timeoutMs` (the timeout-policy plugin) for `task_output`'s wait?
|
||||
|
||||
The [timeout library](2026-07-06-timeout-deadline-library.md) gives `wait()` its timing internals — `ctx.tasks.wait` arms a `deadline()` and classifies wait-timeout vs caller-abort with `timeoutOf` scoped to `TASK_WAIT_TIMEOUT` — but the tool-call-level policy is deliberately NOT adopted: timeout-policy replaces a timed-out call with a structured `TOOL_TIMEOUT` failure, whereas a timed-out `task_output(wait: true)` is a SUCCESS that must still report `[status: running]` (the model needs the task's state either way, and the task keeps running). The wait therefore bounds its own deadline through the tool's `waitTimeoutMs`/`maxWaitTimeoutMs` config. For the same reason, no timeout policy manages a background task's LIFETIME: once the id is returned the work is off the tool-call deadline entirely — cancellation belongs to `task_kill` and owner cleanup.
|
||||
|
||||
### Why not a push-sink producer contract (`appendOutput`/`settle`), as Kimi Code's manager uses?
|
||||
|
||||
A sink centralizes output buffering, truncation, and spill in the runtime, which is elegant when the runtime owns output storage. In this codebase those concerns already live — bounded, tested, spill-file-aware — inside `dsh-bash-local`, and keeping process concerns in the executor is the point of the bash seam. The pull contract (`readOutput()` returning a formatted delta) reuses that machinery as-is; a sink would relocate it for no v1 gain. If a durable backend later makes the runtime own output storage, the producer contract is the one seam to revisit.
|
||||
|
||||
@@ -8,7 +8,7 @@ The background task registry (`ctx.tasks`): a runtime-global, CONCRETE service (
|
||||
- `get(id, caller?)` / `list(caller?)` — non-consuming snapshots; `list` returns only caller-owned plus unowned tasks (a global listing would leak foreign labels).
|
||||
- `read(id, caller?): TaskRead` — stream kinds consume the per-task cursor (v1's single intended reader is the owning model — a non-consuming multi-reader surface would be a cursor/snapshot API extension, not a `read` change); final kinds read the terminal output idempotently.
|
||||
- `kill(id, caller?, reason?)` — `'requested'` (live task: producer `cancel` runs first — a throw fails the kill loud and leaves the task untouched — then `stopping`) or `'already-terminal'`. Every successful kill marks the task `reported` (the killer saw the end → completion notice suppressed).
|
||||
- `wait(id, timeoutMs, caller?, signal?)` — resolves with the terminal snapshot (marked `reported`), or the live snapshot at timeout; an aborted signal rejects the WAIT only.
|
||||
- `wait(id, timeoutMs, caller?, signal?)` — resolves with the terminal snapshot (marked `reported`), or the live snapshot at timeout; an aborted signal rejects the WAIT only. Timing is a [`dsh-timeout`](../../util/timeout/README.md) `deadline()` scoped to the `TASK_WAIT_TIMEOUT` code, so a nested foreign deadline never misreads as a wait timeout.
|
||||
- `onTaskDone(listener)` — exactly once per task with the terminal snapshot; effect-scoped, per-listener containment, silent after service disposal.
|
||||
- `attachSurface(name)` — declares a control surface exists (the model tools, or a deployment's custom surface); effect-scoped.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-tasks",
|
||||
"description": "Background task registry (ctx.tasks) for the DeepSeek Harness — shared ids, owner isolation, polling, cancellation, and completion listeners for long-running tool work",
|
||||
"description": "Background task registry (ctx.tasks) for the DeepSeek Harness \u2014 shared ids, owner isolation, polling, cancellation, and completion listeners for long-running tool work",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -24,12 +24,14 @@
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-brand": "^0.0.1",
|
||||
"@deepseek-ai/dsh-timeout": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
import { Context, Service } from 'cordis'
|
||||
import type { Agent, AgentId } from '@deepseek-ai/dsh-agent'
|
||||
import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout'
|
||||
import { TaskId } from './types.ts'
|
||||
import type { TaskDoneListener, TaskOutcome, TaskRead, TaskRegistration, TaskSnapshot, TaskStatus } from './types.ts'
|
||||
|
||||
@@ -49,6 +50,14 @@ declare module 'cordis' {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `dsh-timeout` code stamped on a {@link TaskService.wait} deadline's
|
||||
* `TimeoutReason`. A wait timeout only ends the WAIT (the task keeps running
|
||||
* and the live snapshot is returned) — scoping `timeoutOf` to this code keeps
|
||||
* a foreign (outer, nested) deadline's timeout from being misread as ours.
|
||||
*/
|
||||
export const TASK_WAIT_TIMEOUT = 'TASK_WAIT_TIMEOUT'
|
||||
|
||||
/** The registry's mutable per-task record (never handed out — see {@link TaskService.snapshot}). */
|
||||
interface TrackedTask {
|
||||
id: TaskId
|
||||
@@ -273,15 +282,22 @@ export class TaskService extends Service {
|
||||
if (signal?.aborted) throw new Error('wait aborted')
|
||||
task.waiters += 1
|
||||
try {
|
||||
// The dsh-timeout deadline fits wait() exactly because both only
|
||||
// NOTIFY: a wait timeout returns the live snapshot (the task keeps
|
||||
// running — nothing is terminated), and timeoutOf scoped to our own
|
||||
// code tells that timeout apart from a caller abort, which rejects
|
||||
// the wait. `using` clears the timer on every exit path.
|
||||
using d = deadline(signal, timeoutMs, TASK_WAIT_TIMEOUT)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const cleanup = (): void => {
|
||||
clearTimeout(timer)
|
||||
signal?.removeEventListener('abort', onAbort)
|
||||
const onAbort = (): void => {
|
||||
if (timeoutOf(d.signal, TASK_WAIT_TIMEOUT) !== undefined) resolve()
|
||||
else reject(new Error('wait aborted'))
|
||||
}
|
||||
const timer = setTimeout(() => { cleanup(); resolve() }, timeoutMs)
|
||||
const onAbort = (): void => { cleanup(); reject(new Error('wait aborted')) }
|
||||
signal?.addEventListener('abort', onAbort, { once: true })
|
||||
void task.settled.then(() => { cleanup(); resolve() })
|
||||
d.signal.addEventListener('abort', onAbort, { once: true })
|
||||
void task.settled.then(() => {
|
||||
d.signal.removeEventListener('abort', onAbort)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
} finally {
|
||||
task.waiters -= 1
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../core/agent"
|
||||
},
|
||||
{
|
||||
"path": "../../util/timeout"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -125,6 +125,11 @@ export function apply(ctx: Context, config: Config): void {
|
||||
+ 'final-output tasks (subagent) return the final answer once the task finishes. '
|
||||
+ 'Every response ends with a [status: ...] line. Non-blocking by default; '
|
||||
+ 'set `wait: true` to block until the task finishes (bounded by a capped timeout) when you are genuinely blocked on its result.',
|
||||
// Deliberately NO ToolDefinition.timeoutMs: the timeout-policy plugin
|
||||
// replaces a timed-out call with a structured TOOL_TIMEOUT failure, but a
|
||||
// timed-out wait here is a SUCCESS that reports [status: running] — the
|
||||
// task's state must reach the model either way, so the wait bounds its
|
||||
// own deadline (waitTimeoutMs/maxWaitTimeoutMs) via ctx.tasks.wait.
|
||||
parameters: {
|
||||
task_id: { type: 'string', required: true, description: 'Task id returned by the tool that started the background work.' },
|
||||
wait: { type: 'boolean', description: 'Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive.' },
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -893,6 +893,9 @@ importers:
|
||||
'@deepseek-ai/dsh-session':
|
||||
specifier: workspace:^
|
||||
version: link:../../core/session
|
||||
'@deepseek-ai/dsh-timeout':
|
||||
specifier: workspace:^
|
||||
version: link:../../util/timeout
|
||||
cordis:
|
||||
specifier: ^4.0.0-rc.6
|
||||
version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4)
|
||||
|
||||
Reference in New Issue
Block a user