Merge remote-tracking branch 'origin/master' into worktree/semantic-session-checkpoints
# Conflicts: # docs/architecture.md # docs/event-producer-consumer.md # examples/acp-agent/tests/snapshots/cancel-tool-calls/session.jsonl # packages/core/agent-loop/README.md # packages/core/agent-loop/src/loop.ts # packages/core/agent-loop/tests/cancel.spec.ts
This commit is contained in:
@@ -15,6 +15,7 @@ The package mounts no console logger, interactive UI, user-interaction service,
|
||||
| `toolOrder` | lexicographic | explicit model-facing tool order in `dsh-system-prompt` |
|
||||
| `tools` | `{ mode: 'native' }` | tool-registry presentation config through `dsh-agent-spine-demo` |
|
||||
| `dshHome` | `$DSH_HOME` or `~/.dsh` | Harness home exposed to model bash and used by local skill discovery |
|
||||
| `sessionTitle` | spine example limits | Fallback title word/byte limits through `dsh-agent-spine-demo` |
|
||||
| `skills` | owner defaults | skill registry, local provider, and model-facing skill tool |
|
||||
| `toolBash` | owner defaults | model-facing bash config, including this producer's background opt-in |
|
||||
| `toolTasks` | owner defaults | generic `task_output` wait bounds |
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface Config {
|
||||
tools?: ToolsConfig
|
||||
/** DeepSeek Harness home directory exposed to bash and used for local skill discovery. */
|
||||
dshHome?: string
|
||||
/** Fallback session-title limits forwarded through agent-spine-demo. */
|
||||
sessionTitle?: NonNullable<agentCore.Config['sessionTitle']>
|
||||
/** Directory the JSONL session backend writes under. Defaults to `./.sessions`. */
|
||||
persistenceRoot?: string
|
||||
/** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
|
||||
@@ -65,6 +67,7 @@ export const Config: z<Config> = z.object({
|
||||
persistenceCompression: JsonlCompressionSchema,
|
||||
persona: z.string(),
|
||||
dshHome: z.string(),
|
||||
sessionTitle: agentCore.SessionTitleConfigSchema,
|
||||
skills: agentCore.SkillConfigSchema,
|
||||
// Absent means lexicographic order; schemastery's native array default is [].
|
||||
toolOrder: z.array(z.string()).default(undefined as unknown as string[]),
|
||||
|
||||
@@ -180,7 +180,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