Merge remote-tracking branch 'origin/master' into codex/tool-json-schema-dsl

# Conflicts:
#	docs/config-catalog.md
#	docs/cookbook/adding-a-tool.i18n.yaml
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/event-producer-consumer.md
#	packages/core/tools/tests/tools.spec.ts
This commit is contained in:
Tianyi Cui
2026-07-21 23:04:32 +08:00
194 changed files with 3308 additions and 1193 deletions

View File

@@ -6,12 +6,14 @@ import type { Scope } from '@deepseek-ai/dsh-scope'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
import ToolRegistry, { CodeRunFailedError, RUN_CODE_NAME, defineTool } from '@deepseek-ai/dsh-tools'
import ToolRegistry, { CodeRunFailedError, RUN_CODE_NAME, TOOL_ABORTED_BEFORE_DISPATCH, defineTool } from '@deepseek-ai/dsh-tools'
import type { Config, PostToolDecision, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEventMap } from '@deepseek-ai/dsh-session'
const testToolSignal = new AbortController().signal
/**
* Code Mode unit tier (per the Agent Note's plan): provider contribution per mode,
* misconfiguration rejections, the run_code dispatch bridge (serialization,
@@ -95,6 +97,7 @@ function fakeAgent(options: { cwd?: string } = { cwd: '/workspace' }): { agent:
/** Dispatch run_code through the registry pipeline, as the loop would. */
async function runCode(ctx: Context, code: string, extras: { agent?: Agent; signal?: AbortSignal } = {}): Promise<ToolExecutionResult> {
return ctx.tools.execute({
signal: testToolSignal,
callId: CallId('call-1'),
name: RUN_CODE_NAME,
arguments: { code },
@@ -357,8 +360,7 @@ describe('the run_code dispatch bridge', () => {
const previous = exec.signal
exec.signal = new AbortController().signal
const result = await next()
if (previous === undefined) delete exec.signal
else exec.signal = previous
exec.signal = previous
return result
})
ctx.on('tools/result', (exec) => {
@@ -574,7 +576,7 @@ describe('the run_code dispatch bridge', () => {
seen.push(args.id)
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, 500)
exec.signal?.addEventListener('abort', () => { sawAbort = true; clearTimeout(timer); resolve() }, { once: true })
exec.signal.addEventListener('abort', () => { sawAbort = true; clearTimeout(timer); resolve() }, { once: true })
})
return [{ type: 'text' as const, text: args.id }]
},
@@ -610,7 +612,7 @@ describe('the run_code dispatch bridge', () => {
started()
await new Promise<void>((resolve) => {
const timer = setTimeout(resolve, 500)
exec.signal?.addEventListener('abort', () => { sawAbort = true; clearTimeout(timer); resolve() }, { once: true })
exec.signal.addEventListener('abort', () => { sawAbort = true; clearTimeout(timer); resolve() }, { once: true })
})
return [{ type: 'text' as const, text: args.id }]
},
@@ -838,7 +840,7 @@ describe('the run_code dispatch bridge', () => {
expect((result.content[0] as { text: string }).text).toBe('{ n: 42 }')
})
it('reports a pre-aborted outer signal as the run failure without dispatching anything', async () => {
it('short-circuits a pre-aborted outer signal before the code runtime', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const calls = registerEcho(ctx)
runtime.behavior = (request) => {
@@ -850,11 +852,16 @@ describe('the run_code dispatch bridge', () => {
controller.abort('too-late')
const result = await runCode(ctx, 'program', { signal: controller.signal })
expect(result.isError).toBe(true)
expect((result.content[0] as { text: string }).text).toContain('code run failed (abort)')
expect(result).toEqual({
content: [{ type: 'text', text: 'Error: tool call aborted before dispatch' }],
isError: true,
error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH },
})
expect(runtime.lastRequest).toBeUndefined()
expect(calls).toEqual([])
})
it('rejects a binding invoked after the run is over without dispatching it', async () => {
it('reports cancellation after rejecting a late binding without dispatching it', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const calls = registerEcho(ctx)
const controller = new AbortController()
@@ -865,8 +872,9 @@ describe('the run_code dispatch bridge', () => {
return { logs: [], value: message }
}
const result = await runCode(ctx, 'program', { signal: controller.signal })
expect(result.isError).toBe(false)
expect((result.content[0] as { text: string }).text).toContain('not dispatched')
expect(result.isError).toBe(true)
expect(result.error).toEqual({ name: 'AbortError', code: 'ABORTED' })
expect((result.content[0] as { text: string }).text).toBe('Error: tool call aborted')
expect(calls).toEqual([])
})

View File

@@ -11,6 +11,8 @@ import ToolRegistry, {
type ToolExecutionMode,
} from '@deepseek-ai/dsh-tools'
const testToolSignal = new AbortController().signal
async function setup() {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
@@ -19,7 +21,7 @@ async function setup() {
}
function exec(name: string, args: unknown): ToolExecutionInput {
return { callId: CallId('c1'), name, arguments: args }
return { signal: testToolSignal, callId: CallId('c1'), name, arguments: args }
}
describe('ToolRegistry.executionMode', () => {

View File

@@ -0,0 +1,100 @@
import { describe, expectTypeOf, it } from 'vitest'
import type { Context } from 'cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type {
ToolDispatchExecution,
ToolExecution,
ToolExecutionInput,
ToolRunContext,
} from '@deepseek-ai/dsh-tools'
function inputAndExecutionContracts(
input: ToolExecutionInput,
execution: ToolExecution,
run: ToolRunContext,
): void {
// @ts-expect-error -- every typed invocation must supply a caller-owned signal.
const missingSignal: ToolExecutionInput = { callId: CallId('missing'), name: 'probe', arguments: {} }
void missingSignal
// @ts-expect-error -- caller input is readonly after construction.
input.signal = new AbortController().signal
// @ts-expect-error -- required readonly properties cannot be deleted.
delete input.signal
// @ts-expect-error -- required signals cannot become undefined.
input.signal = undefined
// @ts-expect-error -- pipeline observers receive a readonly execution view.
execution.signal = new AbortController().signal
// @ts-expect-error -- pipeline observers cannot remove the required signal.
delete execution.signal
// @ts-expect-error -- tool bodies receive a readonly run context.
run.signal = new AbortController().signal
// @ts-expect-error -- tool bodies cannot remove the required signal.
delete run.signal
// @ts-expect-error -- tool bodies cannot replace the required signal with undefined.
run.signal = undefined
}
void inputAndExecutionContracts
function observerContracts(ctx: Context): void {
ctx.on('tools/pre-execute', (exec, next) => {
// @ts-expect-error -- pre-policy sees a readonly signal.
exec.signal = new AbortController().signal
// @ts-expect-error -- pre-policy cannot remove the required signal.
delete exec.signal
// @ts-expect-error -- pre-policy cannot replace the required signal with undefined.
exec.signal = undefined
return next()
})
ctx.on('tools/post-execute', (exec, _result, next) => {
// @ts-expect-error -- post-policy sees a readonly signal.
exec.signal = new AbortController().signal
// @ts-expect-error -- post-policy sees a readonly signal.
delete exec.signal
// @ts-expect-error -- post-policy cannot replace the required signal with undefined.
exec.signal = undefined
return next()
})
ctx.on('tools/result', (exec) => {
// @ts-expect-error -- result observers see a readonly signal.
exec.signal = new AbortController().signal
// @ts-expect-error -- result observers cannot remove the required signal.
delete exec.signal
// @ts-expect-error -- result observers see a readonly signal.
exec.signal = undefined
})
ctx.on('tools/execute', (exec, next) => {
exec.signal = new AbortController().signal
// @ts-expect-error -- around-dispatch may replace but not remove the signal.
delete exec.signal
// @ts-expect-error -- around-dispatch cannot replace the required signal with undefined.
exec.signal = undefined
return next()
})
}
void observerContracts
const inferredTool = defineTool({
name: 'signal-inference',
description: 'Pins contextual signal inference.',
parameters: {},
async execute(_args, exec) {
expectTypeOf(exec.signal).toEqualTypeOf<AbortSignal>()
// @ts-expect-error -- defineTool contextually exposes a readonly signal.
exec.signal = new AbortController().signal
return []
},
})
void inferredTool
describe('tool execution signal types', () => {
it('requires an exact AbortSignal at every readonly tool view', () => {
expectTypeOf<ToolExecutionInput['signal']>().toEqualTypeOf<AbortSignal>()
expectTypeOf<ToolExecution['signal']>().toEqualTypeOf<AbortSignal>()
expectTypeOf<ToolRunContext['signal']>().toEqualTypeOf<AbortSignal>()
expectTypeOf<ToolDispatchExecution['signal']>().toEqualTypeOf<AbortSignal>()
expectTypeOf<typeof inferredTool.execute>().toBeFunction()
})
})

View File

@@ -6,6 +6,8 @@ import type { ToolExecution, ToolExecutionResult, ToolExecutionToken } from '@de
import * as ToolsInvariant from '@deepseek-ai/dsh-tools/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
const testToolSignal = new AbortController().signal
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(InvariantService)
@@ -19,6 +21,7 @@ const execution = (overrides: Partial<ToolExecution> = {}): ToolExecution => ({
name: 'echo',
arguments: Object.freeze({ text: 'hi' }),
...overrides,
signal: overrides.signal ?? testToolSignal,
})
const outcome = (): ToolExecutionResult => Object.freeze({

View File

@@ -12,6 +12,8 @@ import { CallId } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { SessionId } from '@deepseek-ai/dsh-session'
const testToolSignal = new AbortController().signal
/** Mount the registry (with its systemPrompt dependency) on a fresh context. */
async function mount(): Promise<Context> {
const ctx = new Context()
@@ -43,6 +45,7 @@ function tool(name: string, reply = `ran:${name}`): ToolDefinition {
async function run(ctx: Context, name: string, agent?: Agent): Promise<string> {
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('c1'),
name,
arguments: {},
@@ -305,6 +308,7 @@ describe('scoped execution dispatch', () => {
expect(await run(ctx, 'danger', key)).toBe('Error: danger denied')
const callerArguments = { source: true }
const safeResult = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('safe-call'),
name: 'safe',
arguments: callerArguments,
@@ -348,7 +352,7 @@ describe('scoped execution dispatch', () => {
if (exec.name === 'parent') parent = exec.token
return next()
})
await ctx.tools.execute({ callId: CallId('parent'), name: 'parent', arguments: {} })
await ctx.tools.execute({ signal: testToolSignal, callId: CallId('parent'), name: 'parent', arguments: {} })
stopCapture()
policyCalls = 0
const signal = new AbortController().signal
@@ -372,6 +376,7 @@ describe('scoped execution dispatch', () => {
signal,
})
const subjectlessResult = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('non-cloneable-subjectless'),
name: 't',
arguments: { invalid: () => undefined },
@@ -414,6 +419,7 @@ describe('scoped execution dispatch', () => {
callId: CallId('stateful-parent'),
name: 't',
arguments: {},
signal: testToolSignal,
get parent(): ToolExecutionToken | undefined {
parentReads += 1
return parentReads === 1 ? undefined : forged
@@ -438,7 +444,7 @@ describe('scoped execution dispatch', () => {
if (exec.name === 'parent') parent = exec.token
return next()
})
await ctx.tools.execute({ callId: CallId('parent'), name: 'parent', arguments: {} })
await ctx.tools.execute({ signal: testToolSignal, callId: CallId('parent'), name: 'parent', arguments: {} })
stopCapture()
const acceptedSignal = new AbortController().signal
const driftSignal = new AbortController().signal
@@ -485,6 +491,7 @@ describe('scoped execution dispatch', () => {
const input = {
callId: CallId('throwing-arguments'),
name: 't',
signal: testToolSignal,
get arguments(): unknown {
argumentReads += 1
throw new Error('getter exploded')
@@ -525,6 +532,7 @@ describe('scoped execution dispatch', () => {
})
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('bad-arguments'), name: 't', arguments: argumentsValue,
})
@@ -545,6 +553,7 @@ describe('scoped execution dispatch', () => {
})
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('unstable-arguments'), name: 't', arguments: argumentsValue,
})
@@ -585,7 +594,7 @@ describe('scoped execution dispatch', () => {
ctx.on('tools/result', () => Promise.reject(new Error('async observer failure')) as never)
ctx.on('tools/result', (_exec, result) => { seen.push(result.isError) })
const result = await ctx.tools.execute({ callId: CallId('final'), name: 't', arguments: {}, agent: key })
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('final'), name: 't', arguments: {}, agent: key })
await Promise.resolve()
expect(result).toMatchObject({ isError: true, content: [{ type: 'text', text: 'outer failure' }] })
expect(seen).toEqual([true, true])

File diff suppressed because it is too large Load Diff