Merge master into codex/session-title
This commit is contained in:
@@ -57,7 +57,7 @@ import type { Config } from '@deepseek-ai/dsh-agent-spine-demo'
|
||||
// workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults.
|
||||
```
|
||||
|
||||
The bundle FORWARDS each field to the child that owns it: `agents` and `maxParallelToolCalls` to `agent-loop` (`agents` defaults to `[]`; the cap defaults there), so each app supplies its own pre-created agents — TUI and headless apps pre-create `main`, while the ACP app creates agents on demand at `session/new`; `llmRetry` to the bounded retry policy; `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry for its presentation mode; `sessionTitle` to the fallback title service; `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer; the required `workspaceContext` choice to `dsh-workspace-context` (`{ maxBytes }` enables loading and `false` disables it); `invariants` to the invariant service; and `toolBash`/`toolTasks` to the two model-facing tool plugins the bundle owns. Omitted `sessionTitle` uses the explicit example policy of 5 words, 40 fallback bytes, and 80 accepted-title bytes. A `goals` object opts into the persisted domain, model tools, and same-session driver while forwarding `goals.domain` and `goals.tool` to their owners; omission or `false` leaves the stack absent so headless callers retain one-turn settlement. Set `skills.enabled: false` to omit both the local provider and model-facing skill tool, and set `toolTasks: false` to retain the task service for foreground producers without exposing `task_output` / `task_list` / `task_kill`. It resolves `dshHome` once through [`@deepseek-ai/dsh-home`](../../util/home/README.md) and forwards that absolute value to tool-bash's managed environment and enabled local skill discovery. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. `toolBash.enableRunInBackground` controls only the bash producer; independently loaded producers keep their own config. Workspace instructions register before the skill catalog so their session-prefix message renders first. App packages use `pickSpineConfig()` to copy only these bundle-owned fields.
|
||||
The bundle FORWARDS each field to the child that owns it: `agents` and `maxParallelToolCalls` to `agent-loop` (`agents` defaults to `[]`; the cap defaults there), so each app supplies its own pre-created agents — TUI and headless apps pre-create `main`, while the ACP app creates agents on demand at `session/new`; `llmRetry` to the bounded retry policy; `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry for its presentation mode; `sessionTitle` to the fallback title service; `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer; the required `workspaceContext` choice to `dsh-workspace-context` (`{ maxBytes }` enables loading and `false` disables it); `invariants` to the invariant service; and `toolBash`/`toolTasks` to the two model-facing tool plugins the bundle owns. Omitted `sessionTitle` uses the explicit example policy of 5 words, 40 fallback bytes, and 80 accepted-title bytes. A `goals` object opts into the persisted domain, model tools, and same-session driver while forwarding `goals.domain` and `goals.tool` to their owners; omission or `false` leaves the stack absent so headless callers retain one-turn settlement. Set `skills.enabled: false` to omit both the local provider and model-facing skill tool, and set `toolTasks: false` to retain the task service for foreground producers without exposing `task_output` / `task_list` / `task_kill`. It resolves `dshHome` once through [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) and forwards that absolute value to tool-bash's managed environment and enabled local skill discovery. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. `toolBash.enableRunInBackground` controls only the bash producer; independently loaded producers keep their own config. Workspace instructions register before the skill catalog so their session-prefix message renders first. App packages use `pickSpineConfig()` to copy only these bundle-owned fields.
|
||||
|
||||
For example, `{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` keeps the package-owned companions mounted but suppresses the blocked owner. Blocklist matches override allowlist matches; see [`dsh-invariants`](../../support/invariants/README.md) for regex and lifecycle rules.
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
"@deepseek-ai/dsh-agent-loop": "^0.0.1",
|
||||
"@deepseek-ai/dsh-goal": "^0.0.1",
|
||||
"@deepseek-ai/dsh-goal-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-home": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-paths": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm-retry": "^0.0.1",
|
||||
"@deepseek-ai/dsh-scope": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
@@ -58,9 +58,9 @@
|
||||
"@deepseek-ai/dsh-goal": "workspace:^",
|
||||
"@deepseek-ai/dsh-goal-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:^",
|
||||
"@deepseek-ai/dsh-home": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-paths": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-retry": "workspace:^",
|
||||
"@deepseek-ai/dsh-scope": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
|
||||
@@ -34,7 +34,7 @@ import * as toolSkill from '@deepseek-ai/dsh-tool-skill'
|
||||
import * as toolTasks from '@deepseek-ai/dsh-tool-tasks'
|
||||
import AgentLoop, { type Config as AgentLoopConfig } from '@deepseek-ai/dsh-agent-loop'
|
||||
import * as llmRetry from '@deepseek-ai/dsh-llm-retry'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-home'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
|
||||
export const name = 'agent-spine-demo'
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ import * as agentInvariant from '@deepseek-ai/dsh-agent/invariant'
|
||||
import * as scopeInvariant from '@deepseek-ai/dsh-scope/invariant'
|
||||
import * as agentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
declare module '@deepseek-ai/dsh-tasks' {
|
||||
interface TaskKindMap {
|
||||
probe: 'probe'
|
||||
@@ -411,6 +413,7 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
|
||||
expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['shared-skill'])
|
||||
const execution: ToolExecution = {
|
||||
signal: testToolSignal,
|
||||
token: Symbol('agent-core-dsh-home-test') as ToolExecution['token'],
|
||||
callId: CallId('agent-core-dsh-home'),
|
||||
name: 'bash',
|
||||
@@ -482,11 +485,12 @@ describe('dsh-agent-spine-demo bundle', () => {
|
||||
})
|
||||
const wait = vi.spyOn(ctx.tasks, 'wait')
|
||||
await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('task-config-forwarding'),
|
||||
name: 'task_output',
|
||||
arguments: { task_id: id, wait: true },
|
||||
})
|
||||
expect(wait).toHaveBeenCalledWith(id, 7, undefined, undefined)
|
||||
expect(wait).toHaveBeenCalledWith(id, 7, undefined, testToolSignal)
|
||||
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"path": "../../support/invariants"
|
||||
},
|
||||
{
|
||||
"path": "../../util/home"
|
||||
"path": "../../util/paths"
|
||||
},
|
||||
{
|
||||
"path": "../../bash/tool-bash"
|
||||
|
||||
@@ -181,12 +181,12 @@ async function waitForStartupIdle(agent: Agent, signal?: AbortSignal): Promise<v
|
||||
return
|
||||
}
|
||||
if (signal.aborted) {
|
||||
agent.cancel(interruptionReason(signal))
|
||||
agent.cancel({ kind: 'user' })
|
||||
throw new CliInterruptedError(interruptionReason(signal))
|
||||
}
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onAbort = (): void => {
|
||||
agent.cancel(interruptionReason(signal))
|
||||
agent.cancel({ kind: 'user' })
|
||||
reject(new CliInterruptedError(interruptionReason(signal)))
|
||||
}
|
||||
signal.addEventListener('abort', onAbort, { once: true })
|
||||
@@ -243,7 +243,7 @@ export async function runOneShot(ctx: Context, options: OneShotOptions): Promise
|
||||
options.onEvent(sessionId, event)
|
||||
} catch (error: unknown) {
|
||||
outputError = toError(error)
|
||||
agent.cancel('stream output failed')
|
||||
agent.cancel({ kind: 'user' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ export async function runOneShot(ctx: Context, options: OneShotOptions): Promise
|
||||
let onAbort: (() => void) | undefined
|
||||
if (signal !== undefined) {
|
||||
onAbort = (): void => {
|
||||
agent.cancel(interruptionReason(signal))
|
||||
agent.cancel({ kind: 'user' })
|
||||
if (targetTurn === undefined) settleRejected(new CliInterruptedError(interruptionReason(signal)))
|
||||
}
|
||||
signal.addEventListener('abort', onAbort, { once: true })
|
||||
@@ -370,7 +370,7 @@ async function bootInterruptibly(
|
||||
export function formatTurnFailure(reason: TurnEndReason): string {
|
||||
switch (reason.kind) {
|
||||
case 'completed': return 'completed'
|
||||
case 'aborted': return reason.reason === undefined ? 'was aborted' : `was aborted: ${reason.reason}`
|
||||
case 'aborted': return 'was aborted'
|
||||
case 'error': return `failed at step ${reason.step}: ${'failure' in reason ? reason.failure.message : reason.message}`
|
||||
case 'disposed': return 'was disposed'
|
||||
case 'max-tokens': return 'reached the model output-token limit'
|
||||
|
||||
@@ -179,7 +179,7 @@ describe.skipIf(!existsSync(cliBin))('dsh-cli-demo BUILT bin', () => {
|
||||
)
|
||||
expect(result, JSON.stringify(result)).toMatchObject({ code, signal: null })
|
||||
expect(result.stdout).toContain('"kind":"aborted"')
|
||||
expect(result.stderr).toContain(`received ${signal}`)
|
||||
expect(result.stderr).toContain('turn 1 was aborted')
|
||||
}, 30_000)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,6 +10,8 @@ import type { ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import * as cliDemo from '../src/index.ts'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
const contexts: Context[] = []
|
||||
|
||||
async function skillConfig(catalogDescriptionMaxLength?: number): Promise<NonNullable<cliDemo.Config['skills']>> {
|
||||
@@ -131,6 +133,7 @@ describe('dsh-cli-demo app composition', () => {
|
||||
|
||||
expect(ctx.get('agentLoop')?.config.maxParallelToolCalls).toBe(3)
|
||||
const execution: ToolExecution = {
|
||||
signal: testToolSignal,
|
||||
token: Symbol('cli-demo-dsh-home-test') as ToolExecution['token'],
|
||||
callId: CallId('cli-demo-dsh-home'),
|
||||
name: 'bash',
|
||||
@@ -148,11 +151,12 @@ describe('dsh-cli-demo app composition', () => {
|
||||
})
|
||||
const wait = vi.spyOn(ctx.tasks, 'wait')
|
||||
await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('cli-demo-task-config'),
|
||||
name: 'task_output',
|
||||
arguments: { task_id: id, wait: true },
|
||||
})
|
||||
expect(wait).toHaveBeenCalledWith(id, 7, undefined, undefined)
|
||||
expect(wait).toHaveBeenCalledWith(id, 7, undefined, testToolSignal)
|
||||
})
|
||||
|
||||
it('accepts false to keep task services without model-facing task controls', async () => {
|
||||
|
||||
@@ -398,9 +398,9 @@ describe('runOneShot and executeCli', () => {
|
||||
await running
|
||||
abort.abort('received SIGINT')
|
||||
const output = await outcome
|
||||
expect(JSON.parse(output.stdout)).toMatchObject({ success: false, reason: { kind: 'aborted', reason: 'received SIGINT' } })
|
||||
expect(JSON.parse(output.stdout)).toMatchObject({ success: false, reason: { kind: 'aborted' } })
|
||||
expect(output.code).toBe(1)
|
||||
expect(output.stderr).toContain('was aborted: received SIGINT')
|
||||
expect(output.stderr).toContain('turn 1 was aborted')
|
||||
expect(agent.status).toBe('disposed')
|
||||
})
|
||||
|
||||
@@ -486,7 +486,7 @@ describe('formatTurnFailure', () => {
|
||||
const cases: [TurnEndReason, string][] = [
|
||||
[{ kind: 'completed' }, 'completed'],
|
||||
[{ kind: 'aborted' }, 'was aborted'],
|
||||
[{ kind: 'aborted', reason: 'stop' }, 'was aborted: stop'],
|
||||
[{ kind: 'aborted' }, 'was aborted'],
|
||||
[{ kind: 'error', step: 2, message: 'bad' }, 'failed at step 2: bad'],
|
||||
[{ kind: 'error', step: 3, failure: { message: 'provider bad', code: 'SERVER' } }, 'failed at step 3: provider bad'],
|
||||
[{ kind: 'disposed' }, 'was disposed'],
|
||||
|
||||
Reference in New Issue
Block a user