fix(tasks): address task API review feedback

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.
This commit is contained in:
Tianyi Cui
2026-07-15 21:45:30 +08:00
parent f4bd2405cc
commit b06ae91fa8
12 changed files with 84 additions and 36 deletions

View File

@@ -4,7 +4,13 @@ import { Session, SessionId } from '@deepseek-ai/dsh-session'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import TaskService, { TaskId } from '@deepseek-ai/dsh-tasks'
import type { TaskHooks, TaskOutcome, TaskSnapshot, TaskStart } from '@deepseek-ai/dsh-tasks'
import type { TaskHooks, TaskKind, TaskOutcome, TaskSnapshot, TaskStart } from '@deepseek-ai/dsh-tasks'
declare module '@deepseek-ai/dsh-tasks' {
interface TaskKindMap {
workflow: 'workflow'
}
}
const agentScopeDisposers = new WeakMap<Agent, () => Promise<void>>()
@@ -81,7 +87,7 @@ describe('TaskService.start', () => {
it('rejects an empty kind and an empty label', async () => {
const ctx = await harness()
expect(() => ctx.tasks.start(producer({ kind: '' }).spec)).toThrow('invalid task kind')
expect(() => ctx.tasks.start(producer({ kind: '' as TaskKind }).spec)).toThrow('invalid task kind')
expect(() => ctx.tasks.start(producer({ label: '' }).spec)).toThrow('invalid task label')
})
@@ -90,6 +96,7 @@ describe('TaskService.start', () => {
expect(ctx.tasks.start(producer().spec)).toBe('bash-1')
expect(ctx.tasks.start(producer().spec)).toBe('bash-2')
expect(ctx.tasks.start(producer({ kind: 'subagent' }).spec)).toBe('subagent-1')
expect(ctx.tasks.start(producer({ kind: 'workflow' }).spec)).toBe('workflow-1')
})
})
@@ -222,13 +229,13 @@ describe('TaskService.kill', () => {
expect(seen[0]).toMatchObject({ id, status: 'killed', reported: true })
})
it('reports an already-terminal task instead of failing', async () => {
it('reports an already-finished task instead of failing', async () => {
const ctx = await harness()
const p = producer()
const id = ctx.tasks.start(p.spec)
p.settle({ status: 'completed' })
await tick()
expect(ctx.tasks.kill(id)).toBe('already-terminal')
expect(ctx.tasks.kill(id)).toBe('already-finished')
})
it('propagates a throwing producer cancel and leaves the task untouched', async () => {
@@ -254,7 +261,7 @@ describe('TaskService.kill', () => {
expect(seen[0]).toMatchObject({ id, reported: false }) // notice would still fire
broken = false
expect(ctx.tasks.kill(id)).toBe('already-terminal')
expect(ctx.tasks.kill(id)).toBe('already-finished')
})
})
@@ -299,7 +306,7 @@ describe('TaskService.wait', () => {
expect(ctx.tasks.get(id).status).toBe('running')
})
it('returns immediately for an already-terminal task', async () => {
it('returns immediately for an already-finished task', async () => {
const ctx = await harness()
const p = producer()
const id = ctx.tasks.start(p.spec)