Merge branch 'codex/tool-json-schema-dsl' into codex/canonical-tool-output

# Conflicts:
#	.agents/notes/implemented/feature/2026-06-30-interception-seams.md
#	docs/config-catalog.md
#	docs/cookbook/adding-a-tool.i18n.yaml
#	docs/cookbook/adding-a-tool.md
#	docs/cookbook/adding-a-tool.zh.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/tools.md
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl
#	packages/bash/tool-bash/src/index.ts
#	packages/core/agent-loop/src/tool-calls.ts
#	packages/core/agent-loop/tests/cancel.spec.ts
#	packages/core/agent-loop/tests/contract-regressions.spec.ts
#	packages/core/agent-loop/tests/tool-calls.spec.ts
#	packages/core/tools/README.md
#	packages/core/tools/src/index.ts
#	packages/core/tools/tests/code-mode.spec.ts
#	packages/core/tools/tests/tools.spec.ts
#	packages/fs/tool-fs-search/tests/integration.spec.ts
#	packages/fs/tool-fs-search/tests/tools.spec.ts
#	packages/fs/tool-fs/tests/integration.spec.ts
#	packages/mcp/mcp-client/src/tools.ts
#	packages/timeout/timeout-policy/tests/timeout-policy.spec.ts
#	packages/web/tool-web/tests/integration.spec.ts
#	packages/web/tool-web/tests/tool-web.spec.ts
This commit is contained in:
Tianyi Cui
2026-07-21 23:39:03 +08:00
194 changed files with 3387 additions and 1222 deletions

View File

@@ -193,17 +193,14 @@ export function apply(ctx: Context, config: Config): void {
meta: args.meta,
...args.args !== undefined ? { args: args.args } : {},
parent,
...exec.signal ? { signal: exec.signal } : {},
signal: exec.signal,
})
// Bridge the tool's abort signal to the run: if the parent step is aborted while the
// script is in flight, cancel the whole run. The signal also enters the engine directly, but
// this local bridge preserves the tool contract even if an implementation ignores it.
const onAbort = (): void => { run.cancel('parent step aborted') }
exec.signal?.addEventListener('abort', onAbort, { once: true })
// `addEventListener` does NOT fire for a signal already aborted before
// this line — cancel explicitly in that case.
if (exec.signal?.aborted) run.cancel('parent step aborted')
exec.signal.addEventListener('abort', onAbort, { once: true })
try {
const result = await run.result
@@ -219,7 +216,7 @@ export function apply(ctx: Context, config: Config): void {
result: result.value as JsonValue,
}
} finally {
exec.signal?.removeEventListener('abort', onAbort)
exec.signal.removeEventListener('abort', onAbort)
// Always reach run quiescence — never leak a live script or children.
await run.dispose()
}

View File

@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRegistry, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { WorkflowRunId, WorkflowService } from '@deepseek-ai/dsh-workflow'
@@ -13,6 +13,8 @@ import WorkerWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread'
import * as toolWorkflow from '../src/index.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
const testToolSignal = new AbortController().signal
/** A controllable engine standing in behind ctx.workflows (the tool's only seam). */
class StubEngine extends WorkflowService {
requests: WorkflowStartRequest[] = []
@@ -60,6 +62,7 @@ const META = { name: 'audit', description: 'd' }
function execute(ctx: Context, args: unknown, extra?: { agent?: Agent; signal?: AbortSignal }): Promise<ToolExecutionResult> {
return ctx.tools.execute({
signal: testToolSignal,
callId: CallId('call-1'),
name: 'workflow',
arguments: args,
@@ -156,14 +159,19 @@ describe('dsh-tool-workflow', () => {
expect(result.error?.info?.code).toBe('INVALID_ARGS')
})
it('cancels the run when exec.signal is ALREADY aborted at call time', async () => {
it('skips workflow startup when exec.signal is already aborted', async () => {
const { ctx, engine, parent } = await setup()
const controller = new AbortController()
controller.abort()
const result = await execute(ctx, { script: SCRIPT, meta: META }, { agent: parent, signal: controller.signal })
expect(result.isError).toBe(true)
expect(engine.cancels).toContain('parent step aborted')
expect(engine.disposed).toBe(1)
expect(result.error).toEqual({
message: 'tool call aborted before dispatch',
info: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH },
})
expect(engine.requests).toHaveLength(0)
expect(engine.cancels).toHaveLength(0)
expect(engine.disposed).toBe(0)
})
it('truncates an oversized rendered value with a notice (maxResultChars)', async () => {