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

@@ -432,11 +432,11 @@ export function apply(ctx: Context, config: Config): void {
subagentProvider: resolved.subagentProvider,
maxTotalAgents: maxRounds,
parent,
...exec.signal === undefined ? {} : { signal: exec.signal },
signal: exec.signal,
})
const onAbort = (): void => { run.cancel('parent step aborted') }
exec.signal?.addEventListener('abort', onAbort, { once: true })
if (exec.signal?.aborted) run.cancel('parent step aborted')
exec.signal.addEventListener('abort', onAbort, { once: true })
if (exec.signal.aborted) run.cancel('parent step aborted')
try {
const settled = await run.result
@@ -446,7 +446,7 @@ export function apply(ctx: Context, config: Config): void {
if (value.status === 'round-failed') throw new Error(renderRoundFailure(value, resolved.maxResultChars))
return [{ type: 'text', text: renderResult(value, resolved.maxResultChars) }]
} finally {
exec.signal?.removeEventListener('abort', onAbort)
exec.signal.removeEventListener('abort', onAbort)
await run.dispose()
}
},

View File

@@ -13,6 +13,7 @@ import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from '
import * as toolRalph from '../src/index.ts'
type MockScript = ConstructorParameters<typeof MockAdapter>[0]
const testToolSignal = new AbortController().signal
/** Mount the shipped Ralph execution stack around one keyless model script. */
async function mountRalph(script: MockScript, config: toolRalph.Config) {
@@ -81,6 +82,7 @@ describe('dsh-tool-ralph over the real spawn and worker-thread stack', () => {
children.push(agent!)
})
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('ralph-integration'),
name: 'ralph',
arguments: { objective: 'Complete both migration slices.', maxRounds: 2 },
@@ -132,6 +134,7 @@ describe('dsh-tool-ralph over the real spawn and worker-thread stack', () => {
})
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('ralph-child-failure'),
name: 'ralph',
arguments: { objective: 'Complete both migration slices.', maxRounds: 2 },
@@ -220,6 +223,7 @@ describe('dsh-tool-ralph over the real spawn and worker-thread stack', () => {
], config)
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('ralph-script-enforcement'),
name: 'ralph',
arguments: { objective: 'Complete the scoped work.', maxRounds: config.maxRounds },

View File

@@ -7,23 +7,27 @@ import { SessionId } from '@deepseek-ai/dsh-session'
import SubagentService from '@deepseek-ai/dsh-subagent'
import type { SubagentCapabilities, SubagentProvider, SubagentRun, SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
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 { WorkflowRunId, WorkflowService } from '@deepseek-ai/dsh-workflow'
import type { WorkflowResult, WorkflowRun, WorkflowStartRequest } from '@deepseek-ai/dsh-workflow'
import * as toolRalph from '../src/index.ts'
const testToolSignal = new AbortController().signal
class StubEngine extends WorkflowService {
requests: WorkflowStartRequest[] = []
cancels: string[] = []
disposed = 0
settle!: (result: WorkflowResult) => void
startError: Error | undefined
onStart: (() => void) | undefined
start(request: WorkflowStartRequest): WorkflowRun {
if (this.startError !== undefined) throw this.startError
this.requests.push(request)
const result = new Promise<WorkflowResult>((resolve) => { this.settle = resolve })
this.onStart?.()
return {
id: WorkflowRunId(`ralph-${this.requests.length}`),
meta: request.meta,
@@ -94,11 +98,11 @@ function execute(
extra?: { agent?: Agent; signal?: AbortSignal },
): Promise<ToolExecutionResult> {
return ctx.tools.execute({
signal: extra?.signal ?? testToolSignal,
callId: CallId('ralph-call'),
name: 'ralph',
arguments: args,
...extra?.agent === undefined ? {} : { agent: extra.agent },
...extra?.signal === undefined ? {} : { signal: extra.signal },
})
}
@@ -256,7 +260,7 @@ describe('dsh-tool-ralph', () => {
expect(engine.disposed).toBe(4)
})
it('bridges mid-flight and already-aborted parent signals to cancellation', async () => {
it('bridges mid-flight cancellation and skips dispatch for an already-aborted parent signal', async () => {
const { ctx, engine, parent } = await setup()
const controller = new AbortController()
const pending = execute(ctx, { objective: 'Work.' }, { agent: parent, signal: controller.signal })
@@ -266,9 +270,24 @@ describe('dsh-tool-ralph', () => {
const already = new AbortController()
already.abort()
expect((await execute(ctx, { objective: 'Work.' }, { agent: parent, signal: already.signal })).isError).toBe(true)
expect(engine.cancels).toEqual(['parent step aborted', 'parent step aborted'])
expect(engine.disposed).toBe(2)
const skipped = await execute(ctx, { objective: 'Work.' }, { agent: parent, signal: already.signal })
expect(skipped.error?.code).toBe(TOOL_ABORTED_BEFORE_DISPATCH)
expect(engine.requests).toHaveLength(1)
expect(engine.cancels).toEqual(['parent step aborted'])
expect(engine.disposed).toBe(1)
})
it('bridges cancellation that arrives while the workflow is starting', async () => {
const { ctx, engine, parent } = await setup()
const controller = new AbortController()
engine.onStart = () => { controller.abort() }
const result = await execute(ctx, { objective: 'Work.' }, { agent: parent, signal: controller.signal })
expect(result.isError).toBe(true)
expect(engine.requests[0]?.signal).toBe(controller.signal)
expect(engine.cancels).toEqual(['parent step aborted'])
expect(engine.disposed).toBe(1)
})
it('rejects absent authority, empty objectives, bad round caps, and schema-invalid calls before start', async () => {

View File

@@ -177,17 +177,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
@@ -199,7 +196,7 @@ export function apply(ctx: Context, config: Config): void {
}
return [{ type: 'text', text: renderResult(run, result, maxResultChars) }]
} 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,
@@ -154,14 +157,16 @@ describe('dsh-tool-workflow', () => {
expect(result.error?.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({ 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 () => {