Merge branch 'codex/tool-json-schema-dsl' into codex/canonical-tool-output
# Conflicts: # docs/event-producer-consumer.md # packages/context/time-context/tests/time-context.spec.ts # packages/context/workspace-context/tests/workspace-context.spec.ts
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-tasks",
|
||||
"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",
|
||||
"description": "Background task registry (ctx.tasks) for the DeepSeek Harness — shared ids, owner isolation, polling, cancellation, and completion listeners for long-running tool work",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -11,11 +11,16 @@
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./invariant": {
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
@@ -24,6 +29,7 @@
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-brand": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-timeout": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
@@ -31,6 +37,7 @@
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-brand": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-timeout": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
|
||||
57
packages/tasks/tasks/src/invariant.ts
Normal file
57
packages/tasks/tasks/src/invariant.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/** Package-owned background-task snapshot invariants. @module @deepseek-ai/dsh-tasks/invariant */
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
import type { TaskSnapshot } from './types.ts'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-tasks'
|
||||
const TERMINAL_STATUSES = new Set(['completed', 'killed', 'failed'])
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'tasks-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/** Validate the cross-field relationships in one registry snapshot. */
|
||||
function validateSnapshot(snapshot: TaskSnapshot, owner: Agent | undefined, fail: InvariantFailure): void {
|
||||
const id = String(snapshot.id)
|
||||
const prefix = `${snapshot.kind}-`
|
||||
const ordinal = Number(id.slice(prefix.length))
|
||||
if (snapshot.kind.length === 0 || !id.startsWith(prefix)
|
||||
|| !Number.isSafeInteger(ordinal) || ordinal < 1) {
|
||||
fail(`task snapshot id ${JSON.stringify(id)} must be ${JSON.stringify(prefix)} followed by a positive ordinal`)
|
||||
}
|
||||
if (snapshot.label.length === 0) fail(`task ${JSON.stringify(id)} label must be non-empty`)
|
||||
if (!Number.isSafeInteger(snapshot.startedAt) || snapshot.startedAt < 0) {
|
||||
fail(`task ${JSON.stringify(id)} startedAt must be a non-negative epoch integer`)
|
||||
}
|
||||
|
||||
const terminal = TERMINAL_STATUSES.has(snapshot.status)
|
||||
if (terminal !== (snapshot.finishedAt !== undefined)) {
|
||||
fail(`task ${JSON.stringify(id)} finishedAt must be present exactly for a terminal status`)
|
||||
}
|
||||
if (snapshot.finishedAt !== undefined
|
||||
&& (!Number.isSafeInteger(snapshot.finishedAt) || snapshot.finishedAt < snapshot.startedAt)) {
|
||||
fail(`task ${JSON.stringify(id)} finishedAt must be an epoch integer no earlier than startedAt`)
|
||||
}
|
||||
|
||||
const expectedOwner = owner?.id
|
||||
if (snapshot.ownerSession !== expectedOwner) {
|
||||
fail(`task ${JSON.stringify(id)} ownerSession does not match its completion owner`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Install checks over current unowned records and every terminal snapshot. */
|
||||
const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
|
||||
for (const snapshot of ctx.tasks.list()) validateSnapshot(snapshot, undefined, fail)
|
||||
ctx.tasks.onTaskDone((snapshot, owner) => { validateSnapshot(snapshot, owner, fail) })
|
||||
}, { inject: ['tasks'] })
|
||||
|
||||
/**
|
||||
* Register the task-registry invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
87
packages/tasks/tasks/tests/invariant.spec.ts
Normal file
87
packages/tasks/tasks/tests/invariant.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import TaskService, { TaskId } from '@deepseek-ai/dsh-tasks'
|
||||
import type { TaskDoneListener, TaskSnapshot } from '@deepseek-ai/dsh-tasks'
|
||||
import * as TasksInvariant from '@deepseek-ai/dsh-tasks/invariant'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const BASE: TaskSnapshot = {
|
||||
id: TaskId('bash-1'),
|
||||
kind: 'bash',
|
||||
label: 'compile',
|
||||
status: 'completed',
|
||||
startedAt: 10,
|
||||
finishedAt: 20,
|
||||
reported: false,
|
||||
}
|
||||
|
||||
const RUNNING: TaskSnapshot = {
|
||||
id: TaskId('bash-1'),
|
||||
kind: 'bash',
|
||||
label: 'compile',
|
||||
status: 'running',
|
||||
startedAt: 10,
|
||||
reported: false,
|
||||
}
|
||||
|
||||
const TERMINAL_WITHOUT_FINISH: TaskSnapshot = {
|
||||
id: TaskId('bash-1'),
|
||||
kind: 'bash',
|
||||
label: 'compile',
|
||||
status: 'completed',
|
||||
startedAt: 10,
|
||||
reported: false,
|
||||
}
|
||||
|
||||
async function setup(seed: TaskSnapshot[] = []): Promise<(snapshot: unknown, owner?: Agent) => void> {
|
||||
const ctx = new Context()
|
||||
let listener: TaskDoneListener | undefined
|
||||
const probe = {
|
||||
list: () => seed,
|
||||
onTaskDone(value: TaskDoneListener) {
|
||||
listener = value
|
||||
return () => { listener = undefined }
|
||||
},
|
||||
} as unknown as TaskService
|
||||
await ctx.plugin(InvariantService)
|
||||
await ctx.plugin({
|
||||
name: 'task-invariant-probe',
|
||||
apply(child: Context) { child.provide('tasks', probe) },
|
||||
})
|
||||
await ctx.plugin(TasksInvariant)
|
||||
if (listener === undefined) throw new Error('task invariant did not subscribe to terminal snapshots')
|
||||
return (snapshot, owner) => { listener!(snapshot as TaskSnapshot, owner) }
|
||||
}
|
||||
|
||||
describe('task-registry invariants', () => {
|
||||
it('accepts coherent current and terminal snapshots', async () => {
|
||||
const notify = await setup([RUNNING])
|
||||
expect(() => { notify(BASE) }).not.toThrow()
|
||||
const owner = { id: SessionId('owner') } as Agent
|
||||
expect(() => { notify({ ...BASE, id: TaskId('subagent-2'), kind: 'subagent', ownerSession: owner.id }, owner) })
|
||||
.not.toThrow()
|
||||
})
|
||||
|
||||
it.each([
|
||||
[{ ...BASE, id: TaskId('-1'), kind: '' }, undefined, /positive ordinal/],
|
||||
[{ ...BASE, id: TaskId('other-1') }, undefined, /must be "bash-" followed by a positive ordinal/],
|
||||
[{ ...BASE, id: TaskId('bash-x') }, undefined, /positive ordinal/],
|
||||
[{ ...BASE, id: TaskId('bash-0') }, undefined, /positive ordinal/],
|
||||
[{ ...BASE, startedAt: -1 }, undefined, /startedAt must be a non-negative epoch integer/],
|
||||
[{ ...BASE, startedAt: 0.5 }, undefined, /startedAt must be a non-negative epoch integer/],
|
||||
[{ ...BASE, status: 'running' }, undefined, /finishedAt must be present exactly for a terminal status/],
|
||||
[TERMINAL_WITHOUT_FINISH, undefined, /finishedAt must be present exactly for a terminal status/],
|
||||
[{ ...BASE, finishedAt: 9 }, undefined, /no earlier than startedAt/],
|
||||
[{ ...BASE, finishedAt: 20.5 }, undefined, /no earlier than startedAt/],
|
||||
[{ ...BASE, ownerSession: SessionId('recorded') }, { id: SessionId('actual') } as Agent, /does not match its completion owner/],
|
||||
] as const)('rejects an incoherent registry snapshot', async (snapshot, owner, message) => {
|
||||
const notify = await setup()
|
||||
expect(() => { notify(snapshot, owner) }).toThrow(message)
|
||||
})
|
||||
|
||||
it('rejects an incoherent record already present at installation', async () => {
|
||||
await expect(setup([{ ...BASE, label: '' }])).rejects.toThrow(/label must be non-empty/)
|
||||
})
|
||||
})
|
||||
@@ -25,6 +25,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../util/timeout"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,11 +11,16 @@
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./invariant": {
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
@@ -23,6 +28,7 @@
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tasks": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
@@ -33,6 +39,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
|
||||
30
packages/tasks/tool-tasks/src/invariant.ts
Normal file
30
packages/tasks/tool-tasks/src/invariant.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-tool-tasks`.
|
||||
* @module @deepseek-ai/dsh-tool-tasks/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-tool-tasks'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'tool-tasks-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution
|
||||
* relations are owned by the capability seam it calls.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
@@ -28,6 +28,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../tasks"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user