diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 1030c19492..00b315e803 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -983,7 +983,7 @@ export interface Config { export type ToolPresentationMode = 'native' | 'code' | 'both' ``` -Source: [`packages/core/tools/src/index.ts:308`](../packages/core/tools/src/index.ts) +Source: [`packages/core/tools/src/index.ts:307`](../packages/core/tools/src/index.ts) ## `@deepseek-ai/dsh-user-approval` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index abecba29b8..2b26d15621 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -256,7 +256,7 @@ async execute(exec: ToolExecutionInput): Promise Types: [ToolDefinition](../core-data-structures/tools.md) · [ToolExecutionInput](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md) -Source: [`packages/core/tools/src/index.ts:364`](../../packages/core/tools/src/index.ts) +Source: [`packages/core/tools/src/index.ts:363`](../../packages/core/tools/src/index.ts) ## `ctx.userInteraction` — `UserInteractionService` diff --git a/docs/core-data-structures/tools.md b/docs/core-data-structures/tools.md index 08c289e5e5..132d22d521 100644 --- a/docs/core-data-structures/tools.md +++ b/docs/core-data-structures/tools.md @@ -137,7 +137,6 @@ type ToolGuard = (execution: Readonly) => string | undefined ```ts type-equiv interface ToolExecutionResult { - callId: CallId content: ContentBlock[] isError: boolean /** @@ -167,6 +166,8 @@ interface ToolExecutionResult { } ``` +The result carries only the outcome. Call identity remains on the immutable `ToolExecution` that accompanies it through every hook and on the durable `tool/call` / `tool/result` session events, so wrappers cannot create a second, disagreeing identity. + The registry materializes and freezes the final accepted result immediately before `tools/result`. Its content, structured error, additional context, and presentation metadata must round-trip losslessly through JSON; an invalid outcome becomes a JSON-safe `isError` result, so the observed live outcome is safe for the later durable `tool/result` append. Each interception waterfall returns a typed **Decision** (the idiom shared with the `agent/*` seams). `tools/pre-execute` listeners receive `(exec, next)` and return a `PreToolDecision`; `tools/execute` wrappers return a `ToolExecutionResult`; `tools/post-execute` listeners receive `(exec, result, next)` and return a `PostToolDecision`: diff --git a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md b/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md index 4f31c48623..ed0a9a2cb4 100644 --- a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md +++ b/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.md @@ -57,9 +57,8 @@ Signal replacement is by **in-place mutation of `exec.signal`**, not by passing `timeout-policy` owns both uses of the `TOOL_TIMEOUT` code: the internal deadline code passed to `deadline()`/`timeoutOf()` (scoped so a nested outer deadline reads as an ordinary cancel) and the structured tool-result error code. Its replacement result is: ```ts ignore-check -function toolTimeoutResult(callId: CallId, timeoutMs: number): ToolExecutionResult { +function toolTimeoutResult(timeoutMs: number): ToolExecutionResult { return { - callId, content: [{ type: 'text', text: `Error: tool call timed out after ${timeoutMs}ms` }], isError: true, error: { name: 'ToolTimeoutError', code: 'TOOL_TIMEOUT' }, diff --git a/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.md b/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.md index f335afafe1..715ce93924 100644 --- a/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.md +++ b/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.md @@ -26,7 +26,7 @@ Amend the session-surface and reconstructable-request RFCs where they describe t ## Acceptance criteria -- `SurfaceManager.nodes` is one ordered seq array with no `SurfaceNode`, link fields, or seq-to-node map; incremental append processing and the internal replace-generation signal remain, while the separate public `invalidate()` deletion stays owned by the dead-surface RFC. +- `SurfaceManager.nodes` is one ordered seq array with no `SurfaceNode`, link fields, or seq-to-node map; incremental append processing and the internal replace-generation signal remain. - Replaying full changed-header snapshots reconstructs exactly the same requests; no header-delta event/type/codec remains. - A v0 seed or persisted log containing legacy `request/header-delta` is rejected before replay, with coverage for JSONL and SQLite load paths. - New-shape v0 JSONL/SQLite replay, provenance, crash repair, compaction, snapshots, invariants, typecheck, coverage, doc-sync, build, and hygiene pass. diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 22b1d8dc79..776bc78d0f 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -979,7 +979,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ToolExecutionResult', - declaration: 'export interface ToolExecutionResult {\n callId: CallId;\n content: ContentBlock[];\n isError: boolean;\n error?: ToolErrorInfo;\n additionalContext?: HookContext;\n meta?: unknown;\n}', + declaration: 'export interface ToolExecutionResult {\n content: ContentBlock[];\n isError: boolean;\n error?: ToolErrorInfo;\n additionalContext?: HookContext;\n meta?: unknown;\n}', }, { name: 'ToolExecutionToken', diff --git a/packages/core/agent-loop/src/loop.ts b/packages/core/agent-loop/src/loop.ts index 18eca40cc1..15cc0aa410 100644 --- a/packages/core/agent-loop/src/loop.ts +++ b/packages/core/agent-loop/src/loop.ts @@ -578,7 +578,8 @@ async function runStep( }) session.append('tool/result', { turn, step, - // Preserve transcript pairing even if a post-execute listener returns another id. + // Correlation comes from the immutable execution input; the result does + // not duplicate this authoritative transcript identity. callId: call.id, content: result.content, isError: result.isError, diff --git a/packages/core/agent-loop/tests/contract-regressions.spec.ts b/packages/core/agent-loop/tests/contract-regressions.spec.ts index 8130d4b893..ee50261e9d 100644 --- a/packages/core/agent-loop/tests/contract-regressions.spec.ts +++ b/packages/core/agent-loop/tests/contract-regressions.spec.ts @@ -1043,8 +1043,7 @@ describe('tool result call identity', () => { // A post-execute listener transforms the result (accept-with-replacement). // The loop must still record the tool/result under the model's authoritative - // call.id (the loop ignores result.callId — which the registry always sets to - // exec.callId anyway — and uses call.id, the model-transcript id). + // call.id, which is the immutable identity carried by the execution input. ctx.on('tools/post-execute', (exec, _result) => { expect(exec.callId).toBe(CallId('c1')) // the loop passed the real id in return Promise.resolve({ kind: 'accept', content: [{ type: 'text', text: 'ok' }] }) @@ -1054,8 +1053,7 @@ describe('tool result call identity', () => { send(agent, 'use tool') await waitForIdle(ctx, agent) - // The logged tool/result.callId is the originating call.id, NOT the - // listener's wrong id. + // The logged tool/result.callId is the originating call.id. const resultEvent = [...agent.session.events].find(e => e.type === 'tool/result') expect(resultEvent?.type).toBe('tool/result') if (resultEvent?.type === 'tool/result') { diff --git a/packages/core/system-prompt/src/index.ts b/packages/core/system-prompt/src/index.ts index 9a8a36face..566395e55a 100644 --- a/packages/core/system-prompt/src/index.ts +++ b/packages/core/system-prompt/src/index.ts @@ -226,7 +226,7 @@ export class SystemPrompt extends Service { private scopedVariableProviders = new Map string | undefined>>() private readonly toolOrder: string[] | undefined - constructor(ctx: Context, public config: Config) { + constructor(ctx: Context, config: Config) { super(ctx, 'systemPrompt') this.toolOrder = validateToolOrder(config.toolOrder) // Keep harness-owned openers independent of the selected loop plugin. diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index 5b2a81528b..0f7974e0f9 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -36,7 +36,7 @@ The live registry pipeline has three transformable waterfalls followed by the ob - `ToolExecutionInput` — the caller-supplied call description: `{ callId, name, arguments, agent?, parent?, signal? }`; callers may pass an enclosing execution's opaque token as `parent` but never choose the new execution's own token. - `ToolExecutionToken` — a fresh branded `Symbol` assigned by the registry. It supports equality correlation only and never crosses a model, log, or worker boundary. - `ToolExecution` — the pipeline-owned call: immutable `{ token, callId, name, arguments, agent?, parent? }` identity plus optional operational `signal`, which an around wrapper may add, replace, remove, and restore. A nested call's `parent` is a `ToolExecutionToken`, not an execution object. -- `ToolExecutionResult` — losslessly JSON-serializable outcome: `{ callId, content, isError, error?, additionalContext?, meta? }`. The registry materializes and freezes the complete post-policy value before final observation. On failure with a `HarnessError`, `error: { name, code }` carries the structured failure class alongside the model-facing text. +- `ToolExecutionResult` — losslessly JSON-serializable outcome: `{ content, isError, error?, additionalContext?, meta? }`. Call identity stays on the immutable `ToolExecution` supplied alongside the result instead of being duplicated on the outcome. The registry materializes and freezes the complete post-policy value before final observation. On failure with a `HarnessError`, `error: { name, code }` carries the structured failure class alongside the model-facing text. - `PreToolDecision` — `{kind:'allow'}` | `{kind:'deny', reason}` | `{kind:'ask', reason?}`. Input rewrite is deliberately not offered; `ask` is serviced by [`ctx.approval`](../../ui/user-approval/README.md) when mounted and otherwise degrades to deny. - `PostToolDecision` — `{kind:'accept', content?, additionalContext?}` (keep the call successful, optionally replacing the model-facing content) | `{kind:'block', feedback, additionalContext?}` (turn it into an `isError` whose content is the corrective feedback). Output replacement is clean because `tool/result` is logged AFTER `execute()` returns. - `ToolGuard` — `(execution) => string | undefined`; the returned string is a final monotonic denial reason evaluated after the reorderable pre-execute waterfall and before dispatch. diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 988c6a0268..d0c75e8035 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -220,7 +220,7 @@ export interface ToolErrorInfo { * distinguish it from a tool body's own error. */ export class ToolNotFoundError extends HarnessError { - constructor(public readonly toolName: string) { + constructor(toolName: string) { super(`unknown tool "${toolName}"`, 'UNKNOWN_TOOL') this.name = 'ToolNotFoundError' } @@ -228,7 +228,6 @@ export class ToolNotFoundError extends HarnessError { /** The outcome of one tool call. */ export interface ToolExecutionResult { - callId: CallId content: ContentBlock[] isError: boolean /** @@ -704,7 +703,7 @@ export class ToolRegistry extends Service { } } catch (error: unknown) { execution = { ...base, arguments: undefined } - const result = this.materializeFinalResult(toolErrorResult(callId, error)) + const result = this.materializeFinalResult(toolErrorResult(error)) this.notifyResult(execution, result) return result } @@ -714,7 +713,7 @@ export class ToolRegistry extends Service { } catch (error: unknown) { // Outer backstop: a throwing pre/post-execute listener, guard, or the // waterfall machinery becomes an isError result, never a turn failure. - result = this.materializeFinalResult(toolErrorResult(execution.callId, error)) + result = this.materializeFinalResult(toolErrorResult(error)) } this.notifyResult(execution, result) return result @@ -739,7 +738,6 @@ export class ToolRegistry extends Service { // Every non-grant, including a failed/unavailable approval request, takes // the same deny path and still reaches post-policy plus result observers. const denied: ToolExecutionResult = { - callId: exec.callId, content: [{ type: 'text', text: `Error: ${denialReason}` }], isError: true, } @@ -770,16 +768,12 @@ export class ToolRegistry extends Service { const returned = await tool.execute(exec.arguments, exec) const content = Array.isArray(returned) ? returned : returned.content const meta = Array.isArray(returned) ? undefined : returned.meta - return { callId: exec.callId, content, isError: false, ...meta !== undefined ? { meta } : {} } + return { content, isError: false, ...meta !== undefined ? { meta } : {} } } catch (error: unknown) { - return toolErrorResult(exec.callId, error) + return toolErrorResult(error) } }, ) - if (result.callId !== exec.callId) { - throw new TypeError(`tools/execute returned callId "${String(result.callId)}" for authoritative call "${exec.callId}"`) - } - return await this.postExecute(exec, result) } @@ -854,7 +848,6 @@ export class ToolRegistry extends Service { const additionalContext = decision.additionalContext if (decision.kind === 'block') { return { - callId: result.callId, content: decision.feedback, isError: true, ...additionalContext ? { additionalContext } : {}, @@ -883,10 +876,9 @@ function createExecutionToken(): ToolExecutionToken { return Symbol('dsh.tool.execution') as ToolExecutionToken } -function toolErrorResult(callId: ToolExecution['callId'], error: unknown): ToolExecutionResult { +function toolErrorResult(error: unknown): ToolExecutionResult { const info = errorInfo(error) return { - callId, content: [{ type: 'text', text: `Error: ${errorMessage(error)}` }], isError: true, ...info ? { error: info } : {}, diff --git a/packages/core/tools/tests/scoped.spec.ts b/packages/core/tools/tests/scoped.spec.ts index 6599112c2a..d4851cdbd7 100644 --- a/packages/core/tools/tests/scoped.spec.ts +++ b/packages/core/tools/tests/scoped.spec.ts @@ -548,7 +548,6 @@ describe('scoped execution dispatch', () => { expect(reads).toBe(1) expect(result).toEqual({ - callId: CallId('unstable-arguments'), content: [{ type: 'text', text: 'ran:t' }], isError: false, }) @@ -564,10 +563,9 @@ describe('scoped execution dispatch', () => { ctx.on('internal/dispatch', (mode, name) => { if (name === 'tools/result') dispatchModes.push(mode) }) - ctx.on('tools/execute', async (exec, next) => { + ctx.on('tools/execute', async (_exec, next) => { await next() return { - callId: exec.callId, content: [{ type: 'text', text: 'outer failure' }], isError: true, } diff --git a/packages/core/tools/tests/tools.spec.ts b/packages/core/tools/tests/tools.spec.ts index b171817e35..59bd34e7ab 100644 --- a/packages/core/tools/tests/tools.spec.ts +++ b/packages/core/tools/tests/tools.spec.ts @@ -80,7 +80,7 @@ describe('ToolRegistry', () => { const ctx = await setup() ctx.tools.register(echoTool) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) - expect(result).toEqual({ callId: CallId('c1'), content: [{ type: 'text', text: 'hi' }], isError: false }) + expect(result).toEqual({ content: [{ type: 'text', text: 'hi' }], isError: false }) }) it('threads a tool-attached meta (object return form) onto the result', async () => { @@ -94,7 +94,6 @@ describe('ToolRegistry', () => { }) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'meta-tool', arguments: {} }) expect(result).toEqual({ - callId: CallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false, meta: { diffs: [{ path: 'a', oldText: null, newText: 'x' }] }, @@ -111,7 +110,7 @@ describe('ToolRegistry', () => { }, }) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'no-meta-tool', arguments: {} }) - expect(result).toEqual({ callId: CallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false }) + expect(result).toEqual({ content: [{ type: 'text', text: 'ok' }], isError: false }) expect('meta' in result).toBe(false) }) @@ -178,13 +177,12 @@ describe('ToolRegistry', () => { }) }) - it('ToolNotFoundError carries the tool name and a stable code', async () => { + it('ToolNotFoundError carries a stable message and code', async () => { const { HarnessError } = await import('@deepseek-ai/dsh-llm') const err = new ToolNotFoundError('ghost') expect(err).toBeInstanceOf(HarnessError) expect(err.name).toBe('ToolNotFoundError') expect(err.code).toBe('UNKNOWN_TOOL') - expect(err.toolName).toBe('ghost') expect(err.message).toBe('unknown tool "ghost"') }) @@ -425,7 +423,7 @@ describe('ToolRegistry', () => { ctx.on('tools/post-execute', async (_exec, _result, next) => { order.push('post'); return next() }) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'traced', arguments: { text: 'hi' } }) - expect(result).toEqual({ callId: CallId('c1'), content: [{ type: 'text', text: 'hi' }], isError: false }) + expect(result).toEqual({ content: [{ type: 'text', text: 'hi' }], isError: false }) // The around seam wraps dispatch; pre gates before it, post runs over its result. expect(order).toEqual(['pre', 'execute:before', 'dispatch', 'execute:after', 'post']) }) @@ -526,8 +524,8 @@ describe('ToolRegistry', () => { async execute() { dispatched = true; return [] }, }) - ctx.on('tools/execute', async (exec: ToolExecution, _next: () => Promise): Promise => - ({ callId: exec.callId, content: [{ type: 'text', text: 'short-circuited' }], isError: false })) + ctx.on('tools/execute', async (_exec: ToolExecution, _next: () => Promise): Promise => + ({ content: [{ type: 'text', text: 'short-circuited' }], isError: false })) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'never-runs', arguments: {} }) expect(dispatched).toBe(false) // returning without next() skips core dispatch @@ -537,8 +535,7 @@ describe('ToolRegistry', () => { it('preserves additionalContext supplied by an around-dispatch result', async () => { const ctx = await setup() ctx.tools.register(echoTool) - ctx.on('tools/execute', async exec => ({ - callId: exec.callId, + ctx.on('tools/execute', async () => ({ content: [{ type: 'text', text: 'short-circuited with context' }], isError: false, additionalContext: { @@ -556,20 +553,6 @@ describe('ToolRegistry', () => { }) }) - it('normalizes a tools/execute result with the wrong call id', async () => { - const ctx = await setup() - ctx.tools.register(echoTool) - ctx.on('tools/execute', async () => ({ callId: CallId('other'), content: [], isError: false })) - - const result = await ctx.tools.execute({ - callId: CallId('malformed-shape'), name: 'echo', arguments: {}, - }) - expect(result.isError).toBe(true) - expect(result.content[0]).toMatchObject({ - text: 'Error: tools/execute returned callId "other" for authoritative call "malformed-shape"', - }) - }) - it('returns an isError result when a tools/execute listener throws', async () => { const ctx = await setup() ctx.tools.register(echoTool) @@ -577,7 +560,6 @@ describe('ToolRegistry', () => { const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result).toEqual({ - callId: CallId('c1'), content: [{ type: 'text', text: 'Error: wrapper broke' }], isError: true, }) @@ -593,7 +575,6 @@ describe('ToolRegistry', () => { const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result).toEqual({ - callId: CallId('c1'), content: [{ type: 'text', text: 'Error: permission hook broke' }], isError: true, }) @@ -609,7 +590,6 @@ describe('ToolRegistry', () => { const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result).toEqual({ - callId: CallId('c1'), content: [{ type: 'text', text: 'Error: post hook broke' }], isError: true, }) @@ -625,7 +605,6 @@ describe('ToolRegistry', () => { const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result).toMatchObject({ - callId: CallId('c1'), isError: true, error: { name: 'HarnessError', code: 'DENIED' }, }) @@ -1254,7 +1233,7 @@ describe('defineTool validation (the runtime-validation RFC, part 1)', () => { }, })) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'reader', arguments: { path: '/x' } }) - expect(result).toEqual({ callId: CallId('c1'), content: [{ type: 'text', text: 'read /x' }], isError: false }) + expect(result).toEqual({ content: [{ type: 'text', text: 'read /x' }], isError: false }) }) it('ToolArgsError carries a stable code and the violation list', () => { diff --git a/packages/support/invariants/tests/invariants.spec.ts b/packages/support/invariants/tests/invariants.spec.ts index 0212501114..c451440e8e 100644 --- a/packages/support/invariants/tests/invariants.spec.ts +++ b/packages/support/invariants/tests/invariants.spec.ts @@ -855,9 +855,9 @@ describe('scoped-dispatch invariants', () => { ['agent/error', [agent, 1, 0, new Error('x')]], ['approval/request', [{ agent, toolName: 'echo' }, () => Promise.resolve('unavailable')]], ['tools/pre-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, () => Promise.resolve({ kind: 'allow' })]], - ['tools/execute', [{ callId: 'c', name: 't', arguments: {}, agent }, () => Promise.resolve({ callId: 'c', content: [], isError: false })]], - ['tools/post-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, { callId: 'c', content: [], isError: false }, () => Promise.resolve({ kind: 'accept' })]], - ['tools/result', [{ callId: 'c', name: 't', arguments: {}, agent }, { callId: 'c', content: [], isError: false }]], + ['tools/execute', [{ callId: 'c', name: 't', arguments: {}, agent }, () => Promise.resolve({ content: [], isError: false })]], + ['tools/post-execute', [{ callId: 'c', name: 't', arguments: {}, agent }, { content: [], isError: false }, () => Promise.resolve({ kind: 'accept' })]], + ['tools/result', [{ callId: 'c', name: 't', arguments: {}, agent }, { content: [], isError: false }]], ] for (const [event, args] of rows) { const subject = agent diff --git a/packages/timeout/timeout-policy/src/index.ts b/packages/timeout/timeout-policy/src/index.ts index 70ead41e1b..e946c2af61 100644 --- a/packages/timeout/timeout-policy/src/index.ts +++ b/packages/timeout/timeout-policy/src/index.ts @@ -6,7 +6,6 @@ */ import type { Context } from 'cordis' -import type { CallId } from '@deepseek-ai/dsh-llm' import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout' import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools' @@ -30,13 +29,11 @@ export const inject = ['tools'] * is the model-facing message; `error.code` is the same {@link TOOL_TIMEOUT} * this plugin owns, so a retry/sandbox plugin (and replay) can route on it. * - * @param callId - the timed-out call's id, carried onto the replacement result. * @param timeoutMs - the elapsed budget, rendered into the model-facing message. * @returns the `isError` {@link ToolExecutionResult} with a `TOOL_TIMEOUT` error. */ -export function toolTimeoutResult(callId: CallId, timeoutMs: number): ToolExecutionResult { +function toolTimeoutResult(timeoutMs: number): ToolExecutionResult { return { - callId, content: [{ type: 'text', text: `Error: tool call timed out after ${timeoutMs}ms` }], isError: true, error: { name: 'ToolTimeoutError', code: TOOL_TIMEOUT }, @@ -68,7 +65,7 @@ export function apply(ctx: Context): void { // quiescence; replace whatever it returned (its own abort result) with the // structured TOOL_TIMEOUT the model sees. if (timeoutOf(d.signal, TOOL_TIMEOUT) !== undefined) { - return toolTimeoutResult(exec.callId, timeoutMs) + return toolTimeoutResult(timeoutMs) } return result } finally { diff --git a/packages/timeout/timeout-policy/tests/timeout-policy.spec.ts b/packages/timeout/timeout-policy/tests/timeout-policy.spec.ts index 30a7307515..bd06ed6e16 100644 --- a/packages/timeout/timeout-policy/tests/timeout-policy.spec.ts +++ b/packages/timeout/timeout-policy/tests/timeout-policy.spec.ts @@ -11,9 +11,9 @@ import { Context } from 'cordis' import Loader from '@cordisjs/plugin-loader' import { CallId, HarnessError } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' -import ToolRegistry, { defineTool, type ToolExecutionInput, type ToolExecutionResult, type PostToolDecision } from '@deepseek-ai/dsh-tools' +import ToolRegistry, { defineTool, type ToolExecutionInput, type PostToolDecision } from '@deepseek-ai/dsh-tools' import * as timeoutPolicy from '@deepseek-ai/dsh-timeout-policy' -import { TOOL_TIMEOUT, toolTimeoutResult } from '@deepseek-ai/dsh-timeout-policy' +import { TOOL_TIMEOUT } from '@deepseek-ai/dsh-timeout-policy' /** Mount the registry + the zero-config timeout-policy enforcer. */ async function setup() { @@ -60,7 +60,7 @@ describe('timeout-policy delegation (unconfigured / fast)', () => { ctx.tools.register(defineTool({ name: 'fast', description: 'd', parameters: {}, timeoutMs: 10_000, async execute() { return [{ type: 'text' as const, text: 'ok' }] } })) const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'fast', arguments: {} }) - expect(result).toEqual({ callId: CallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false }) + expect(result).toEqual({ content: [{ type: 'text', text: 'ok' }], isError: false }) }) it('a budgeted tool receives the DERIVED deadline signal (not the caller signal) during dispatch', async () => { @@ -109,7 +109,6 @@ describe('timeout-policy TOOL_TIMEOUT replacement (deadline wins)', () => { await vi.advanceTimersByTimeAsync(150) const result = await pending expect(result).toEqual({ - callId: CallId('c1'), content: [{ type: 'text', text: 'Error: tool call timed out after 100ms' }], isError: true, error: { name: 'ToolTimeoutError', code: 'TOOL_TIMEOUT' }, @@ -140,16 +139,7 @@ describe('timeout-policy TOOL_TIMEOUT replacement (deadline wins)', () => { }) }) -describe('toolTimeoutResult', () => { - it('builds the structured TOOL_TIMEOUT result', () => { - expect(toolTimeoutResult(CallId('c9'), 250)).toEqual({ - callId: CallId('c9'), - content: [{ type: 'text', text: 'Error: tool call timed out after 250ms' }], - isError: true, - error: { name: 'ToolTimeoutError', code: 'TOOL_TIMEOUT' }, - } satisfies ToolExecutionResult) - }) - +describe('timeout-policy contract', () => { it('exposes the owned code constant', () => { expect(TOOL_TIMEOUT).toBe('TOOL_TIMEOUT') })