From a789637db66ea9a74048620f33a2d8f6489ecb9c Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 19 Aug 2026 13:03:32 +0800 Subject: [PATCH 01/13] refactor(llm): rename CallId to ToolCallId --- apps/cli/tests/fixtures/dsh-badge/snapshot.ts | 4 +- apps/cli/tests/web-agent-presets.e2e.ts | 4 +- apps/web/tests/background-job-list.e2e.ts | 4 +- .../tests/chat-continuous-conversation.e2e.ts | 6 +- apps/web/tests/chat-scroll-contract.e2e.ts | 4 +- apps/web/tests/chat-scroll-fixture.ts | 4 +- apps/web/tests/complex-history.perf.ts | 6 +- apps/web/tests/minimal-preset.snapshot.ts | 8 +- apps/web/tests/produced-file-mentions.e2e.ts | 4 +- apps/web/tests/produced-files.e2e.ts | 4 +- apps/web/tests/replay-round-trip.e2e.ts | 4 +- apps/web/tests/schedule-after.e2e.ts | 8 +- apps/web/tests/shipped-composition.e2e.ts | 8 +- .../07-into-the-harness.i18n.yaml | 4 +- docs/cordis-tutorial/07-into-the-harness.md | 6 +- .../cordis-tutorial/07-into-the-harness.zh.md | 6 +- docs/persistence-catalog.i18n.yaml | 4 +- docs/persistence-catalog.md | 8 +- docs/persistence-catalog.zh.md | 8 +- docs/subsystems/approval.i18n.yaml | 4 +- docs/subsystems/approval.md | 2 +- docs/subsystems/approval.zh.md | 2 +- docs/subsystems/compaction.i18n.yaml | 4 +- docs/subsystems/compaction.md | 2 +- docs/subsystems/compaction.zh.md | 2 +- docs/subsystems/core.i18n.yaml | 4 +- docs/subsystems/core.md | 4 +- docs/subsystems/core.zh.md | 4 +- docs/subsystems/llm-streaming.i18n.yaml | 4 +- docs/subsystems/llm-streaming.md | 4 +- docs/subsystems/llm-streaming.zh.md | 4 +- docs/subsystems/session.i18n.yaml | 4 +- docs/subsystems/session.md | 2 +- docs/subsystems/session.zh.md | 2 +- docs/subsystems/spill.i18n.yaml | 4 +- docs/subsystems/spill.md | 2 +- docs/subsystems/spill.zh.md | 2 +- docs/subsystems/tools.i18n.yaml | 4 +- docs/subsystems/tools.md | 8 +- docs/subsystems/tools.zh.md | 8 +- .../develop/practice/llm-adapter.i18n.yaml | 4 +- docs/user/develop/practice/llm-adapter.md | 6 +- docs/user/develop/practice/llm-adapter.zh.md | 6 +- .../tests/fixtures/shell/tool-pwsh/driver.ts | 8 +- .../subagent-acp/mock-delegating-llm.ts | 6 +- .../headless-agent/tests/code-mode.e2e.ts | 4 +- .../tests/fixtures/cli-mock-llm.ts | 6 +- .../tests/fixtures/team-llm.mjs | 4 +- .../tests/semantic-checkpoint.snapshot.ts | 6 +- .../subagent-dsh-sdk/mock-delegating-llm.ts | 6 +- packages/acp/acp/tests/approval.spec.ts | 6 +- packages/acp/acp/tests/edges.spec.ts | 6 +- .../client/connection/src/client/fixture.ts | 4 +- .../runtime/tests/event-script.client.ts | 4 +- .../src/client/contract/slots.ts | 10 +- .../src/client/contract/views.ts | 6 +- .../ui-conversation/src/client/index.ts | 2 +- .../ui-conversation/src/client/stores.ts | 6 +- .../tests/compaction-basic.spec.ts | 16 +- .../tests/compaction-loop-repro.spec.ts | 4 +- .../src/types.ts | 4 +- .../tests/tool-result-pruner.spec.ts | 12 +- .../compaction/tests/tool-pairing.spec.ts | 32 +-- .../tests/agent-instructions.spec.ts | 184 ++++++++--------- .../tests/session-reference.spec.ts | 4 +- .../time-context/tests/time-context.spec.ts | 4 +- .../agent-loop/tests/agent-initiator.spec.ts | 4 +- .../tests/contract-regressions.spec.ts | 20 +- .../agent-loop/tests/coverage-edges.spec.ts | 6 +- .../agent-loop/tests/interception.spec.ts | 6 +- packages/core/agent-loop/tests/loop.spec.ts | 8 +- .../core/agent-loop/tests/mock-adapter.ts | 4 +- .../core/agent-loop/tests/tool-calls.spec.ts | 54 ++--- packages/core/session/src/chunk-rows.ts | 6 +- packages/core/session/src/invariant.ts | 6 +- packages/core/session/src/repair.ts | 4 +- packages/core/session/src/types.ts | 4 +- .../core/session/tests/chunk-rows.spec.ts | 12 +- packages/core/session/tests/fork.spec.ts | 4 +- packages/core/session/tests/invariant.spec.ts | 28 +-- .../core/session/tests/properties.spec.ts | 4 +- packages/core/session/tests/repair.spec.ts | 30 +-- packages/core/session/tests/session.spec.ts | 10 +- packages/core/session/tests/surface.spec.ts | 10 +- packages/core/tools/src/code-mode.ts | 4 +- packages/core/tools/src/index.ts | 10 +- packages/core/tools/src/types.ts | 8 +- packages/core/tools/tests/code-mode.spec.ts | 22 +- .../core/tools/tests/execution-mode.spec.ts | 4 +- .../tests/execution-signal-types.spec.ts | 4 +- packages/core/tools/tests/invariant.spec.ts | 82 ++++---- packages/core/tools/tests/scoped.spec.ts | 28 +-- packages/core/tools/tests/tools.spec.ts | 188 +++++++++--------- .../agent-spine-demo/tests/agent-core.spec.ts | 8 +- .../tests/multi-project-sandbox.e2e.ts | 30 +-- .../tool-agent-team/tests/tool-team.spec.ts | 4 +- .../src/client/slot-catalog.ts | 4 +- .../cordis-host-runner/tests/helpers.ts | 4 +- .../extensions/tool-cordis/src/api-catalog.ts | 20 +- .../tool-fs-search/tests/integration.spec.ts | 6 +- .../fs/tool-fs-search/tests/rg-path.spec.ts | 4 +- .../fs/tool-fs-search/tests/tools.spec.ts | 6 +- packages/fs/tool-fs/tests/integration.spec.ts | 10 +- packages/fs/tool-fs/tests/read-image.spec.ts | 6 +- packages/fs/tool-fs/tests/tools.spec.ts | 12 +- .../tests/tools.spec.ts | 4 +- .../goal/tool-goal/tests/tool-goal.spec.ts | 8 +- .../tests/repeat-tool-reminder.spec.ts | 4 +- .../tests/timeout-policy.spec.ts | 24 +-- .../hooks-claude-code/tests/coverage-cases.ts | 4 +- .../hooks/hooks-codex/tests/coverage-cases.ts | 8 +- packages/host/apiproxy/src/api-proxy.ts | 4 +- packages/host/apiproxy/src/api/events.ts | 4 +- .../apiproxy/tests/api-proxy-view.spec.ts | 38 ++-- .../tool-ask-user/tests/tool-ask-user.spec.ts | 18 +- .../interaction/user-approval/src/index.ts | 6 +- .../user-approval/tests/approval.spec.ts | 6 +- .../jobs/tool-jobs/tests/tool-jobs.spec.ts | 4 +- packages/llm/llm-deepseek/src/translate.ts | 6 +- .../llm/llm-deepseek/tests/adapter.e2e.ts | 4 +- .../llm/llm-deepseek/tests/serialize.spec.ts | 30 +-- packages/llm/llm-pi-ai/src/context.ts | 10 +- packages/llm/llm-pi-ai/src/stream.ts | 6 +- packages/llm/llm-pi-ai/tests/adapter.e2e.ts | 4 +- packages/llm/llm-pi-ai/tests/context.spec.ts | 18 +- packages/llm/llm-pi-ai/tests/convert.spec.ts | 30 +-- .../llm/llm-pi-ai/tests/provider-apis.e2e.ts | 4 +- packages/llm/llm-retry/tests/retry.spec.ts | 4 +- packages/llm/llm/src/assembler.ts | 6 +- packages/llm/llm/src/brand.ts | 8 +- packages/llm/llm/src/message.ts | 6 +- packages/llm/llm/src/types.ts | 8 +- packages/llm/llm/tests/assembler.spec.ts | 24 +-- packages/llm/llm/tests/invariant.spec.ts | 6 +- packages/llm/llm/tests/message.spec.ts | 4 +- packages/llm/llm/tests/properties.spec.ts | 6 +- .../llm/token-meter/tests/token-meter.spec.ts | 6 +- .../mcp/mcp-client/tests/mcp-client.e2e.ts | 6 +- .../mcp/mcp-client/tests/mcp-client.spec.ts | 94 ++++----- .../mcp/mcp-client/tests/reconnect.spec.ts | 6 +- .../plan/plan-mode/tests/plan-mode.spec.ts | 10 +- packages/sandbox/sandbox/src/escalation.ts | 2 +- .../schedule/schedule/tests/plugin.spec.ts | 4 +- .../schedule/schedule/tests/tools.spec.ts | 6 +- .../tests/search-helpers.spec.ts | 4 +- .../tests/sqlite-integration.spec.ts | 6 +- .../tests/tool-session-query.spec.ts | 8 +- .../tests/fixtures/crash-child.ts | 4 +- .../tests/session-checkpoint-policy.spec.ts | 10 +- .../session-persistence/tests/contract.ts | 16 +- .../session-title-llm/tests/llm.spec.ts | 4 +- .../shell/shell-env/tests/shell-env.spec.ts | 6 +- .../tests/loader-composition.spec.ts | 4 +- .../tool-bash-persistent/tests/tools.spec.ts | 4 +- packages/shell/tool-bash/tests/tools.spec.ts | 26 +-- .../shell/tool-pwsh/tests/integration.spec.ts | 4 +- packages/shell/tool-pwsh/tests/tools.spec.ts | 12 +- .../skill/tool-skill/tests/tool-skill.spec.ts | 32 +-- .../spill-local/tests/spill-local.spec.ts | 4 +- packages/spill/spill-policy/src/index.ts | 4 +- .../spill-policy/tests/spill-policy.spec.ts | 16 +- packages/spill/spill/src/types.ts | 4 +- packages/spill/spill/tests/service.spec.ts | 4 +- .../tests/structured.spec.ts | 10 +- .../tests/subagent-in-process-driver.spec.ts | 6 +- .../subagent/tests/continuation.spec.ts | 6 +- .../tests/list-agents.spec.ts | 4 +- .../tests/tool-subagent-control.spec.ts | 4 +- .../tests/tool-subagent-report.spec.ts | 4 +- .../tool-subagent/tests/tool-subagent.spec.ts | 54 ++--- .../tests/loader-composition.spec.ts | 8 +- .../tool-terminal/tests/tools.spec.ts | 6 +- .../llm-replay/tests/llm-replay.spec.ts | 6 +- .../tests/loader-composition.spec.ts | 6 +- .../todo/tool-todo/tests/tool-todo.spec.ts | 4 +- packages/util/brand/README.i18n.yaml | 4 +- packages/util/brand/README.md | 4 +- packages/util/brand/README.zh.md | 4 +- packages/util/brand/src/index.ts | 4 +- .../web/tool-web/tests/integration.spec.ts | 6 +- packages/web/tool-web/tests/spill.spec.ts | 4 +- packages/web/tool-web/tests/tool-web.spec.ts | 16 +- .../tool-ralph/tests/integration.spec.ts | 10 +- .../tool-ralph/tests/tool-ralph.spec.ts | 4 +- .../tool-workflow/tests/tool-workflow.spec.ts | 4 +- scripts/gen-persistence-catalog.ts | 2 +- 186 files changed, 1013 insertions(+), 1013 deletions(-) diff --git a/apps/cli/tests/fixtures/dsh-badge/snapshot.ts b/apps/cli/tests/fixtures/dsh-badge/snapshot.ts index cdc4dbb6d3..d579e8fe89 100644 --- a/apps/cli/tests/fixtures/dsh-badge/snapshot.ts +++ b/apps/cli/tests/fixtures/dsh-badge/snapshot.ts @@ -1,7 +1,7 @@ import { fileURLToPath } from 'node:url' import { Context } from '@deepseek-ai/cordis' import { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot' import { SessionId } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-skill' @@ -45,7 +45,7 @@ try { : undefined const summary = (await ctx.skills.list()).find(skill => skill.name === 'dsh-badge') const result = await ctx.tools.execute({ - callId: CallId('dsh-badge-snapshot'), + callId: ToolCallId('dsh-badge-snapshot'), name: 'skill', arguments: { name: 'dsh-badge' }, signal: new AbortController().signal, diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index 0e98af0477..6d04de6380 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -13,7 +13,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' import { settingsNamespace } from '@deepseek-ai/dsh-settings' import { resolveSessionPreset, SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-presets' import { applyChildComposition, childSessionMeta } from '@deepseek-ai/dsh-subagent' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import type {} from '@deepseek-ai/dsh-compaction-basic' import type {} from '@deepseek-ai/dsh-skill' import type {} from '@deepseek-ai/dsh-tools' @@ -403,7 +403,7 @@ describe('the shipped Web composition', () => { // The preset's own loader tool resolves the global-layer skill. const loaded = await ctx.tools.execute({ - callId: CallId('preset-skills-load'), + callId: ToolCallId('preset-skills-load'), name: 'skill', arguments: { name: 'dsh-badge' }, signal: new AbortController().signal, diff --git a/apps/web/tests/background-job-list.e2e.ts b/apps/web/tests/background-job-list.e2e.ts index 0e0a98de93..9253edaaef 100644 --- a/apps/web/tests/background-job-list.e2e.ts +++ b/apps/web/tests/background-job-list.e2e.ts @@ -10,7 +10,7 @@ import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import type { Agent } from '@deepseek-ai/dsh-agent' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import { JobId } from '@deepseek-ai/dsh-jobs' import { @@ -90,7 +90,7 @@ describe.skipIf(MODE === 'record')('web e2e: background job list', () => { const started = await scaffold.ctx.tools.execute({ signal: new AbortController().signal, - callId: CallId('background-job-list-e2e'), + callId: ToolCallId('background-job-list-e2e'), name: 'bash', arguments: { command: COMMAND, description: 'Hold a background slot open', run_in_background: true }, agent, diff --git a/apps/web/tests/chat-continuous-conversation.e2e.ts b/apps/web/tests/chat-continuous-conversation.e2e.ts index cd15f2e054..7413049b9f 100644 --- a/apps/web/tests/chat-continuous-conversation.e2e.ts +++ b/apps/web/tests/chat-continuous-conversation.e2e.ts @@ -9,7 +9,7 @@ import { join } from 'node:path' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' -import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm' +import { ToolCallId, type StreamChunk } from '@deepseek-ai/dsh-llm' import type { ReplayEntry, ReplayOverrideDoc } from '@deepseek-ai/dsh-llm-replay' import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session' import { @@ -32,7 +32,7 @@ interface TurnSpec { readonly firstMarker: string readonly doneMarker: string readonly deltas: readonly string[] - readonly callId?: ReturnType + readonly callId?: ReturnType readonly toolResultMarker?: string } @@ -81,7 +81,7 @@ function turnSpec(index: number): TurnSpec { firstMarker, doneMarker, deltas, - callId: CallId(`continuous-chat-tool-${id}`), + callId: ToolCallId(`continuous-chat-tool-${id}`), toolResultMarker: `CONTINUOUS_CHAT_TOOL_RESULT_${id}`, } } diff --git a/apps/web/tests/chat-scroll-contract.e2e.ts b/apps/web/tests/chat-scroll-contract.e2e.ts index 2e1892f8b6..cb1d4c8245 100644 --- a/apps/web/tests/chat-scroll-contract.e2e.ts +++ b/apps/web/tests/chat-scroll-contract.e2e.ts @@ -9,7 +9,7 @@ import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import type { StreamChunk } from '@deepseek-ai/dsh-llm' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import type { ReplayEntry, ReplayOverrideDoc } from '@deepseek-ai/dsh-llm-replay' import type { SessionEvent } from '@deepseek-ai/dsh-session' import { createChatScrollFixture, type ChatScrollFixture } from './chat-scroll-fixture.ts' @@ -34,7 +34,7 @@ const LIVE_TEXT_PROMPT = 'CHAT_SCROLL_LIVE_USER Continue this long conversation const LIVE_TEXT_FIRST = 'CHAT_SCROLL_LIVE_FIRST' const LIVE_TEXT_DONE = 'CHAT_SCROLL_LIVE_DONE' const LIVE_TOOL_PROMPT = 'CHAT_SCROLL_TOOL_USER Run the requested diagnostic and then summarize it.' -const LIVE_TOOL_CALL_ID = CallId('chat-scroll-live-tool-call') +const LIVE_TOOL_CALL_ID = ToolCallId('chat-scroll-live-tool-call') const LIVE_TOOL_RESULT = 'CHAT_SCROLL_LIVE_TOOL_RESULT' const LIVE_TOOL_FIRST = 'CHAT_SCROLL_TOOL_STREAM_FIRST' const LIVE_TOOL_DONE = 'CHAT_SCROLL_TOOL_STREAM_DONE' diff --git a/apps/web/tests/chat-scroll-fixture.ts b/apps/web/tests/chat-scroll-fixture.ts index caf9cf13d9..ed6c80da68 100644 --- a/apps/web/tests/chat-scroll-fixture.ts +++ b/apps/web/tests/chat-scroll-fixture.ts @@ -3,7 +3,7 @@ // persisted conversations, while unique markers identify semantic rows // without depending on CSS-module names or virtualizer DOM positions. import { - CallId, + ToolCallId, createAssistantMessage, createToolResultMessage, createUserMessage, @@ -105,7 +105,7 @@ function appendToolStep( ): void { const calls = [1, 2].map((index) => { const marker = markers.tool(turn, index) - const callId = CallId(`chat-scroll-${suffix(turn)}-${String(index)}`) + const callId = ToolCallId(`chat-scroll-${suffix(turn)}-${String(index)}`) const args = JSON.stringify({ command: `printf '${marker}\\n'`, description: marker, diff --git a/apps/web/tests/complex-history.perf.ts b/apps/web/tests/complex-history.perf.ts index eeb9931473..96948960fd 100644 --- a/apps/web/tests/complex-history.perf.ts +++ b/apps/web/tests/complex-history.perf.ts @@ -11,7 +11,7 @@ import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import type { StreamChunk } from '@deepseek-ai/dsh-llm' import { - CallId, + ToolCallId, createAssistantMessage, createToolResultMessage, createUserMessage, @@ -233,7 +233,7 @@ function appendToolStep( toolCount: number, ): void { const calls = Array.from({ length: toolCount }, (_, index) => { - const callId = CallId(`perf-call-${String(turn)}-${String(index)}`) + const callId = ToolCallId(`perf-call-${String(turn)}-${String(index)}`) const args = JSON.stringify({ turn, index, @@ -467,7 +467,7 @@ function soakTurn(index: number): ConversationTurnSpec { } function toolStream(index: number, marker: string): StreamChunk[] { - const callId = CallId(`performance-tool-${marker.toLowerCase()}-${String(index)}`) + const callId = ToolCallId(`performance-tool-${marker.toLowerCase()}-${String(index)}`) const args = JSON.stringify({ command: `printf '${marker}\\n'`, description: `Emit performance marker ${String(index)}`, diff --git a/apps/web/tests/minimal-preset.snapshot.ts b/apps/web/tests/minimal-preset.snapshot.ts index 011f71ea8d..f2cd816e28 100644 --- a/apps/web/tests/minimal-preset.snapshot.ts +++ b/apps/web/tests/minimal-preset.snapshot.ts @@ -3,7 +3,7 @@ import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import type { AgentHandle } from '@deepseek-ai/dsh-agent' -import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm' +import { ToolCallId, createUserMessage } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-agent-presets' import type {} from '@deepseek-ai/dsh-system-prompt' @@ -68,14 +68,14 @@ describe('minimal agent preset', () => { const signal = new AbortController().signal await scaffold.ctx.tools.execute({ signal, - callId: CallId('minimal-bash-state-setup'), + callId: ToolCallId('minimal-bash-state-setup'), name: 'bash', arguments: { command: `cd ${JSON.stringify(stateDir)} && export DSH_MINIMAL_STATE=PERSISTED` }, agent: agentHandle.agent, }) const bash = await scaffold.ctx.tools.execute({ signal, - callId: CallId('minimal-bash-state-read'), + callId: ToolCallId('minimal-bash-state-read'), name: 'bash', arguments: { command: 'printf \'%s:%s\n\' "$DSH_MINIMAL_STATE" "$PWD"' }, agent: agentHandle.agent, @@ -84,7 +84,7 @@ describe('minimal agent preset', () => { await writeFile(seedPath, 'MINIMAL_EDITOR_OK\n') const editor = await scaffold.ctx.tools.execute({ signal, - callId: CallId('minimal-editor-smoke'), + callId: ToolCallId('minimal-editor-smoke'), name: 'str_replace_editor', arguments: { command: 'view', path: seedPath }, agent: agentHandle.agent, diff --git a/apps/web/tests/produced-file-mentions.e2e.ts b/apps/web/tests/produced-file-mentions.e2e.ts index badd9b845e..b07f4936c6 100644 --- a/apps/web/tests/produced-file-mentions.e2e.ts +++ b/apps/web/tests/produced-file-mentions.e2e.ts @@ -9,7 +9,7 @@ import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' -import { CallId, createAssistantMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm' +import { ToolCallId, createAssistantMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm' import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-session-title' import { @@ -46,7 +46,7 @@ function mentionFixture(): string { session.append('step/start', { turn: 1, step: 1 }) const calls = WRITES.map((path, index) => ({ path, - callId: CallId(`file-mention-${String(index)}`), + callId: ToolCallId(`file-mention-${String(index)}`), args: JSON.stringify({ file_path: path, content: `content of ${path}\n` }), })) session.append('assistant/message', { diff --git a/apps/web/tests/produced-files.e2e.ts b/apps/web/tests/produced-files.e2e.ts index 2dcc92f65e..7b88d0566b 100644 --- a/apps/web/tests/produced-files.e2e.ts +++ b/apps/web/tests/produced-files.e2e.ts @@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed, vi } from 'vitest' -import { CallId, createAssistantMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm' +import { ToolCallId, createAssistantMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm' import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-session-title' import { @@ -49,7 +49,7 @@ function producedFixture(): string { session.append('step/start', { turn: 1, step: 1 }) const calls = PRODUCED.map((path, index) => ({ path, - callId: CallId(`produced-files-${String(index)}`), + callId: ToolCallId(`produced-files-${String(index)}`), args: JSON.stringify({ file_path: path, content: `content of ${path}\n` }), })) session.append('assistant/message', { diff --git a/apps/web/tests/replay-round-trip.e2e.ts b/apps/web/tests/replay-round-trip.e2e.ts index f55cb0e7b5..738ef0b885 100644 --- a/apps/web/tests/replay-round-trip.e2e.ts +++ b/apps/web/tests/replay-round-trip.e2e.ts @@ -14,7 +14,7 @@ import { fileURLToPath } from 'node:url' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session' import { assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, @@ -98,7 +98,7 @@ describe('web e2e: fresh round trip through the real assembly', () => { if (agent === undefined) throw new Error(`the settled Web agent ${settledSessionId} is no longer live`) const result = await scaffold.ctx.tools.execute({ signal: AbortSignal.timeout(5_000), - callId: CallId('web-url-probe'), + callId: ToolCallId('web-url-probe'), name: 'bash', arguments: { command: 'printf \'%s\\n\' "$DSH_WEB_URL"', diff --git a/apps/web/tests/schedule-after.e2e.ts b/apps/web/tests/schedule-after.e2e.ts index 70e79019ec..620e6b9b5d 100644 --- a/apps/web/tests/schedule-after.e2e.ts +++ b/apps/web/tests/schedule-after.e2e.ts @@ -6,7 +6,7 @@ import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import type { AgentHandle } from '@deepseek-ai/dsh-agent' -import { CallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm' +import { ToolCallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' import { @@ -121,7 +121,7 @@ class BrowserZoneAtAdapter extends LlmAdapter { this.selectedAt = localAt(target, AT_BROWSER_ZONE) this.scheduledAt = new Date(target).toISOString() const argumentsJson = JSON.stringify({ prompt: AT_PROMPT, at: this.selectedAt }) - const callId = CallId('schedule-at-browser-zone') + const callId = ToolCallId('schedule-at-browser-zone') yield { type: 'block-start', index: 0, blockType: 'tool-call' } yield { type: 'tool-call-delta', @@ -258,7 +258,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => { await workspace.attachSession(afterHandle.agent.id) const afterCreated = await scaffold.ctx.tools.execute({ signal: AbortSignal.timeout(10_000), - callId: CallId('schedule-after-create'), + callId: ToolCallId('schedule-after-create'), name: 'schedule_create', arguments: { prompt: AFTER_PROMPT, after_seconds: 1 }, agent: afterHandle.agent, @@ -314,7 +314,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => { await workspace.attachSession(everyHandle.agent.id) const everyListed = await scaffold.ctx.tools.execute({ signal: AbortSignal.timeout(10_000), - callId: CallId('schedule-every-list'), + callId: ToolCallId('schedule-every-list'), name: 'schedule_list', arguments: {}, agent: everyHandle.agent, diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts index 295e861b95..fc4e0de595 100644 --- a/apps/web/tests/shipped-composition.e2e.ts +++ b/apps/web/tests/shipped-composition.e2e.ts @@ -7,7 +7,7 @@ import { readFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { fileURLToPath } from 'node:url' import { afterEach, expect, it } from 'vitest' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox' import { SessionId } from '@deepseek-ai/dsh-session' import { settingsNamespace } from '@deepseek-ai/dsh-settings' @@ -199,7 +199,7 @@ it('lets a preset producer reach the background-job registry', async () => { // fails here — with every task control still listed in the catalog above. const started = await ctx.tools.execute({ signal, - callId: CallId('shipped-bash-background'), + callId: ToolCallId('shipped-bash-background'), name: 'bash', arguments: { command: 'printf SHIPPED_BACKGROUND_OK', @@ -217,7 +217,7 @@ it('lets a preset producer reach the background-job registry', async () => { // owner. A per-preset registry would list nothing here even on success. const listed = await ctx.tools.execute({ signal, - callId: CallId('shipped-task-list'), + callId: ToolCallId('shipped-task-list'), name: 'job_list', arguments: {}, agent: handle.agent, @@ -231,7 +231,7 @@ it('lets a preset producer reach the background-job registry', async () => { // through a preset-plane control, which is the linkage the realm severed. const collected = await ctx.tools.execute({ signal, - callId: CallId('shipped-task-output'), + callId: ToolCallId('shipped-task-output'), name: 'job_output', arguments: { job_id: 'bash-1', wait: true }, agent: handle.agent, diff --git a/docs/cordis-tutorial/07-into-the-harness.i18n.yaml b/docs/cordis-tutorial/07-into-the-harness.i18n.yaml index 0923def71f..413cd3134a 100644 --- a/docs/cordis-tutorial/07-into-the-harness.i18n.yaml +++ b/docs/cordis-tutorial/07-into-the-harness.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/cordis-tutorial/07-into-the-harness.md -07-into-the-harness.md: 2d3c23f9f7f7fc6bd6cabd4e7e68ebfc46e20665 -07-into-the-harness.zh.md: 0e0fe031be92c8284d2ff6331d753b9b25994ee9 +07-into-the-harness.md: 1bfe05fb511b54067b913a1a2d0903a0772a22d7 +07-into-the-harness.zh.md: 5ed4ff9d95d6e8b14402e525e1e9bb5328bfb678 diff --git a/docs/cordis-tutorial/07-into-the-harness.md b/docs/cordis-tutorial/07-into-the-harness.md index 2d3c23f9f7..1bfe05fb51 100644 --- a/docs/cordis-tutorial/07-into-the-harness.md +++ b/docs/cordis-tutorial/07-into-the-harness.md @@ -11,7 +11,7 @@ Create `greet-tool.ts` in `tmp/cordis-tutorial`: ```ts import type { Context } from '@deepseek-ai/cordis' import { defineTool } from '@deepseek-ai/dsh-tools' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' export const name = 'greet-tool' export const inject = ['tools'] @@ -33,10 +33,10 @@ export function apply(ctx: Context) { })) // Drive one call through the real execution pipeline, standing in for - // the model. CallId brands the correlation id a provider would issue. + // the model. ToolCallId brands the correlation id a provider would issue. void (async () => { const result = await ctx.tools.execute({ - callId: CallId('demo-1'), + callId: ToolCallId('demo-1'), name: 'greet', arguments: { name: 'Cordis' }, signal: new AbortController().signal, diff --git a/docs/cordis-tutorial/07-into-the-harness.zh.md b/docs/cordis-tutorial/07-into-the-harness.zh.md index 0e0fe031be..5ed4ff9d95 100644 --- a/docs/cordis-tutorial/07-into-the-harness.zh.md +++ b/docs/cordis-tutorial/07-into-the-harness.zh.md @@ -11,7 +11,7 @@ ```ts import type { Context } from '@deepseek-ai/cordis' import { defineTool } from '@deepseek-ai/dsh-tools' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' export const name = 'greet-tool' export const inject = ['tools'] @@ -33,10 +33,10 @@ export function apply(ctx: Context) { })) // Drive one call through the real execution pipeline, standing in for - // the model. CallId brands the correlation id a provider would issue. + // the model. ToolCallId brands the correlation id a provider would issue. void (async () => { const result = await ctx.tools.execute({ - callId: CallId('demo-1'), + callId: ToolCallId('demo-1'), name: 'greet', arguments: { name: 'Cordis' }, signal: new AbortController().signal, diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index d2523f7c33..1a1c32c1ed 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-catalog.md -persistence-catalog.md: f8c10821e4b6daa8f10b0cff63f838ecb381125c -persistence-catalog.zh.md: 4cefec2eba0a3a3fa7f4c587236fa78e476ad9d7 +persistence-catalog.md: 44a9f61b7b53670186c565efc0b4c587e216d6b8 +persistence-catalog.zh.md: 2ea0e7c236301b786a3f5aab66ad58e802f017c7 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index f8c10821e4..44a9f61b7b 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -153,12 +153,12 @@ Source: [`packages/preset/agent-presets/src/session.ts:26`](../packages/preset/a 'approval/asked': { id: ApprovalRequestId toolName: string - callId?: CallId + callId?: ToolCallId reason?: string } ``` -Types: [CallId](subsystems/core.md) +Types: [ToolCallId](subsystems/core.md) Source: [`packages/interaction/user-approval/src/index.ts:44`](../packages/interaction/user-approval/src/index.ts) @@ -799,10 +799,10 @@ Source: [`packages/core/session/src/types.ts:303`](../packages/core/session/src/ * JSON string exactly as the model produced it (unparsed). `callId` pairs the * call with its `tool/result`. */ -'tool/call': { turn: number; step: number; callId: CallId; name: string; arguments: string } +'tool/call': { turn: number; step: number; callId: ToolCallId; name: string; arguments: string } ``` -Types: [CallId](subsystems/core.md) +Types: [ToolCallId](subsystems/core.md) Source: [`packages/core/session/src/types.ts:283`](../packages/core/session/src/types.ts) diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 4cefec2eba..2ea0e7c236 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -155,12 +155,12 @@ export type SessionEvent = { 'approval/asked': { id: ApprovalRequestId toolName: string - callId?: CallId + callId?: ToolCallId reason?: string } ``` -类型:[CallId](subsystems/core.zh.md) +类型:[ToolCallId](subsystems/core.zh.md) 来源:[`packages/interaction/user-approval/src/index.ts:44`](../packages/interaction/user-approval/src/index.ts) @@ -801,10 +801,10 @@ export type SessionEvent = { * JSON string exactly as the model produced it (unparsed). `callId` pairs the * call with its `tool/result`. */ -'tool/call': { turn: number; step: number; callId: CallId; name: string; arguments: string } +'tool/call': { turn: number; step: number; callId: ToolCallId; name: string; arguments: string } ``` -类型:[CallId](subsystems/core.zh.md) +类型:[ToolCallId](subsystems/core.zh.md) 来源:[`packages/core/session/src/types.ts:283`](../packages/core/session/src/types.ts) diff --git a/docs/subsystems/approval.i18n.yaml b/docs/subsystems/approval.i18n.yaml index 0fa3e7df83..8d41cbdadb 100644 --- a/docs/subsystems/approval.i18n.yaml +++ b/docs/subsystems/approval.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/approval.md -approval.md: 7d3d09314f8fbb151cc8a00dbbee5e2cc14e2c9a -approval.zh.md: 22d0b0ec4242fcb2ad6fb82c29893a5914369238 +approval.md: 276cac2c00e5b781e9cdbb5da461d035e61de719 +approval.zh.md: 5fe63b5d1aa39ddced51af2c0eaf860e4df10e86 diff --git a/docs/subsystems/approval.md b/docs/subsystems/approval.md index 7d3d09314f..276cac2c00 100644 --- a/docs/subsystems/approval.md +++ b/docs/subsystems/approval.md @@ -70,7 +70,7 @@ interface ApprovalRequest { * The exact tool call being decided, when the asker has one — lets a UI * attach the prompt to the tool call it already streamed. */ - readonly callId?: CallId + readonly callId?: ToolCallId /** The asker's human-readable explanation of WHY it is asking. */ readonly reason?: string /** diff --git a/docs/subsystems/approval.zh.md b/docs/subsystems/approval.zh.md index 22d0b0ec42..5fe63b5d1a 100644 --- a/docs/subsystems/approval.zh.md +++ b/docs/subsystems/approval.zh.md @@ -70,7 +70,7 @@ interface ApprovalRequest { * The exact tool call being decided, when the asker has one — lets a UI * attach the prompt to the tool call it already streamed. */ - readonly callId?: CallId + readonly callId?: ToolCallId /** The asker's human-readable explanation of WHY it is asking. */ readonly reason?: string /** diff --git a/docs/subsystems/compaction.i18n.yaml b/docs/subsystems/compaction.i18n.yaml index 6ddba3abd7..d286c03073 100644 --- a/docs/subsystems/compaction.i18n.yaml +++ b/docs/subsystems/compaction.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/compaction.md -compaction.md: 4b6e9dee81cb30d42bb6194776457e354102b529 -compaction.zh.md: a9c57b64798be1d0361125eac68d589b3e0f1c66 +compaction.md: ebd8e6e9d7530ff5b0bb9faf4a4d5d24b00e464d +compaction.zh.md: e51e8f7d627a339e3e5a9c079d5b5a57f384772d diff --git a/docs/subsystems/compaction.md b/docs/subsystems/compaction.md index 4b6e9dee81..ebd8e6e9d7 100644 --- a/docs/subsystems/compaction.md +++ b/docs/subsystems/compaction.md @@ -99,7 +99,7 @@ interface PrunedEntry { /** Newly appended pruned tool-result event. */ readonly replacementSeq: number /** Tool call shared by the original and replacement. */ - readonly callId: CallId + readonly callId: ToolCallId /** Original text size in Unicode code points. */ readonly charsBefore: number /** Replacement text size in Unicode code points. */ diff --git a/docs/subsystems/compaction.zh.md b/docs/subsystems/compaction.zh.md index a9c57b6479..e51e8f7d62 100644 --- a/docs/subsystems/compaction.zh.md +++ b/docs/subsystems/compaction.zh.md @@ -99,7 +99,7 @@ interface PrunedEntry { /** Newly appended pruned tool-result event. */ readonly replacementSeq: number /** Tool call shared by the original and replacement. */ - readonly callId: CallId + readonly callId: ToolCallId /** Original text size in Unicode code points. */ readonly charsBefore: number /** Replacement text size in Unicode code points. */ diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml index 62f63c4e31..975bec65bb 100644 --- a/docs/subsystems/core.i18n.yaml +++ b/docs/subsystems/core.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/core.md -core.md: 18057c9a9a308e439bb6a08f598f51f158243496 -core.zh.md: dfb1d8ad482b2cf9c6de94bde04c4dbff83a0082 +core.md: 37c185ff8c90f88aecb0b2a7e426bc8f22162578 +core.zh.md: 6559e25a5cdb534d98c3fd28cd68c506bdbaf1f4 diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index 18057c9a9a..37c185ff8c 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -295,7 +295,7 @@ Two large discriminated unions are the ones consumers `switch` over most: **`Str ### Branded IDs -IDs passed between packages are **branded** — structurally strings, but non-interchangeable at the type level (a `SessionId` cannot be passed where a `CallId` is expected). Construction goes through a per-type factory; comparison, logging, and JSON behave as ordinary strings. +IDs passed between packages are **branded** — structurally strings, but non-interchangeable at the type level (a `SessionId` cannot be passed where a `ToolCallId` is expected). Construction goes through a per-type factory; comparison, logging, and JSON behave as ordinary strings. The `Branded` primitive lives in its own type-only package, [dsh-brand](../../packages/util/brand) (no runtime code, no harness-package dependency), so any package can brand the ids it owns without depending on an unrelated capability package. @@ -306,7 +306,7 @@ Source: [`packages/util/brand/src/index.ts`](../../packages/util/brand/src/index type Branded = string & { readonly [BRAND]: B } ``` -The two core IDs are `CallId` (correlates a tool call with its result; dsh-llm) and `SessionId` (the shared live agent and durable session identity; dsh-session). Capability packages brand their own ids too, such as `JobId` in [jobs.md](jobs.md). +The two core IDs are `ToolCallId` (correlates a tool call with its result; dsh-llm) and `SessionId` (the shared live agent and durable session identity; dsh-session). Capability packages brand their own ids too, such as `JobId` in [jobs.md](jobs.md). diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index dfb1d8ad48..6559e25a5c 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -305,7 +305,7 @@ declare module '@deepseek-ai/dsh-llm' { ### 品牌化 ID -在包之间传递的 ID 都经过**品牌化**——结构上是字符串,但在类型层面不可互换(不能把 `SessionId` 传给需要 `CallId` 的位置)。每种类型通过各自的工厂构造;比较、日志记录和 JSON 行为与普通字符串相同。 +在包之间传递的 ID 都经过**品牌化**——结构上是字符串,但在类型层面不可互换(不能把 `SessionId` 传给需要 `ToolCallId` 的位置)。每种类型通过各自的工厂构造;比较、日志记录和 JSON 行为与普通字符串相同。 `Branded` 原语位于独立的纯类型包 [dsh-brand](../../packages/util/brand) 中(没有运行时代码,也不依赖 harness 包),因此任何包都能品牌化其拥有的 id,而无需依赖无关的能力包。 @@ -316,7 +316,7 @@ declare module '@deepseek-ai/dsh-llm' { type Branded = string & { readonly [BRAND]: B } ``` -两个核心 ID 是 `CallId`(关联工具调用及其结果;dsh-llm)和 `SessionId`(活跃 agent 与持久会话共享的标识;dsh-session)。能力包也会品牌化各自的 id,例如 [jobs.md](jobs.zh.md) 中的 `JobId`。 +两个核心 ID 是 `ToolCallId`(关联工具调用及其结果;dsh-llm)和 `SessionId`(活跃 agent 与持久会话共享的标识;dsh-session)。能力包也会品牌化各自的 id,例如 [jobs.md](jobs.zh.md) 中的 `JobId`。 diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml index 5c118e9782..4d43e8e37c 100644 --- a/docs/subsystems/llm-streaming.i18n.yaml +++ b/docs/subsystems/llm-streaming.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/llm-streaming.md -llm-streaming.md: 4f322ca1024b9d74a4906e34f93fc6f8e4082cbf -llm-streaming.zh.md: c74cabe27c1f5fdd44711ac0aae7cd6b0a7ba7dd +llm-streaming.md: adb8fdde50c898b42f74a1562f95ab267afa6db6 +llm-streaming.zh.md: 197ae86a2182b93c3fbf4005b3fc51df96dc49b3 diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md index 4f322ca102..adb8fdde50 100644 --- a/docs/subsystems/llm-streaming.md +++ b/docs/subsystems/llm-streaming.md @@ -28,7 +28,7 @@ interface ContentBlockMap { } ``` -The block interfaces (full fields in source): `TextBlock` (`text`), `ReasoningBlock` (thinking, distinct from visible text), `ImageBlock` (a durable [image attachment](attachment.md)), `ToolCallBlock` (`id: CallId`, `name`, raw-JSON `arguments`), and `ToolResultBlock` (`toolCallId`, nested `content: ContentBlock[]`, `isError?`). `ContentBlock = ContentBlockMap[ContentBlockType]`. A new modality belongs in the merge-extensible map only when its adapter, UI, compaction, and durable replay paths honor it. +The block interfaces (full fields in source): `TextBlock` (`text`), `ReasoningBlock` (thinking, distinct from visible text), `ImageBlock` (a durable [image attachment](attachment.md)), `ToolCallBlock` (`id: ToolCallId`, `name`, raw-JSON `arguments`), and `ToolResultBlock` (`toolCallId`, nested `content: ContentBlock[]`, `isError?`). `ContentBlock = ContentBlockMap[ContentBlockType]`. A new modality belongs in the merge-extensible map only when its adapter, UI, compaction, and durable replay paths honor it. Source: [`packages/llm/llm/src/message.ts`](../../packages/llm/llm/src/message.ts) @@ -193,7 +193,7 @@ type StreamChunk = | { type: 'block-start'; index: number; blockType: ContentBlockType } | { type: 'text-delta'; index: number; text: string } | { type: 'reasoning-delta'; index: number; text: string } - | { type: 'tool-call-delta'; index: number; id: CallId; name?: string; argumentsDelta: string } + | { type: 'tool-call-delta'; index: number; id: ToolCallId; name?: string; argumentsDelta: string } | { type: 'block-end'; index: number; block: ContentBlock } | { type: 'usage'; usage: TokenUsage } | { diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md index c74cabe27c..197ae86a21 100644 --- a/docs/subsystems/llm-streaming.zh.md +++ b/docs/subsystems/llm-streaming.zh.md @@ -28,7 +28,7 @@ interface ContentBlockMap { } ``` -各块接口(完整字段见源码):`TextBlock`(`text`)、`ReasoningBlock`(thinking,区别于可见文本)、`ImageBlock`(一个持久的[图片附件](attachment.zh.md))、`ToolCallBlock`(`id: CallId`、`name`、原始 JSON `arguments`),以及 `ToolResultBlock`(`toolCallId`、嵌套 `content: ContentBlock[]`、`isError?`)。`ContentBlock = ContentBlockMap[ContentBlockType]`。仅当适配器、UI、压缩(compaction)和持久回放路径均支持某种新模态时,才将其纳入可合并扩展的 map。 +各块接口(完整字段见源码):`TextBlock`(`text`)、`ReasoningBlock`(thinking,区别于可见文本)、`ImageBlock`(一个持久的[图片附件](attachment.zh.md))、`ToolCallBlock`(`id: ToolCallId`、`name`、原始 JSON `arguments`),以及 `ToolResultBlock`(`toolCallId`、嵌套 `content: ContentBlock[]`、`isError?`)。`ContentBlock = ContentBlockMap[ContentBlockType]`。仅当适配器、UI、压缩(compaction)和持久回放路径均支持某种新模态时,才将其纳入可合并扩展的 map。 源码:[`packages/llm/llm/src/message.ts`](../../packages/llm/llm/src/message.ts) @@ -193,7 +193,7 @@ type StreamChunk = | { type: 'block-start'; index: number; blockType: ContentBlockType } | { type: 'text-delta'; index: number; text: string } | { type: 'reasoning-delta'; index: number; text: string } - | { type: 'tool-call-delta'; index: number; id: CallId; name?: string; argumentsDelta: string } + | { type: 'tool-call-delta'; index: number; id: ToolCallId; name?: string; argumentsDelta: string } | { type: 'block-end'; index: number; block: ContentBlock } | { type: 'usage'; usage: TokenUsage } | { diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml index 814f019958..cf0d0003d4 100644 --- a/docs/subsystems/session.i18n.yaml +++ b/docs/subsystems/session.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/session.md -session.md: 96520c72b83bf713b7fe09563f7d6e44c0386bcf -session.zh.md: 28a259226b8149dbe26c7650bc574de4aa43a740 +session.md: a23ed4c43befc1c39b110560d933ff2bb1bce941 +session.zh.md: a564ddd34705d530267f3f5bba5afe08b7dcc0ae diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md index 96520c72b8..a23ed4c43b 100644 --- a/docs/subsystems/session.md +++ b/docs/subsystems/session.md @@ -71,7 +71,7 @@ interface SessionEventMap { * JSON string exactly as the model produced it (unparsed). `callId` pairs the * call with its `tool/result`. */ - 'tool/call': { turn: number; step: number; callId: CallId; name: string; arguments: string } + 'tool/call': { turn: number; step: number; callId: ToolCallId; name: string; arguments: string } /** * A completed tool call's model-facing result, optional internal failure * identity, and optional tool-private `meta` presentation payload. `meta` is diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md index 28a259226b..a564ddd347 100644 --- a/docs/subsystems/session.zh.md +++ b/docs/subsystems/session.zh.md @@ -71,7 +71,7 @@ interface SessionEventMap { * JSON string exactly as the model produced it (unparsed). `callId` pairs the * call with its `tool/result`. */ - 'tool/call': { turn: number; step: number; callId: CallId; name: string; arguments: string } + 'tool/call': { turn: number; step: number; callId: ToolCallId; name: string; arguments: string } /** * A completed tool call's model-facing result, optional internal failure * identity, and optional tool-private `meta` presentation payload. `meta` is diff --git a/docs/subsystems/spill.i18n.yaml b/docs/subsystems/spill.i18n.yaml index 700683c015..916af08a5b 100644 --- a/docs/subsystems/spill.i18n.yaml +++ b/docs/subsystems/spill.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/spill.md -spill.md: 8356fc45f8581c6776238f212592c4ea2841a96a -spill.zh.md: 353c5fea63ff6284c922a9bbf7d6d07f124fb9bd +spill.md: 2a3201cf2fa0d7b034a01f64d0448536bfe8d861 +spill.zh.md: d4c5b3853be46780cd93e2f826bf399bff42c405 diff --git a/docs/subsystems/spill.md b/docs/subsystems/spill.md index 8356fc45f8..2a3201cf2f 100644 --- a/docs/subsystems/spill.md +++ b/docs/subsystems/spill.md @@ -50,7 +50,7 @@ interface SpillSource { /** The tool whose result was spilled (e.g. `web_fetch`). */ toolName: string /** The model-issued call id the result belongs to. */ - callId: CallId + callId: ToolCallId /** A short human label for the artifact (e.g. `result`). */ label: string } diff --git a/docs/subsystems/spill.zh.md b/docs/subsystems/spill.zh.md index 353c5fea63..d4c5b3853b 100644 --- a/docs/subsystems/spill.zh.md +++ b/docs/subsystems/spill.zh.md @@ -50,7 +50,7 @@ interface SpillSource { /** The tool whose result was spilled (e.g. `web_fetch`). */ toolName: string /** The model-issued call id the result belongs to. */ - callId: CallId + callId: ToolCallId /** A short human label for the artifact (e.g. `result`). */ label: string } diff --git a/docs/subsystems/tools.i18n.yaml b/docs/subsystems/tools.i18n.yaml index ed327e53ce..3de50c917a 100644 --- a/docs/subsystems/tools.i18n.yaml +++ b/docs/subsystems/tools.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/tools.md -tools.md: dd89d40c1401732dd5e6d8dfb5ccb08a89c58405 -tools.zh.md: 0d3e59bc222527e66dd5bd626203f4e1bb6aad9a +tools.md: 5ac2401d662bca52cac529c0620a39930565a7c7 +tools.zh.md: a081c58a2ba4df50b035a4dae8fa7f9ab7d9de1a diff --git a/docs/subsystems/tools.md b/docs/subsystems/tools.md index dd89d40c14..5ac2401d66 100644 --- a/docs/subsystems/tools.md +++ b/docs/subsystems/tools.md @@ -183,12 +183,12 @@ type ToolExecutionToken = symbol & { readonly [toolExecutionTokenBrand]: true } * callers do not choose that token. */ interface ToolExecutionInput { - readonly callId: CallId + readonly callId: ToolCallId /** * Root model-requested call owning this execution tree. Callers omit it for * a root execution; nested dispatchers propagate the enclosing value. */ - readonly rootCallId?: CallId + readonly rootCallId?: ToolCallId readonly name: string /** Losslessly JSON-serializable parsed arguments (tools validate their own schema). */ readonly arguments: unknown @@ -270,7 +270,7 @@ interface CodeDispatchLog { /** The calling agent (the scope routing key and the spill owner), when the outer call has one. */ readonly agent?: Agent /** Deterministic sub-call id (`:code:`). */ - readonly subCallId: CallId + readonly subCallId: ToolCallId /** The dispatched sub-tool name. */ readonly name: string /** Whether the sub-call settled as an error. */ @@ -290,7 +290,7 @@ interface CodeDispatchLog { */ interface ToolExecution extends ToolExecutionInput { /** Root model-requested call, resolved for every root and nested execution. */ - readonly rootCallId: CallId + readonly rootCallId: ToolCallId /** Registry-assigned identity shared with nested calls only as their opaque `parent` token. */ readonly token: ToolExecutionToken } diff --git a/docs/subsystems/tools.zh.md b/docs/subsystems/tools.zh.md index 0d3e59bc22..a081c58a2b 100644 --- a/docs/subsystems/tools.zh.md +++ b/docs/subsystems/tools.zh.md @@ -183,12 +183,12 @@ type ToolExecutionToken = symbol & { readonly [toolExecutionTokenBrand]: true } * callers do not choose that token. */ interface ToolExecutionInput { - readonly callId: CallId + readonly callId: ToolCallId /** * Root model-requested call owning this execution tree. Callers omit it for * a root execution; nested dispatchers propagate the enclosing value. */ - readonly rootCallId?: CallId + readonly rootCallId?: ToolCallId readonly name: string /** Losslessly JSON-serializable parsed arguments (tools validate their own schema). */ readonly arguments: unknown @@ -270,7 +270,7 @@ interface CodeDispatchLog { /** The calling agent (the scope routing key and the spill owner), when the outer call has one. */ readonly agent?: Agent /** Deterministic sub-call id (`:code:`). */ - readonly subCallId: CallId + readonly subCallId: ToolCallId /** The dispatched sub-tool name. */ readonly name: string /** Whether the sub-call settled as an error. */ @@ -290,7 +290,7 @@ interface CodeDispatchLog { */ interface ToolExecution extends ToolExecutionInput { /** Root model-requested call, resolved for every root and nested execution. */ - readonly rootCallId: CallId + readonly rootCallId: ToolCallId /** Registry-assigned identity shared with nested calls only as their opaque `parent` token. */ readonly token: ToolExecutionToken } diff --git a/docs/user/develop/practice/llm-adapter.i18n.yaml b/docs/user/develop/practice/llm-adapter.i18n.yaml index d7d431bd5e..57e3e0cd7e 100644 --- a/docs/user/develop/practice/llm-adapter.i18n.yaml +++ b/docs/user/develop/practice/llm-adapter.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/user/develop/practice/llm-adapter.md -llm-adapter.md: 882e82d880ac622cf886c6c24e75a1987e1c6702 -llm-adapter.zh.md: 27c480af2a19a68ac35900a2ce1b5e9dbdc85bf8 +llm-adapter.md: ff5fc5657e2be0a5fe0d4e10f148865e62f92b18 +llm-adapter.zh.md: b05bfad6cbc4038f54f816b212068d85e54c2100 diff --git a/docs/user/develop/practice/llm-adapter.md b/docs/user/develop/practice/llm-adapter.md index 882e82d880..ff5fc5657e 100644 --- a/docs/user/develop/practice/llm-adapter.md +++ b/docs/user/develop/practice/llm-adapter.md @@ -54,7 +54,7 @@ export function apply(ctx: Context, config: Config) { `stream()` yields chunks using this protocol: ```ts -import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm' +import { ToolCallId, type StreamChunk } from '@deepseek-ai/dsh-llm' async function* exampleChunks(): AsyncIterable { // 1. Start each content block with block-start. @@ -76,7 +76,7 @@ async function* exampleChunks(): AsyncIterable { yield { type: 'tool-call-delta', index: 1, - id: CallId('call-123'), + id: ToolCallId('call-123'), name: 'bash', argumentsDelta: '{"command":"ls"}', } @@ -85,7 +85,7 @@ async function* exampleChunks(): AsyncIterable { index: 1, block: { type: 'tool-call', - id: CallId('call-123'), + id: ToolCallId('call-123'), name: 'bash', arguments: '{"command":"ls"}', }, diff --git a/docs/user/develop/practice/llm-adapter.zh.md b/docs/user/develop/practice/llm-adapter.zh.md index 27c480af2a..b05bfad6cb 100644 --- a/docs/user/develop/practice/llm-adapter.zh.md +++ b/docs/user/develop/practice/llm-adapter.zh.md @@ -54,7 +54,7 @@ export function apply(ctx: Context, config: Config) { `stream()` 必须按以下协议生成分片: ```ts -import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm' +import { ToolCallId, type StreamChunk } from '@deepseek-ai/dsh-llm' async function* exampleChunks(): AsyncIterable { // 1. Start each content block with block-start. @@ -76,7 +76,7 @@ async function* exampleChunks(): AsyncIterable { yield { type: 'tool-call-delta', index: 1, - id: CallId('call-123'), + id: ToolCallId('call-123'), name: 'bash', argumentsDelta: '{"command":"ls"}', } @@ -85,7 +85,7 @@ async function* exampleChunks(): AsyncIterable { index: 1, block: { type: 'tool-call', - id: CallId('call-123'), + id: ToolCallId('call-123'), name: 'bash', arguments: '{"command":"ls"}', }, diff --git a/examples/acp-agent/tests/fixtures/shell/tool-pwsh/driver.ts b/examples/acp-agent/tests/fixtures/shell/tool-pwsh/driver.ts index bd6f63cb34..6b68d72023 100644 --- a/examples/acp-agent/tests/fixtures/shell/tool-pwsh/driver.ts +++ b/examples/acp-agent/tests/fixtures/shell/tool-pwsh/driver.ts @@ -8,7 +8,7 @@ import { writeFile } from 'node:fs/promises' import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' const configPath = process.argv[2] if (configPath === undefined) throw new Error('tool-pwsh driver requires a config path') @@ -21,7 +21,7 @@ try { const foreground = await ctx.tools.execute({ signal: new AbortController().signal, - callId: CallId('loader-fg'), + callId: ToolCallId('loader-fg'), name: 'pwsh', arguments: { command: 'Write-Output loader-ok', description: 'loader foreground' }, }) @@ -29,7 +29,7 @@ try { const background = await ctx.tools.execute({ signal: new AbortController().signal, - callId: CallId('loader-bg'), + callId: ToolCallId('loader-bg'), name: 'pwsh', arguments: { command: 'Start-Sleep -Milliseconds 200; Write-Output loader-bg-ok', @@ -46,7 +46,7 @@ try { while (Date.now() < deadline) { const read = await ctx.tools.execute({ signal: new AbortController().signal, - callId: CallId('loader-bg-read'), + callId: ToolCallId('loader-bg-read'), name: 'job_output', arguments: { job_id: jobId }, }) diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-acp/mock-delegating-llm.ts b/examples/acp-agent/tests/fixtures/subagent/subagent-acp/mock-delegating-llm.ts index f98c007125..f318424153 100644 --- a/examples/acp-agent/tests/fixtures/subagent/subagent-acp/mock-delegating-llm.ts +++ b/examples/acp-agent/tests/fixtures/subagent/subagent-acp/mock-delegating-llm.ts @@ -1,6 +1,6 @@ import type { Context } from '@deepseek-ai/cordis' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' -import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' +import { ToolCallId, LlmAdapter } from '@deepseek-ai/dsh-llm' /** * Test adapter for the `mock-delegate` model: the first request calls the @@ -20,8 +20,8 @@ class MockDelegatingAdapter extends LlmAdapter { if (toolResultText.length === 0) { const args = JSON.stringify({ description: 'cwd probe', prompt: 'report your workspace' }) yield { type: 'block-start', index: 0, blockType: 'tool-call' } - yield { type: 'tool-call-delta', index: 0, id: CallId('call-delegate'), name: 'subagent', argumentsDelta: args } - yield { type: 'block-end', index: 0, block: { type: 'tool-call', id: CallId('call-delegate'), name: 'subagent', arguments: args } } + yield { type: 'tool-call-delta', index: 0, id: ToolCallId('call-delegate'), name: 'subagent', argumentsDelta: args } + yield { type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('call-delegate'), name: 'subagent', arguments: args } } yield { type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } } yield { type: 'finish', reason: { kind: 'tool-calls' } } return diff --git a/examples/headless-agent/tests/code-mode.e2e.ts b/examples/headless-agent/tests/code-mode.e2e.ts index cca7641575..ca34a38628 100644 --- a/examples/headless-agent/tests/code-mode.e2e.ts +++ b/examples/headless-agent/tests/code-mode.e2e.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import LlmRuntime, { createUserMessage, CallId, HarnessError } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, ToolCallId, HarnessError } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import type { SessionEvent } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' @@ -93,7 +93,7 @@ function runCode( agent?: Agent, ): Promise { return harness.tools.execute({ - callId: CallId(`keyless-code-${++keylessCall}`), + callId: ToolCallId(`keyless-code-${++keylessCall}`), name: RUN_CODE_NAME, arguments: { code, description: 'Run the e2e program' }, signal, diff --git a/examples/headless-agent/tests/fixtures/cli-mock-llm.ts b/examples/headless-agent/tests/fixtures/cli-mock-llm.ts index 57cb384138..e89816b46a 100644 --- a/examples/headless-agent/tests/fixtures/cli-mock-llm.ts +++ b/examples/headless-agent/tests/fixtures/cli-mock-llm.ts @@ -1,6 +1,6 @@ import type { Context } from '@deepseek-ai/cordis' import { - CallId, + ToolCallId, LlmAdapter, ReasoningEffortId, type GenerateOptions, @@ -37,8 +37,8 @@ class CliMockAdapter extends LlmAdapter { if (toolResult === undefined) { const args = JSON.stringify({ command: 'printf CLI_TOOL_ROUND_TRIP', description: 'Prove the CLI tool round trip.' }) yield { type: 'block-start', index: 0, blockType: 'tool-call' } - yield { type: 'tool-call-delta', index: 0, id: CallId('cli-smoke-call'), name: 'bash', argumentsDelta: args } - yield { type: 'block-end', index: 0, block: { type: 'tool-call', id: CallId('cli-smoke-call'), name: 'bash', arguments: args } } + yield { type: 'tool-call-delta', index: 0, id: ToolCallId('cli-smoke-call'), name: 'bash', argumentsDelta: args } + yield { type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('cli-smoke-call'), name: 'bash', arguments: args } } yield { type: 'usage', usage: { inputTokens: 11, outputTokens: 3, cacheReadTokens: 2 } } yield { type: 'finish', reason: { kind: 'tool-calls' } } return diff --git a/examples/headless-agent/tests/fixtures/team-llm.mjs b/examples/headless-agent/tests/fixtures/team-llm.mjs index c1c973c771..72fb16a21d 100644 --- a/examples/headless-agent/tests/fixtures/team-llm.mjs +++ b/examples/headless-agent/tests/fixtures/team-llm.mjs @@ -1,6 +1,6 @@ /** Deterministic keyless Agent Teams adapter for the real headless Loader snapshot. */ -import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' +import { ToolCallId, LlmAdapter } from '@deepseek-ai/dsh-llm' let nextCall = 0 @@ -38,7 +38,7 @@ function latestToolText(messages) { function toolChunks(specs) { const chunks = [] for (const [index, spec] of specs.entries()) { - const id = CallId(`team-fixture-${++nextCall}`) + const id = ToolCallId(`team-fixture-${++nextCall}`) const args = JSON.stringify(spec.args) chunks.push( { type: 'block-start', index, blockType: 'tool-call' }, diff --git a/examples/headless-agent/tests/semantic-checkpoint.snapshot.ts b/examples/headless-agent/tests/semantic-checkpoint.snapshot.ts index 618e6cc038..a592eea6be 100644 --- a/examples/headless-agent/tests/semantic-checkpoint.snapshot.ts +++ b/examples/headless-agent/tests/semantic-checkpoint.snapshot.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url' import { Context } from '@deepseek-ai/cordis' import { normalizeSessionSnapshot, type NormalizeContext } from '@deepseek-ai/dsh-acp-snapshot' import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke' -import { createUserMessage, CallId , createMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId , createMessage } from '@deepseek-ai/dsh-llm' import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session' import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import { describe, expect, it } from 'vitest' @@ -46,7 +46,7 @@ async function seedInterruptedSession(root: string, cwd: string): Promise { const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] }) const agent = harness.ctx.agents.get(SessionId(sessionId))! agent.session.append('turn/start', { turn: 1 }) - return { agent, toolName: 'bash', callId: CallId('call-9'), ...overrides } + return { agent, toolName: 'bash', callId: ToolCallId('call-9'), ...overrides } } it('maps the two advertised one-shot choices', async () => { @@ -63,7 +63,7 @@ describe('ACP machine permission policy', () => { const foreign = { session: { id: request.agent.session.id, events: [{ type: 'turn/start' }], append: () => ({}) }, } as unknown as Agent - await expect(harness.ctx.approval.request({ agent: foreign, toolName: 'bash', callId: CallId('call') })) + await expect(harness.ctx.approval.request({ agent: foreign, toolName: 'bash', callId: ToolCallId('call') })) .resolves.toBe('unavailable') expect(harness.permissionRequests).toHaveLength(0) }) diff --git a/packages/acp/acp/tests/edges.spec.ts b/packages/acp/acp/tests/edges.spec.ts index 84bbff3b3d..e4abfebf97 100644 --- a/packages/acp/acp/tests/edges.spec.ts +++ b/packages/acp/acp/tests/edges.spec.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk' -import { createUserMessage, CallId, type StreamChunk } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, type StreamChunk } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import { defineContentToolFixture } from '@deepseek-ai/dsh-tools' import { makeBridgeHarness, textResponse, type BridgeHarness } from './harness.ts' @@ -8,8 +8,8 @@ import { makeBridgeHarness, textResponse, type BridgeHarness } from './harness.t function toolCallResponse(): StreamChunk[] { return [ { type: 'block-start', index: 0, blockType: 'tool-call' }, - { type: 'tool-call-delta', index: 0, id: CallId('call-1'), name: 'echo', argumentsDelta: '{}' }, - { type: 'block-end', index: 0, block: { type: 'tool-call', id: CallId('call-1'), name: 'echo', arguments: '{}' } }, + { type: 'tool-call-delta', index: 0, id: ToolCallId('call-1'), name: 'echo', argumentsDelta: '{}' }, + { type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('call-1'), name: 'echo', arguments: '{}' } }, { type: 'finish', reason: { kind: 'tool-calls' } }, ] } diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index cfcae2eb1f..f9e44e1cf6 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -11,7 +11,7 @@ import { createUserMessage, isTokenDelta, } from '@deepseek-ai/dsh-llm/message' -import { CallId } from '@deepseek-ai/dsh-llm/brand' +import { ToolCallId } from '@deepseek-ai/dsh-llm/brand' import type { AssistantMessage, ContentBlock, @@ -62,7 +62,7 @@ function assistantMessage(content: ContentBlock[], model = 'fx-1'): AssistantMes } function toolResultMessage(callId: string, content: ContentBlock[], isError: boolean): ToolResultMessage { - return createToolResultMessage({ callId: CallId(callId), content, isError }) + return createToolResultMessage({ callId: ToolCallId(callId), content, isError }) } const MARKDOWN_FIXTURE = [ diff --git a/packages/client/runtime/tests/event-script.client.ts b/packages/client/runtime/tests/event-script.client.ts index a024af0ab7..e368f96a0f 100644 --- a/packages/client/runtime/tests/event-script.client.ts +++ b/packages/client/runtime/tests/event-script.client.ts @@ -1,4 +1,4 @@ -import { createUserMessage, createMessage, createToolResultMessage, CallId } from '@deepseek-ai/dsh-llm' +import { createUserMessage, createMessage, createToolResultMessage, ToolCallId } from '@deepseek-ai/dsh-llm' // Minimal SessionEvent builders for orchestration tests (shape mirrors what the // host emits; only the fields the object layer reads). import type { ContentBlock } from '@deepseek-ai/dsh-llm/types' @@ -45,7 +45,7 @@ export const ev = { turn, step, message: createToolResultMessage({ - callId: CallId(callId), + callId: ToolCallId(callId), content: text(body), isError: false, }), diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index 307ac38482..268dd69fab 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -20,7 +20,7 @@ import type { import type { createChatStore } from '../stores.ts' import type { ComposerSubmitGesture, InputSubmitMode } from './composer-submission.ts' import type { ChatNode, ChatNodeKind } from './chat-nodes.ts' -import type { CallId, SelectionTarget, ViewTab } from './views.ts' +import type { ToolCallId, SelectionTarget, ViewTab } from './views.ts' /** Browser-owned image that has not crossed the durable host boundary. */ export interface ComposerAttachment { @@ -344,7 +344,7 @@ export interface InputZone { */ export interface ConvViewOwnerProps { /** One-shot inspect request from another view (chat's Inspect button); null when idle. */ - inspect?: { callId: CallId } | null + inspect?: { callId: ToolCallId } | null /** Acknowledge the inspect request once applied (clears the store field). */ onInspectDone?: () => void } @@ -415,11 +415,11 @@ export interface ChatNodeTurnDataInjected { /** Stable owner currency delivered to one keyed Chat business renderer. */ export interface ChatNodeOwnerProps { /** Selected Tool call, when the shared details store names one. */ - selectedCallId?: CallId | undefined + selectedCallId?: ToolCallId | undefined /** Session workspace root; Tool summaries display paths relative to it. */ cwd?: string | undefined openFile: (path: string) => void - inspectCall: (callId: CallId) => void + inspectCall: (callId: ToolCallId) => void forkAt: (seq: number) => void /** Render a historical image group through the attachment slot. */ renderMessageImages: RenderMessageImages @@ -761,7 +761,7 @@ export interface ChatViewInjected { /** Resolve a session-authorized historical image for inline display. */ loadImage: (attachment: ImageAttachmentRef) => Promise /** Hand a call off to the trajectory view: write the one-shot inspect target and switch tabs. */ - inspectCall: (callId: CallId) => void + inspectCall: (callId: ToolCallId) => void /** * Per-session scroll memory surviving view switches (in-memory, never * persisted): the view saves on every scroll and restores on remount; a diff --git a/packages/client/ui-conversation/src/client/contract/views.ts b/packages/client/ui-conversation/src/client/contract/views.ts index 1680e0322d..daa4b11256 100644 --- a/packages/client/ui-conversation/src/client/contract/views.ts +++ b/packages/client/ui-conversation/src/client/contract/views.ts @@ -1,10 +1,10 @@ /** Shared conversation view, selection, and store-state contracts. */ /** Tool call identity as carried on the wire (branded upstream in connection). */ -export type CallId = string +export type ToolCallId = string /** Selection target for the details linkage channel (toolcall is the step special case). */ -export interface SelectionTarget { turnSeq: number; stepSeq?: number; callId?: CallId; toolName?: string } +export interface SelectionTarget { turnSeq: number; stepSeq?: number; callId?: ToolCallId; toolName?: string } /** * One conversation view tab, projected from a 'conversation.view' slot @@ -28,5 +28,5 @@ export interface ChatStoreState { * view consumes it and acknowledges by clearing. Read with `?? null` — * persisted snapshots from before this field rehydrate without it. */ - inspect: { callId: CallId } | null + inspect: { callId: ToolCallId } | null } diff --git a/packages/client/ui-conversation/src/client/index.ts b/packages/client/ui-conversation/src/client/index.ts index 78732229b0..fc8f217f4c 100644 --- a/packages/client/ui-conversation/src/client/index.ts +++ b/packages/client/ui-conversation/src/client/index.ts @@ -20,7 +20,7 @@ export type { IConversation } from './service.ts' export type { DraftAttachmentId } from './input/contract.ts' export type { - CallId, ChatStoreState, SelectionTarget, ViewTab, + ToolCallId, ChatStoreState, SelectionTarget, ViewTab, } from './contract/views.ts' export type { ConversationKey } from './locales.ts' export type { diff --git a/packages/client/ui-conversation/src/client/stores.ts b/packages/client/ui-conversation/src/client/stores.ts index 5a4913f0fa..7de9c4b78b 100644 --- a/packages/client/ui-conversation/src/client/stores.ts +++ b/packages/client/ui-conversation/src/client/stores.ts @@ -3,14 +3,14 @@ * The plugin creates its handle at apply time so identity follows the fiber. */ import { defineStore, type EngineStoreHandle } from '@deepseek-ai/dsh-client-runtime/client' -import type { CallId, ChatStoreState, SelectionTarget } from './contract/views.ts' +import type { ToolCallId, ChatStoreState, SelectionTarget } from './contract/views.ts' /** Declared action shape used to give the exported factory a stable return type. */ type ChatActions = { select: (draft: ChatStoreState, target: SelectionTarget | null) => void setDraft: (draft: ChatStoreState, text: string) => void setView: (draft: ChatStoreState, view: string) => void - setInspect: (draft: ChatStoreState, target: { callId: CallId } | null) => void + setInspect: (draft: ChatStoreState, target: { callId: ToolCallId } | null) => void } /** @@ -28,7 +28,7 @@ export function createChatStore(): EngineStoreHandle { d.selection = target }, setDraft: (d, text: string) => { d.draft = text }, setView: (d, view: string) => { d.view = view }, - setInspect: (d, target: { callId: CallId } | null) => { d.inspect = target }, + setInspect: (d, target: { callId: ToolCallId } | null) => { d.inspect = target }, }, }) } diff --git a/packages/compaction/compaction-basic/tests/compaction-basic.spec.ts b/packages/compaction/compaction-basic/tests/compaction-basic.spec.ts index 2cf07d3f70..e873a21603 100644 --- a/packages/compaction/compaction-basic/tests/compaction-basic.spec.ts +++ b/packages/compaction/compaction-basic/tests/compaction-basic.spec.ts @@ -12,7 +12,7 @@ import { resolveTargetPolicy, } from '@deepseek-ai/dsh-compaction-basic/src/config.ts' import type { CompactionResult } from '@deepseek-ai/dsh-compaction' -import LlmRuntime, { createUserMessage, CallId, CONTEXT_WINDOW_EXCEEDED_CODE, createToolResultMessage, LlmAdapter , createMessage } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, ToolCallId, CONTEXT_WINDOW_EXCEEDED_CODE, createToolResultMessage, LlmAdapter , createMessage } from '@deepseek-ai/dsh-llm' import type { ContentBlock, GenerateOptions, @@ -142,7 +142,7 @@ function conversation(turns = 4, text = 'fixture '.repeat(40).trim()): Session { function toolConversation(): Session { const session = Session.create(SessionId('tools')) for (let turn = 1; turn <= 3; turn += 1) { - const callId = CallId(`call-${turn}`) + const callId = ToolCallId(`call-${turn}`) session.append('turn/start', { turn }) session.append('user/message', createUserMessage({ content: [{ type: 'text', text: `request ${turn} `.repeat(300) }], @@ -190,7 +190,7 @@ function toolConversation(): Session { /** One closed routed tool step followed by an open turn for rewrite events. */ function oversizedToolResult(chars = 3_000, withCompactablePrompt = false): Session { const session = Session.create(SessionId(`oversized-tool-${chars}`)) - const callId = CallId('oversized') + const callId = ToolCallId('oversized') session.append('turn/start', { turn: 1 }) if (withCompactablePrompt) { session.append('user/message', createUserMessage({ @@ -568,7 +568,7 @@ describe('pressure measurement and retention', () => { it('declines forced overflow when the whole surface is one indivisible tool pair', async () => { const compact = service(compactConfig) const session = Session.create(SessionId('single-tool-pair')) - const callId = CallId('single-call') + const callId = ToolCallId('single-call') session.append('turn/start', { turn: 1 }) session.append('step/start', { turn: 1, step: 1 }) session.append('request/header', { @@ -734,7 +734,7 @@ describe('pressure measurement and retention', () => { it('declines when rounding a cut would consume the only tool pair', () => { const ctx = createContext() const session = Session.create(SessionId('one-tool-pair')) - const callId = CallId('only') + const callId = ToolCallId('only') session.append('turn/start', { turn: 1 }) session.append('step/start', { turn: 1, step: 1 }) session.append('assistant/message', { @@ -1183,7 +1183,7 @@ describe('default one-shot summarizer', () => { const { adapter, compact } = await summarizerHarness([ { type: 'reasoning', text: 'private' }, { type: 'text', text: 'public summary' }, - { type: 'tool-call', id: CallId('unexpected'), name: 'x', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('unexpected'), name: 'x', arguments: '{}' }, ], undefined, MODEL, { auto: false, summarizationProvider: MODEL, @@ -1199,7 +1199,7 @@ describe('default one-shot summarizer', () => { rawOutput: [ { type: 'reasoning', text: 'private' }, { type: 'text', text: 'public summary' }, - { type: 'tool-call', id: CallId('unexpected'), name: 'x', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('unexpected'), name: 'x', arguments: '{}' }, ], llmStreamCall: true, provider: MODEL, @@ -1417,7 +1417,7 @@ describe('default one-shot summarizer', () => { it('rejects image summary output nested in a tool result', async () => { const { compact } = await summarizerHarness([{ type: 'tool-result', - toolCallId: CallId('summary-tool'), + toolCallId: ToolCallId('summary-tool'), content: [{ type: 'image', attachment: { diff --git a/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts b/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts index 867d11c2a6..46d0384803 100644 --- a/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts +++ b/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts @@ -3,7 +3,7 @@ import { Context } from '@deepseek-ai/cordis' import { toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compaction' import { createUserMessage, CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, resolveRetryPolicy , createMessage } from '@deepseek-ai/dsh-llm' import type { ContentBlock, GenerateOptions, LlmResolvedModelInfo, ResolvedRetryPolicy, StreamChunk } from '@deepseek-ai/dsh-llm' -import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' +import { ToolCallId, LlmAdapter } from '@deepseek-ai/dsh-llm' import { defineContentToolFixture } from '@deepseek-ai/dsh-tools' import type { Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' @@ -54,7 +54,7 @@ class StepwiseToolAdapter extends LlmAdapter { const n = this.calls this.calls += 1 if (n < this.toolSteps) { - const id = CallId(`c${n}`) + const id = ToolCallId(`c${n}`) const args = `{"i":${n}}` yield { type: 'block-start', index: 0, blockType: 'text' } yield { type: 'block-end', index: 0, block: { type: 'text', text: `step ${n}` } } diff --git a/packages/compaction/compaction-tool-result-pruner/src/types.ts b/packages/compaction/compaction-tool-result-pruner/src/types.ts index 13544b163e..d8228765a3 100644 --- a/packages/compaction/compaction-tool-result-pruner/src/types.ts +++ b/packages/compaction/compaction-tool-result-pruner/src/types.ts @@ -1,4 +1,4 @@ -import type { CallId } from '@deepseek-ai/dsh-llm' +import type { ToolCallId } from '@deepseek-ai/dsh-llm' /** Character-budget policy for deterministic tool-result pruning. */ export interface ToolResultPruneConfig { @@ -24,7 +24,7 @@ export interface PrunedEntry { /** Newly appended pruned tool-result event. */ readonly replacementSeq: number /** Tool call shared by the original and replacement. */ - readonly callId: CallId + readonly callId: ToolCallId /** Original text size in Unicode code points. */ readonly charsBefore: number /** Replacement text size in Unicode code points. */ diff --git a/packages/compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts b/packages/compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts index 3216a6e774..c634e11bdb 100644 --- a/packages/compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts +++ b/packages/compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' +import { ToolCallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, @@ -43,7 +43,7 @@ function appendToolStep( content: ContentBlock[], extra: Record = {}, ): number { - const callId = CallId(call) + const callId = ToolCallId(call) session.append('turn/start', { turn, }) @@ -126,7 +126,7 @@ describe('ToolResultPruner content transform', () => { const reasoning: ContentBlock = { type: 'reasoning', text: 'private-rich-block' } const call: ContentBlock = { type: 'tool-call', - id: CallId('nested'), + id: ToolCallId('nested'), name: 'nested', arguments: '{}', } @@ -178,7 +178,7 @@ describe('ToolResultPruner session transaction', () => { expect(result.pruned).toHaveLength(1) expect(result.charsRemoved).toBeGreaterThan(0) const entry = result.pruned[0]! - expect(entry).toMatchObject({ originalSeq, callId: CallId('one'), charsBefore: 100 }) + expect(entry).toMatchObject({ originalSeq, callId: ToolCallId('one'), charsBefore: 100 }) expect(entry.charsAfter).toBeLessThanOrEqual(50) const original = session.events[originalSeq]! @@ -201,7 +201,7 @@ describe('ToolResultPruner session transaction', () => { step: 1, isError: true, message: { - source: { kind: 'tool', callId: CallId('one') }, + source: { kind: 'tool', callId: ToolCallId('one') }, }, error: { name: 'ExitError', code: 'EXIT_1' }, meta: { diff: ['a', 'b'] }, @@ -236,7 +236,7 @@ describe('ToolResultPruner session transaction', () => { const prune = service() const first = prune.pruneSession(session) const second = prune.pruneSession(session) - expect(first.pruned.map(entry => entry.callId)).toEqual([CallId('a'), CallId('c')]) + expect(first.pruned.map(entry => entry.callId)).toEqual([ToolCallId('a'), ToolCallId('c')]) expect(first.charsRemoved).toBe( first.pruned.reduce((sum, entry) => sum + entry.charsBefore - entry.charsAfter, 0), ) diff --git a/packages/compaction/compaction/tests/tool-pairing.spec.ts b/packages/compaction/compaction/tests/tool-pairing.spec.ts index 226542a038..cd4e2a874f 100644 --- a/packages/compaction/compaction/tests/tool-pairing.spec.ts +++ b/packages/compaction/compaction/tests/tool-pairing.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { createUserMessage, CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' import { toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compaction' import { Session, SessionId } from '@deepseek-ai/dsh-session' import type { SessionEvent } from '@deepseek-ai/dsh-session' @@ -35,7 +35,7 @@ function closedToolStep(): Session { step: 1, message: createMessage({ role: 'assistant', - content: [{ type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }], + content: [{ type: 'tool-call', id: ToolCallId('c1'), name: 'bash', arguments: '{}' }], source: { kind: 'model', ...{ provider: 'mock', model: 'mock' }, @@ -46,7 +46,7 @@ function closedToolStep(): Session { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('c1'), + callId: ToolCallId('c1'), content: [{ type: 'text', text: 'done' }], isError: false, }), @@ -70,7 +70,7 @@ describe('tool-pairing boundaries', () => { step: 1, message: createMessage({ role: 'assistant', - content: [{ type: 'tool-call', id: CallId('open'), name: 'bash', arguments: '{}' }], + content: [{ type: 'tool-call', id: ToolCallId('open'), name: 'bash', arguments: '{}' }], source: { kind: 'model', ...{ provider: 'mock', model: 'mock' }, @@ -88,8 +88,8 @@ describe('tool-pairing boundaries', () => { message: createMessage({ role: 'assistant', content: [ - { type: 'tool-call', id: CallId('c1'), name: 'one', arguments: '{}' }, - { type: 'tool-call', id: CallId('c2'), name: 'two', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('c1'), name: 'one', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('c2'), name: 'two', arguments: '{}' }, ], source: { kind: 'model', @@ -100,7 +100,7 @@ describe('tool-pairing boundaries', () => { session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('c1'), + callId: ToolCallId('c1'), content: [], isError: false, }), @@ -108,7 +108,7 @@ describe('tool-pairing boundaries', () => { session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('c2'), + callId: ToolCallId('c2'), content: [], isError: false, }), @@ -125,7 +125,7 @@ describe('tool-pairing boundaries', () => { step: 1, message: createMessage({ role: 'assistant', - content: [{ type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }], + content: [{ type: 'tool-call', id: ToolCallId('c1'), name: 'bash', arguments: '{}' }], source: { kind: 'model', ...{ provider: 'mock', model: 'mock' }, @@ -139,7 +139,7 @@ describe('tool-pairing boundaries', () => { midStep.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('c1'), + callId: ToolCallId('c1'), content: [], isError: false, }), @@ -217,7 +217,7 @@ describe('tool-pairing cache refresh', () => { step: 1, message: createMessage({ role: 'assistant', - content: [{ type: 'tool-call', id: CallId('c1'), name: 'one', arguments: '{}' }], + content: [{ type: 'tool-call', id: ToolCallId('c1'), name: 'one', arguments: '{}' }], source: { kind: 'model', ...{ provider: 'mock', model: 'mock' }, @@ -231,7 +231,7 @@ describe('tool-pairing cache refresh', () => { data: { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('c1'), + callId: ToolCallId('c1'), content: [], isError: false, }), @@ -297,7 +297,7 @@ describe('tool-pairing cache refresh', () => { step: 1, message: createMessage({ role: 'assistant', - content: [{ type: 'tool-call', id: CallId('c2'), name: 'two', arguments: '{}' }], + content: [{ type: 'tool-call', id: ToolCallId('c2'), name: 'two', arguments: '{}' }], source: { kind: 'model', ...{ provider: 'mock', model: 'mock' }, @@ -311,7 +311,7 @@ describe('tool-pairing cache refresh', () => { data: { turn: 2, step: 1, message: createToolResultMessage({ - callId: CallId('c2'), + callId: ToolCallId('c2'), content: [], isError: false, }), @@ -370,7 +370,7 @@ describe('tool-pairing corrupt surfaces', () => { session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('orphan'), + callId: ToolCallId('orphan'), content: [], isError: false, }), @@ -387,7 +387,7 @@ describe('tool-pairing corrupt surfaces', () => { session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('orphan'), + callId: ToolCallId('orphan'), content: [], isError: false, }), diff --git a/packages/context/agent-instructions/tests/agent-instructions.spec.ts b/packages/context/agent-instructions/tests/agent-instructions.spec.ts index 4fc60bc78d..a1101a390c 100644 --- a/packages/context/agent-instructions/tests/agent-instructions.spec.ts +++ b/packages/context/agent-instructions/tests/agent-instructions.spec.ts @@ -5,7 +5,7 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import * as workspaceContext from '@deepseek-ai/dsh-agent-instructions' -import LlmRuntime, { createUserMessage, CallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, ToolCallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, SessionId, SESSION_FORMAT_VERSION, type SessionEvent, type UserMessage } from '@deepseek-ai/dsh-session' import AgentRegistry, { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' @@ -1657,7 +1657,7 @@ describe('workspace context request injection', () => { await write(join(root, 'AGENTS.md'), 'updated repo rule') await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-remount'), + callId: ToolCallId('read-after-remount'), name: 'read', arguments: { file_path: 'file.txt' }, agent, @@ -1854,7 +1854,7 @@ describe('workspace context request injection', () => { await write(join(root, 'AGENTS.md'), 'new root rule with more detail') await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-baseline-change'), name: 'read', arguments: { file_path: 'file.txt' }, agent, + callId: ToolCallId('read-after-baseline-change'), name: 'read', arguments: { file_path: 'file.txt' }, agent, }) expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({ @@ -1883,7 +1883,7 @@ describe('workspace context request injection', () => { await rm(join(root, 'AGENTS.md')) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-baseline-remove'), name: 'read', arguments: { file_path: 'file.txt' }, agent, + callId: ToolCallId('read-after-baseline-remove'), name: 'read', arguments: { file_path: 'file.txt' }, agent, }) expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({ @@ -2499,9 +2499,9 @@ describe('dynamic nested workspace context injection', () => { const adapter = new MockAdapter([ [ { type: 'block-start', index: 0, blockType: 'tool-call' }, - { type: 'block-end', index: 0, block: { type: 'tool-call', id: CallId('read-before-abort'), name: 'read', arguments: '{"file_path":"pkg/deep/file.txt"}' } }, + { type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('read-before-abort'), name: 'read', arguments: '{"file_path":"pkg/deep/file.txt"}' } }, { type: 'block-start', index: 1, blockType: 'tool-call' }, - { type: 'block-end', index: 1, block: { type: 'tool-call', id: CallId('abort-after-read'), name: 'abort_step', arguments: '{}' } }, + { type: 'block-end', index: 1, block: { type: 'tool-call', id: ToolCallId('abort-after-read'), name: 'abort_step', arguments: '{}' } }, { type: 'finish', reason: { kind: 'tool-calls' } }, ] satisfies StreamChunk[], toolCallResponse('read-after-abort', 'read', { file_path: join('pkg', 'deep', 'file.txt') }), @@ -2595,7 +2595,7 @@ describe('dynamic nested workspace context injection', () => { const reason = new Error('cancel dynamic reconciliation') controller.abort(reason) const exec = stubToolExecution({ - callId: CallId('cancelled-dynamic-read'), + callId: ToolCallId('cancelled-dynamic-read'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent: stubAgent(root), @@ -2632,7 +2632,7 @@ describe('dynamic nested workspace context injection', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-nested'), + callId: ToolCallId('read-nested'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -2678,7 +2678,7 @@ describe('dynamic nested workspace context injection', () => { ctx.emit('tools/result', stubToolExecution({ signal: controller.signal, - callId: CallId('read-before-signal-end'), + callId: ToolCallId('read-before-signal-end'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, @@ -2711,7 +2711,7 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-configured-nested-candidate'), + callId: ToolCallId('read-configured-nested-candidate'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -2744,7 +2744,7 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-nested-overlay'), + callId: ToolCallId('read-nested-overlay'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -2787,7 +2787,7 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-nested-overlay-disabled'), + callId: ToolCallId('read-nested-overlay-disabled'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -2815,7 +2815,7 @@ describe('dynamic nested workspace context injection', () => { const first = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-nested-1'), + callId: ToolCallId('read-nested-1'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -2823,7 +2823,7 @@ describe('dynamic nested workspace context injection', () => { await appendAdditionalContexts(ctx, agent) const second = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-nested-2'), + callId: ToolCallId('read-nested-2'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -2858,12 +2858,12 @@ describe('dynamic nested workspace context injection', () => { const first = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-before-version-fast-path'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-before-version-fast-path'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await appendAdditionalContexts(ctx, agent) const second = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-with-version-fast-path'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-with-version-fast-path'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) expect(first.additionalContexts).toBeUndefined() @@ -2895,18 +2895,18 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-before-same-digest-version-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-before-same-digest-version-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await appendAdditionalContexts(ctx, agent) fs.entries.set(instructionPath, { type: 'file', content: 'same package rule', version: FsVersion('revision-2') }) const afterVersionChange = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-same-digest-version-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-after-same-digest-version-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await syncWorkspaceContext(ctx, agent) const afterRefresh = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-version-cache-refresh'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-after-version-cache-refresh'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) expect(afterVersionChange.additionalContexts).toBeUndefined() @@ -2941,11 +2941,11 @@ describe('dynamic nested workspace context injection', () => { const secondAgent = stubAgent(root) const first = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-from-first-session'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent: firstAgent, + callId: ToolCallId('read-from-first-session'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent: firstAgent, }) const second = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-from-second-session'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent: secondAgent, + callId: ToolCallId('read-from-second-session'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent: secondAgent, }) expect(first.additionalContexts).toBeUndefined() @@ -2973,13 +2973,13 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-before-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-before-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await appendAdditionalContexts(ctx, agent) await write(join(root, 'pkg/AGENTS.md'), 'new package rule with more detail') await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-after-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({ @@ -3016,7 +3016,7 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-both-siblings'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-both-siblings'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) const firstText = blocksText(((await syncedWorkspaceContext(ctx, agent))).content) expect(firstText).toContain('native package rule') @@ -3025,7 +3025,7 @@ describe('dynamic nested workspace context injection', () => { await rm(join(root, 'pkg/AGENTS.md')) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-one-sibling-removed'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-after-one-sibling-removed'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) // Removing one candidate only removes its own scope; the sibling scope is untouched. @@ -3054,7 +3054,7 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-nested-dup-siblings'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, + callId: ToolCallId('read-nested-dup-siblings'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, }) expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({ @@ -3276,7 +3276,7 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-before-dup-convergence'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-before-dup-convergence'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) const firstText = blocksText(((await syncedWorkspaceContext(ctx, agent))).content) expect(firstText).toContain('canonical nested rule') @@ -3285,7 +3285,7 @@ describe('dynamic nested workspace context injection', () => { await write(join(root, 'pkg/CLAUDE.md'), 'canonical nested rule') await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-dup-convergence'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-after-dup-convergence'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) const convergence = await syncedWorkspaceContext(ctx, agent) @@ -3313,14 +3313,14 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-before-earlier-converges'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-before-earlier-converges'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await appendAdditionalContexts(ctx, agent) // Only the earlier candidate changes; the sibling stays byte-identical but now duplicates it. await write(join(root, 'pkg/AGENTS.md'), 'secondary nested rule') await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-earlier-converges'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-after-earlier-converges'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({ @@ -3351,13 +3351,13 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-before-remove'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-before-remove'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await appendAdditionalContexts(ctx, agent) await rm(join(root, 'pkg/AGENTS.md')) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-remove'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-after-remove'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({ @@ -3391,7 +3391,7 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-before-symlink-dir'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-before-symlink-dir'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) const firstText = blocksText(((await syncedWorkspaceContext(ctx, agent))).content) await appendAdditionalContexts(ctx, agent) @@ -3405,7 +3405,7 @@ describe('dynamic nested workspace context injection', () => { await symlink(join(root, 'pkg/elsewhere'), join(root, 'pkg/AGENTS.md')) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-symlink-dir'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-after-symlink-dir'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({ @@ -3431,20 +3431,20 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-before-tombstone'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-before-tombstone'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await appendAdditionalContexts(ctx, agent) await rm(join(root, 'pkg/AGENTS.md')) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-to-create-tombstone'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-to-create-tombstone'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await appendAdditionalContexts(ctx, agent) await write(join(root, 'pkg/AGENTS.md'), 'restored package rule') await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-tombstone'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-after-tombstone'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({ @@ -3476,13 +3476,13 @@ describe('dynamic nested workspace context injection', () => { const first = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-before-provider-failure'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-before-provider-failure'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await appendAdditionalContexts(ctx, agent) fs.throwOnStat.add(join(root, 'pkg/AGENTS.md')) const duringFailure = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-during-provider-failure'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-during-provider-failure'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) expect(first.additionalContexts).toBeUndefined() @@ -3506,7 +3506,7 @@ describe('dynamic nested workspace context injection', () => { const agent = stubAgent(root) const first = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-before-resume'), + callId: ToolCallId('read-before-resume'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -3516,7 +3516,7 @@ describe('dynamic nested workspace context injection', () => { const afterResume = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-resume'), + callId: ToolCallId('read-after-resume'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent: resumed, @@ -3542,7 +3542,7 @@ describe('dynamic nested workspace context injection', () => { const original = stubAgent(root) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-before-offline-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent: original, + callId: ToolCallId('read-before-offline-change'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent: original, }) await appendAdditionalContexts(ctx, original) await write(join(root, 'pkg/AGENTS.md'), 'new nested rule after resume') @@ -3573,7 +3573,7 @@ describe('dynamic nested workspace context injection', () => { const agent = stubAgent(root) const first = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-before-compact'), + callId: ToolCallId('read-before-compact'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -3581,7 +3581,7 @@ describe('dynamic nested workspace context injection', () => { const contextSeq = (await appendAdditionalContexts(ctx, agent))! const visibleBeforeCompact = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-while-visible'), + callId: ToolCallId('read-while-visible'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -3597,7 +3597,7 @@ describe('dynamic nested workspace context injection', () => { const afterCompact = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-compact'), + callId: ToolCallId('read-after-compact'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -3629,7 +3629,7 @@ describe('dynamic nested workspace context injection', () => { const whileVisible = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-visible-baseline'), + callId: ToolCallId('read-visible-baseline'), name: 'read', arguments: { file_path: 'file.txt' }, agent, @@ -3644,7 +3644,7 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-compacted-baseline'), + callId: ToolCallId('read-compacted-baseline'), name: 'read', arguments: { file_path: 'file.txt' }, agent, @@ -3653,7 +3653,7 @@ describe('dynamic nested workspace context injection', () => { await appendAdditionalContexts(ctx, agent) const afterRearm = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-rearmed-baseline'), + callId: ToolCallId('read-rearmed-baseline'), name: 'read', arguments: { file_path: 'file.txt' }, agent, @@ -3685,7 +3685,7 @@ describe('dynamic nested workspace context injection', () => { const agent = stubAgent(root) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-package'), + callId: ToolCallId('read-package'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, @@ -3695,7 +3695,7 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-subtree'), + callId: ToolCallId('read-subtree'), name: 'read', arguments: { file_path: join('pkg', 'sub', 'file.txt') }, agent, @@ -3723,7 +3723,7 @@ describe('dynamic nested workspace context injection', () => { const agent = stubAgent(root) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-subtree-omitting-parent'), + callId: ToolCallId('read-subtree-omitting-parent'), name: 'read', arguments: { file_path: join('pkg', 'sub', 'file.txt') }, agent, @@ -3733,7 +3733,7 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-parent-after-omit'), + callId: ToolCallId('read-parent-after-omit'), name: 'read', arguments: { file_path: join('pkg', 'other.txt') }, agent, @@ -3785,7 +3785,7 @@ describe('dynamic nested workspace context injection', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-spoofed-state'), + callId: ToolCallId('read-after-spoofed-state'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -3812,14 +3812,14 @@ describe('dynamic nested workspace context injection', () => { const rootResult = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-root-file'), + callId: ToolCallId('read-root-file'), name: 'read', arguments: { file_path: 'root.txt' }, agent, }) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-absolute-nested-file'), + callId: ToolCallId('read-absolute-nested-file'), name: 'read', arguments: { file_path: join(root, 'pkg/deep/file.txt') }, agent, @@ -3855,7 +3855,7 @@ describe('dynamic nested workspace context injection', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-with-unreadable-nested-instruction'), + callId: ToolCallId('read-with-unreadable-nested-instruction'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -3900,7 +3900,7 @@ describe('dynamic nested workspace context injection', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-with-downstream'), + callId: ToolCallId('read-with-downstream'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -3953,7 +3953,7 @@ describe('dynamic nested workspace context injection', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-blocked-downstream'), + callId: ToolCallId('read-blocked-downstream'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -3996,7 +3996,7 @@ describe('dynamic nested workspace context injection', () => { const blocked = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('outer-block-first'), + callId: ToolCallId('outer-block-first'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -4007,7 +4007,7 @@ describe('dynamic nested workspace context injection', () => { shouldBlock = false const accepted = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('outer-block-retry'), + callId: ToolCallId('outer-block-retry'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -4043,7 +4043,7 @@ describe('dynamic nested workspace context injection', () => { async execute(_args, exec) { const nested = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId(`${exec.callId}:nested`), + callId: ToolCallId(`${exec.callId}:nested`), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, ...exec.agent === undefined ? {} : { agent: exec.agent }, @@ -4065,7 +4065,7 @@ describe('dynamic nested workspace context injection', () => { const blocked = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('composite-first'), name: 'composite-read', arguments: {}, agent, + callId: ToolCallId('composite-first'), name: 'composite-read', arguments: {}, agent, }) expect(blocked.isError).toBe(true) @@ -4099,7 +4099,7 @@ describe('dynamic nested workspace context injection', () => { token: Symbol('nested-read') as ToolExecutionToken, parent: outerToken, signal: testToolSignal, - callId: CallId('nested-read'), + callId: ToolCallId('nested-read'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, @@ -4108,7 +4108,7 @@ describe('dynamic nested workspace context injection', () => { token: Symbol('nested-non-file') as ToolExecutionToken, parent: outerToken, signal: testToolSignal, - callId: CallId('nested-non-file'), + callId: ToolCallId('nested-non-file'), name: 'search', arguments: {}, agent, @@ -4117,7 +4117,7 @@ describe('dynamic nested workspace context injection', () => { token: Symbol('second-nested-read') as ToolExecutionToken, parent: outerToken, signal: testToolSignal, - callId: CallId('second-nested-read'), + callId: ToolCallId('second-nested-read'), name: 'read', arguments: { file_path: join('pkg', 'second.txt') }, agent, @@ -4125,7 +4125,7 @@ describe('dynamic nested workspace context injection', () => { ctx.emit('tools/result', stubToolExecution({ token: outerToken, signal: testToolSignal, - callId: CallId('outer-code-run'), + callId: ToolCallId('outer-code-run'), name: 'run_code', arguments: {}, agent, @@ -4163,7 +4163,7 @@ describe('dynamic nested workspace context injection', () => { ctx.emit('tools/result', stubToolExecution({ signal: testToolSignal, - callId: CallId('read-after-closed-step'), + callId: ToolCallId('read-after-closed-step'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, @@ -4185,38 +4185,38 @@ describe('dynamic nested workspace context injection', () => { await ctx.plugin(workspaceContext, { maxBytes: 65536 }) const fs = ctx.fs as RecordingFileSystem const agent = stubAgent('/') - const plainResult = { callId: CallId('plain'), content: [], isError: false as const, value: null } + const plainResult = { callId: ToolCallId('plain'), content: [], isError: false as const, value: null } const aborted = new AbortController() aborted.abort(new Error('cancelled')) ctx.emit('tools/result', stubToolExecution({ - signal: testToolSignal, callId: CallId('agentless'), name: 'read', arguments: { file_path: 'file.txt' }, + signal: testToolSignal, callId: ToolCallId('agentless'), name: 'read', arguments: { file_path: 'file.txt' }, }), plainResult) ctx.emit('tools/result', stubToolExecution({ - signal: testToolSignal, callId: CallId('failed'), name: 'read', arguments: { file_path: 'failed/file.txt' }, agent, + signal: testToolSignal, callId: ToolCallId('failed'), name: 'read', arguments: { file_path: 'failed/file.txt' }, agent, }), { content: [], isError: true, error: { message: 'failed' } }) ctx.emit('tools/result', stubToolExecution({ - signal: aborted.signal, callId: CallId('aborted'), name: 'read', arguments: { file_path: 'aborted/file.txt' }, agent, + signal: aborted.signal, callId: ToolCallId('aborted'), name: 'read', arguments: { file_path: 'aborted/file.txt' }, agent, }), plainResult) ctx.emit('tools/result', stubToolExecution({ signal: testToolSignal, - callId: CallId('null-arguments'), name: 'read', arguments: null, agent, + callId: ToolCallId('null-arguments'), name: 'read', arguments: null, agent, }), plainResult) ctx.emit('tools/result', stubToolExecution({ signal: testToolSignal, - callId: CallId('missing-path'), name: 'read', arguments: {}, agent, + callId: ToolCallId('missing-path'), name: 'read', arguments: {}, agent, }), plainResult) ctx.emit('tools/result', stubToolExecution({ signal: testToolSignal, - callId: CallId('non-string-path'), name: 'read', arguments: { file_path: 1 }, agent, + callId: ToolCallId('non-string-path'), name: 'read', arguments: { file_path: 1 }, agent, }), plainResult) ctx.emit('tools/result', stubToolExecution({ signal: testToolSignal, - callId: CallId('blank-path'), name: 'read', arguments: { file_path: ' ' }, agent, + callId: ToolCallId('blank-path'), name: 'read', arguments: { file_path: ' ' }, agent, }), plainResult) ctx.emit('tools/result', stubToolExecution({ signal: testToolSignal, - callId: CallId('non-fs'), name: 'composite', arguments: {}, agent, + callId: ToolCallId('non-fs'), name: 'composite', arguments: {}, agent, }), plainResult) await Promise.resolve() @@ -4243,7 +4243,7 @@ describe('dynamic nested workspace context injection', () => { ctx.emit('tools/result', stubToolExecution({ signal: testToolSignal, - callId: CallId('projection-failure'), + callId: ToolCallId('projection-failure'), name: 'read', arguments: { file_path: 'file.txt' }, agent, @@ -4269,7 +4269,7 @@ describe('dynamic nested workspace context injection', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-with-disabled-budget'), + callId: ToolCallId('read-with-disabled-budget'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent: stubAgent(root), @@ -4302,12 +4302,12 @@ describe('dynamic nested workspace context injection', () => { const first = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-tiny-budget-1'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-tiny-budget-1'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await syncWorkspaceContext(ctx, agent) const second = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-tiny-budget-2'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('read-tiny-budget-2'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await syncWorkspaceContext(ctx, agent) @@ -4335,7 +4335,7 @@ describe('dynamic nested workspace context injection', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-missing'), + callId: ToolCallId('read-missing'), name: 'read', arguments: { file_path: join('pkg', 'missing.txt') }, agent: stubAgent(root), @@ -4363,7 +4363,7 @@ describe('dynamic nested workspace context injection', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('read-after-dispose'), + callId: ToolCallId('read-after-dispose'), name: 'read', arguments: { file_path: join('pkg', 'deep', 'file.txt') }, agent, @@ -4423,7 +4423,7 @@ describe('workspace context inbox synchronization', () => { const agent = stubAgent(root) ctx.emit('tools/result', stubToolExecution({ signal: testToolSignal, - callId: CallId('tiny-budget-touch'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('tiny-budget-touch'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }), acceptedResult) await syncWorkspaceContext(ctx, agent) @@ -4451,7 +4451,7 @@ describe('workspace context inbox synchronization', () => { const agent = stubAgent(root) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('pending-v1'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('pending-v1'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await syncWorkspaceContext(ctx, agent) expect(blocksText(agent.inbox.nextStep[0]?.content)).toContain('pending version one') @@ -4464,7 +4464,7 @@ describe('workspace context inbox synchronization', () => { await write(join(root, 'pkg/AGENTS.md'), 'pending version two with more detail') await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('pending-v2'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('pending-v2'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await syncWorkspaceContext(ctx, agent) expect(agent.inbox.nextStep).toHaveLength(1) @@ -4475,7 +4475,7 @@ describe('workspace context inbox synchronization', () => { await rm(join(root, 'pkg/AGENTS.md')) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('pending-delete'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, + callId: ToolCallId('pending-delete'), name: 'read', arguments: { file_path: join('pkg', 'file.txt') }, agent, }) await syncWorkspaceContext(ctx, agent) expect(agent.inbox.nextStep).toEqual([]) @@ -4499,7 +4499,7 @@ describe('workspace context inbox synchronization', () => { const agent = stubAgent(root) const first = stubToolExecution({ signal: testToolSignal, - callId: CallId('projected-before-abort'), name: 'read', arguments: { file_path: join('a', 'file.txt') }, agent, + callId: ToolCallId('projected-before-abort'), name: 'read', arguments: { file_path: join('a', 'file.txt') }, agent, }) ctx.emit('tools/result', first, acceptedResult) const controller = new AbortController() @@ -4512,7 +4512,7 @@ describe('workspace context inbox synchronization', () => { ctx.emit('tools/result', stubToolExecution({ signal: testToolSignal, - callId: CallId('projected-after-abort'), name: 'read', arguments: { file_path: join('b', 'file.txt') }, agent, + callId: ToolCallId('projected-after-abort'), name: 'read', arguments: { file_path: join('b', 'file.txt') }, agent, }), acceptedResult) await syncWorkspaceContext(ctx, agent) const text = blocksText(agent.inbox.nextStep[0]?.content) @@ -4539,11 +4539,11 @@ describe('workspace context inbox synchronization', () => { const agent = stubAgent(root) const first = stubToolExecution({ signal: testToolSignal, - callId: CallId('concurrent-a'), name: 'read', arguments: { file_path: join('a', 'file.txt') }, agent, + callId: ToolCallId('concurrent-a'), name: 'read', arguments: { file_path: join('a', 'file.txt') }, agent, }) const second = stubToolExecution({ signal: testToolSignal, - callId: CallId('concurrent-b'), name: 'read', arguments: { file_path: join('b', 'file.txt') }, agent, + callId: ToolCallId('concurrent-b'), name: 'read', arguments: { file_path: join('b', 'file.txt') }, agent, }) ctx.emit('tools/result', first, acceptedResult) @@ -4577,14 +4577,14 @@ describe('workspace context inbox synchronization', () => { const original = stubAgent(root) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('recover-pending-a'), name: 'read', arguments: { file_path: join('a', 'file.txt') }, agent: original, + callId: ToolCallId('recover-pending-a'), name: 'read', arguments: { file_path: join('a', 'file.txt') }, agent: original, }) await syncWorkspaceContext(ctx, original) const resumed = stubAgent(root, [...original.session.events]) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('recover-pending-b'), name: 'read', arguments: { file_path: join('b', 'file.txt') }, agent: resumed, + callId: ToolCallId('recover-pending-b'), name: 'read', arguments: { file_path: join('b', 'file.txt') }, agent: resumed, }) await syncWorkspaceContext(ctx, resumed) diff --git a/packages/context/session-reference/tests/session-reference.spec.ts b/packages/context/session-reference/tests/session-reference.spec.ts index 921e1703c4..ab0211ce52 100644 --- a/packages/context/session-reference/tests/session-reference.spec.ts +++ b/packages/context/session-reference/tests/session-reference.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent' import { CompactionId, compactCheckpointSource } from '@deepseek-ai/dsh-compaction' -import { createUserMessage, CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session' import SessionQueryEngine from '@deepseek-ai/dsh-session-query' import SessionReferenceResolver, { @@ -122,7 +122,7 @@ function appendConversation(session: Session): void { { turn: 2, step: 1, message: createToolResultMessage({ - callId: CallId('call'), + callId: ToolCallId('call'), content: [{ type: 'text', text: 'tool output' }], isError: false, }), diff --git a/packages/context/time-context/tests/time-context.spec.ts b/packages/context/time-context/tests/time-context.spec.ts index 833f7e4b9c..7952b95131 100644 --- a/packages/context/time-context/tests/time-context.spec.ts +++ b/packages/context/time-context/tests/time-context.spec.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' -import { createUserMessage, CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, LlmAdapter } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' import AgentRegistry, { agentEvents, Inbox, type Agent } from '@deepseek-ai/dsh-agent' @@ -113,7 +113,7 @@ function toolCallResponse(): StreamChunk[] { { type: 'block-end', index: 0, - block: { type: 'tool-call', id: CallId('tick-1'), name: 'tick', arguments: '{}' }, + block: { type: 'tool-call', id: ToolCallId('tick-1'), name: 'tick', arguments: '{}' }, }, { type: 'finish', reason: { kind: 'tool-calls' } }, ] diff --git a/packages/core/agent-loop/tests/agent-initiator.spec.ts b/packages/core/agent-loop/tests/agent-initiator.spec.ts index 0cd3ac39e4..70e3d26842 100644 --- a/packages/core/agent-loop/tests/agent-initiator.spec.ts +++ b/packages/core/agent-loop/tests/agent-initiator.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import { Context, type Fiber } from '@deepseek-ai/cordis' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' -import LlmRuntime, { createUserMessage, CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, ToolCallId, LlmAdapter } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' @@ -310,7 +310,7 @@ describe('AgentLoop initiator scope', () => { const direct = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('direct'), + callId: ToolCallId('direct'), name: 'agentless-probe', arguments: {}, }) diff --git a/packages/core/agent-loop/tests/contract-regressions.spec.ts b/packages/core/agent-loop/tests/contract-regressions.spec.ts index c085dc7de0..25a0c7a85a 100644 --- a/packages/core/agent-loop/tests/contract-regressions.spec.ts +++ b/packages/core/agent-loop/tests/contract-regressions.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import LlmRuntime, { createUserMessage, CallId, LlmError, MessageSource, ProviderRequestId, StreamChunk } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, ToolCallId, LlmError, MessageSource, ProviderRequestId, StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, SessionEvent, SessionId, TurnEndReason, type UserMessage } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { defineContentToolFixture, type PostToolDecision } from '@deepseek-ai/dsh-tools' @@ -135,9 +135,9 @@ describe('abort during tool execution ends the turn', () => { it('records post-tool context when a later call aborts the batch', async () => { const adapter = new MockAdapter([[ { type: 'block-start', index: 0, blockType: 'tool-call' }, - { type: 'block-end', index: 0, block: { type: 'tool-call', id: CallId('c1'), name: 'first', arguments: '{}' } }, + { type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('c1'), name: 'first', arguments: '{}' } }, { type: 'block-start', index: 1, blockType: 'tool-call' }, - { type: 'block-end', index: 1, block: { type: 'tool-call', id: CallId('c2'), name: 'aborter', arguments: '{}' } }, + { type: 'block-end', index: 1, block: { type: 'tool-call', id: ToolCallId('c2'), name: 'aborter', arguments: '{}' } }, { type: 'finish', reason: { kind: 'tool-calls' } }, ] satisfies StreamChunk[]]) const ctx = await harness(adapter) @@ -160,7 +160,7 @@ describe('abort during tool execution ends the turn', () => { }, })) ctx.on('tools/post-execute', async (exec, _result, next): Promise => { - if (exec.callId !== CallId('c1')) return next() + if (exec.callId !== ToolCallId('c1')) return next() return { kind: 'accept', additionalContexts: [createUserMessage({ @@ -259,9 +259,9 @@ describe('abort during tool execution ends the turn', () => { const adapter = new MockAdapter([ [ { type: 'block-start', index: 0, blockType: 'tool-call' }, - { type: 'block-end', index: 0, block: { type: 'tool-call', id: CallId('c1'), name: 'aborter', arguments: '{}' } }, + { type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('c1'), name: 'aborter', arguments: '{}' } }, { type: 'block-start', index: 1, blockType: 'tool-call' }, - { type: 'block-end', index: 1, block: { type: 'tool-call', id: CallId('c2'), name: 'second', arguments: '{}' } }, + { type: 'block-end', index: 1, block: { type: 'tool-call', id: ToolCallId('c2'), name: 'second', arguments: '{}' } }, { type: 'finish', reason: { kind: 'tool-calls' } }, ] satisfies StreamChunk[], textResponse('later turn'), @@ -553,7 +553,7 @@ describe('discriminated SessionEvent narrows without casts', () => { it('narrows event.data from event.type', () => { const session = Session.create(SessionId('s')) const appended: SessionEvent = session.append('tool/call', { - turn: 1, step: 1, callId: CallId('c1'), name: 'echo', arguments: '{}', + turn: 1, step: 1, callId: ToolCallId('c1'), name: 'echo', arguments: '{}', }) // compile-time: this switch narrows; runtime: values flow through switch (appended.type) { @@ -1069,7 +1069,7 @@ describe('tool result call identity', () => { // The loop must still record the tool/result under the model's authoritative // 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 + expect(exec.callId).toBe(ToolCallId('c1')) // the loop passed the real id in return Promise.resolve({ kind: 'accept', content: [{ type: 'text', text: 'ok' }] }) }, { prepend: true }) @@ -1081,7 +1081,7 @@ describe('tool result call identity', () => { const resultEvent = [...agent.session.events].find(e => e.type === 'tool/result') expect(resultEvent?.type).toBe('tool/result') if (resultEvent?.type === 'tool/result') { - expect(resultEvent.data.message.source.callId).toBe(CallId('c1')) + expect(resultEvent.data.message.source.callId).toBe(ToolCallId('c1')) } // And deriveMessages pairs the tool-result with the assistant tool-call: @@ -1092,7 +1092,7 @@ describe('tool result call identity', () => { .find(b => b.type === 'tool-result') expect(toolResultBlock?.type).toBe('tool-result') if (toolResultBlock?.type === 'tool-result') { - expect(toolResultBlock.toolCallId).toBe(CallId('c1')) + expect(toolResultBlock.toolCallId).toBe(ToolCallId('c1')) } }) }) diff --git a/packages/core/agent-loop/tests/coverage-edges.spec.ts b/packages/core/agent-loop/tests/coverage-edges.spec.ts index 21506c9a0b..b42b3789ec 100644 --- a/packages/core/agent-loop/tests/coverage-edges.spec.ts +++ b/packages/core/agent-loop/tests/coverage-edges.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import LlmRuntime, { createUserMessage, CallId, LlmError, StreamChunk, errorChain } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, ToolCallId, LlmError, StreamChunk, errorChain } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session' import type { SessionEvent } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' @@ -47,7 +47,7 @@ describe('tool JSON parse', () => { // model emits tool-call with malformed arguments (not valid JSON) [ { type: 'block-start' as const, index: 0, blockType: 'tool-call' as const }, - { type: 'block-end' as const, index: 0, block: { type: 'tool-call' as const, id: CallId('c1'), name: 'echo', arguments: 'not json' } }, + { type: 'block-end' as const, index: 0, block: { type: 'tool-call' as const, id: ToolCallId('c1'), name: 'echo', arguments: 'not json' } }, { type: 'finish' as const, reason: { kind: 'tool-calls' as const } }, ] satisfies StreamChunk[], textResponse('done'), @@ -80,7 +80,7 @@ describe('tool JSON parse', () => { const adapter = new MockAdapter([ [ { type: 'block-start' as const, index: 0, blockType: 'tool-call' as const }, - { type: 'block-end' as const, index: 0, block: { type: 'tool-call' as const, id: CallId('c1'), name: 'noarg', arguments: '' } }, + { type: 'block-end' as const, index: 0, block: { type: 'tool-call' as const, id: ToolCallId('c1'), name: 'noarg', arguments: '' } }, { type: 'finish' as const, reason: { kind: 'tool-calls' as const } }, ] satisfies StreamChunk[], textResponse('done'), diff --git a/packages/core/agent-loop/tests/interception.spec.ts b/packages/core/agent-loop/tests/interception.spec.ts index 72e3165f78..bc2c381c2a 100644 --- a/packages/core/agent-loop/tests/interception.spec.ts +++ b/packages/core/agent-loop/tests/interception.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import LlmRuntime, { createUserMessage, CallId } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId, type SessionEvent, @@ -613,9 +613,9 @@ describe('tool additionalContexts buffering across a step', () => { // One assistant step with TWO tool calls; the second model response stops. const twoCalls = [ { type: 'block-start' as const, index: 0, blockType: 'tool-call' as const }, - { type: 'block-end' as const, index: 0, block: { type: 'tool-call' as const, id: CallId('c1'), name: 'echo', arguments: '{"text":"a"}' } }, + { type: 'block-end' as const, index: 0, block: { type: 'tool-call' as const, id: ToolCallId('c1'), name: 'echo', arguments: '{"text":"a"}' } }, { type: 'block-start' as const, index: 1, blockType: 'tool-call' as const }, - { type: 'block-end' as const, index: 1, block: { type: 'tool-call' as const, id: CallId('c2'), name: 'echo', arguments: '{"text":"b"}' } }, + { type: 'block-end' as const, index: 1, block: { type: 'tool-call' as const, id: ToolCallId('c2'), name: 'echo', arguments: '{"text":"b"}' } }, { type: 'usage' as const, usage: { inputTokens: 5, outputTokens: 5 } }, { type: 'finish' as const, reason: { kind: 'tool-calls' as const } }, ] diff --git a/packages/core/agent-loop/tests/loop.spec.ts b/packages/core/agent-loop/tests/loop.spec.ts index 2105b86f42..eb1c38f09b 100644 --- a/packages/core/agent-loop/tests/loop.spec.ts +++ b/packages/core/agent-loop/tests/loop.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import LlmRuntime, { createUserMessage, CallId, LlmError, StreamChunk } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, ToolCallId, LlmError, StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools' @@ -1052,7 +1052,7 @@ describe('agent loop', () => { }) it('does not dispatch tool calls from a max-tokens-truncated step', async () => { - const callId = CallId('c1') + const callId = ToolCallId('c1') const adapter = new MockAdapter([[ { type: 'block-start', index: 0, blockType: 'tool-call' }, { type: 'tool-call-delta', index: 0, id: callId, name: 'echo', argumentsDelta: '{"text":"x"}' }, @@ -1107,7 +1107,7 @@ describe('agent loop', () => { it('appends an empty completion anchor for a max-tokens step with no usage', async () => { // The truncated tool call is dropped from durable content, while the // successful provider call still needs an exact replay anchor. - const callId = CallId('c1') + const callId = ToolCallId('c1') const adapter = new MockAdapter([[ { type: 'block-start', index: 0, blockType: 'tool-call' }, { type: 'tool-call-delta', index: 0, id: callId, name: 'echo', argumentsDelta: '{"text":"x"}' }, @@ -1185,7 +1185,7 @@ describe('agent loop', () => { }) it('keeps safe max-tokens assistant content while dropping truncated tool calls', async () => { - const callId = CallId('c1') + const callId = ToolCallId('c1') const adapter = new MockAdapter([[ { type: 'block-start', index: 0, blockType: 'text' }, { type: 'text-delta', index: 0, text: 'partial text' }, diff --git a/packages/core/agent-loop/tests/mock-adapter.ts b/packages/core/agent-loop/tests/mock-adapter.ts index c5341fb7ed..7543fd21c1 100644 --- a/packages/core/agent-loop/tests/mock-adapter.ts +++ b/packages/core/agent-loop/tests/mock-adapter.ts @@ -1,5 +1,5 @@ import type { GenerateOptions, LlmModelReasoningInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm' -import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' +import { ToolCallId, LlmAdapter } from '@deepseek-ai/dsh-llm' /** Helpers to write scripted responses tersely. */ export function textResponse(text: string): StreamChunk[] { @@ -28,7 +28,7 @@ export function maxTokensResponse(text: string): StreamChunk[] { } export function toolCallResponse(rawCallId: string, name: string, args: object, text?: string): StreamChunk[] { - const callId = CallId(rawCallId) + const callId = ToolCallId(rawCallId) const argumentsJson = JSON.stringify(args) const chunks: StreamChunk[] = [] let index = 0 diff --git a/packages/core/agent-loop/tests/tool-calls.spec.ts b/packages/core/agent-loop/tests/tool-calls.spec.ts index 87f69cc7d0..f65454949c 100644 --- a/packages/core/agent-loop/tests/tool-calls.spec.ts +++ b/packages/core/agent-loop/tests/tool-calls.spec.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { createUserMessage, CallId, StreamChunk } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionEvent, SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import LlmRuntime from '@deepseek-ai/dsh-llm' @@ -49,7 +49,7 @@ function multiCall(calls: { id: string; name: string; args: object }[]): StreamC calls.forEach((call, index) => { chunks.push( { type: 'block-start', index, blockType: 'tool-call' }, - { type: 'block-end', index, block: { type: 'tool-call', id: CallId(call.id), name: call.name, arguments: JSON.stringify(call.args) } }, + { type: 'block-end', index, block: { type: 'tool-call', id: ToolCallId(call.id), name: call.name, arguments: JSON.stringify(call.args) } }, ) }) chunks.push( @@ -196,7 +196,7 @@ describe('tool-call scheduler: grouping and barriers', () => { const replacement = gatedExclusiveTool('x') const disposeInitial = ctx.tools.register(initial.tool) ctx.on('tools/result', (exec) => { - if (exec.callId !== CallId('c1')) return + if (exec.callId !== ToolCallId('c1')) return disposeInitial() ctx.tools.register(replacement.tool) }) @@ -206,7 +206,7 @@ describe('tool-call scheduler: grouping and barriers', () => { await until(() => initial.started.length === 2) initial.release('1') await until(() => events(agent).some(event => - event.type === 'tool/result' && event.data.message.source.callId === CallId('c1'))) + event.type === 'tool/result' && event.data.message.source.callId === ToolCallId('c1'))) await new Promise(r => setTimeout(r, 5)) expect(replacement.started).toEqual([]) initial.release('2') @@ -238,7 +238,7 @@ describe('tool-call scheduler: model-order results despite out-of-order settleme await waitForIdle(ctx, agent) const results = events(agent).filter(e => e.type === 'tool/result') - expect(results.map(e => e.data.message.source.callId)).toEqual([CallId('c1'), CallId('c2')]) + expect(results.map(e => e.data.message.source.callId)).toEqual([ToolCallId('c1'), ToolCallId('c2')]) }) it('derived history pairs calls in model order regardless of tool/call log interleaving', async () => { @@ -257,7 +257,7 @@ describe('tool-call scheduler: model-order results despite out-of-order settleme const messages = agent.session.deriveMessages() const toolResults = messages.flatMap(m => m.content.filter(b => b.type === 'tool-result')) - expect(toolResults.map(b => b.toolCallId)).toEqual([CallId('c1'), CallId('c2')]) + expect(toolResults.map(b => b.toolCallId)).toEqual([ToolCallId('c1'), ToolCallId('c2')]) }) }) @@ -316,7 +316,7 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () => gated.release('4') await waitForIdle(ctx, agent) expect(events(agent).filter(e => e.type === 'tool/result').map(e => e.data.message.source.callId)) - .toEqual([CallId('c1'), CallId('c2'), CallId('c3'), CallId('c4')]) + .toEqual([ToolCallId('c1'), ToolCallId('c2'), ToolCallId('c3'), ToolCallId('c4')]) }) it('maxParallelToolCalls: 1 is fully serial (no second start before the first settles)', async () => { @@ -386,8 +386,8 @@ describe('tool-call scheduler: ordered middleware and additional contexts', () = gated.release('3'); gated.release('2'); gated.release('1') await waitForIdle(ctx, agent) - expect(pre).toEqual([CallId('c1'), CallId('c2'), CallId('c3')].map(String)) - expect(post).toEqual([CallId('c1'), CallId('c2'), CallId('c3')].map(String)) + expect(pre).toEqual([ToolCallId('c1'), ToolCallId('c2'), ToolCallId('c3')].map(String)) + expect(post).toEqual([ToolCallId('c1'), ToolCallId('c2'), ToolCallId('c3')].map(String)) }) it('injects additional contexts in model call order, not settlement order', async () => { @@ -432,8 +432,8 @@ describe('tool-call scheduler: ordered middleware and additional contexts', () = ctx.tools.register(gated.tool) const post: string[] = [] ctx.on('tools/pre-execute', async (exec, next): Promise => { - if (exec.callId === CallId('c2')) return { kind: 'deny', reason: 'blocked by policy' } - if (exec.callId === CallId('c3')) throw new Error('pre exploded') + if (exec.callId === ToolCallId('c2')) return { kind: 'deny', reason: 'blocked by policy' } + if (exec.callId === ToolCallId('c3')) throw new Error('pre exploded') return next() }) ctx.on('tools/post-execute', async (exec, _result, next): Promise => { @@ -450,7 +450,7 @@ describe('tool-call scheduler: ordered middleware and additional contexts', () = expect(gated.started).toEqual(['1']) expect(post).toEqual(['c1', 'c2']) const results = events(agent).filter(e => e.type === 'tool/result') - expect(results.map(e => e.data.message.source.callId)).toEqual([CallId('c1'), CallId('c2'), CallId('c3')]) + expect(results.map(e => e.data.message.source.callId)).toEqual([ToolCallId('c1'), ToolCallId('c2'), ToolCallId('c3')]) expect((results[1]!.data.message.content[0].content[0] as { text: string }).text).toContain('blocked by policy') expect((results[2]!.data.message.content[0].content[0] as { text: string }).text).toContain('pre exploded') }) @@ -477,14 +477,14 @@ describe('tool-call scheduler: abort handling', () => { expect(gated.started).toEqual([]) expect(events(agent).filter(e => e.type === 'tool/call').map(e => e.data.callId)) - .toEqual([CallId('c1'), CallId('c2')]) + .toEqual([ToolCallId('c1'), ToolCallId('c2')]) expect(events(agent).filter(e => e.type === 'tool/result').map(e => ({ callId: e.data.message.source.callId, isError: e.data.message.content[0].isError, error: e.data.error, }))).toEqual([ - { callId: CallId('c1'), isError: true, error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH } }, - { callId: CallId('c2'), isError: true, error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH } }, + { callId: ToolCallId('c1'), isError: true, error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH } }, + { callId: ToolCallId('c2'), isError: true, error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH } }, ]) }) @@ -498,7 +498,7 @@ describe('tool-call scheduler: abort handling', () => { ctx.tools.register(gated.tool) const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) ctx.on('tools/pre-execute', async (exec, next): Promise => { - if (exec.callId === CallId('c1')) { + if (exec.callId === ToolCallId('c1')) { agent.cancel({ kind: 'user' }) } return next() @@ -509,14 +509,14 @@ describe('tool-call scheduler: abort handling', () => { expect(gated.started).toEqual([]) expect(events(agent).filter(e => e.type === 'tool/call').map(e => e.data.callId)) - .toEqual([CallId('c1'), CallId('c2')]) + .toEqual([ToolCallId('c1'), ToolCallId('c2')]) expect(events(agent).filter(e => e.type === 'tool/result').map(e => ({ callId: e.data.message.source.callId, isError: e.data.message.content[0].isError, error: e.data.error, }))).toEqual([ - { callId: CallId('c1'), isError: true, error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH } }, - { callId: CallId('c2'), isError: true, error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH } }, + { callId: ToolCallId('c1'), isError: true, error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH } }, + { callId: ToolCallId('c2'), isError: true, error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH } }, ]) }) @@ -545,9 +545,9 @@ describe('tool-call scheduler: abort handling', () => { expect(gated.started).toEqual(['1', '2']) expect(events(agent).filter(e => e.type === 'tool/call').map(e => e.data.callId)) - .toEqual([CallId('c1'), CallId('c2'), CallId('c3'), CallId('c4')]) + .toEqual([ToolCallId('c1'), ToolCallId('c2'), ToolCallId('c3'), ToolCallId('c4')]) expect(events(agent).filter(e => e.type === 'tool/result').map(e => e.data.message.source.callId)) - .toEqual([CallId('c1'), CallId('c2'), CallId('c3'), CallId('c4')]) + .toEqual([ToolCallId('c1'), ToolCallId('c2'), ToolCallId('c3'), ToolCallId('c4')]) expect(events(agent).filter(e => e.type === 'tool/result').slice(-2).map(e => ({ callId: e.data.message.source.callId, isError: e.data.message.content[0].isError, @@ -555,12 +555,12 @@ describe('tool-call scheduler: abort handling', () => { }))) .toEqual([ { - callId: CallId('c3'), + callId: ToolCallId('c3'), isError: true, error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH }, }, { - callId: CallId('c4'), + callId: ToolCallId('c4'), isError: true, error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH }, }, @@ -618,11 +618,11 @@ describe('tool-call scheduler: abort handling', () => { expect(exclusive).toEqual([]) expect(events(agent).filter(e => e.type === 'tool/call').map(e => e.data.callId)) - .toEqual([CallId('c1'), CallId('c2'), CallId('c3')]) + .toEqual([ToolCallId('c1'), ToolCallId('c2'), ToolCallId('c3')]) expect(events(agent).filter(e => e.type === 'tool/result').at(-1)?.data) .toMatchObject({ message: { - source: { kind: 'tool', callId: CallId('c3') }, + source: { kind: 'tool', callId: ToolCallId('c3') }, content: [{ isError: true }], }, error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH }, @@ -651,7 +651,7 @@ describe('tool-call scheduler: failure quiescence', () => { let thirdPrepareEntered = false scheduler.prepare = async (exec) => { const prepared = await prepare(exec) - if (exec.callId === CallId('c3')) { + if (exec.callId === ToolCallId('c3')) { thirdPrepareEntered = true await prepareGate.promise } @@ -660,7 +660,7 @@ describe('tool-call scheduler: failure quiescence', () => { const schedulerError = new Error('scheduler exploded') const drainedError = new Error('sibling failed while draining') let rejectFirst: ((error: Error) => void) | undefined - scheduler.dispatch = exec => exec.callId === CallId('c1') + scheduler.dispatch = exec => exec.callId === ToolCallId('c1') ? new Promise((_resolve, reject) => { rejectFirst = reject }) : dispatch(exec).then(() => { throw drainedError }) const agent = ctx.agentLoop.create(SessionId('scheduler-failure'), { provider: 'mock', model: 'mock' }) diff --git a/packages/core/session/src/chunk-rows.ts b/packages/core/session/src/chunk-rows.ts index ef46e40dbb..34d57511b8 100644 --- a/packages/core/session/src/chunk-rows.ts +++ b/packages/core/session/src/chunk-rows.ts @@ -18,7 +18,7 @@ * @module @deepseek-ai/dsh-session/chunk-rows */ -import { CallId, assertNever } from '@deepseek-ai/dsh-llm' +import { ToolCallId, assertNever } from '@deepseek-ai/dsh-llm' import type { StreamChunk } from '@deepseek-ai/dsh-llm' import type { SessionEvent } from './types.ts' @@ -50,7 +50,7 @@ interface TextRunData extends RunDataBase { /** Payload of a `tool-call-chunks` row: the run-constant call identity plus each member's raw arguments fragment. */ interface ToolCallRunData extends RunDataBase { - id: CallId + id: ToolCallId /** Present iff every member carried it, with one uniform value (a mixed run never packs). */ name?: string args: string[] @@ -167,7 +167,7 @@ function buildRow(kind: DeltaKind, run: readonly DeltaEvent[]): ChunkRow { ...envelope, data: { ...base, - id: CallId(call.id), + id: ToolCallId(call.id), ...Object.hasOwn(call, 'name') ? { name: call.name as string } : {}, args: run.map(event => (event.data.chunk as { argumentsDelta: string }).argumentsDelta), }, diff --git a/packages/core/session/src/invariant.ts b/packages/core/session/src/invariant.ts index da7cd55964..6d0d687286 100644 --- a/packages/core/session/src/invariant.ts +++ b/packages/core/session/src/invariant.ts @@ -7,7 +7,7 @@ import type { Context } from '@deepseek-ai/cordis' import { assertNever } from '@deepseek-ai/dsh-llm' -import type { CallId } from '@deepseek-ai/dsh-llm' +import type { ToolCallId } from '@deepseek-ai/dsh-llm' import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' import { TOOL_NOT_STARTED } from './repair.ts' @@ -26,7 +26,7 @@ interface SessionTrace { openStep: number | null nextTurn: number nextStep: number - pendingCalls: Set + pendingCalls: Set } /** One accepted event's deferred mutation of a committed session trace. */ @@ -34,7 +34,7 @@ interface SessionTraceTransition { scalars: Pick pendingCalls: | { kind: 'none' } - | { kind: 'add' | 'delete'; callId: CallId } + | { kind: 'add' | 'delete'; callId: ToolCallId } | { kind: 'clear' } } diff --git a/packages/core/session/src/repair.ts b/packages/core/session/src/repair.ts index d91cb72494..18e7a18f0c 100644 --- a/packages/core/session/src/repair.ts +++ b/packages/core/session/src/repair.ts @@ -5,7 +5,7 @@ * @module @deepseek-ai/dsh-session/repair */ -import { MessageId, freezeMessage, type CallId } from '@deepseek-ai/dsh-llm' +import { MessageId, freezeMessage, type ToolCallId } from '@deepseek-ai/dsh-llm' import type { ToolResultMessage } from '@deepseek-ai/dsh-llm' import type { SessionEvent } from './types.ts' @@ -29,7 +29,7 @@ export function interruptedTurnClosers(events: readonly SessionEvent[]): Session let openStep: number | null = null // Reset at each turn boundary so earlier calls cannot leak into tail repair. // Assistant blocks register calls; later `tool/call` events add their seqs to `sourceEventSeqs`. - const pendingCalls = new Map() + const pendingCalls = new Map() for (const event of events) { switch (event.type) { case 'turn/start': diff --git a/packages/core/session/src/types.ts b/packages/core/session/src/types.ts index 31ce28a01b..2d22a5b940 100644 --- a/packages/core/session/src/types.ts +++ b/packages/core/session/src/types.ts @@ -1,7 +1,7 @@ import type { Branded } from '@deepseek-ai/dsh-brand' import type { AssistantMessage, - CallId, + ToolCallId, LlmCallConfig, LlmCallConfigAdapterDefaults, LlmFailure, @@ -280,7 +280,7 @@ export interface SessionEventMap { * JSON string exactly as the model produced it (unparsed). `callId` pairs the * call with its `tool/result`. */ - 'tool/call': { turn: number; step: number; callId: CallId; name: string; arguments: string } + 'tool/call': { turn: number; step: number; callId: ToolCallId; name: string; arguments: string } /** * A completed tool call's model-facing result, optional internal failure * identity, and optional tool-private `meta` presentation payload. `meta` is diff --git a/packages/core/session/tests/chunk-rows.spec.ts b/packages/core/session/tests/chunk-rows.spec.ts index 4fe14894e5..a8c39ce10e 100644 --- a/packages/core/session/tests/chunk-rows.spec.ts +++ b/packages/core/session/tests/chunk-rows.spec.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from 'vitest' import fc from 'fast-check' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import type { StreamChunk } from '@deepseek-ai/dsh-llm' import { decodeStorageRecord, packChunkRuns } from '@deepseek-ai/dsh-session' import type { ChunkRow, SessionEvent, StorageRecord } from '@deepseek-ai/dsh-session' @@ -43,7 +43,7 @@ describe('packChunkRuns', () => { it('packs reasoning and tool-call runs under their own tags', () => { const reasoning = deltaRun('reasoning-delta', 3) const toolCall = [4, 5, 6].map(seq => - chunkEvent(seq, 1000 + seq, { type: 'tool-call-delta', index: 1, id: CallId('c1'), name: 'write', argumentsDelta: `a${seq}` })) + chunkEvent(seq, 1000 + seq, { type: 'tool-call-delta', index: 1, id: ToolCallId('c1'), name: 'write', argumentsDelta: `a${seq}` })) const packed = packChunkRuns([...reasoning, ...toolCall]) expect(packed.map(r => (r as ChunkRow).type)).toStrictEqual(['reasoning-chunks', 'tool-call-chunks']) const row = packed[1] as ChunkRow & { type: 'tool-call-chunks' } @@ -53,7 +53,7 @@ describe('packChunkRuns', () => { it('packs a name-less tool-call run and round-trips field absence', () => { const events = [0, 1, 2].map(seq => - chunkEvent(seq, 1000, { type: 'tool-call-delta', index: 0, id: CallId('c1'), argumentsDelta: `a${seq}` })) + chunkEvent(seq, 1000, { type: 'tool-call-delta', index: 0, id: ToolCallId('c1'), argumentsDelta: `a${seq}` })) const packed = packChunkRuns(events) expect(packed).toHaveLength(1) expect(Object.hasOwn((packed[0] as ChunkRow).data, 'name')).toBe(false) @@ -91,7 +91,7 @@ describe('packChunkRuns', () => { it('breaks a tool-call run on call-id or name change', () => { const call = (seq: number, id: string, name?: string): SessionEvent => - chunkEvent(seq, 1000, { type: 'tool-call-delta', index: 0, id: CallId(id), ...name !== undefined ? { name } : {}, argumentsDelta: 'a' }) + chunkEvent(seq, 1000, { type: 'tool-call-delta', index: 0, id: ToolCallId(id), ...name !== undefined ? { name } : {}, argumentsDelta: 'a' }) const idSwitch = [call(0, 'c1', 'w'), call(1, 'c1', 'w'), call(2, 'c2', 'w')] expect(packChunkRuns(idSwitch)).toStrictEqual(idSwitch) const namePresence = [call(0, 'c1', 'w'), call(1, 'c1', 'w'), call(2, 'c1')] @@ -186,13 +186,13 @@ const deltaChunkArb: fc.Arbitrary = fc.oneof( fc.record({ type: fc.constant<'tool-call-delta'>('tool-call-delta'), index: fc.nat(2), - id: fc.constantFrom(CallId('c1'), CallId('c2')), + id: fc.constantFrom(ToolCallId('c1'), ToolCallId('c2')), argumentsDelta: fc.string(), }), fc.record({ type: fc.constant<'tool-call-delta'>('tool-call-delta'), index: fc.nat(2), - id: fc.constantFrom(CallId('c1'), CallId('c2')), + id: fc.constantFrom(ToolCallId('c1'), ToolCallId('c2')), name: fc.constantFrom('write', 'read'), argumentsDelta: fc.string(), }), diff --git a/packages/core/session/tests/fork.spec.ts b/packages/core/session/tests/fork.spec.ts index 1cd9cb7287..6a7770b4b6 100644 --- a/packages/core/session/tests/fork.spec.ts +++ b/packages/core/session/tests/fork.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { createUserMessage, CallId , createMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId , createMessage } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, SessionForkError, SessionId } from '@deepseek-ai/dsh-session' import type { SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session' @@ -268,7 +268,7 @@ describe('SessionStore.fork', () => { return lastSeq(session) }], ['tool/call', (session) => { - const callId = CallId('call-open') + const callId = ToolCallId('call-open') session.append('turn/start', { turn: 1 }) session.append('step/start', { turn: 1, step: 1 }) session.append('assistant/message', { diff --git a/packages/core/session/tests/invariant.spec.ts b/packages/core/session/tests/invariant.spec.ts index b98391cc3b..3aac95e2bc 100644 --- a/packages/core/session/tests/invariant.spec.ts +++ b/packages/core/session/tests/invariant.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { createScope, scopeTarget } from '@deepseek-ai/dsh-scope' -import { createUserMessage, CallId, createMessage, createToolResultMessage, freezeMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, createMessage, createToolResultMessage, freezeMessage } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId, TOOL_NOT_STARTED } from '@deepseek-ai/dsh-session' import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant' import InvariantRegistry, { InvariantError } from '@deepseek-ai/dsh-invariants' @@ -46,18 +46,18 @@ describe('session-log invariants', () => { step: 1, message: createMessage({ role: 'assistant', - content: [{ type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{}' }], + content: [{ type: 'tool-call', id: ToolCallId('c1'), name: 'echo', arguments: '{}' }], source: { kind: 'model', ...{ provider: 'mock', model: 'mock' }, }, }), }, { surfaceOp: 'append' }) - session.append('tool/call', { turn: 1, step: 1, callId: CallId('c1'), name: 'echo', arguments: '{}' }) + session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c1'), name: 'echo', arguments: '{}' }) session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('c1'), + callId: ToolCallId('c1'), content: [], isError: false, }), @@ -223,7 +223,7 @@ describe('session-log invariants', () => { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('ghost'), + callId: ToolCallId('ghost'), content: [], isError: false, }), @@ -238,7 +238,7 @@ describe('session-log invariants', () => { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('closed'), + callId: ToolCallId('closed'), content: [], isError: false, }), @@ -253,7 +253,7 @@ describe('session-log invariants', () => { session.append('tool/call', { turn: 1, step: 1, - callId: CallId('rewrite'), + callId: ToolCallId('rewrite'), name: 'echo', arguments: '{}', }) @@ -261,7 +261,7 @@ describe('session-log invariants', () => { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('rewrite'), + callId: ToolCallId('rewrite'), content: [{ type: 'text', text: 'original' }], isError: false, }), @@ -293,7 +293,7 @@ describe('session-log invariants', () => { session.append('tool/call', { turn: 1, step: 1, - callId: CallId('rewrite'), + callId: ToolCallId('rewrite'), name: 'echo', arguments: '{}', }) @@ -301,7 +301,7 @@ describe('session-log invariants', () => { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('rewrite'), + callId: ToolCallId('rewrite'), content: [{ type: 'text', text: 'original' }], isError: false, }), @@ -333,7 +333,7 @@ describe('session-log invariants', () => { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('crashed'), + callId: ToolCallId('crashed'), content: [], isError: true, }), @@ -347,7 +347,7 @@ describe('session-log invariants', () => { expect(() => { unresolved.append('turn/start', { turn: 1 }) unresolved.append('step/start', { turn: 1, step: 1 }) - unresolved.append('tool/call', { turn: 1, step: 1, callId: CallId('c1'), name: 'echo', arguments: '{}' }) + unresolved.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c1'), name: 'echo', arguments: '{}' }) unresolved.append('step/end', { turn: 1, step: 1 }) unresolved.append('turn/end', { turn: 1, reason: { kind: 'error', error: { message: 'boom', code: 'UNKNOWN' } } }) }).not.toThrow() @@ -358,14 +358,14 @@ describe('session-log invariants', () => { const session = ctx.sessions.create() session.append('turn/start', { turn: 1 }) session.append('step/start', { turn: 1, step: 1 }) - session.append('tool/call', { turn: 1, step: 1, callId: CallId('c1'), name: 'echo', arguments: '{}' }) + session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c1'), name: 'echo', arguments: '{}' }) session.append('step/end', { turn: 1, step: 1 }) session.append('step/start', { turn: 1, step: 2 }) expect(() => session.append('tool/result', { turn: 1, step: 2, message: createToolResultMessage({ - callId: CallId('c1'), + callId: ToolCallId('c1'), content: [], isError: false, }), diff --git a/packages/core/session/tests/properties.spec.ts b/packages/core/session/tests/properties.spec.ts index 32af9bbe48..e27c0c8d44 100644 --- a/packages/core/session/tests/properties.spec.ts +++ b/packages/core/session/tests/properties.spec.ts @@ -9,7 +9,7 @@ import { describe, expect, it } from 'vitest' import fc from 'fast-check' -import { createUserMessage, CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import type { SessionEventMap, SessionEventType, SurfaceIntent } from '@deepseek-ai/dsh-session' @@ -61,7 +61,7 @@ const messageEventArb: fc.Arbitrary = fc.oneof( .map((r): Appendable => ({ type: 'tool/result', data: { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId(r.id), + callId: ToolCallId(r.id), content: r.content, isError: r.isError, }), diff --git a/packages/core/session/tests/repair.spec.ts b/packages/core/session/tests/repair.spec.ts index 941819c0af..6d474956fa 100644 --- a/packages/core/session/tests/repair.spec.ts +++ b/packages/core/session/tests/repair.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' +import { ToolCallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' import { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '../src/index.ts' import type { SessionEvent, SurfaceEvent } from '../src/index.ts' @@ -57,7 +57,7 @@ describe('interruptedTurnClosers', () => { role: 'assistant', content: [ { type: 'text', text: 'calling a tool' }, - { type: 'tool-call', id: CallId('call-1'), name: 'bash', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('call-1'), name: 'bash', arguments: '{}' }, ], source: { kind: 'model', @@ -75,7 +75,7 @@ describe('interruptedTurnClosers', () => { turn: 2, step: 1, message: { - source: { callId: CallId('call-1') }, + source: { callId: ToolCallId('call-1') }, content: [{ isError: true }], }, error: { code: TOOL_NOT_STARTED }, @@ -94,7 +94,7 @@ describe('interruptedTurnClosers', () => { message: createMessage({ role: 'assistant', content: [ - { type: 'tool-call', id: CallId('call-1'), name: 'bash', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('call-1'), name: 'bash', arguments: '{}' }, ], source: { kind: 'model', @@ -105,7 +105,7 @@ describe('interruptedTurnClosers', () => { { type: 'tool/result', seq: 3, time: 3, data: { turn: 2, step: 1, message: createToolResultMessage({ - callId: CallId('call-1'), + callId: ToolCallId('call-1'), content: [{ type: 'text', text: 'ok' }], isError: false, }), @@ -125,7 +125,7 @@ describe('interruptedTurnClosers', () => { message: createMessage({ role: 'assistant', content: [ - { type: 'tool-call', id: CallId('call-1'), name: 'bash', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('call-1'), name: 'bash', arguments: '{}' }, ], source: { kind: 'model', @@ -152,7 +152,7 @@ describe('interruptedTurnClosers', () => { message: createMessage({ role: 'assistant', content: [ - { type: 'tool-call', id: CallId('old-call'), name: 'bash', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('old-call'), name: 'bash', arguments: '{}' }, ], source: { kind: 'model', @@ -163,7 +163,7 @@ describe('interruptedTurnClosers', () => { { type: 'tool/result', seq: 3, time: 3, data: { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('old-call'), + callId: ToolCallId('old-call'), content: [], isError: false, }), @@ -177,7 +177,7 @@ describe('interruptedTurnClosers', () => { message: createMessage({ role: 'assistant', content: [ - { type: 'tool-call', id: CallId('new-call'), name: 'bash', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('new-call'), name: 'bash', arguments: '{}' }, ], source: { kind: 'model', @@ -201,8 +201,8 @@ describe('interruptedTurnClosers', () => { message: createMessage({ role: 'assistant', content: [ - { type: 'tool-call', id: CallId('call-a'), name: 'bash', arguments: '{}' }, - { type: 'tool-call', id: CallId('call-b'), name: 'bash', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('call-a'), name: 'bash', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('call-b'), name: 'bash', arguments: '{}' }, ], source: { kind: 'model', @@ -214,7 +214,7 @@ describe('interruptedTurnClosers', () => { { type: 'tool/result', seq: 3, time: 3, data: { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('call-a'), + callId: ToolCallId('call-a'), content: [], isError: false, }), @@ -235,7 +235,7 @@ describe('interruptedTurnClosers', () => { message: createMessage({ role: 'assistant', content: [ - { type: 'tool-call', id: CallId('call-1'), name: 'bash', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('call-1'), name: 'bash', arguments: '{}' }, ], source: { kind: 'model', @@ -243,7 +243,7 @@ describe('interruptedTurnClosers', () => { }, }), } }, - { type: 'tool/call', seq: 3, time: 3, data: { turn: 1, step: 1, callId: CallId('call-1'), name: 'bash', arguments: '{}' } }, + { type: 'tool/call', seq: 3, time: 3, data: { turn: 1, step: 1, callId: ToolCallId('call-1'), name: 'bash', arguments: '{}' } }, ] const closers = interruptedTurnClosers(events) expect(closers.map(e => e.type)).toEqual(['tool/result', 'step/end', 'turn/end']) @@ -266,7 +266,7 @@ describe('interruptedTurnClosers', () => { const events: SessionEvent[] = [ userTurnStart(1, 0), { type: 'step/start', seq: 1, time: 1, data: { turn: 1, step: 1 } }, - { type: 'tool/call', seq: 2, time: 2, data: { turn: 1, step: 1, callId: CallId('orphan'), name: 'bash', arguments: '{}' } }, + { type: 'tool/call', seq: 2, time: 2, data: { turn: 1, step: 1, callId: ToolCallId('orphan'), name: 'bash', arguments: '{}' } }, ] const closers = interruptedTurnClosers(events) // No pending calls → no synthetic tool/result, just step/end + turn/end. diff --git a/packages/core/session/tests/session.spec.ts b/packages/core/session/tests/session.spec.ts index 47453ea6b5..dd2d095454 100644 --- a/packages/core/session/tests/session.spec.ts +++ b/packages/core/session/tests/session.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, expectTypeOf, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { createUserMessage, CallId, createMessage, createToolResultMessage, MessageId, ReasoningEffortId } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, createMessage, createToolResultMessage, MessageId, ReasoningEffortId } from '@deepseek-ai/dsh-llm' import SessionStore, { adoptSessionEvent, SESSION_FORMAT_VERSION, @@ -33,7 +33,7 @@ describe('Session', () => { role: 'assistant', content: [ { type: 'text', text: 'let me check' }, - { type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('c1'), name: 'echo', arguments: '{}' }, ], source: { kind: 'model', @@ -44,7 +44,7 @@ describe('Session', () => { session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('c1'), + callId: ToolCallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false, }), @@ -55,7 +55,7 @@ describe('Session', () => { expect(messages.map(m => m.role)).toEqual(['user', 'assistant', 'user']) // raw chunks must NOT appear in derived history expect(messages[1]!.content).toHaveLength(2) - expect(messages[2]!.content[0]).toMatchObject({ type: 'tool-result', toolCallId: CallId('c1') }) + expect(messages[2]!.content[0]).toMatchObject({ type: 'tool-result', toolCallId: ToolCallId('c1') }) }) it('accepts and round-trips a max-tokens turn/end reason', () => { @@ -442,7 +442,7 @@ describe('Session', () => { session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('c1'), + callId: ToolCallId('c1'), content: [{ type: 'text', text: 'tool out' }], isError: false, }), diff --git a/packages/core/session/tests/surface.spec.ts b/packages/core/session/tests/surface.spec.ts index 7c7ca160b6..4be69465c3 100644 --- a/packages/core/session/tests/surface.spec.ts +++ b/packages/core/session/tests/surface.spec.ts @@ -15,7 +15,7 @@ import { createToolResultMessage, createUserMessage, freezeMessage, - CallId, + ToolCallId, MessageId, } from '@deepseek-ai/dsh-llm' @@ -68,7 +68,7 @@ function toolResultEvent( turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId(callId), + callId: ToolCallId(callId), content: [{ type: 'text', text: `result ${seq}` }], isError: false, }), @@ -175,7 +175,7 @@ describe('foldSurface tool-result rewrites', () => { }) it.each([ - ['toolCallId', { toolCallId: CallId('changed') }], + ['toolCallId', { toolCallId: ToolCallId('changed') }], ['isError', { isError: true }], ] as const)('rejects a replacement that changes the result block %s', (_field, patch) => { const original = toolResultEvent(0, 'original') @@ -460,7 +460,7 @@ describe('SurfaceManager', () => { s.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('c1'), + callId: ToolCallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false, }), @@ -474,7 +474,7 @@ describe('SurfaceManager', () => { original.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('c1'), + callId: ToolCallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false, }), diff --git a/packages/core/tools/src/code-mode.ts b/packages/core/tools/src/code-mode.ts index ad0e59b77e..4b794bbd48 100644 --- a/packages/core/tools/src/code-mode.ts +++ b/packages/core/tools/src/code-mode.ts @@ -6,7 +6,7 @@ * @module @deepseek-ai/dsh-tools/src/code-mode */ -import { CallId, createUserMessage, HarnessError } from '@deepseek-ai/dsh-llm' +import { ToolCallId, createUserMessage, HarnessError } from '@deepseek-ai/dsh-llm' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import type { CodeBindingFunction, CodeRunResult, CodeRuntime } from '@deepseek-ai/dsh-code-runtime' import { snapshotJsonValue } from '@deepseek-ai/dsh-session' @@ -469,7 +469,7 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO } const normalized = jsonNormalizeArgs(rawArgs) const n = ++dispatches - const subCallId = CallId(`${String(exec.callId)}:code:${n}`) + const subCallId = ToolCallId(`${String(exec.callId)}:code:${n}`) const input = { callId: subCallId, rootCallId: exec.rootCallId, diff --git a/packages/core/tools/src/index.ts b/packages/core/tools/src/index.ts index 377015feb3..a660de7876 100644 --- a/packages/core/tools/src/index.ts +++ b/packages/core/tools/src/index.ts @@ -8,7 +8,7 @@ import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { AnonymousEntries, NamedEntries, ScopedLayers, scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope' import type { ScopeKey, ScopeLayer, Scoped } from '@deepseek-ai/dsh-scope' -import type { CallId, ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm' +import type { ToolCallId, ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm' import { assertNever, deepFreeze, HarnessError } from '@deepseek-ai/dsh-llm' import type { Agent } from '@deepseek-ai/dsh-agent' import { snapshotJsonValue } from '@deepseek-ai/dsh-session' @@ -312,12 +312,12 @@ export type ToolExecutionToken = symbol & { readonly [toolExecutionTokenBrand]: * callers do not choose that token. */ export interface ToolExecutionInput { - readonly callId: CallId + readonly callId: ToolCallId /** * Root model-requested call owning this execution tree. Callers omit it for * a root execution; nested dispatchers propagate the enclosing value. */ - readonly rootCallId?: CallId + readonly rootCallId?: ToolCallId readonly name: string /** Losslessly JSON-serializable parsed arguments (tools validate their own schema). */ readonly arguments: unknown @@ -360,7 +360,7 @@ export interface CodeDispatchLog { /** The calling agent (the scope routing key and the spill owner), when the outer call has one. */ readonly agent?: Agent /** Deterministic sub-call id (`:code:`). */ - readonly subCallId: CallId + readonly subCallId: ToolCallId /** The dispatched sub-tool name. */ readonly name: string /** Whether the sub-call settled as an error. */ @@ -378,7 +378,7 @@ export interface CodeDispatchLog { */ export interface ToolExecution extends ToolExecutionInput { /** Root model-requested call, resolved for every root and nested execution. */ - readonly rootCallId: CallId + readonly rootCallId: ToolCallId /** Registry-assigned identity shared with nested calls only as their opaque `parent` token. */ readonly token: ToolExecutionToken } diff --git a/packages/core/tools/src/types.ts b/packages/core/tools/src/types.ts index 4dd6f7a9a8..6de9c10714 100644 --- a/packages/core/tools/src/types.ts +++ b/packages/core/tools/src/types.ts @@ -4,14 +4,14 @@ * @module @deepseek-ai/dsh-tools/types */ -import type { CallId } from '@deepseek-ai/dsh-llm/brand' +import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand' import type { ContentBlock } from '@deepseek-ai/dsh-llm/types' /** Payload recorded when one nested Code Mode Tool dispatch starts. */ export interface CodeDispatchStartEventData { - rootCallId: CallId - parentCallId: CallId - subCallId: CallId + rootCallId: ToolCallId + parentCallId: ToolCallId + subCallId: ToolCallId name: string arguments: unknown } diff --git a/packages/core/tools/tests/code-mode.spec.ts b/packages/core/tools/tests/code-mode.spec.ts index 98d849f097..61c30f819b 100644 --- a/packages/core/tools/tests/code-mode.spec.ts +++ b/packages/core/tools/tests/code-mode.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' import { createScope } from '@deepseek-ai/dsh-scope' import type { Scope } from '@deepseek-ai/dsh-scope' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' @@ -107,7 +107,7 @@ async function runCode( ): Promise { return ctx.tools.execute({ signal: testToolSignal, - callId: CallId('call-1'), + callId: ToolCallId('call-1'), name: RUN_CODE_NAME, arguments: { code, description: extras.description ?? 'Run the test program' }, ...extras.agent ? { agent: extras.agent } : {}, @@ -1624,9 +1624,9 @@ describe('the run_code dispatch bridge', () => { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' }, }), { surfaceOp: 'append' }) session.append('tool/code-dispatch', { - rootCallId: CallId('p1'), - parentCallId: CallId('p1'), - subCallId: CallId('p1:code:1'), + rootCallId: ToolCallId('p1'), + parentCallId: ToolCallId('p1'), + subCallId: ToolCallId('p1:code:1'), name: 'echo', arguments: { value: 'x' }, isError: false, @@ -1666,7 +1666,7 @@ describe('the run_code dispatch bridge', () => { registerEcho(ctx, 'write') const result = await registry.execute({ signal: testToolSignal, - callId: CallId('call-1'), + callId: ToolCallId('call-1'), name: 'write', arguments: { text: 'hello' }, }) @@ -1688,7 +1688,7 @@ describe('the run_code dispatch bridge', () => { aborted.abort() const result = await registry.execute({ signal: aborted.signal, - callId: CallId('call-1'), + callId: ToolCallId('call-1'), name: 'write', arguments: { text: 'hello' }, }) @@ -1720,7 +1720,7 @@ describe('per-agent presentation', () => { // mode is its own rather than the deployment's. const denied = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('coded-direct'), + callId: ToolCallId('coded-direct'), name: 'echo', arguments: { value: 'coded' }, agent, @@ -1756,14 +1756,14 @@ describe('per-agent presentation', () => { // `dsh-agent-tool-presentation` produces. expect(ctx.tools.executionMode({ signal: testToolSignal, - callId: CallId('preset-coded-schedule'), + callId: ToolCallId('preset-coded-schedule'), name: 'echo', arguments: { value: 'joined' }, agent: joined.agent, })).toEqual({ kind: 'exclusive' }) const denied = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('preset-coded-direct'), + callId: ToolCallId('preset-coded-direct'), name: 'echo', arguments: { value: 'joined' }, agent: joined.agent, @@ -1776,7 +1776,7 @@ describe('per-agent presentation', () => { expect(native.tools.map(tool => tool.name)).toEqual(['echo']) const allowed = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('native-sibling-direct'), + callId: ToolCallId('native-sibling-direct'), name: 'echo', arguments: { value: 'loner' }, agent: loner.agent, diff --git a/packages/core/tools/tests/execution-mode.spec.ts b/packages/core/tools/tests/execution-mode.spec.ts index a8db75169c..333909c24a 100644 --- a/packages/core/tools/tests/execution-mode.spec.ts +++ b/packages/core/tools/tests/execution-mode.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, expectTypeOf, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { defineContentToolFixture, @@ -21,7 +21,7 @@ async function setup() { } function exec(name: string, args: unknown): ToolExecutionInput { - return { signal: testToolSignal, callId: CallId('c1'), name, arguments: args } + return { signal: testToolSignal, callId: ToolCallId('c1'), name, arguments: args } } describe('ToolRuntime.executionMode', () => { diff --git a/packages/core/tools/tests/execution-signal-types.spec.ts b/packages/core/tools/tests/execution-signal-types.spec.ts index 0ac4ac85c0..95bd182e58 100644 --- a/packages/core/tools/tests/execution-signal-types.spec.ts +++ b/packages/core/tools/tests/execution-signal-types.spec.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, it } from 'vitest' import type { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { defineTool } from '@deepseek-ai/dsh-tools' import type { ToolDispatchExecution, @@ -15,7 +15,7 @@ function inputAndExecutionContracts( run: ToolRunContext, ): void { // @ts-expect-error -- every typed invocation must supply a caller-owned signal. - const missingSignal: ToolExecutionInput = { callId: CallId('missing'), name: 'probe', arguments: {} } + const missingSignal: ToolExecutionInput = { callId: ToolCallId('missing'), name: 'probe', arguments: {} } void missingSignal // @ts-expect-error -- caller input is readonly after construction. diff --git a/packages/core/tools/tests/invariant.spec.ts b/packages/core/tools/tests/invariant.spec.ts index 3ca9191308..24c3c443f4 100644 --- a/packages/core/tools/tests/invariant.spec.ts +++ b/packages/core/tools/tests/invariant.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { scopeTarget } from '@deepseek-ai/dsh-scope' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session' import type { ToolExecution, ToolExecutionResult, ToolExecutionToken } from '@deepseek-ai/dsh-tools' import * as ToolsInvariant from '@deepseek-ai/dsh-tools/invariant' @@ -19,12 +19,12 @@ async function setup(): Promise { const execution = (overrides: Partial = {}): ToolExecution => ({ token: Symbol('tool') as ToolExecutionToken, - callId: CallId('call-1'), + callId: ToolCallId('call-1'), name: 'echo', arguments: Object.freeze({ text: 'hi' }), ...overrides, signal: overrides.signal ?? testToolSignal, - rootCallId: overrides.rootCallId ?? overrides.callId ?? CallId('call-1'), + rootCallId: overrides.rootCallId ?? overrides.callId ?? ToolCallId('call-1'), }) const outcome = (): ToolExecutionResult => Object.freeze({ @@ -55,7 +55,7 @@ describe('tool-pipeline invariants', () => { Object.freeze(dispatched) emitResult(ctx, dispatched, outcome()) - const denied = execution({ callId: CallId('call-2') }) + const denied = execution({ callId: ToolCallId('call-2') }) await stage(ctx, 'tools/pre-execute', denied) await ctx.waterfall(ctx as never, 'tools/post-execute', denied, outcome(), () => Promise.resolve({ kind: 'accept' as const })) Object.freeze(denied) @@ -69,7 +69,7 @@ describe('tool-pipeline invariants', () => { await stage(ctx, 'tools/pre-execute', exec) await expect(stage(ctx, 'tools/pre-execute', exec)).rejects.toThrow(/repeated/) - const noPre = execution({ callId: CallId('call-2') }) + const noPre = execution({ callId: ToolCallId('call-2') }) await expect(stage(ctx, 'tools/execute', noPre)).rejects.toThrow(/must follow tools\/pre-execute/) expect(() => ctx.waterfall( ctx as never, 'tools/post-execute', noPre, outcome(), @@ -93,9 +93,9 @@ describe('tool-pipeline invariants', () => { const ctx = await setup() const session = ctx.sessions.create() const data = { - rootCallId: CallId('parent'), - parentCallId: CallId('parent'), - subCallId: CallId('child'), + rootCallId: ToolCallId('parent'), + parentCallId: ToolCallId('parent'), + subCallId: ToolCallId('child'), name: 'echo', arguments: {}, } @@ -109,18 +109,18 @@ describe('tool-pipeline invariants', () => { const ctx = await setup() const session = ctx.sessions.create() expect(() => session.append('tool/code-dispatch-start', { - rootCallId: CallId('rejected-root'), - parentCallId: CallId('rejected-root'), - subCallId: CallId('reused-child'), + rootCallId: ToolCallId('rejected-root'), + parentCallId: ToolCallId('rejected-root'), + subCallId: ToolCallId('reused-child'), name: 'echo', arguments: {}, })).toThrow(/outside any open turn/) session.append('turn/start', { turn: 1 }) expect(() => session.append('tool/code-dispatch-start', { - rootCallId: CallId('accepted-root'), - parentCallId: CallId('accepted-root'), - subCallId: CallId('reused-child'), + rootCallId: ToolCallId('accepted-root'), + parentCallId: ToolCallId('accepted-root'), + subCallId: ToolCallId('reused-child'), name: 'echo', arguments: {}, })).not.toThrow() @@ -131,24 +131,24 @@ describe('tool-pipeline invariants', () => { const session = ctx.sessions.create() session.append('turn/start', { turn: 1 }) session.append('tool/code-dispatch-start', { - rootCallId: CallId('root'), - parentCallId: CallId('root'), - subCallId: CallId('child'), + rootCallId: ToolCallId('root'), + parentCallId: ToolCallId('root'), + subCallId: ToolCallId('child'), name: 'run_code', arguments: {}, }) session.append('tool/code-dispatch-start', { - rootCallId: CallId('root'), - parentCallId: CallId('child'), - subCallId: CallId('grandchild'), + rootCallId: ToolCallId('root'), + parentCallId: ToolCallId('child'), + subCallId: ToolCallId('grandchild'), name: 'echo', arguments: {}, }) expect(() => session.append('tool/code-dispatch-start', { - rootCallId: CallId('another-root'), - parentCallId: CallId('child'), - subCallId: CallId('invalid-grandchild'), + rootCallId: ToolCallId('another-root'), + parentCallId: ToolCallId('child'), + subCallId: ToolCallId('invalid-grandchild'), name: 'echo', arguments: {}, })).toThrow(/parentCallId child does not belong to rootCallId another-root/) @@ -161,24 +161,24 @@ describe('tool-pipeline invariants', () => { const session = ctx.sessions.create() session.append('turn/start', { turn: 1 }) expect(() => session.append('tool/code-dispatch-start', { - rootCallId: CallId(''), - parentCallId: CallId('root'), - subCallId: CallId('child'), + rootCallId: ToolCallId(''), + parentCallId: ToolCallId('root'), + subCallId: ToolCallId('child'), name: 'echo', arguments: {}, })).toThrow(/must carry non-empty rootCallId/) session.append('tool/code-dispatch-start', { - rootCallId: CallId('root'), - parentCallId: CallId('root'), - subCallId: CallId('child'), + rootCallId: ToolCallId('root'), + parentCallId: ToolCallId('root'), + subCallId: ToolCallId('child'), name: 'echo', arguments: {}, }) expect(() => session.append('tool/code-dispatch-start', { - rootCallId: CallId('other-root'), - parentCallId: CallId('other-root'), - subCallId: CallId('child'), + rootCallId: ToolCallId('other-root'), + parentCallId: ToolCallId('other-root'), + subCallId: ToolCallId('child'), name: 'echo', arguments: {}, })).toThrow(/changed rootCallId for subCallId child/) @@ -194,9 +194,9 @@ describe('tool-pipeline invariants', () => { seq: 1, time: 1, data: { - rootCallId: CallId('root'), - parentCallId: CallId('root'), - subCallId: CallId('child'), + rootCallId: ToolCallId('root'), + parentCallId: ToolCallId('root'), + subCallId: ToolCallId('child'), name: 'echo', arguments: {}, }, @@ -210,9 +210,9 @@ describe('tool-pipeline invariants', () => { const session = ctx.sessions.create() session.append('turn/start', { turn: 1 }) session.append('tool/code-dispatch', { - rootCallId: CallId('parent'), - parentCallId: CallId('parent'), - subCallId: CallId('child'), + rootCallId: ToolCallId('parent'), + parentCallId: ToolCallId('parent'), + subCallId: ToolCallId('child'), name: 'echo', arguments: {}, isError: false, @@ -227,9 +227,9 @@ describe('tool-pipeline invariants', () => { const ctx = new Context() await ctx.plugin(SessionStore) ctx.sessions.create().append('tool/code-dispatch-start', { - rootCallId: CallId('parent'), - parentCallId: CallId('parent'), - subCallId: CallId('child'), + rootCallId: ToolCallId('parent'), + parentCallId: ToolCallId('parent'), + subCallId: ToolCallId('child'), name: 'echo', arguments: {}, }) diff --git a/packages/core/tools/tests/scoped.spec.ts b/packages/core/tools/tests/scoped.spec.ts index 353b408fa8..7eac6489f7 100644 --- a/packages/core/tools/tests/scoped.spec.ts +++ b/packages/core/tools/tests/scoped.spec.ts @@ -8,7 +8,7 @@ import ToolRuntime from '@deepseek-ai/dsh-tools' import type { PreToolDecision, ToolDefinition, ToolExecution, ToolExecutionInput, ToolExecutionToken } from '@deepseek-ai/dsh-tools' import type { Agent } from '@deepseek-ai/dsh-agent' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import type { SessionId } from '@deepseek-ai/dsh-session' const testToolSignal = new AbortController().signal @@ -49,7 +49,7 @@ function tool(name: string, reply = `ran:${name}`): ToolDefinition { async function run(ctx: Context, name: string, agent?: Agent): Promise { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('c1'), + callId: ToolCallId('c1'), name, arguments: {}, ...agent ? { agent } : {}, @@ -418,7 +418,7 @@ describe('scoped execution dispatch', () => { const callerArguments = { source: true } const safeResult = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('safe-call'), + callId: ToolCallId('safe-call'), name: 'safe', arguments: callerArguments, agent: key, @@ -461,7 +461,7 @@ describe('scoped execution dispatch', () => { if (exec.name === 'parent') parent = exec.token return next() }) - await ctx.tools.execute({ signal: testToolSignal, callId: CallId('parent'), name: 'parent', arguments: {} }) + await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('parent'), name: 'parent', arguments: {} }) stopCapture() policyCalls = 0 const signal = new AbortController().signal @@ -477,7 +477,7 @@ describe('scoped execution dispatch', () => { const callerArguments = { invalid: () => undefined } const scopedResult = await ctx.tools.execute({ - callId: CallId('non-cloneable'), + callId: ToolCallId('non-cloneable'), name: 't', arguments: callerArguments, agent: key, @@ -486,7 +486,7 @@ describe('scoped execution dispatch', () => { }) const subjectlessResult = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('non-cloneable-subjectless'), + callId: ToolCallId('non-cloneable-subjectless'), name: 't', arguments: { invalid: () => undefined }, }) @@ -525,7 +525,7 @@ describe('scoped execution dispatch', () => { const forged = { fake: true } as unknown as ToolExecutionToken let parentReads = 0 const input = { - callId: CallId('stateful-parent'), + callId: ToolCallId('stateful-parent'), name: 't', arguments: {}, signal: testToolSignal, @@ -553,14 +553,14 @@ describe('scoped execution dispatch', () => { if (exec.name === 'parent') parent = exec.token return next() }) - await ctx.tools.execute({ signal: testToolSignal, callId: CallId('parent'), name: 'parent', arguments: {} }) + await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('parent'), name: 'parent', arguments: {} }) stopCapture() const acceptedSignal = new AbortController().signal const driftSignal = new AbortController().signal const forged = { fake: true } as unknown as ToolExecutionToken const reads = { callId: 0, name: 0, arguments: 0, agent: 0, parent: 0, signal: 0 } const input = { - get callId() { reads.callId += 1; return CallId('unstable-error') }, + get callId() { reads.callId += 1; return ToolCallId('unstable-error') }, get name() { reads.name += 1; return 't' }, get arguments(): unknown { reads.arguments += 1; return { invalid: () => undefined } }, get agent() { reads.agent += 1; return reads.agent === 1 ? key : driftAgent }, @@ -578,7 +578,7 @@ describe('scoped execution dispatch', () => { expect(reads).toEqual({ callId: 1, name: 1, arguments: 1, agent: 1, parent: 1, signal: 1 }) expect(scopedObserved).toBe(1) expect(observed).toMatchObject({ - callId: CallId('unstable-error'), + callId: ToolCallId('unstable-error'), name: 't', agent: key, parent, @@ -598,7 +598,7 @@ describe('scoped execution dispatch', () => { expect(result.isError).toBe(true) }) const input = { - callId: CallId('throwing-arguments'), + callId: ToolCallId('throwing-arguments'), name: 't', signal: testToolSignal, get arguments(): unknown { @@ -642,7 +642,7 @@ describe('scoped execution dispatch', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('bad-arguments'), name: 't', arguments: argumentsValue, + callId: ToolCallId('bad-arguments'), name: 't', arguments: argumentsValue, }) expect(result.isError).toBe(true) @@ -663,7 +663,7 @@ describe('scoped execution dispatch', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('unstable-arguments'), name: 't', arguments: argumentsValue, + callId: ToolCallId('unstable-arguments'), name: 't', arguments: argumentsValue, }) expect(reads).toBe(1) @@ -705,7 +705,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({ signal: testToolSignal, callId: CallId('final'), name: 't', arguments: {}, agent: key }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('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]) diff --git a/packages/core/tools/tests/tools.spec.ts b/packages/core/tools/tests/tools.spec.ts index ec770592a6..15f7203cfa 100644 --- a/packages/core/tools/tests/tools.spec.ts +++ b/packages/core/tools/tests/tools.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, expectTypeOf, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { createUserMessage, CallId, HarnessError, type ContentBlock } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, HarnessError, type ContentBlock } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import type { Agent } from '@deepseek-ai/dsh-agent' import ApprovalService, { type ApprovalOutcome, type ApprovalRequest } from '@deepseek-ai/dsh-user-approval' @@ -88,7 +88,7 @@ describe('ToolRuntime', () => { ctx.tools.register(echoTool) let observed: ToolExecutionResult | undefined ctx.on('tools/result', (_exec, result) => { observed = result }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result).toEqual({ content: [{ type: 'text', text: 'hi' }], isError: false, value: 'hi' }) expect(observed).toEqual(result) }) @@ -106,7 +106,7 @@ describe('ToolRuntime', () => { return 'ok' }, }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'meta-tool', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'meta-tool', arguments: {} }) expect(result).toEqual({ content: [{ type: 'text', text: 'ok' }], isError: false, @@ -124,7 +124,7 @@ describe('ToolRuntime', () => { return 'ok' }, }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'no-meta-tool', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'no-meta-tool', arguments: {} }) expect(result).toEqual({ content: [{ type: 'text', text: 'ok' }], isError: false, value: 'ok' }) expect('meta' in result).toBe(false) }) @@ -147,7 +147,7 @@ describe('ToolRuntime', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('bad-meta'), name: 'bad-meta', arguments: {}, + callId: ToolCallId('bad-meta'), name: 'bad-meta', arguments: {}, }) expect(result.isError).toBe(true) expect(result.content[0]?.type === 'text' && result.content[0].text).toContain('Error:') @@ -185,7 +185,7 @@ describe('ToolRuntime', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('throwing-meta'), name: 'throwing-meta', arguments: {}, + callId: ToolCallId('throwing-meta'), name: 'throwing-meta', arguments: {}, }) expect(result.isError).toBe(true) @@ -210,7 +210,7 @@ describe('ToolRuntime', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('throwing-finalizer'), name: 'throwing-finalizer', arguments: {}, + callId: ToolCallId('throwing-finalizer'), name: 'throwing-finalizer', arguments: {}, }) expect(result).toEqual({ @@ -251,8 +251,8 @@ describe('ToolRuntime', () => { execute: async () => 42 as unknown as string, })) - const lossy = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('lossy'), name: 'lossy-output', arguments: {} }) - const mismatch = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('mismatch'), name: 'wrong-output', arguments: {} }) + const lossy = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('lossy'), name: 'lossy-output', arguments: {} }) + const mismatch = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('mismatch'), name: 'wrong-output', arguments: {} }) expect(lossy.error).toMatchObject({ info: { name: 'ToolOutputError', code: 'INVALID_TOOL_OUTPUT' } }) expect(lossy.content[0]?.type === 'text' ? lossy.content[0].text : '').toContain('not lossless JSON') expect(mismatch.error).toMatchObject({ info: { name: 'ToolOutputError', code: 'INVALID_TOOL_OUTPUT' } }) @@ -275,7 +275,7 @@ describe('ToolRuntime', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('hostile-body'), name: 'hostile-body', arguments: {}, + callId: ToolCallId('hostile-body'), name: 'hostile-body', arguments: {}, }) expect(result.error?.message).toContain('value snapshot failed: body snapshot getter exploded') expect(result.error?.info).toEqual({ name: 'ToolOutputError', code: 'INVALID_TOOL_OUTPUT' }) @@ -301,7 +301,7 @@ describe('ToolRuntime', () => { execute: async () => 'ok', })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId(projector), name: `throwing-${projector}`, arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId(projector), name: `throwing-${projector}`, arguments: {} }) expect(result.isError).toBe(true) expect(result.error?.message) .toContain(projector === 'render' ? 'renderer exploded' : 'metadata exploded') @@ -333,7 +333,7 @@ describe('ToolRuntime', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId(`hostile-${projector}`), name: `hostile-${projector}`, arguments: {}, + callId: ToolCallId(`hostile-${projector}`), name: `hostile-${projector}`, arguments: {}, }) expect(result.error?.message).toContain('snapshot getter exploded') expect(result.error?.info).toEqual({ name: 'ToolOutputError', code: 'INVALID_TOOL_OUTPUT' }) @@ -370,9 +370,9 @@ describe('ToolRuntime', () => { } }) - const content = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('content'), name: 'projected', arguments: {} }) + const content = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('content'), name: 'projected', arguments: {} }) replacement = 'value' - const value = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('value'), name: 'projected', arguments: {} }) + const value = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('value'), name: 'projected', arguments: {} }) expect(content).toEqual({ isError: false, @@ -402,7 +402,7 @@ describe('ToolRuntime', () => { value: 'replacement', content: [{ type: 'text', text: 'also replacement' }], } as unknown as PostToolDecision)) - const bothResult = await both.tools.execute({ signal: testToolSignal, callId: CallId('both'), name: 'echo', arguments: {} }) + const bothResult = await both.tools.execute({ signal: testToolSignal, callId: ToolCallId('both'), name: 'echo', arguments: {} }) expect(bothResult).toMatchObject({ isError: true, error: { message: 'tools/post-execute accept decision cannot replace both value and content' }, @@ -411,7 +411,7 @@ describe('ToolRuntime', () => { const invalid = await setup() invalid.tools.register(echoTool) invalid.on('tools/post-execute', async () => ({ kind: 'accept', value: 1 })) - const invalidResult = await invalid.tools.execute({ signal: testToolSignal, callId: CallId('invalid'), name: 'echo', arguments: {} }) + const invalidResult = await invalid.tools.execute({ signal: testToolSignal, callId: ToolCallId('invalid'), name: 'echo', arguments: {} }) expect(invalidResult.error).toMatchObject({ info: { code: 'INVALID_TOOL_OUTPUT' } }) expect('value' in invalidResult).toBe(false) }) @@ -424,7 +424,7 @@ describe('ToolRuntime', () => { feedback: [{ type: 'text', text: 'blocked by policy' }], })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('block'), name: 'echo', arguments: { text: 'secret' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('block'), name: 'echo', arguments: { text: 'secret' } }) expect(result).toEqual({ isError: true, error: { message: 'blocked by policy' }, @@ -438,7 +438,7 @@ describe('ToolRuntime', () => { ctx.tools.register(echoTool) ctx.on('tools/post-execute', async () => ({ kind: 'accept', value: 'replacement' })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('replace-value'), name: 'echo', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('replace-value'), name: 'echo', arguments: {} }) expect(result).toEqual({ isError: false, value: 'replacement', @@ -454,7 +454,7 @@ describe('ToolRuntime', () => { ctx.tools.register(echoTool) ctx.on('tools/post-execute', async () => ({ kind: 'block', feedback: [...feedback] })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('block-message'), name: 'echo', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('block-message'), name: 'echo', arguments: {} }) expect(result.error?.message).toBe(message) }) @@ -466,7 +466,7 @@ describe('ToolRuntime', () => { feedback: [{ type: 'text', text: 'blocked', invalid: () => undefined } as never], })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('invalid-block'), name: 'echo', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('invalid-block'), name: 'echo', arguments: {} }) expect(result).toMatchObject({ isError: true, error: { message: 'tool result must be losslessly JSON-serializable' }, @@ -484,7 +484,7 @@ describe('ToolRuntime', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('failed-replace'), name: 'throw-before-replace', arguments: {}, + callId: ToolCallId('failed-replace'), name: 'throw-before-replace', arguments: {}, }) expect(result.error?.message).toBe('tools/post-execute cannot replace the value of a failed result') }) @@ -497,7 +497,7 @@ describe('ToolRuntime', () => { return { kind: 'accept', value: 'replacement' } }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('post-disposed'), name: 'echo', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('post-disposed'), name: 'echo', arguments: {} }) expect(result.error).toEqual({ message: 'unknown tool "echo"', info: { name: 'ToolNotFoundError', code: 'UNKNOWN_TOOL' }, @@ -517,7 +517,7 @@ describe('ToolRuntime', () => { })], })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('wrapper-failure'), name: 'echo', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('wrapper-failure'), name: 'echo', arguments: {} }) expect(result).toEqual({ isError: true, error: { message: 'wrapped failure' }, @@ -540,7 +540,7 @@ describe('ToolRuntime', () => { return { isError: false, value: 'replacement', content: [] } }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('wrapper-disposed'), name: 'echo', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('wrapper-disposed'), name: 'echo', arguments: {} }) expect(result.error).toEqual({ message: 'unknown tool "echo"', info: { name: 'ToolNotFoundError', code: 'UNKNOWN_TOOL' }, @@ -554,10 +554,10 @@ describe('ToolRuntime', () => { name: 'meta-suppression', output: { ...echoTool.output, presentationMeta: () => ({ card: true }) }, }) - const direct = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('direct'), name: 'meta-suppression', arguments: {} }) + const direct = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('direct'), name: 'meta-suppression', arguments: {} }) const nested = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('nested'), + callId: ToolCallId('nested'), name: 'meta-suppression', arguments: {}, parent: Symbol('outer') as ToolExecutionToken, @@ -588,7 +588,7 @@ describe('ToolRuntime', () => { async execute(_args, exec) { call += 1 const nested = await ctx.tools.execute({ - signal: exec.signal, callId: CallId(`nested-${call}`), name: 'terminal-nested', arguments: {}, parent: exec.token, + signal: exec.signal, callId: ToolCallId(`nested-${call}`), name: 'terminal-nested', arguments: {}, parent: exec.token, }) if (nested.concludesTurn) exec.concludeTurn() return nested.isError ? 'nested failed, composite recovered' : 'nested succeeded' @@ -602,7 +602,7 @@ describe('ToolRuntime', () => { return { kind: 'block', feedback: [{ type: 'text', text: 'nested success rejected' }] } }) const recovered = await ctx.tools.execute({ - signal: testToolSignal, callId: CallId('composite-vetoed'), name: 'composite', arguments: {}, + signal: testToolSignal, callId: ToolCallId('composite-vetoed'), name: 'composite', arguments: {}, }) expect(recovered.isError).toBe(false) expect(recovered.concludesTurn).toBeUndefined() @@ -611,7 +611,7 @@ describe('ToolRuntime', () => { // The same nested call succeeding carries the marker; the composite // forwards it onto its own successful result. const concluded = await ctx.tools.execute({ - signal: testToolSignal, callId: CallId('composite-ok'), name: 'composite', arguments: {}, + signal: testToolSignal, callId: ToolCallId('composite-ok'), name: 'composite', arguments: {}, }) expect(concluded.isError).toBe(false) expect(concluded.concludesTurn).toBe(true) @@ -627,7 +627,7 @@ describe('ToolRuntime', () => { }, }) - const unknown = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'nope', arguments: {} }) + const unknown = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'nope', arguments: {} }) expect(unknown.isError).toBe(true) expect(unknown.content[0]).toMatchObject({ text: 'Error: unknown tool "nope"' }) // An unknown tool is a routable failure class, same as a tool-thrown one. @@ -636,7 +636,7 @@ describe('ToolRuntime', () => { info: { name: 'ToolNotFoundError', code: 'UNKNOWN_TOOL' }, }) - const thrown = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c2'), name: 'boom', arguments: {} }) + const thrown = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c2'), name: 'boom', arguments: {} }) expect(thrown.isError).toBe(true) expect(thrown.content[0]).toMatchObject({ text: 'Error: exploded' }) }) @@ -657,7 +657,7 @@ describe('ToolRuntime', () => { await expect(ctx.tools.execute({ signal: testToolSignal, - callId: CallId('hostile'), name: 'hostile-throw', arguments: {}, + callId: ToolCallId('hostile'), name: 'hostile-throw', arguments: {}, })).resolves.toMatchObject({ isError: true, content: [{ type: 'text', text: 'Error: ' }], @@ -688,7 +688,7 @@ describe('ToolRuntime', () => { return next() }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'Error: denied by policy' }) expect(postSawFrozen).toBe(true) @@ -701,7 +701,7 @@ describe('ToolRuntime', () => { ctx.on('tools/pre-execute', async (_exec, _next): Promise => ({ kind: 'ask', reason: 'needs approval' })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'Error: needs approval' }) }) @@ -712,7 +712,7 @@ describe('ToolRuntime', () => { ctx.on('tools/pre-execute', async (_exec, _next): Promise => ({ kind: 'ask' })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'Error: tool "echo" requires approval (not yet supported)' }) }) @@ -749,7 +749,7 @@ describe('ToolRuntime', () => { ({ kind: 'ask', reason: 'hook wants a human' })) const result = await ctx.tools.execute({ - callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' }, agent, signal: controller.signal, + callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' }, agent, signal: controller.signal, }) expect(result).toMatchObject({ isError: false, content: [{ type: 'text', text: 'hi' }] }) @@ -763,7 +763,7 @@ describe('ToolRuntime', () => { ctx.on('approval/request', () => Promise.resolve('rejected')) ctx.on('tools/pre-execute', async (_exec, _next): Promise => ({ kind: 'ask' })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: {}, agent: fakeAgent() }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: {}, agent: fakeAgent() }) expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'Error: the user rejected tool "echo"' }) }) @@ -773,7 +773,7 @@ describe('ToolRuntime', () => { ctx.on('approval/request', () => Promise.resolve('cancelled')) ctx.on('tools/pre-execute', async (_exec, _next): Promise => ({ kind: 'ask' })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: {}, agent: fakeAgent() }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: {}, agent: fakeAgent() }) expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'Error: approval for tool "echo" was cancelled' }) }) @@ -795,7 +795,7 @@ describe('ToolRuntime', () => { ctx.on('tools/pre-execute', async (_exec, _next): Promise => ({ kind: 'ask' })) const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('approval-cancelled'), + callId: ToolCallId('approval-cancelled'), name: 'approval-probe', arguments: {}, agent: fakeAgent(), @@ -817,7 +817,7 @@ describe('ToolRuntime', () => { const ctx = await approvalSetup() ctx.on('tools/pre-execute', async (_exec, _next): Promise => ({ kind: 'ask' })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: {}, agent: fakeAgent() }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: {}, agent: fakeAgent() }) expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'Error: tool "echo" requires approval, but no approval channel is available' }) }) @@ -831,7 +831,7 @@ describe('ToolRuntime', () => { }) ctx.on('tools/pre-execute', async (_exec, _next): Promise => ({ kind: 'ask' })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: {} }) expect(asked).toBe(false) expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'Error: tool "echo" requires approval, but the call has no agent to route it through' }) @@ -846,7 +846,7 @@ describe('ToolRuntime', () => { ctx.provide('approval', { request: () => Promise.resolve('yolo') } as unknown as ApprovalService) ctx.on('tools/pre-execute', async (_exec, _next): Promise => ({ kind: 'ask' })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: {}, agent: fakeAgent() }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: {}, agent: fakeAgent() }) expect(result.isError).toBe(true) const text = result.content[0]?.type === 'text' ? result.content[0].text : '' expect(text).toContain('unreachable') @@ -860,7 +860,7 @@ describe('ToolRuntime', () => { ctx.on('tools/post-execute', async (_exec, _result, _next): Promise => ({ kind: 'accept', content: [{ type: 'text', text: 'rewritten' }] })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result.isError).toBe(false) expect(result.content[0]).toMatchObject({ text: 'rewritten' }) }) @@ -872,7 +872,7 @@ describe('ToolRuntime', () => { ctx.on('tools/post-execute', async (_exec, _result, _next): Promise => ({ kind: 'block', feedback: [{ type: 'text', text: 'output rejected: try again' }] })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'output rejected: try again' }) }) @@ -895,7 +895,7 @@ describe('ToolRuntime', () => { throw new HarnessError('policy failed', 'POLICY_FAILED') }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('bounded'), name: 'bounded', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('bounded'), name: 'bounded', arguments: {} }) expect(result).toEqual({ content: [{ type: 'text', text: 'bounded failure' }], @@ -919,7 +919,7 @@ describe('ToolRuntime', () => { }, }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('identity-finalizer'), name: 'identity-finalizer', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('identity-finalizer'), name: 'identity-finalizer', arguments: {} }) expect(result.isError).toBe(false) expect(result.content).toEqual([{ type: 'text', text: '' }]) @@ -939,7 +939,7 @@ describe('ToolRuntime', () => { })], })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'rejected' }) expect(result.additionalContexts).toMatchObject([{ content: [{ text: 'why it was rejected' }], source: { kind: 'plugin', plugin: 'test' } }]) @@ -954,7 +954,7 @@ describe('ToolRuntime', () => { content: [{ type: 'text', text: 'fyi' }], source: { kind: 'plugin', plugin: 'test' }, })] })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result.additionalContexts).toMatchObject([{ content: [{ text: 'fyi' }], source: { kind: 'plugin', plugin: 'test' } }]) }) @@ -999,7 +999,7 @@ describe('ToolRuntime', () => { } }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('composite'), name: 'composite', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('composite'), name: 'composite', arguments: {} }) expect(result.additionalContexts?.map(context => context.source)).toEqual([ { kind: 'plugin', plugin: 'nested-1' }, @@ -1023,7 +1023,7 @@ describe('ToolRuntime', () => { }, })) - const failed = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('failed'), name: 'failing-composite', arguments: {} }) + const failed = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('failed'), name: 'failing-composite', arguments: {} }) expect(failed.isError).toBe(true) expect(failed.additionalContexts?.map(context => context.source)).toEqual([{ kind: 'plugin', plugin: 'nested' }]) @@ -1034,7 +1034,7 @@ describe('ToolRuntime', () => { content: [{ type: 'text', text: 'block-only' }], source: { kind: 'plugin', plugin: 'blocker' }, })], })) - const blocked = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('blocked'), name: 'failing-composite', arguments: {} }) + const blocked = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('blocked'), name: 'failing-composite', arguments: {} }) expect(blocked.isError).toBe(true) expect(blocked.additionalContexts?.map(context => context.source)).toEqual([{ kind: 'plugin', plugin: 'blocker' }]) }) @@ -1057,7 +1057,7 @@ describe('ToolRuntime', () => { return decision }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'x' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'x' } }) expect(result.isError).toBe(false) // pre runs fully (gate) before dispatch, then post runs over the result. expect(order).toEqual(['pre:before', 'pre:after', 'post:before', 'post:after']) @@ -1085,7 +1085,7 @@ describe('ToolRuntime', () => { }) ctx.on('tools/post-execute', async (_exec, _result, next) => { order.push('post'); return next() }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'traced', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'traced', arguments: { text: 'hi' } }) expect(result).toEqual({ content: [{ type: 'text', text: 'hi' }], isError: false, value: [{ type: 'text', text: 'hi' }] }) // The around-dispatch extension point wraps dispatch; pre gates before it, post runs over its result. expect(order).toEqual(['pre', 'execute:before', 'dispatch', 'execute:after', 'post']) @@ -1109,7 +1109,7 @@ describe('ToolRuntime', () => { const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('cancelled-in-pre'), name: 'must-not-run', arguments: {}, signal: controller.signal, + callId: ToolCallId('cancelled-in-pre'), name: 'must-not-run', arguments: {}, signal: controller.signal, }) await entered.promise controller.abort('cancelled in policy') @@ -1140,7 +1140,7 @@ describe('ToolRuntime', () => { }) const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('denied-after-cancel'), name: 'denied-after-cancel', arguments: {}, signal: controller.signal, + callId: ToolCallId('denied-after-cancel'), name: 'denied-after-cancel', arguments: {}, signal: controller.signal, }) await entered.promise @@ -1173,7 +1173,7 @@ describe('ToolRuntime', () => { const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('cancelled-pre-error'), name: 'must-not-run', arguments: {}, signal: controller.signal, + callId: ToolCallId('cancelled-pre-error'), name: 'must-not-run', arguments: {}, signal: controller.signal, }) await entered.promise controller.abort('cancelled in policy') @@ -1212,7 +1212,7 @@ describe('ToolRuntime', () => { const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('cancelled-in-around'), name: 'must-not-run', arguments: {}, signal: controller.signal, + callId: ToolCallId('cancelled-in-around'), name: 'must-not-run', arguments: {}, signal: controller.signal, }) await entered.promise controller.abort('cancelled in wrapper') @@ -1246,7 +1246,7 @@ describe('ToolRuntime', () => { const controller = new AbortController() const result = await ctx.tools.execute({ - callId: CallId('cancelled-wrapper'), name: 'must-not-run', arguments: {}, signal: controller.signal, + callId: ToolCallId('cancelled-wrapper'), name: 'must-not-run', arguments: {}, signal: controller.signal, }) expect(result.error).toEqual({ @@ -1281,7 +1281,7 @@ describe('ToolRuntime', () => { }) const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('cancelled-short-circuit'), + callId: ToolCallId('cancelled-short-circuit'), name: 'short-circuited', arguments: {}, signal: controller.signal, @@ -1323,7 +1323,7 @@ describe('ToolRuntime', () => { }) const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('cancelled-after-body'), name: 'completed-before-wrapper', arguments: {}, signal: controller.signal, + callId: ToolCallId('cancelled-after-body'), name: 'completed-before-wrapper', arguments: {}, signal: controller.signal, }) await entered.promise controller.abort('cancelled while wrapper settled') @@ -1366,7 +1366,7 @@ describe('ToolRuntime', () => { }) const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('cancelled-in-post'), name: 'completed-before-post', arguments: {}, signal: controller.signal, + callId: ToolCallId('cancelled-in-post'), name: 'completed-before-post', arguments: {}, signal: controller.signal, }) await entered.promise controller.abort('cancelled while post policy waits') @@ -1400,7 +1400,7 @@ describe('ToolRuntime', () => { }) const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('wrapper-failure'), name: 'wrapper-failure', arguments: {}, signal: controller.signal, + callId: ToolCallId('wrapper-failure'), name: 'wrapper-failure', arguments: {}, signal: controller.signal, }) await entered.promise @@ -1432,7 +1432,7 @@ describe('ToolRuntime', () => { }) const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('tool-failure'), name: 'tool-failure', arguments: {}, signal: controller.signal, + callId: ToolCallId('tool-failure'), name: 'tool-failure', arguments: {}, signal: controller.signal, }) await entered.promise @@ -1457,7 +1457,7 @@ describe('ToolRuntime', () => { }) const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('post-failure'), name: 'echo', arguments: {}, signal: controller.signal, + callId: ToolCallId('post-failure'), name: 'echo', arguments: {}, signal: controller.signal, }) await entered.promise @@ -1500,7 +1500,7 @@ describe('ToolRuntime', () => { const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('cancelled-body'), name: 'cooperative', arguments: {}, signal: controller.signal, + callId: ToolCallId('cancelled-body'), name: 'cooperative', arguments: {}, signal: controller.signal, }) await entered.promise expect(bodySignal).not.toBe(controller.signal) @@ -1534,7 +1534,7 @@ describe('ToolRuntime', () => { const controller = new AbortController() await ctx.tools.execute({ - callId: CallId('restored-signal'), name: 'echo', arguments: {}, signal: controller.signal, + callId: ToolCallId('restored-signal'), name: 'echo', arguments: {}, signal: controller.signal, }) expect(postSignal).toBe(controller.signal) @@ -1558,7 +1558,7 @@ describe('ToolRuntime', () => { }) const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('drain-body'), name: 'uncooperative', arguments: {}, signal: controller.signal, + callId: ToolCallId('drain-body'), name: 'uncooperative', arguments: {}, signal: controller.signal, }) await entered.promise controller.abort('must still drain') @@ -1605,7 +1605,7 @@ describe('ToolRuntime', () => { }) const result = await ctx.tools.execute({ - callId: CallId('pre-aborted'), + callId: ToolCallId('pre-aborted'), name: 'domain-abort', get arguments() { argumentReads += 1; return callerArguments }, signal: callerSignal, @@ -1637,7 +1637,7 @@ describe('ToolRuntime', () => { ctx.on('tools/result', () => { observed += 1 }) const result = await ctx.tools.execute({ - callId: CallId('invalid-pre-aborted'), + callId: ToolCallId('invalid-pre-aborted'), name: 'missing', arguments: { invalid: () => undefined }, signal: AbortSignal.abort('already cancelled'), @@ -1662,7 +1662,7 @@ describe('ToolRuntime', () => { return next() }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'Error: nope' }) expect(entered).toBe(false) // A denied call never enters the around-dispatch extension point. @@ -1685,7 +1685,7 @@ describe('ToolRuntime', () => { return result }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'boom', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'boom', arguments: {} }) expect(seen).toEqual({ isError: true, error: { message: 'kaboom', info: { name: 'HarnessError', code: 'BOOM' } }, @@ -1710,7 +1710,7 @@ describe('ToolRuntime', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('frozen-canonical'), name: 'echo', arguments: { text: 'original' }, + callId: ToolCallId('frozen-canonical'), name: 'echo', arguments: { text: 'original' }, }) expect(mutationAttempts).toEqual([false, false]) expect(result.isError ? undefined : result.value).toBe('original') @@ -1731,7 +1731,7 @@ describe('ToolRuntime', () => { return next() }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'boom', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'boom', arguments: {} }) expect(postSaw).toBe(true) // the normalized isError still flows through post-execute expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'Error: exploded' }) @@ -1759,7 +1759,7 @@ describe('ToolRuntime', () => { return next() }) - await ctx.tools.execute({ callId: CallId('c1'), name: 'signal-probe', arguments: {}, signal: upstream }) + await ctx.tools.execute({ callId: ToolCallId('c1'), name: 'signal-probe', arguments: {}, signal: upstream }) expect(seenSignal).toBeDefined() expect(seenSignal).not.toBe(upstream) expect(seenSignal).not.toBe(replacement) @@ -1777,7 +1777,7 @@ describe('ToolRuntime', () => { ctx.on('tools/execute', async (_exec: ToolDispatchExecution, _next: () => Promise): Promise => ({ content: [{ type: 'text', text: 'ignored authored content' }], isError: false, value: 'short-circuited' })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'never-runs', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'never-runs', arguments: {} }) expect(dispatched).toBe(false) // returning without next() skips core dispatch expect(result.content[0]).toMatchObject({ text: 'short-circuited' }) }) @@ -1817,10 +1817,10 @@ describe('ToolRuntime', () => { }) const first = await ctx.tools.execute({ - signal: testToolSignal, callId: CallId('cached-first'), name: 'string-output', arguments: {}, + signal: testToolSignal, callId: ToolCallId('cached-first'), name: 'string-output', arguments: {}, }) const second = await ctx.tools.execute({ - signal: testToolSignal, callId: CallId('cached-second'), name: 'object-output', arguments: {}, + signal: testToolSignal, callId: ToolCallId('cached-second'), name: 'object-output', arguments: {}, }) expect(first.isError ? undefined : first.value).toBe('cached') @@ -1846,7 +1846,7 @@ describe('ToolRuntime', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('around-context'), name: 'echo', arguments: {}, + callId: ToolCallId('around-context'), name: 'echo', arguments: {}, }) expect(result.additionalContexts).toEqual([{ id: expect.any(String) as unknown, @@ -1861,7 +1861,7 @@ describe('ToolRuntime', () => { ctx.tools.register(echoTool) ctx.on('tools/execute', async () => { throw new Error('wrapper broke') }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result).toEqual({ content: [{ type: 'text', text: 'Error: wrapper broke' }], error: { message: 'wrapper broke' }, @@ -1876,7 +1876,7 @@ describe('ToolRuntime', () => { throw new Error('permission hook broke') }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result).toEqual({ content: [{ type: 'text', text: 'Error: permission hook broke' }], @@ -1892,7 +1892,7 @@ describe('ToolRuntime', () => { throw new Error('post hook broke') }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result).toEqual({ content: [{ type: 'text', text: 'Error: post hook broke' }], @@ -1908,7 +1908,7 @@ describe('ToolRuntime', () => { throw new HarnessError('denied', 'DENIED') }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: { text: 'hi' } }) expect(result).toMatchObject({ isError: true, @@ -2136,7 +2136,7 @@ describe('defineTool / schema DSL', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('c1'), + callId: ToolCallId('c1'), name: 'typed-echo', arguments: { text: 'hello', uppercase: true }, }) @@ -2195,7 +2195,7 @@ describe('defineTool / schema DSL', () => { // Execution round-trip const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('c1'), + callId: ToolCallId('c1'), name: 'roundtrip', arguments: { req: 'hello' }, }) @@ -2232,7 +2232,7 @@ describe('defineTool / schema DSL', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('c1'), + callId: ToolCallId('c1'), name: 'raw-tool', arguments: { path: '/tmp' }, }) @@ -2409,7 +2409,7 @@ describe('schema DSL optional and nested contracts', () => { throw { message: 'denied by object' } }, }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'object-thrower', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'object-thrower', arguments: {} }) expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'Error: denied by object' }) }) @@ -2424,7 +2424,7 @@ describe('schema DSL optional and nested contracts', () => { throw 'kaboom' }, }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'string-thrower', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'string-thrower', arguments: {} }) expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'Error: kaboom' }) }) @@ -2439,7 +2439,7 @@ describe('schema DSL optional and nested contracts', () => { throw { code: 500 } }, }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'object-no-message', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'object-no-message', arguments: {} }) expect(result.isError).toBe(true) const firstContent = result.content[0]! expect(firstContent.type).toBe('text') @@ -2578,7 +2578,7 @@ describe('defineTool validation (the runtime-validation Agent Note, part 1)', () }, })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'reader', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'reader', arguments: {} }) expect(result.isError).toBe(true) expect(result.content[0]).toMatchObject({ text: 'Error: invalid arguments: missing required property "path"', @@ -2595,7 +2595,7 @@ describe('defineTool validation (the runtime-validation Agent Note, part 1)', () return [{ type: 'text', text: `read ${args.path}` }] }, })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'reader', arguments: { path: '/x' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'reader', arguments: { path: '/x' } }) expect(result).toEqual({ content: [{ type: 'text', text: 'read /x' }], isError: false, @@ -2622,7 +2622,7 @@ describe('defineTool validation (the runtime-validation Agent Note, part 1)', () return [{ type: 'text', text: args.path }] }, })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'reader', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'reader', arguments: {} }) expect(result.isError).toBe(true) expect(result.error).toEqual({ message: 'invalid arguments: missing required property "path"', @@ -2640,7 +2640,7 @@ describe('defineTool validation (the runtime-validation Agent Note, part 1)', () throw new HarnessError('disk full', 'ENOSPC') }, }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'coded', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'coded', arguments: {} }) expect(result.isError).toBe(true) expect(result.error).toEqual({ message: 'disk full', info: { name: 'HarnessError', code: 'ENOSPC' } }) expect(result.content[0]).toMatchObject({ text: 'Error: disk full' }) @@ -2655,7 +2655,7 @@ describe('defineTool validation (the runtime-validation Agent Note, part 1)', () throw new Error('just a message') }, }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'plain', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'plain', arguments: {} }) expect(result.isError).toBe(true) expect(result.error).toEqual({ message: 'just a message' }) expect(result.content[0]).toMatchObject({ text: 'Error: just a message' }) @@ -2678,7 +2678,7 @@ describe('defineTool validation (the runtime-validation Agent Note, part 1)', () }) // Missing the "required" path — but raw tools validate their own input, so // this reaches execute rather than being rejected by the harness. - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'raw', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'raw', arguments: {} }) expect(result.isError).toBe(false) }) diff --git a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts index 56f7903fcb..7c2e1f5fba 100644 --- a/packages/examples/agent-spine-demo/tests/agent-core.spec.ts +++ b/packages/examples/agent-spine-demo/tests/agent-core.spec.ts @@ -14,7 +14,7 @@ import * as ToolFs from '@deepseek-ai/dsh-tool-fs' import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' import { createUserMessage, - CallId, + ToolCallId, LlmAdapter, LlmError, resolveRetryPolicy, @@ -605,8 +605,8 @@ describe('dsh-agent-spine-demo bundle', () => { const execution: ToolExecution = { signal: testToolSignal, token: Symbol('agent-core-dsh-home-test') as ToolExecution['token'], - callId: CallId('agent-core-dsh-home'), - rootCallId: CallId('agent-core-dsh-home'), + callId: ToolCallId('agent-core-dsh-home'), + rootCallId: ToolCallId('agent-core-dsh-home'), name: 'bash', arguments: { command: 'true' }, } @@ -685,7 +685,7 @@ describe('dsh-agent-spine-demo bundle', () => { const wait = vi.spyOn(ctx.jobs, 'wait') await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('task-config-forwarding'), + callId: ToolCallId('task-config-forwarding'), name: 'job_output', arguments: { job_id: id, wait: true }, }) diff --git a/packages/examples/agent-spine-demo/tests/multi-project-sandbox.e2e.ts b/packages/examples/agent-spine-demo/tests/multi-project-sandbox.e2e.ts index f06fe24924..8bc735f3f6 100644 --- a/packages/examples/agent-spine-demo/tests/multi-project-sandbox.e2e.ts +++ b/packages/examples/agent-spine-demo/tests/multi-project-sandbox.e2e.ts @@ -8,7 +8,7 @@ import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox' import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local' import * as FsPolicy from '@deepseek-ai/dsh-fs-observation-policy' import SandboxedFileSystem from '@deepseek-ai/dsh-fs-sandbox' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local' import { bwrapProfileArgs, seatbeltProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts' import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy' @@ -85,22 +85,22 @@ describe('one-context multi-project sandbox', () => { const { active, agentA, agentB } = await agents() const [aOwn, bOwn, aCross, bCross] = await Promise.all([ active.tools.execute({ - callId: CallId('bash-a-own'), name: 'bash', agent: agentA, + callId: ToolCallId('bash-a-own'), name: 'bash', agent: agentA, signal: new AbortController().signal, arguments: { command: 'printf a > a-owned.txt', description: 'Write project A marker' }, }), active.tools.execute({ - callId: CallId('bash-b-own'), name: 'bash', agent: agentB, + callId: ToolCallId('bash-b-own'), name: 'bash', agent: agentB, signal: new AbortController().signal, arguments: { command: 'printf b > b-owned.txt', description: 'Write project B marker' }, }), active.tools.execute({ - callId: CallId('bash-a-cross'), name: 'bash', agent: agentA, + callId: ToolCallId('bash-a-cross'), name: 'bash', agent: agentA, signal: new AbortController().signal, arguments: { command: `printf cross > ../${basename(projectB)}/from-a.txt`, description: 'Attempt project B write' }, }), active.tools.execute({ - callId: CallId('bash-b-cross'), name: 'bash', agent: agentB, + callId: ToolCallId('bash-b-cross'), name: 'bash', agent: agentB, signal: new AbortController().signal, arguments: { command: `printf cross > ../${basename(projectA)}/from-b.txt`, description: 'Attempt project A write' }, }), @@ -122,22 +122,22 @@ describe('one-context multi-project sandbox', () => { const { active, agentA, agentB } = await agents() const [aOwn, bOwn, aCross, bCross] = await Promise.all([ active.tools.execute({ - callId: CallId('fs-a-own'), name: 'write', agent: agentA, + callId: ToolCallId('fs-a-own'), name: 'write', agent: agentA, signal: new AbortController().signal, arguments: { file_path: 'a-owned.txt', content: 'a' }, }), active.tools.execute({ - callId: CallId('fs-b-own'), name: 'write', agent: agentB, + callId: ToolCallId('fs-b-own'), name: 'write', agent: agentB, signal: new AbortController().signal, arguments: { file_path: 'b-owned.txt', content: 'b' }, }), active.tools.execute({ - callId: CallId('fs-a-cross'), name: 'write', agent: agentA, + callId: ToolCallId('fs-a-cross'), name: 'write', agent: agentA, signal: new AbortController().signal, arguments: { file_path: join(projectB, 'from-a.txt'), content: 'cross' }, }), active.tools.execute({ - callId: CallId('fs-b-cross'), name: 'write', agent: agentB, + callId: ToolCallId('fs-b-cross'), name: 'write', agent: agentB, signal: new AbortController().signal, arguments: { file_path: join(projectA, 'from-b.txt'), content: 'cross' }, }), @@ -171,22 +171,22 @@ describe('one-context multi-project sandbox', () => { const [bashOwn, bashLexical, fsOwn, fsLexical] = await Promise.all([ active.tools.execute({ - callId: CallId('bash-symlink-own'), name: 'bash', agent: handle.agent, + callId: ToolCallId('bash-symlink-own'), name: 'bash', agent: handle.agent, signal: new AbortController().signal, arguments: { command: 'printf bash > bash-owned.txt', description: 'Write physical workspace marker' }, }), active.tools.execute({ - callId: CallId('bash-symlink-lexical'), name: 'bash', agent: handle.agent, + callId: ToolCallId('bash-symlink-lexical'), name: 'bash', agent: handle.agent, signal: new AbortController().signal, arguments: { command: `printf escaped > ${join(lexicalRoot, 'bash-escaped.txt')}`, description: 'Attempt lexical workspace write' }, }), active.tools.execute({ - callId: CallId('fs-symlink-own'), name: 'write', agent: handle.agent, + callId: ToolCallId('fs-symlink-own'), name: 'write', agent: handle.agent, signal: new AbortController().signal, arguments: { file_path: 'fs-owned.txt', content: 'fs' }, }), active.tools.execute({ - callId: CallId('fs-symlink-lexical'), name: 'write', agent: handle.agent, + callId: ToolCallId('fs-symlink-lexical'), name: 'write', agent: handle.agent, signal: new AbortController().signal, arguments: { file_path: join(lexicalRoot, 'fs-escaped.txt'), content: 'escaped' }, }), @@ -222,12 +222,12 @@ describe('one-context multi-project sandbox', () => { const [bashRead, fsRead] = await Promise.all([ active.tools.execute({ - callId: CallId('bash-symlink-parent-read'), name: 'bash', agent: handle.agent, + callId: ToolCallId('bash-symlink-parent-read'), name: 'bash', agent: handle.agent, signal: new AbortController().signal, arguments: { command: 'cat ../shared.txt', description: 'Read through the physical parent' }, }), active.tools.execute({ - callId: CallId('fs-symlink-parent-read'), name: 'read', agent: handle.agent, + callId: ToolCallId('fs-symlink-parent-read'), name: 'read', agent: handle.agent, signal: new AbortController().signal, arguments: { file_path: '../shared.txt' }, }), diff --git a/packages/experimental/tool-agent-team/tests/tool-team.spec.ts b/packages/experimental/tool-agent-team/tests/tool-team.spec.ts index ebc60f92cb..510a4469ed 100644 --- a/packages/experimental/tool-agent-team/tests/tool-team.spec.ts +++ b/packages/experimental/tool-agent-team/tests/tool-team.spec.ts @@ -6,7 +6,7 @@ import { Context } from '@deepseek-ai/cordis' import type { Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { scopeOf } from '@deepseek-ai/dsh-scope' import { SessionId } from '@deepseek-ai/dsh-session' import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' @@ -68,7 +68,7 @@ function execute( signal: AbortSignal = SIGNAL, ) { return ctx.tools.execute({ - callId: CallId(`team-call-${++callNumber}`), + callId: ToolCallId(`team-call-${++callNumber}`), name, arguments: args, signal, diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 08b22ffdef..dd46f7f310 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -216,7 +216,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ }, ], ownerProps: [ - '/** Stable owner currency delivered to one keyed Chat business renderer. */\nexport interface ChatNodeOwnerProps {\n /** Selected Tool call, when the shared details store names one. */\n selectedCallId?: CallId | undefined\n /** Session workspace root; Tool summaries display paths relative to it. */\n cwd?: string | undefined\n openFile: (path: string) => void\n inspectCall: (callId: CallId) => void\n forkAt: (seq: number) => void\n /** Render a historical image group through the attachment slot. */\n renderMessageImages: RenderMessageImages\n fileMentions: (owner: TurnTailOwnerProps) => MarkdownFileMentions | undefined\n}', + '/** Stable owner currency delivered to one keyed Chat business renderer. */\nexport interface ChatNodeOwnerProps {\n /** Selected Tool call, when the shared details store names one. */\n selectedCallId?: ToolCallId | undefined\n /** Session workspace root; Tool summaries display paths relative to it. */\n cwd?: string | undefined\n openFile: (path: string) => void\n inspectCall: (callId: ToolCallId) => void\n forkAt: (seq: number) => void\n /** Render a historical image group through the attachment slot. */\n renderMessageImages: RenderMessageImages\n fileMentions: (owner: TurnTailOwnerProps) => MarkdownFileMentions | undefined\n}', ], ownerPropsReferences: [ 'MarkdownFileMentions', @@ -1119,7 +1119,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ }, ], ownerProps: [ - '/**\n * View-slot owner share: the cross-view inspect handoff (otherwise views need\n * nothing from the render site — sessionId and the snapshot hook arrive as\n * framework-standard props; tool rows go through each view\'s own declared\n * toolview hole).\n */\nexport interface ConvViewOwnerProps {\n /** One-shot inspect request from another view (chat\'s Inspect button); null when idle. */\n inspect?: { callId: CallId } | null\n /** Acknowledge the inspect request once applied (clears the store field). */\n onInspectDone?: () => void\n}', + '/**\n * View-slot owner share: the cross-view inspect handoff (otherwise views need\n * nothing from the render site — sessionId and the snapshot hook arrive as\n * framework-standard props; tool rows go through each view\'s own declared\n * toolview hole).\n */\nexport interface ConvViewOwnerProps {\n /** One-shot inspect request from another view (chat\'s Inspect button); null when idle. */\n inspect?: { callId: ToolCallId } | null\n /** Acknowledge the inspect request once applied (clears the store field). */\n onInspectDone?: () => void\n}', ], ownerPropsReferences: [], standardProps: [ diff --git a/packages/extensions/cordis-host-runner/tests/helpers.ts b/packages/extensions/cordis-host-runner/tests/helpers.ts index 894e72fe6d..f89a85f268 100644 --- a/packages/extensions/cordis-host-runner/tests/helpers.ts +++ b/packages/extensions/cordis-host-runner/tests/helpers.ts @@ -1,6 +1,6 @@ import { Context } from '@deepseek-ai/cordis' import Timer from '@deepseek-ai/cordis-plugin-timer' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import type { ToolDefinition, ToolExecutionResult } from '@deepseek-ai/dsh-tools' @@ -147,7 +147,7 @@ let callCounter = 0 export function call(ctx: Context, name: string, args: unknown): Promise { return ctx.tools.execute({ signal: new AbortController().signal, - callId: CallId(`call-${++callCounter}`), + callId: ToolCallId(`call-${++callCounter}`), name, arguments: args, }) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index c821662e85..f73f765c0d 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -2894,7 +2894,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ApprovalRequest', - declaration: 'export interface ApprovalRequest {\n readonly agent: Agent;\n readonly toolName: string;\n readonly callId?: CallId;\n readonly reason?: string;\n readonly signal?: AbortSignal;\n}', + declaration: 'export interface ApprovalRequest {\n readonly agent: Agent;\n readonly toolName: string;\n readonly callId?: ToolCallId;\n readonly reason?: string;\n readonly signal?: AbortSignal;\n}', }, { name: 'ApprovalService', @@ -3038,7 +3038,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'CodeDispatchLog', - declaration: 'export interface CodeDispatchLog {\n readonly exec: ToolExecution;\n readonly agent?: Agent;\n readonly subCallId: CallId;\n readonly name: string;\n readonly isError: boolean;\n readonly content: ContentBlock[];\n}', + declaration: 'export interface CodeDispatchLog {\n readonly exec: ToolExecution;\n readonly agent?: Agent;\n readonly subCallId: ToolCallId;\n readonly name: string;\n readonly isError: boolean;\n readonly content: ContentBlock[];\n}', }, { name: 'CodeJsonValue', @@ -3874,7 +3874,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'PrunedEntry', - declaration: 'export interface PrunedEntry {\n readonly originalSeq: number;\n readonly replacementSeq: number;\n readonly callId: CallId;\n readonly charsBefore: number;\n readonly charsAfter: number;\n}', + declaration: 'export interface PrunedEntry {\n readonly originalSeq: number;\n readonly replacementSeq: number;\n readonly callId: ToolCallId;\n readonly charsBefore: number;\n readonly charsAfter: number;\n}', }, { name: 'PruneResult', @@ -4066,7 +4066,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SessionEventMap', - declaration: 'export interface SessionEventMap {\n \'turn/start\': {\n turn: number;\n };\n \'turn/end\': {\n turn: number;\n reason: TurnEndReason;\n };\n \'step/start\': {\n turn: number;\n step: number;\n };\n \'step/end\': {\n turn: number;\n step: number;\n };\n \'user/message\': UserMessage;\n \'assistant/chunk\': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n \'assistant/message\': {\n turn: number;\n step: number;\n message: AssistantMessage;\n usage?: TokenUsage;\n interrupted?: true;\n };\n \'tool/call\': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n \'tool/result\': {\n turn: number;\n step: number;\n message: ToolResultMessage;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n \'todo/write\': {\n todos: TodoItem[];\n };\n \'request/header\': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n \'request/context\': RequestContext;\n \'session/end-seed\': Record;\n}', + declaration: 'export interface SessionEventMap {\n \'turn/start\': {\n turn: number;\n };\n \'turn/end\': {\n turn: number;\n reason: TurnEndReason;\n };\n \'step/start\': {\n turn: number;\n step: number;\n };\n \'step/end\': {\n turn: number;\n step: number;\n };\n \'user/message\': UserMessage;\n \'assistant/chunk\': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n \'assistant/message\': {\n turn: number;\n step: number;\n message: AssistantMessage;\n usage?: TokenUsage;\n interrupted?: true;\n };\n \'tool/call\': {\n turn: number;\n step: number;\n callId: ToolCallId;\n name: string;\n arguments: string;\n };\n \'tool/result\': {\n turn: number;\n step: number;\n message: ToolResultMessage;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n \'todo/write\': {\n todos: TodoItem[];\n };\n \'request/header\': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n \'request/context\': RequestContext;\n \'session/end-seed\': Record;\n}', }, { name: 'SessionEventMetadataFilter', @@ -4426,7 +4426,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SpillSource', - declaration: 'export interface SpillSource {\n toolName: string;\n callId: CallId;\n label: string;\n}', + declaration: 'export interface SpillSource {\n toolName: string;\n callId: ToolCallId;\n label: string;\n}', }, { name: 'StorageBackend', @@ -4442,7 +4442,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'StreamChunk', - declaration: 'export type StreamChunk = {\n type: \'block-start\';\n index: number;\n blockType: ContentBlockType;\n} | {\n type: \'text-delta\';\n index: number;\n text: string;\n} | {\n type: \'reasoning-delta\';\n index: number;\n text: string;\n} | {\n type: \'tool-call-delta\';\n index: number;\n id: CallId;\n name?: string;\n argumentsDelta: string;\n} | {\n type: \'block-end\';\n index: number;\n block: ContentBlock;\n} | {\n type: \'usage\';\n usage: TokenUsage;\n} | {\n type: \'finish\';\n reason: FinishReason;\n replayState?: ReplayEnvelope;\n};', + declaration: 'export type StreamChunk = {\n type: \'block-start\';\n index: number;\n blockType: ContentBlockType;\n} | {\n type: \'text-delta\';\n index: number;\n text: string;\n} | {\n type: \'reasoning-delta\';\n index: number;\n text: string;\n} | {\n type: \'tool-call-delta\';\n index: number;\n id: ToolCallId;\n name?: string;\n argumentsDelta: string;\n} | {\n type: \'block-end\';\n index: number;\n block: ContentBlock;\n} | {\n type: \'usage\';\n usage: TokenUsage;\n} | {\n type: \'finish\';\n reason: FinishReason;\n replayState?: ReplayEnvelope;\n};', }, { name: 'SubagentCapabilities', @@ -4750,7 +4750,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ToolExecution', - declaration: 'export interface ToolExecution extends ToolExecutionInput {\n readonly rootCallId: CallId;\n readonly token: ToolExecutionToken;\n}', + declaration: 'export interface ToolExecution extends ToolExecutionInput {\n readonly rootCallId: ToolCallId;\n readonly token: ToolExecutionToken;\n}', }, { name: 'ToolExecutionFailure', @@ -4758,7 +4758,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ToolExecutionInput', - declaration: 'export interface ToolExecutionInput {\n readonly callId: CallId;\n readonly rootCallId?: CallId;\n readonly name: string;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n}', + declaration: 'export interface ToolExecutionInput {\n readonly callId: ToolCallId;\n readonly rootCallId?: ToolCallId;\n readonly name: string;\n readonly arguments: unknown;\n readonly agent?: Agent;\n readonly parent?: ToolExecutionToken;\n readonly signal: AbortSignal;\n}', }, { name: 'ToolExecutionMode', @@ -4786,7 +4786,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ToolMessageSource', - declaration: 'export interface ToolMessageSource {\n kind: \'tool\';\n callId: CallId;\n}', + declaration: 'export interface ToolMessageSource {\n kind: \'tool\';\n callId: ToolCallId;\n}', }, { name: 'ToolOutputDefinition', @@ -4810,7 +4810,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ToolResultBlock', - declaration: 'export interface ToolResultBlock {\n type: \'tool-result\';\n toolCallId: CallId;\n content: ContentBlock[];\n isError?: boolean;\n}', + declaration: 'export interface ToolResultBlock {\n type: \'tool-result\';\n toolCallId: ToolCallId;\n content: ContentBlock[];\n isError?: boolean;\n}', }, { name: 'ToolResultMessage', diff --git a/packages/fs/tool-fs-search/tests/integration.spec.ts b/packages/fs/tool-fs-search/tests/integration.spec.ts index be1becbf25..94be0e1088 100644 --- a/packages/fs/tool-fs-search/tests/integration.spec.ts +++ b/packages/fs/tool-fs-search/tests/integration.spec.ts @@ -15,7 +15,7 @@ import { mkdir, mkdtemp, rm, utimes, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools' import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local' @@ -30,7 +30,7 @@ let callCounter = 0 function call(name: string, args: unknown, agentObj?: object) { return ctx.tools.execute({ signal: testToolSignal, - callId: CallId(`it-${++callCounter}`), + callId: ToolCallId(`it-${++callCounter}`), name, arguments: args, ...agentObj ? { agent: agentObj as never } : {}, @@ -180,7 +180,7 @@ describe('search tools over the real subprocess service + the packaged rg', () = const controller = new AbortController() controller.abort() const result = await ctx.tools.execute({ - callId: CallId(`it-${++callCounter}`), + callId: ToolCallId(`it-${++callCounter}`), name: 'grep', arguments: { pattern: 'x' }, signal: controller.signal, diff --git a/packages/fs/tool-fs-search/tests/rg-path.spec.ts b/packages/fs/tool-fs-search/tests/rg-path.spec.ts index 3ad75edaaf..7369ef521c 100644 --- a/packages/fs/tool-fs-search/tests/rg-path.spec.ts +++ b/packages/fs/tool-fs-search/tests/rg-path.spec.ts @@ -8,7 +8,7 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import type { ToolExecution } from '@deepseek-ai/dsh-tools' import { resolveRgPath, runRipgrep } from '@deepseek-ai/dsh-tool-fs-search' @@ -24,7 +24,7 @@ describe('lazy packaged-ripgrep resolution', () => { it('fails the first search call with SEARCH_FAILED instead of failing module load', async () => { // The resolution rejects before any spawn, so no subprocess service is needed. const controller = new AbortController() - const exec = { signal: controller.signal, name: 'glob', callId: CallId('missing-platform-package') } as unknown as ToolExecution + const exec = { signal: controller.signal, name: 'glob', callId: ToolCallId('missing-platform-package') } as unknown as ToolExecution await expect(runRipgrep(new Context(), exec, 'glob', ['--files'], 1_000_000, 3_000, 64 * 1024)) .rejects.toMatchObject({ name: 'SearchError', code: 'SEARCH_FAILED' }) diff --git a/packages/fs/tool-fs-search/tests/tools.spec.ts b/packages/fs/tool-fs-search/tests/tools.spec.ts index abb7e75773..465000ba37 100644 --- a/packages/fs/tool-fs-search/tests/tools.spec.ts +++ b/packages/fs/tool-fs-search/tests/tools.spec.ts @@ -13,7 +13,7 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { join, sep } from 'node:path' -import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { TOOL_ABORTED_BEFORE_DISPATCH, type ToolExecution, type ToolExecutionToken } from '@deepseek-ai/dsh-tools' import { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess' @@ -213,7 +213,7 @@ function call( ) { return ctx.tools.execute({ signal: testToolSignal, - callId: CallId(`call-${++callCounter}`), + callId: ToolCallId(`call-${++callCounter}`), name, arguments: args, ...options.agent ? { agent: options.agent as never } : {}, @@ -467,7 +467,7 @@ describe('workdir derivation and signal forwarding', () => { const { ctx } = await setup() const controller = new AbortController() controller.abort() - const exec = { signal: controller.signal, name: 'glob', callId: CallId('direct-pre-abort') } as unknown as ToolExecution + const exec = { signal: controller.signal, name: 'glob', callId: ToolCallId('direct-pre-abort') } as unknown as ToolExecution await expect(runRipgrep(ctx, exec, 'glob', ['--files'], 1_000_000, 3_000, 64 * 1024)).rejects .toMatchObject({ name: 'SearchError', code: 'SEARCH_ABORTED' }) }) diff --git a/packages/fs/tool-fs/tests/integration.spec.ts b/packages/fs/tool-fs/tests/integration.spec.ts index 0d185399d4..28315545c1 100644 --- a/packages/fs/tool-fs/tests/integration.spec.ts +++ b/packages/fs/tool-fs/tests/integration.spec.ts @@ -10,7 +10,7 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools' import { LocalFileSystem } from '@deepseek-ai/dsh-fs-local' @@ -29,7 +29,7 @@ let callCounter = 0 function call(name: string, args: unknown) { return ctx.tools.execute({ signal: testToolSignal, - callId: CallId(`call-${++callCounter}`), + callId: ToolCallId(`call-${++callCounter}`), name, arguments: args, agent: { session } as never, @@ -393,7 +393,7 @@ describe('per-session cwd', () => { const callIn = (sessionObj: object, name: string, args: unknown) => ctx.tools.execute({ signal: testToolSignal, - callId: CallId(`call-${++callCounter}`), + callId: ToolCallId(`call-${++callCounter}`), name, arguments: args, agent: { session: sessionObj } as never, @@ -436,9 +436,9 @@ describe('signal, concurrency, and the fs/observed contract', () => { const session = { header: {} } const callSig = (signal: AbortSignal, name: string, args: unknown) => - ctx.tools.execute({ callId: CallId(`c-${++callCounter}`), name, arguments: args, agent: { session } as never, signal }) + ctx.tools.execute({ callId: ToolCallId(`c-${++callCounter}`), name, arguments: args, agent: { session } as never, signal }) const callOwned = (name: string, args: unknown) => - ctx.tools.execute({ signal: testToolSignal, callId: CallId(`c-${++callCounter}`), name, arguments: args, agent: { session } as never }) + ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId(`c-${++callCounter}`), name, arguments: args, agent: { session } as never }) it('a pre-aborted registry call skips read/write/edit with ABORTED_BEFORE_DISPATCH', async () => { await writeFile(join(dir, 'a.txt'), 'hello') diff --git a/packages/fs/tool-fs/tests/read-image.spec.ts b/packages/fs/tool-fs/tests/read-image.spec.ts index 7c52db5a8f..800b826ba8 100644 --- a/packages/fs/tool-fs/tests/read-image.spec.ts +++ b/packages/fs/tool-fs/tests/read-image.spec.ts @@ -12,7 +12,7 @@ import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime' import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime' -import { CallId, LlmAdapter, LlmRuntime } from '@deepseek-ai/dsh-llm' +import { ToolCallId, LlmAdapter, LlmRuntime } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, LlmModelInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { RUN_CODE_NAME } from '@deepseek-ai/dsh-tools' @@ -137,7 +137,7 @@ let callCounter = 0 function call(ctx: Context, name: string, args: unknown, agent?: object) { return ctx.tools.execute({ signal: testToolSignal, - callId: CallId(`img-call-${++callCounter}`), + callId: ToolCallId(`img-call-${++callCounter}`), name, arguments: args, ...agent ? { agent: agent as never } : {}, @@ -523,7 +523,7 @@ describe('registration surface', () => { it('declares read_image parallel-safe and presents a read-family card', async () => { const ctx = await setup() expect(ctx.tools.executionMode({ - signal: testToolSignal, callId: CallId('img-parallel'), name: 'read_image', arguments: { file_path: 'a.png' }, + signal: testToolSignal, callId: ToolCallId('img-parallel'), name: 'read_image', arguments: { file_path: 'a.png' }, })).toEqual({ kind: 'parallel' }) expect(ctx.tools.get('read_image')?.presentCall?.({ file_path: 'shot.png' })).toEqual({ card: 'generic', diff --git a/packages/fs/tool-fs/tests/tools.spec.ts b/packages/fs/tool-fs/tests/tools.spec.ts index cad6531766..6a1fc07bba 100644 --- a/packages/fs/tool-fs/tests/tools.spec.ts +++ b/packages/fs/tool-fs/tests/tools.spec.ts @@ -8,7 +8,7 @@ import { Context } from '@deepseek-ai/cordis' import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve, sep } from 'node:path' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { type ToolResult } from '@deepseek-ai/dsh-tools' import { FileSystem, FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs' @@ -113,7 +113,7 @@ let callCounter = 0 function call(ctx: Context, name: string, args: unknown, agent?: object) { return ctx.tools.execute({ signal: testToolSignal, - callId: CallId(`call-${++callCounter}`), + callId: ToolCallId(`call-${++callCounter}`), name, arguments: args, ...agent ? { agent: agent as never } : {}, @@ -158,11 +158,11 @@ describe('registration', () => { it('declares read parallel-safe while write/edit remain exclusive', async () => { const { ctx } = await setup() - expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('read-safe'), name: 'read', arguments: { file_path: 'a.txt' } })) + expect(ctx.tools.executionMode({ signal: testToolSignal, callId: ToolCallId('read-safe'), name: 'read', arguments: { file_path: 'a.txt' } })) .toEqual({ kind: 'parallel' }) - expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('write-exclusive'), name: 'write', arguments: { file_path: 'a.txt', content: 'x' } })) + expect(ctx.tools.executionMode({ signal: testToolSignal, callId: ToolCallId('write-exclusive'), name: 'write', arguments: { file_path: 'a.txt', content: 'x' } })) .toEqual({ kind: 'exclusive' }) - expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('edit-exclusive'), name: 'edit', arguments: { file_path: 'a.txt', old_string: 'x', new_string: 'y' } })) + expect(ctx.tools.executionMode({ signal: testToolSignal, callId: ToolCallId('edit-exclusive'), name: 'edit', arguments: { file_path: 'a.txt', old_string: 'x', new_string: 'y' } })) .toEqual({ kind: 'exclusive' }) }) @@ -881,7 +881,7 @@ describe('sandbox escalation API (write/edit)', () => { // Pass a signal so the escalation ask forwards it to the approval request // (the request rides the tool-execution abort signal). await ctx.tools.execute({ - callId: CallId('call-fs-esc-grant'), + callId: ToolCallId('call-fs-esc-grant'), name: 'write', arguments: { file_path: 'a.txt', content: 'x', sandbox_permissions: 'danger-full-access', justification: 'the test needs it' }, agent: escalationAgent() as never, diff --git a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts index e363666606..88fb19364d 100644 --- a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts +++ b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts @@ -4,7 +4,7 @@ import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { FsVersion } from '@deepseek-ai/dsh-fs' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -55,7 +55,7 @@ function text(result: { content: { type: string; text?: string }[] }): string { function call(ctx: Context, owner: Agent | undefined, args: unknown) { return ctx.tools.execute({ signal: new AbortController().signal, - callId: CallId(`str-replace-editor-${++callNumber}`), + callId: ToolCallId(`str-replace-editor-${++callNumber}`), name: 'str_replace_editor', arguments: args, ...owner === undefined ? {} : { agent: owner }, diff --git a/packages/goal/tool-goal/tests/tool-goal.spec.ts b/packages/goal/tool-goal/tests/tool-goal.spec.ts index cb5a2a9874..5fc0215c0b 100644 --- a/packages/goal/tool-goal/tests/tool-goal.spec.ts +++ b/packages/goal/tool-goal/tests/tool-goal.spec.ts @@ -5,7 +5,7 @@ import AgentRegistry, { agentEvents, Inbox } from '@deepseek-ai/dsh-agent' import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' import GoalService, { GoalId } from '@deepseek-ai/dsh-goal' import type { GoalRef } from '@deepseek-ai/dsh-goal' -import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' import type { MessageSource } from '@deepseek-ai/dsh-llm' import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' @@ -91,7 +91,7 @@ async function execute( ): Promise { const run = () => ctx.tools.execute({ signal: testToolSignal, - callId: CallId(`call-${Math.random()}`), + callId: ToolCallId(`call-${Math.random()}`), name, arguments: args, ...agent === undefined ? {} : { agent }, @@ -123,7 +123,7 @@ describe('goal tool registration and presentation', () => { expect(['create_goal', 'get_goal', 'update_goal'].map(name => ctx.tools.get(name)?.name)) .toEqual(['create_goal', 'get_goal', 'update_goal']) for (const name of ['create_goal', 'get_goal', 'update_goal']) { - expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId(name), name, arguments: {} })) + expect(ctx.tools.executionMode({ signal: testToolSignal, callId: ToolCallId(name), name, arguments: {} })) .toEqual({ kind: 'exclusive' }) } const section = (await ctx.systemPrompt.assemble()).sections.find(item => item.name === 'tool:goal') @@ -217,7 +217,7 @@ describe('goal tool execution authority', () => { openTurn(root, { kind: 'user' }) const driverless = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('call-driverless'), + callId: ToolCallId('call-driverless'), name: 'get_goal', arguments: {}, agent: root.agent, diff --git a/packages/guard/repeat-tool-reminder/tests/repeat-tool-reminder.spec.ts b/packages/guard/repeat-tool-reminder/tests/repeat-tool-reminder.spec.ts index c2dda01558..ea0f0ceef8 100644 --- a/packages/guard/repeat-tool-reminder/tests/repeat-tool-reminder.spec.ts +++ b/packages/guard/repeat-tool-reminder/tests/repeat-tool-reminder.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' import { defineContentToolFixture } from '@deepseek-ai/dsh-tools' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -293,7 +293,7 @@ describe('chain semantics', () => { it('ignores direct executes with no agent (they neither crash nor advance any chain)', async () => { const ctx = await harness({ thresholds: [2] }) - const direct = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('d1'), name: 'probe', arguments: { q: 1 } }) + const direct = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('d1'), name: 'probe', arguments: { q: 1 } }) expect(direct.isError).toBe(false) ctx.llm.registerAdapter(['mock'], new MockAdapter([ diff --git a/packages/guard/timeout-policy/tests/timeout-policy.spec.ts b/packages/guard/timeout-policy/tests/timeout-policy.spec.ts index 73c4783b69..a987abfa64 100644 --- a/packages/guard/timeout-policy/tests/timeout-policy.spec.ts +++ b/packages/guard/timeout-policy/tests/timeout-policy.spec.ts @@ -9,7 +9,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' -import { CallId, HarnessError } from '@deepseek-ai/dsh-llm' +import { ToolCallId, HarnessError } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { defineContentToolFixture, TOOL_ABORTED, type ToolExecutionInput, type PostToolDecision } from '@deepseek-ai/dsh-tools' import * as timeoutPolicy from '@deepseek-ai/dsh-tool-call-timeout-policy' @@ -52,7 +52,7 @@ describe('timeout-policy delegation (unconfigured / fast)', () => { ctx.tools.register(defineContentToolFixture({ name: 'probe', description: 'd', parameters: {}, async execute(_a, exec) { seenSignal = exec.signal; return [{ type: 'text' as const, text: 'ok' }] } })) const upstream = new AbortController().signal - const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'probe', arguments: {}, signal: upstream }) + const result = await ctx.tools.execute({ callId: ToolCallId('c1'), name: 'probe', arguments: {}, signal: upstream }) expect(result.isError).toBe(false) expect(seenSignal).toBe(upstream) }) @@ -61,7 +61,7 @@ describe('timeout-policy delegation (unconfigured / fast)', () => { const ctx = await setup() ctx.tools.register(defineContentToolFixture({ name: 'fast', description: 'd', parameters: {}, timeoutMs: 10_000, async execute() { return [{ type: 'text' as const, text: 'ok' }] } })) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'fast', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'fast', arguments: {} }) expect(result).toEqual({ content: [{ type: 'text', text: 'ok' }], isError: false, @@ -75,7 +75,7 @@ describe('timeout-policy delegation (unconfigured / fast)', () => { ctx.tools.register(defineContentToolFixture({ name: 'probe', description: 'd', parameters: {}, timeoutMs: 10_000, async execute(_a, exec) { seenSignal = exec.signal; return [{ type: 'text' as const, text: 'ok' }] } })) const upstream = new AbortController().signal - await ctx.tools.execute({ callId: CallId('c1'), name: 'probe', arguments: {}, signal: upstream }) + await ctx.tools.execute({ callId: ToolCallId('c1'), name: 'probe', arguments: {}, signal: upstream }) expect(seenSignal).toBeDefined() expect(seenSignal).not.toBe(upstream) }) @@ -89,7 +89,7 @@ describe('timeout-policy signal restoration', () => { let postSignal: AbortSignal | undefined | 'unset' = 'unset' ctx.on('tools/post-execute', async (exec, _result, next): Promise => { postSignal = exec.signal; return next() }) const upstream = new AbortController().signal - await ctx.tools.execute({ callId: CallId('c1'), name: 'fast', arguments: {}, signal: upstream }) + await ctx.tools.execute({ callId: ToolCallId('c1'), name: 'fast', arguments: {}, signal: upstream }) expect(postSignal).toBe(upstream) }) }) @@ -101,7 +101,7 @@ describe('timeout-policy TOOL_TIMEOUT replacement (deadline wins)', () => { it('replaces a cooperative tool result with TOOL_TIMEOUT when its own deadline fires', async () => { const ctx = await setup() ctx.tools.register(cooperativeTool) - const pending = ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'slow', arguments: {} }) + const pending = ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'slow', arguments: {} }) await vi.advanceTimersByTimeAsync(150) const result = await pending expect(result).toEqual({ @@ -117,7 +117,7 @@ describe('timeout-policy TOOL_TIMEOUT replacement (deadline wins)', () => { it('replaces a provider-owned abort ERROR result with TOOL_TIMEOUT when the signal was ours', async () => { const ctx = await setup() ctx.tools.register(abortThrowingTool) - const pending = ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'aborter', arguments: {} }) + const pending = ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'aborter', arguments: {} }) await vi.advanceTimersByTimeAsync(150) const result = await pending expect(result.isError).toBe(true) @@ -143,7 +143,7 @@ describe('timeout-policy TOOL_TIMEOUT replacement (deadline wins)', () => { }, })) const upstream = new AbortController() - const pending = ctx.tools.execute({ callId: CallId('c1'), name: 'slow', arguments: {}, signal: upstream.signal }) + const pending = ctx.tools.execute({ callId: ToolCallId('c1'), name: 'slow', arguments: {}, signal: upstream.signal }) await entered.promise upstream.abort('user cancelled') await vi.advanceTimersByTimeAsync(0) @@ -175,7 +175,7 @@ describe('timeout-policy TOOL_TIMEOUT replacement (deadline wins)', () => { })) const upstream = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('timeout-first'), name: 'slow-cleanup', arguments: {}, signal: upstream.signal, + callId: ToolCallId('timeout-first'), name: 'slow-cleanup', arguments: {}, signal: upstream.signal, }) await vi.advanceTimersByTimeAsync(100) @@ -209,10 +209,10 @@ describe('timeout-policy disposal (HMR safety)', () => { async execute(_a, exec) { seenSignal = exec.signal; return [{ type: 'text' as const, text: 'ok' }] } })) const fiber = await ctx.plugin(timeoutPolicy) const upstream = new AbortController().signal - await ctx.tools.execute({ callId: CallId('c1'), name: 'probe', arguments: {}, signal: upstream }) + await ctx.tools.execute({ callId: ToolCallId('c1'), name: 'probe', arguments: {}, signal: upstream }) expect(seenSignal).not.toBe(upstream) await fiber.dispose() - await ctx.tools.execute({ callId: CallId('c2'), name: 'probe', arguments: {}, signal: upstream }) + await ctx.tools.execute({ callId: ToolCallId('c2'), name: 'probe', arguments: {}, signal: upstream }) expect(seenSignal).toBe(upstream) }) }) @@ -237,7 +237,7 @@ describe('dsh-tool-call-timeout-policy real-load-path guard', () => { const loader = Object.create(Loader.prototype) as Loader const unwrapped = loader.unwrapExports(timeoutPolicy) as Parameters[0] const fiber = await ctx.plugin(unwrapped) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'fast', arguments: {} } satisfies ToolExecutionInput) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'fast', arguments: {} } satisfies ToolExecutionInput) expect(result.isError).toBe(false) await fiber.dispose() }) diff --git a/packages/hooks/hooks-claude-code/tests/coverage-cases.ts b/packages/hooks/hooks-claude-code/tests/coverage-cases.ts index 08e20e3e49..416010d69f 100644 --- a/packages/hooks/hooks-claude-code/tests/coverage-cases.ts +++ b/packages/hooks/hooks-claude-code/tests/coverage-cases.ts @@ -153,8 +153,8 @@ export function defineCoverageCases(group: CoverageGroup): void { const ctx = await harness(path, new MockAdapter([])) let ran = false ctx.tools.register(defineContentToolFixture({ name: 'echo', description: 'e', parameters: {}, async execute() { ran = true; return [{ type: 'text', text: 'x' }] } })) - const { CallId } = await import('@deepseek-ai/dsh-llm') - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'echo', arguments: {} }) + const { ToolCallId } = await import('@deepseek-ai/dsh-llm') + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'echo', arguments: {} }) expect(ran).toBe(false) expect(result.isError).toBe(true) }) diff --git a/packages/hooks/hooks-codex/tests/coverage-cases.ts b/packages/hooks/hooks-codex/tests/coverage-cases.ts index 53742e2e96..dcb5cd4443 100644 --- a/packages/hooks/hooks-codex/tests/coverage-cases.ts +++ b/packages/hooks/hooks-codex/tests/coverage-cases.ts @@ -458,8 +458,8 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro const ctx = await harness(join(d, 'hooks.json'), new MockAdapter([])) let ran = false ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { ran = true; return [{ type: 'text', text: 'x' }] } })) - const { CallId } = await import('@deepseek-ai/dsh-llm') - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'Bash', arguments: { command: 'x' } }) + const { ToolCallId } = await import('@deepseek-ai/dsh-llm') + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'Bash', arguments: { command: 'x' } }) expect(ran).toBe(false) // denied expect(result.isError).toBe(true) }) @@ -469,8 +469,8 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro hooks(d, { PostToolUse: [{ hooks: [{ type: 'command', command: sh(d, 'pc.sh', '#!/usr/bin/env bash\necho \'{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"x"}}\'\n') }] }] }) const ctx = await harness(join(d, 'hooks.json'), new MockAdapter([])) ctx.tools.register(defineContentToolFixture({ name: 'Bash', description: 'b', parameters: { command: { type: 'string' } }, async execute() { return [{ type: 'text', text: 'ok' }] } })) - const { CallId } = await import('@deepseek-ai/dsh-llm') - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'Bash', arguments: { command: 'x' } }) + const { ToolCallId } = await import('@deepseek-ai/dsh-llm') + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'Bash', arguments: { command: 'x' } }) expect(result.isError).toBeFalsy() expect(result.additionalContexts?.[0]?.content.some(b => b.type === 'text' && b.text === 'x')).toBe(true) }) diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index e708353c00..1dae23edf6 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -87,7 +87,7 @@ import type { SettingsDescriptor, SettingsNamespace, SettingsPathOp } from '@dee import { credentialRef } from '@deepseek-ai/dsh-credentials' // Value edge: the rename impl narrows the title service's validation failure; the import also resolves `ctx.get('sessionTitle')`. import { SessionTitleInvalidError } from '@deepseek-ai/dsh-session-title' -import type { CallId } from '@deepseek-ai/dsh-llm/brand' +import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand' import type { ScopeKey } from '@deepseek-ai/dsh-scope' import type { ApprovalOutcome, ApprovalRequestId } from '@deepseek-ai/dsh-user-approval' // Side-effect type import: resolves the `approval/request` waterfall and @@ -632,7 +632,7 @@ interface PendingApproval { sessionId: SessionId approvalId: ApprovalRequestId toolName: string - callId?: CallId + callId?: ToolCallId reason?: string resolve(outcome: ApprovalOutcome): void } diff --git a/packages/host/apiproxy/src/api/events.ts b/packages/host/apiproxy/src/api/events.ts index 0ff088745f..eeac10b4b7 100644 --- a/packages/host/apiproxy/src/api/events.ts +++ b/packages/host/apiproxy/src/api/events.ts @@ -10,7 +10,7 @@ import type { AskUserQuestionItem } from '@deepseek-ai/dsh-user-questions/types' import type { ApprovalOutcome, ApprovalRequestId } from '@deepseek-ai/dsh-user-approval/types' import type { Message } from '@deepseek-ai/dsh-llm/types' import type { MessageId } from '@deepseek-ai/dsh-llm/brand' -import type { CallId } from '@deepseek-ai/dsh-llm/brand' +import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand' import type { JsonValue, SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types' import type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools/presentation' import type { RpcError, RpcId, RpcRequest } from './rpc.ts' @@ -69,7 +69,7 @@ export interface EventsApi { export type MuxFrame = | { type: 'session/event'; sessionId: SessionId; event: SessionEvent; view?: ToolEventView } | { type: 'session/subscribed'; sessionId: SessionId; lastSeq: number } - | { type: 'approval/requested'; sessionId: SessionId; approvalId: ApprovalRequestId; toolName: string; callId?: CallId; reason?: string } + | { type: 'approval/requested'; sessionId: SessionId; approvalId: ApprovalRequestId; toolName: string; callId?: ToolCallId; reason?: string } | { type: 'approval/resolved'; sessionId: SessionId; approvalId: ApprovalRequestId; outcome: ApprovalOutcome } | { type: 'question/requested'; sessionId: SessionId; questions: AskUserQuestionItem[] } | { type: 'question/resolved'; sessionId: SessionId; questionRpcId: RpcId; outcome: 'answered' | 'cancelled' } diff --git a/packages/host/apiproxy/tests/api-proxy-view.spec.ts b/packages/host/apiproxy/tests/api-proxy-view.spec.ts index 6955b9416c..cc6fa2e858 100644 --- a/packages/host/apiproxy/tests/api-proxy-view.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-view.spec.ts @@ -14,7 +14,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import SessionStore from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools' -import { CallId, createMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm' +import { ToolCallId, createMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session' import type { ToolDefinition } from '@deepseek-ai/dsh-tools' @@ -113,24 +113,24 @@ describe('mux live view computation', () => { const session = ctx.sessions.create() session.append('turn/start', { turn: 1 }) - session.append('tool/call', { turn: 1, step: 1, callId: CallId('c-gen'), name: 'gen', arguments: '{}' }) - session.append('tool/call', { turn: 1, step: 1, callId: CallId('c-term'), name: 'term', arguments: '{"cmd":"echo hi"}' }) - session.append('tool/call', { turn: 1, step: 1, callId: CallId('c-diff'), name: 'diffy', arguments: '{}' }) - session.append('tool/call', { turn: 1, step: 1, callId: CallId('c-call-only'), name: 'call-only', arguments: '{}' }) + session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c-gen'), name: 'gen', arguments: '{}' }) + session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c-term'), name: 'term', arguments: '{"cmd":"echo hi"}' }) + session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c-diff'), name: 'diffy', arguments: '{}' }) + session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c-call-only'), name: 'call-only', arguments: '{}' }) session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('c-call-only'), + callId: ToolCallId('c-call-only'), content: [{ type: 'text', text: rawResult }], isError: false, }), }, { surfaceOp: 'append' }) - session.append('tool/call', { turn: 1, step: 1, callId: CallId('c-plain'), name: 'plain', arguments: '{}' }) - session.append('tool/call', { turn: 1, step: 1, callId: CallId('c-boom'), name: 'boom', arguments: '{}' }) + session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c-plain'), name: 'plain', arguments: '{}' }) + session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c-boom'), name: 'boom', arguments: '{}' }) session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('c-gen'), + callId: ToolCallId('c-gen'), content: [{ type: 'text', text: 'ok' }], isError: false, }), @@ -176,12 +176,12 @@ describe('mux live view computation', () => { // .session is read on this path). ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent) session.append('turn/start', { turn: 1 }) - session.append('tool/call', { turn: 1, step: 1, callId: CallId('h-term'), name: 'term', arguments: '{"cmd":"ls"}' }) + session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('h-term'), name: 'term', arguments: '{"cmd":"ls"}' }) // meta rides through to presentResult's ToolResult (the spread arm). session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('h-term'), + callId: ToolCallId('h-term'), content: [{ type: 'text', text: 'ok' }], isError: false, }), @@ -191,27 +191,27 @@ describe('mux live view computation', () => { session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('h-orphan'), + callId: ToolCallId('h-orphan'), content: [{ type: 'text', text: 'x' }], isError: false, }), }, { surfaceOp: 'append' }) // Paired, but the call's stored arguments do not parse: backscan soft-falls. - session.append('tool/call', { turn: 1, step: 1, callId: CallId('h-bad'), name: 'term', arguments: '{broken' }) + session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('h-bad'), name: 'term', arguments: '{broken' }) session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('h-bad'), + callId: ToolCallId('h-bad'), content: [{ type: 'text', text: 'y' }], isError: false, }), }, { surfaceOp: 'append' }) // Presenterless tool: pairing succeeds but presentResult is absent. - session.append('tool/call', { turn: 1, step: 1, callId: CallId('h-plain'), name: 'plain', arguments: '{}' }) + session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('h-plain'), name: 'plain', arguments: '{}' }) session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('h-plain'), + callId: ToolCallId('h-plain'), content: [{ type: 'text', text: 'z' }], isError: false, }), @@ -335,7 +335,7 @@ describe('mux live view computation', () => { session = inner.sessions.create('session-doomed' as SessionId) }, { inject: ['sessions'] })) session?.append('turn/start', { turn: 1 }) - session?.append('tool/call', { turn: 1, step: 1, callId: CallId('c-doomed'), name: 'term', arguments: '{"cmd":"x"}' }) + session?.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c-doomed'), name: 'term', arguments: '{"cmd":"x"}' }) // Disposing the owning fiber detaches the session mid-stream; the // session/disposed listener must clear its open-call table entry. await fiber.dispose() @@ -354,14 +354,14 @@ describe('mux live view computation', () => { const session = ctx.sessions.create() session.append('turn/start', { turn: 1 }) - session.append('tool/call', { turn: 1, step: 1, callId: CallId('c-late'), name: 'term', arguments: '{"cmd":"tail"}' }) + session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c-late'), name: 'term', arguments: '{"cmd":"tail"}' }) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) // The turn/end above cleared the live table; pairing must fall back to // scanning the session's in-memory events. session.append('tool/result', { turn: 1, step: 1, message: createToolResultMessage({ - callId: CallId('c-late'), + callId: ToolCallId('c-late'), content: [{ type: 'text', text: 'ok' }], isError: false, }), diff --git a/packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts b/packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts index 0c28660c64..4d97f798bd 100644 --- a/packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts +++ b/packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime from '@deepseek-ai/dsh-tools' @@ -87,7 +87,7 @@ describe('ask_user_question tool', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('ask-1'), + callId: ToolCallId('ask-1'), name: 'ask_user_question', arguments: { questions: [{ @@ -123,7 +123,7 @@ describe('ask_user_question tool', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('ask-recommended'), + callId: ToolCallId('ask-recommended'), name: 'ask_user_question', arguments: { questions: [{ @@ -159,7 +159,7 @@ describe('ask_user_question tool', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('ask-multi'), + callId: ToolCallId('ask-multi'), name: 'ask_user_question', arguments: { questions: [ @@ -207,7 +207,7 @@ describe('ask_user_question tool', () => { const controller = new AbortController() await ctx.tools.execute({ - callId: CallId('ask-2'), + callId: ToolCallId('ask-2'), name: 'ask_user_question', arguments: { questions: [{ id: 'continue', question: 'Continue?' }] }, signal: controller.signal, @@ -230,7 +230,7 @@ describe('ask_user_question tool', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('ask-3'), + callId: ToolCallId('ask-3'), name: 'ask_user_question', arguments: { questions: [{ id: 'continue', header: 'Confirm', question: 'Continue?' }] }, agent, @@ -245,7 +245,7 @@ describe('ask_user_question tool', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('ask-no-provider'), + callId: ToolCallId('ask-no-provider'), name: 'ask_user_question', arguments: { questions: [{ id: 'continue', question: 'Continue?' }] }, }) @@ -272,7 +272,7 @@ describe('ask_user_question tool', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('ask-delegated'), + callId: ToolCallId('ask-delegated'), name: 'ask_user_question', arguments: { questions: [{ id: 'continue', question: 'Continue?' }] }, agent: child, @@ -294,7 +294,7 @@ describe('ask_user_question tool', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('ask-empty'), + callId: ToolCallId('ask-empty'), name: 'ask_user_question', arguments: { questions: [] }, }) diff --git a/packages/interaction/user-approval/src/index.ts b/packages/interaction/user-approval/src/index.ts index b0618f6b3d..f787dae216 100644 --- a/packages/interaction/user-approval/src/index.ts +++ b/packages/interaction/user-approval/src/index.ts @@ -8,7 +8,7 @@ import { randomUUID } from 'node:crypto' import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import type { Agent } from '@deepseek-ai/dsh-agent' -import { createUserMessage, type CallId } from '@deepseek-ai/dsh-llm' +import { createUserMessage, type ToolCallId } from '@deepseek-ai/dsh-llm' import { scopeTarget } from '@deepseek-ai/dsh-scope' import type { Scoped } from '@deepseek-ai/dsh-scope' import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' @@ -44,7 +44,7 @@ declare module '@deepseek-ai/dsh-session/types' { 'approval/asked': { id: ApprovalRequestId toolName: string - callId?: CallId + callId?: ToolCallId reason?: string } /** @@ -163,7 +163,7 @@ export interface ApprovalRequest { * The exact tool call being decided, when the asker has one — lets a UI * attach the prompt to the tool call it already streamed. */ - readonly callId?: CallId + readonly callId?: ToolCallId /** The asker's human-readable explanation of WHY it is asking. */ readonly reason?: string /** diff --git a/packages/interaction/user-approval/tests/approval.spec.ts b/packages/interaction/user-approval/tests/approval.spec.ts index 3271ecc7a5..eb0484a1db 100644 --- a/packages/interaction/user-approval/tests/approval.spec.ts +++ b/packages/interaction/user-approval/tests/approval.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import type { Agent } from '@deepseek-ai/dsh-agent' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { carrierKeyOf, createScope } from '@deepseek-ai/dsh-scope' import type { Scope } from '@deepseek-ai/dsh-scope' import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session' @@ -60,7 +60,7 @@ describe('ApprovalService.request', () => { const ctx = await mounted() const { agent, appended } = fakeAgent() - const outcome = await ctx.approval.request(requestOf(agent, { callId: CallId('call-1'), reason: 'hook says ask' })) + const outcome = await ctx.approval.request(requestOf(agent, { callId: ToolCallId('call-1'), reason: 'hook says ask' })) expect(outcome).toBe('unavailable') expect(appended.map(e => e.type)).toEqual(['approval/asked', 'approval/decided']) @@ -95,7 +95,7 @@ describe('ApprovalService.request', () => { }) const request = requestOf(agent, { toolName: 'scoped-tool', - callId: CallId('scoped-call'), + callId: ToolCallId('scoped-call'), reason: 'scoped reason', }) diff --git a/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts b/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts index 29fc44a11b..4c6a81e48a 100644 --- a/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts +++ b/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime from '@deepseek-ai/dsh-tools' import AgentRegistry, { emitAgentEvent } from '@deepseek-ai/dsh-agent' @@ -92,7 +92,7 @@ function producer(overrides: Partial & JobHooks> = {}) { let callCounter = 0 function call(ctx: Context, name: string, args: unknown, agent?: Agent) { - return ctx.tools.execute({ signal: testToolSignal, callId: CallId(`call-${++callCounter}`), name, arguments: args, ...agent ? { agent } : {} }) + return ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId(`call-${++callCounter}`), name, arguments: args, ...agent ? { agent } : {} }) } function text(result: { content: { type: string; text?: string }[] }): string { diff --git a/packages/llm/llm-deepseek/src/translate.ts b/packages/llm/llm-deepseek/src/translate.ts index f1a6267355..89cb9da0a4 100644 --- a/packages/llm/llm-deepseek/src/translate.ts +++ b/packages/llm/llm-deepseek/src/translate.ts @@ -8,7 +8,7 @@ * @module dsh-llm-deepseek/translate */ -import { CallId, EMPTY_RESPONSE_CODE, LlmError } from '@deepseek-ai/dsh-llm' +import { ToolCallId, EMPTY_RESPONSE_CODE, LlmError } from '@deepseek-ai/dsh-llm' import type { ContentBlock, FinishReason, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm' import { DONE } from './sse.ts' import type { WireChunk, WireUsage } from './types.ts' @@ -68,7 +68,7 @@ function closeBlock(block: OpenBlock): ContentBlock { case 'reasoning': return { type: 'reasoning', text: block.text } case 'tool-call': return { type: 'tool-call', - id: CallId(block.callId ?? ''), + id: ToolCallId(block.callId ?? ''), name: block.name ?? '', arguments: block.text, } @@ -163,7 +163,7 @@ export async function* translate(payloads: AsyncIterable): AsyncGenerato yield { type: 'tool-call-delta', index: block.index, - id: CallId(block.callId ?? ''), + id: ToolCallId(block.callId ?? ''), ...block.name !== undefined ? { name: block.name } : {}, argumentsDelta: fragment, } diff --git a/packages/llm/llm-deepseek/tests/adapter.e2e.ts b/packages/llm/llm-deepseek/tests/adapter.e2e.ts index 5ac490b110..ecb0131819 100644 --- a/packages/llm/llm-deepseek/tests/adapter.e2e.ts +++ b/packages/llm/llm-deepseek/tests/adapter.e2e.ts @@ -4,7 +4,7 @@ import { join } from 'node:path' import { createHash } from 'node:crypto' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import LlmRuntime, { createUserMessage, CallId, ReasoningEffortId , createMessage } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, ToolCallId, ReasoningEffortId , createMessage } from '@deepseek-ai/dsh-llm' import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm' import AttachmentStore, { AttachmentId } from '@deepseek-ai/dsh-attachment' import type { @@ -218,7 +218,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', () createUserMessage({ content: [{ type: 'tool-result', - toolCallId: CallId(call!.id), + toolCallId: ToolCallId(call!.id), content: [{ type: 'text', text: 'Sunny, 22°C' }], }], source: { kind: 'plugin', plugin: 'test' }, diff --git a/packages/llm/llm-deepseek/tests/serialize.spec.ts b/packages/llm/llm-deepseek/tests/serialize.spec.ts index 742efc863d..2d7ed2976b 100644 --- a/packages/llm/llm-deepseek/tests/serialize.spec.ts +++ b/packages/llm/llm-deepseek/tests/serialize.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { AttachmentError, AttachmentId } from '@deepseek-ai/dsh-attachment' import type { AttachmentStore, ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment' -import { createUserMessage, CallId, ReasoningEffortId, createMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, ReasoningEffortId, createMessage } from '@deepseek-ai/dsh-llm' import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm' import { serializeMessages, @@ -77,7 +77,7 @@ describe('serializeMessages', () => { role: 'assistant', content: [ { type: 'reasoning', text: 'I should check the weather.' }, - { type: 'tool-call', id: CallId('call-1'), name: 'get_weather', arguments: '{"city":"Paris"}' }, + { type: 'tool-call', id: ToolCallId('call-1'), name: 'get_weather', arguments: '{"city":"Paris"}' }, ], source: { kind: 'plugin', plugin: 'test' }, }), @@ -97,8 +97,8 @@ describe('serializeMessages', () => { createMessage({ role: 'assistant', content: [ - { type: 'tool-call', id: CallId('a'), name: 'one', arguments: '{}' }, - { type: 'tool-call', id: CallId('b'), name: 'two', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('a'), name: 'one', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('b'), name: 'two', arguments: '{}' }, ], source: { kind: 'plugin', plugin: 'test' }, }), @@ -112,7 +112,7 @@ describe('serializeMessages', () => { createUserMessage({ content: [{ type: 'tool-result', - toolCallId: CallId('call-1'), + toolCallId: ToolCallId('call-1'), content: [{ type: 'text', text: 'Sunny 22C' }], }], source: { kind: 'plugin', plugin: 'test' }, @@ -124,7 +124,7 @@ describe('serializeMessages', () => { it('sends a sentinel for empty tool-result content', () => { const wire = serializeMessages([ createUserMessage({ - content: [{ type: 'tool-result', toolCallId: CallId('call-1'), content: [] }], + content: [{ type: 'tool-result', toolCallId: ToolCallId('call-1'), content: [] }], source: { kind: 'plugin', plugin: 'test' }, }), ]) @@ -136,7 +136,7 @@ describe('serializeMessages', () => { createUserMessage({ content: [ { type: 'text', text: 'context note' }, - { type: 'tool-result', toolCallId: CallId('call-1'), content: [{ type: 'text', text: 'ok' }] }, + { type: 'tool-result', toolCallId: ToolCallId('call-1'), content: [{ type: 'text', text: 'ok' }] }, ], source: { kind: 'plugin', plugin: 'test' }, }), @@ -359,7 +359,7 @@ describe('image serialization', () => { createUserMessage({ content: [{ type: 'tool-result', - toolCallId: CallId('first'), + toolCallId: ToolCallId('first'), content: [{ type: 'image', attachment: imageRef() }], }], source: { kind: 'plugin', plugin: 'test' }, @@ -367,7 +367,7 @@ describe('image serialization', () => { createUserMessage({ content: [{ type: 'tool-result', - toolCallId: CallId('second'), + toolCallId: ToolCallId('second'), content: [ { type: 'text', text: 'caption' }, { type: 'image', attachment: imageRef('image/jpeg') }, @@ -402,7 +402,7 @@ describe('image serialization', () => { { type: 'chart', data: 'ignored' } as unknown as ContentBlock, { type: 'tool-result', - toolCallId: CallId('result'), + toolCallId: ToolCallId('result'), content: [{ type: 'text', text: 'ok' }], }, ], @@ -423,14 +423,14 @@ describe('image serialization', () => { content: [ { type: 'tool-result', - toolCallId: CallId('nested'), + toolCallId: ToolCallId('nested'), content: [{ type: 'tool-result', - toolCallId: CallId('inner'), + toolCallId: ToolCallId('inner'), content: [{ type: 'text', text: 'inside' }], }], }, - { type: 'tool-result', toolCallId: CallId('empty'), content: [] }, + { type: 'tool-result', toolCallId: ToolCallId('empty'), content: [] }, ], source: { kind: 'plugin', plugin: 'test' }, })] @@ -449,7 +449,7 @@ describe('image serialization', () => { const imageResult = (id: string) => createUserMessage({ content: [{ type: 'tool-result', - toolCallId: CallId(id), + toolCallId: ToolCallId(id), content: [{ type: 'image', attachment: imageRef() }], }], source: { kind: 'plugin' as const, plugin: 'test' }, @@ -607,7 +607,7 @@ describe('review fixes: assistant content shapes', () => { it('serializes tool-call turns with empty string content, not null', () => { const wire = serializeMessages([createMessage({ role: 'assistant', - content: [{ type: 'tool-call', id: CallId('c'), name: 'f', arguments: '{}' }], + content: [{ type: 'tool-call', id: ToolCallId('c'), name: 'f', arguments: '{}' }], source: { kind: 'plugin', plugin: 'test' }, })]) expect(wire[0]).toMatchObject({ content: '' }) diff --git a/packages/llm/llm-pi-ai/src/context.ts b/packages/llm/llm-pi-ai/src/context.ts index d66a48115d..582dd8ac07 100644 --- a/packages/llm/llm-pi-ai/src/context.ts +++ b/packages/llm/llm-pi-ai/src/context.ts @@ -4,7 +4,7 @@ * @module dsh-llm-pi-ai/context */ -import { CallId, contentHasImage, LlmError, offloadRequestImages } from '@deepseek-ai/dsh-llm' +import { ToolCallId, contentHasImage, LlmError, offloadRequestImages } from '@deepseek-ai/dsh-llm' import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm' import type { AttachmentStore } from '@deepseek-ai/dsh-attachment' import type { Context as PiContext, ImageContent, Message as PiMessage, TextContent, Tool as PiTool } from '@earendil-works/pi-ai' @@ -97,7 +97,7 @@ function piContext(options: GenerateOptions, messages: PiMessage[]): PiContext { } function textOnlyContext(options: GenerateOptions, onReplayDegrade?: (reason: string) => void): PiContext { - const toolNames = new Map() + const toolNames = new Map() const messages: PiMessage[] = [] for (const message of options.messages) { if (contentHasImage(message.content)) { @@ -109,7 +109,7 @@ function textOnlyContext(options: GenerateOptions, onReplayDegrade?: (reason: st } if (message.role === 'assistant') { const assistant = toPiAssistant(message, onReplayDegrade) - for (const block of assistant.content) if (block.type === 'toolCall') toolNames.set(CallId(block.id), block.name) + for (const block of assistant.content) if (block.type === 'toolCall') toolNames.set(ToolCallId(block.id), block.name) messages.push(assistant) continue } @@ -183,7 +183,7 @@ async function toPiContextWithImages( ): Promise { assertSupportedImageRoles(options.messages) const requestMessages = offloadRequestImages(options.messages, maxRequestImageBytes) - const toolNames = new Map() + const toolNames = new Map() const messages: PiMessage[] = [] for (const message of requestMessages) { @@ -197,7 +197,7 @@ async function toPiContextWithImages( if (message.role === 'assistant') { const assistant = toPiAssistant(message, onReplayDegrade) for (const block of assistant.content) { - if (block.type === 'toolCall') toolNames.set(CallId(block.id), block.name) + if (block.type === 'toolCall') toolNames.set(ToolCallId(block.id), block.name) } messages.push(assistant) continue diff --git a/packages/llm/llm-pi-ai/src/stream.ts b/packages/llm/llm-pi-ai/src/stream.ts index d45df45a39..3270686ee9 100644 --- a/packages/llm/llm-pi-ai/src/stream.ts +++ b/packages/llm/llm-pi-ai/src/stream.ts @@ -8,7 +8,7 @@ * @module dsh-llm-pi-ai/stream */ -import { CallId, CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, QUOTA_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm' +import { ToolCallId, CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, QUOTA_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm' import type { FinishReason, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm' import { isContextOverflow } from '@earendil-works/pi-ai' import type { AssistantMessage, AssistantMessageEvent, Usage as PiUsage } from '@earendil-works/pi-ai' @@ -168,7 +168,7 @@ export async function* toStreamChunks( yield { type: 'tool-call-delta', index: event.contentIndex, - id: CallId(known?.id ?? ''), + id: ToolCallId(known?.id ?? ''), ...known?.name !== undefined && known.name.length > 0 ? { name: known.name } : {}, argumentsDelta: event.delta, } @@ -180,7 +180,7 @@ export async function* toStreamChunks( index: event.contentIndex, block: { type: 'tool-call', - id: CallId(event.toolCall.id), + id: ToolCallId(event.toolCall.id), name: event.toolCall.name, // pi-ai hands back the PARSED arguments; the harness vocabulary // keeps the raw string. diff --git a/packages/llm/llm-pi-ai/tests/adapter.e2e.ts b/packages/llm/llm-pi-ai/tests/adapter.e2e.ts index 0a7a589e07..99b2be8721 100644 --- a/packages/llm/llm-pi-ai/tests/adapter.e2e.ts +++ b/packages/llm/llm-pi-ai/tests/adapter.e2e.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import LlmRuntime, { createUserMessage, CallId, ReasoningEffortId } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, ToolCallId, ReasoningEffortId } from '@deepseek-ai/dsh-llm' import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm' import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai' import type { PiAiProviderProfile } from '@deepseek-ai/dsh-llm-pi-ai' @@ -129,7 +129,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-pi-ai e2e (real API)', () => createUserMessage({ content: [{ type: 'tool-result', - toolCallId: CallId(call!.id), + toolCallId: ToolCallId(call!.id), content: [{ type: 'text', text: 'Sunny, 22°C' }], }], source: { kind: 'plugin', plugin: 'test' }, diff --git a/packages/llm/llm-pi-ai/tests/context.spec.ts b/packages/llm/llm-pi-ai/tests/context.spec.ts index b82c6b63f5..ce05054e53 100644 --- a/packages/llm/llm-pi-ai/tests/context.spec.ts +++ b/packages/llm/llm-pi-ai/tests/context.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { AttachmentId } from '@deepseek-ai/dsh-attachment' import type { AttachmentStore, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' -import { CallId, createMessage, createUserMessage, OFFLOADED_IMAGE_TEXT } from '@deepseek-ai/dsh-llm' +import { ToolCallId, createMessage, createUserMessage, OFFLOADED_IMAGE_TEXT } from '@deepseek-ai/dsh-llm' import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm' import { toPiContext } from '../src/context.ts' import { toPiAssistant } from '../src/replay.ts' @@ -44,7 +44,7 @@ describe('pi-ai request context conversion', () => { }) it('converts complete text-only history and rejects nested images without storage', () => { - const callId = CallId('call-1') + const callId = ToolCallId('call-1') expect(toPiContext(request([ history('system', [{ type: 'text', text: 'history system' }]), history('assistant', [{ type: 'tool-call', id: callId, name: 'lookup', arguments: '{}' }]), @@ -81,8 +81,8 @@ describe('pi-ai request context conversion', () => { }) it('resolves user and tool-result images while preserving explicit fallbacks', async () => { - const callId = CallId('missing-call') - const knownCallId = CallId('known-call') + const callId = ToolCallId('missing-call') + const knownCallId = ToolCallId('known-call') const context = await toPiContext(request([ user([{ type: 'text', text: '' }]), history('assistant', [ @@ -141,7 +141,7 @@ describe('pi-ai request context conversion', () => { }) it('recursively converts nested tool-result text and images', async () => { - const callId = CallId('nested-call') + const callId = ToolCallId('nested-call') const context = await toPiContext(request([user([{ type: 'tool-result', toolCallId: callId, @@ -173,7 +173,7 @@ describe('pi-ai request context conversion', () => { }) it('flattens nested text-only tool results and ignores other block types without storage', () => { - const callId = CallId('nested-text') + const callId = ToolCallId('nested-text') expect(toPiContext(request([user([{ type: 'tool-result', toolCallId: callId, @@ -197,7 +197,7 @@ describe('pi-ai request context conversion', () => { const readImage = vi.fn(() => Promise.resolve({ ref: { ...ref, bytes: 3 }, data: Uint8Array.of(1, 2, 3) })) const store = { readImage } as unknown as AttachmentStore const sized: ImageAttachmentRef = { ...ref, bytes: 3 } - const callId = CallId('shot-call') + const callId = ToolCallId('shot-call') // Three 3-byte images cost 4 base64 characters each (12 total); a bound of // 8 forces exactly the oldest one out, including one nested in a tool result. const context = await toPiContext(request([ @@ -280,12 +280,12 @@ describe('pi-ai request context conversion', () => { }) it('keeps empty text-only users while separating result-only messages', () => { - const callId = CallId('unknown-call') + const callId = ToolCallId('unknown-call') expect(toPiContext(request([ user([]), history('assistant', [ { type: 'text', text: 'answer' }, - { type: 'tool-call', id: CallId('other-call'), name: 'lookup', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('other-call'), name: 'lookup', arguments: '{}' }, ]), user([{ type: 'tool-result', diff --git a/packages/llm/llm-pi-ai/tests/convert.spec.ts b/packages/llm/llm-pi-ai/tests/convert.spec.ts index 2a3b41b0c4..3dcee2f58e 100644 --- a/packages/llm/llm-pi-ai/tests/convert.spec.ts +++ b/packages/llm/llm-pi-ai/tests/convert.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { AttachmentId } from '@deepseek-ai/dsh-attachment' import type { AttachmentStore } from '@deepseek-ai/dsh-attachment' -import { createUserMessage, CallId, CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, createMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, createMessage } from '@deepseek-ai/dsh-llm' import type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm' import type { AssistantMessage, AssistantMessageEvent, Usage } from '@earendil-works/pi-ai' import { toPiContext } from '../src/context.ts' @@ -112,14 +112,14 @@ describe('toPiContext', () => { messages: [createUserMessage({ content: [{ type: 'tool-result', - toolCallId: CallId('outer'), + toolCallId: ToolCallId('outer'), content: [ - { type: 'tool-result', toolCallId: CallId('empty'), content: [] }, + { type: 'tool-result', toolCallId: ToolCallId('empty'), content: [] }, { type: 'text', text: 'before' }, - { type: 'tool-result', toolCallId: CallId('text'), content: [{ type: 'text', text: 'middle' }] }, + { type: 'tool-result', toolCallId: ToolCallId('text'), content: [{ type: 'text', text: 'middle' }] }, { type: 'tool-result', - toolCallId: CallId('inner'), + toolCallId: ToolCallId('inner'), content: [ { type: 'image', attachment }, { type: 'text', text: 'after' }, @@ -171,7 +171,7 @@ describe('toPiContext', () => { content: [ { type: 'reasoning', text: 'hmm' }, { type: 'text', text: 'calling' }, - { type: 'tool-call', id: CallId('c1'), name: 'f', arguments: '{"a":1}' }, + { type: 'tool-call', id: ToolCallId('c1'), name: 'f', arguments: '{"a":1}' }, ], source: { kind: 'plugin', plugin: 'test' }, })], @@ -222,7 +222,7 @@ describe('toPiContext', () => { model: 'm', messages: [createMessage({ role: 'assistant', - content: [{ type: 'tool-call', id: CallId('c1'), name: 'f', arguments: '{broken' }], + content: [{ type: 'tool-call', id: ToolCallId('c1'), name: 'f', arguments: '{broken' }], source: { kind: 'plugin', plugin: 'test' }, })], }) @@ -236,7 +236,7 @@ describe('toPiContext', () => { model: 'm', messages: [createMessage({ role: 'assistant', - content: [{ type: 'tool-call', id: CallId('c1'), name: 'f', arguments: '[1,2]' }], + content: [{ type: 'tool-call', id: ToolCallId('c1'), name: 'f', arguments: '[1,2]' }], source: { kind: 'plugin', plugin: 'test' }, })], }) @@ -250,16 +250,16 @@ describe('toPiContext', () => { messages: [ createMessage({ role: 'assistant', - content: [{ type: 'tool-call', id: CallId('c1'), name: 'get_weather', arguments: '{}' }], + content: [{ type: 'tool-call', id: ToolCallId('c1'), name: 'get_weather', arguments: '{}' }], source: { kind: 'plugin', plugin: 'test' }, }), createUserMessage({ content: [{ type: 'tool-result', - toolCallId: CallId('c1'), + toolCallId: ToolCallId('c1'), content: [ { type: 'text', text: 'Sunny' }, - { type: 'tool-result', toolCallId: CallId('nested'), content: [{ type: 'text', text: '!' }] }, + { type: 'tool-result', toolCallId: ToolCallId('nested'), content: [{ type: 'text', text: '!' }] }, { type: 'chart', data: 'ignored' } as unknown as ContentBlock, ], }], @@ -282,7 +282,7 @@ describe('toPiContext', () => { provider: 'deepseek', model: 'm', messages: [createUserMessage({ - content: [{ type: 'tool-result', toolCallId: CallId('zz'), content: [], isError: true }], + content: [{ type: 'tool-result', toolCallId: ToolCallId('zz'), content: [], isError: true }], source: { kind: 'plugin', plugin: 'test' }, })], }) @@ -306,7 +306,7 @@ describe('toPiContext', () => { createUserMessage({ content: [ { type: 'text', text: 'note' }, - { type: 'tool-result', toolCallId: CallId('c1'), content: [{ type: 'text', text: 'ok' }] }, + { type: 'tool-result', toolCallId: ToolCallId('c1'), content: [{ type: 'text', text: 'ok' }] }, ], source: { kind: 'plugin', plugin: 'test' }, }), @@ -353,7 +353,7 @@ describe('toPiContext', () => { content: [ { type: 'reasoning', text: 'private reasoning' }, { type: 'text', text: 'calling' }, - { type: 'tool-call', id: CallId('c1'), name: 'f', arguments: '{"a":1}' }, + { type: 'tool-call', id: ToolCallId('c1'), name: 'f', arguments: '{"a":1}' }, ], source: { kind: 'model', @@ -394,7 +394,7 @@ describe('toPiContext', () => { content: [ { type: 'reasoning', text: 'private reasoning' }, { type: 'text', text: 'calling' }, - { type: 'tool-call', id: CallId('c1'), name: 'f', arguments: '{"a":1}' }, + { type: 'tool-call', id: ToolCallId('c1'), name: 'f', arguments: '{"a":1}' }, ], source: { kind: 'model', diff --git a/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts b/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts index 1fe529336f..7b74665ecf 100644 --- a/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts +++ b/packages/llm/llm-pi-ai/tests/provider-apis.e2e.ts @@ -8,7 +8,7 @@ import type { SaveImageAttachment, StoredImageAttachment, } from '@deepseek-ai/dsh-attachment' -import LlmRuntime, { createUserMessage, CallId } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm' import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai' import type { PiAiReplayResponse } from '../src/replay.ts' @@ -192,7 +192,7 @@ for (const profile of providerCases) { createUserMessage({ content: [{ type: 'tool-result', - toolCallId: CallId(call!.id), + toolCallId: ToolCallId(call!.id), content: [{ type: 'text', text: 'The code blue means ocean.' }], }], source: { kind: 'plugin', plugin: 'test' }, diff --git a/packages/llm/llm-retry/tests/retry.spec.ts b/packages/llm/llm-retry/tests/retry.spec.ts index c2643a6d45..bc1ed6bab6 100644 --- a/packages/llm/llm-retry/tests/retry.spec.ts +++ b/packages/llm/llm-retry/tests/retry.spec.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import type { Fiber } from '@deepseek-ai/cordis' -import LlmRuntime, { createUserMessage, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, resolveRetryPolicy } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, ToolCallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, resolveRetryPolicy } from '@deepseek-ai/dsh-llm' import type { AlwaysRetryPolicyConfig, BackoffConfig, @@ -60,7 +60,7 @@ class ScriptedAdapter extends LlmAdapter { } async function* partialToolFailure(error: Error): AsyncGenerator { - const id = CallId('discarded-call') + const id = ToolCallId('discarded-call') yield { type: 'block-start', index: 0, blockType: 'text' } yield { type: 'text-delta', index: 0, text: 'discarded partial output' } yield { type: 'block-end', index: 0, block: { type: 'text', text: 'discarded partial output' } } diff --git a/packages/llm/llm/src/assembler.ts b/packages/llm/llm/src/assembler.ts index 7dc5221c9b..96472d09e9 100644 --- a/packages/llm/llm/src/assembler.ts +++ b/packages/llm/llm/src/assembler.ts @@ -6,7 +6,7 @@ * @module @deepseek-ai/dsh-llm/assembler */ -import { CallId } from './brand.ts' +import { ToolCallId } from './brand.ts' import { assertNever } from './never.ts' import { createMessage } from './message.ts' import type { Message, MessageSource } from './message.ts' @@ -15,7 +15,7 @@ import type { ContentBlock, FinishReason, ReplayEnvelope, StreamChunk, TokenUsag interface PartialBlock { blockType: string text: string - toolCallId?: CallId + toolCallId?: ToolCallId toolCallName?: string toolCallArguments: string /** Set by `block-end` — authoritative, and freezes the partial. */ @@ -111,7 +111,7 @@ export class BlockAssembler { case 'reasoning': return { type: 'reasoning', text: partial.text } case 'tool-call': return { type: 'tool-call', - id: partial.toolCallId ?? CallId(`call-${index}`), + id: partial.toolCallId ?? ToolCallId(`call-${index}`), name: partial.toolCallName ?? '', arguments: partial.toolCallArguments, } diff --git a/packages/llm/llm/src/brand.ts b/packages/llm/llm/src/brand.ts index e0b3a38c8e..42410829fa 100644 --- a/packages/llm/llm/src/brand.ts +++ b/packages/llm/llm/src/brand.ts @@ -28,15 +28,15 @@ export function MessageId(id: string): MessageId { * Correlates a model-issued tool call with its result. Provider-issued for * real adapters; synthesized by mocks/assembler fallbacks. */ -export type CallId = Branded<'CallId'> +export type ToolCallId = Branded<'ToolCallId'> /** - * Brand a string as a {@link CallId}. + * Brand a string as a {@link ToolCallId}. * @param id - the provider-issued (or synthesized) call id. * @returns the same string, branded; no validation is performed. */ -export function CallId(id: string): CallId { - return id as CallId +export function ToolCallId(id: string): ToolCallId { + return id as ToolCallId } /** Provider-issued request identifier retained for diagnostics across package boundaries. */ diff --git a/packages/llm/llm/src/message.ts b/packages/llm/llm/src/message.ts index 608b56475d..1f4181aae5 100644 --- a/packages/llm/llm/src/message.ts +++ b/packages/llm/llm/src/message.ts @@ -1,6 +1,6 @@ /** Message value types, identity, and immutable construction helpers. */ -import { MessageId, type CallId } from './brand.ts' +import { MessageId, type ToolCallId } from './brand.ts' import { deepFreeze } from './call-config.ts' import type { ContentBlock, StreamChunk, ToolResultBlock } from './types.ts' @@ -26,7 +26,7 @@ export interface ModelMessageSource extends AssistantProvenance { /** Required source of a user-role message carrying one tool result. */ export interface ToolMessageSource { kind: 'tool' - callId: CallId + callId: ToolCallId } /** @@ -218,7 +218,7 @@ export function createAssistantMessage( /** Input whose acceptance creates one tool-result message. */ export interface ToolResultMessageInput { - readonly callId: CallId + readonly callId: ToolCallId readonly content: ContentBlock[] readonly isError: boolean } diff --git a/packages/llm/llm/src/types.ts b/packages/llm/llm/src/types.ts index 8c5be187dd..ad295a7d32 100644 --- a/packages/llm/llm/src/types.ts +++ b/packages/llm/llm/src/types.ts @@ -6,7 +6,7 @@ import type { Branded } from '@deepseek-ai/dsh-brand' import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' -import type { CallId, ProviderRequestId, ReasoningEffortId } from './brand.ts' +import type { ToolCallId, ProviderRequestId, ReasoningEffortId } from './brand.ts' import type { Message } from './message.ts' declare module '@deepseek-ai/cordis' { @@ -78,7 +78,7 @@ export interface ImageBlock { export interface ToolCallBlock { type: 'tool-call' /** Provider-issued call id; correlates with the matching tool result. */ - id: CallId + id: ToolCallId name: string /** Raw JSON string as produced by the model. */ arguments: string @@ -87,7 +87,7 @@ export interface ToolCallBlock { /** The result of a tool invocation, sent back to the model. */ export interface ToolResultBlock { type: 'tool-result' - toolCallId: CallId + toolCallId: ToolCallId content: ContentBlock[] isError?: boolean } @@ -313,7 +313,7 @@ export type StreamChunk = | { type: 'block-start'; index: number; blockType: ContentBlockType } | { type: 'text-delta'; index: number; text: string } | { type: 'reasoning-delta'; index: number; text: string } - | { type: 'tool-call-delta'; index: number; id: CallId; name?: string; argumentsDelta: string } + | { type: 'tool-call-delta'; index: number; id: ToolCallId; name?: string; argumentsDelta: string } | { type: 'block-end'; index: number; block: ContentBlock } | { type: 'usage'; usage: TokenUsage } | { diff --git a/packages/llm/llm/tests/assembler.spec.ts b/packages/llm/llm/tests/assembler.spec.ts index 2ecfcfd860..5aff02d41b 100644 --- a/packages/llm/llm/tests/assembler.spec.ts +++ b/packages/llm/llm/tests/assembler.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { BlockAssembler, CallId, type StreamChunk } from '@deepseek-ai/dsh-llm' +import { BlockAssembler, ToolCallId, type StreamChunk } from '@deepseek-ai/dsh-llm' describe('BlockAssembler', () => { it('assembles interleaved text, reasoning, and tool-call deltas', () => { @@ -11,8 +11,8 @@ describe('BlockAssembler', () => { { type: 'text-delta', index: 1, text: 'Hello' }, { type: 'text-delta', index: 1, text: ' world' }, { type: 'block-start', index: 2, blockType: 'tool-call' }, - { type: 'tool-call-delta', index: 2, id: CallId('call-1'), name: 'echo', argumentsDelta: '{"text":' }, - { type: 'tool-call-delta', index: 2, id: CallId('call-1'), argumentsDelta: '"hi"}' }, + { type: 'tool-call-delta', index: 2, id: ToolCallId('call-1'), name: 'echo', argumentsDelta: '{"text":' }, + { type: 'tool-call-delta', index: 2, id: ToolCallId('call-1'), argumentsDelta: '"hi"}' }, { type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } }, { type: 'finish', reason: { kind: 'tool-calls' } }, ] @@ -22,7 +22,7 @@ describe('BlockAssembler', () => { expect(assembler.blocks()).toEqual([ { type: 'reasoning', text: 'thinking…' }, { type: 'text', text: 'Hello world' }, - { type: 'tool-call', id: CallId('call-1'), name: 'echo', arguments: '{"text":"hi"}' }, + { type: 'tool-call', id: ToolCallId('call-1'), name: 'echo', arguments: '{"text":"hi"}' }, ]) expect(assembler.usage).toEqual({ inputTokens: 10, outputTokens: 5 }) expect(assembler.finish).toEqual({ kind: 'tool-calls' }) @@ -93,11 +93,11 @@ describe('BlockAssembler', () => { it('ignores tool-call-delta stragglers after block-end', () => { const assembler = new BlockAssembler() assembler.push({ type: 'block-start', index: 0, blockType: 'tool-call' }) - assembler.push({ type: 'tool-call-delta', index: 0, id: CallId('c1'), name: 'echo', argumentsDelta: '{}' }) - assembler.push({ type: 'block-end', index: 0, block: { type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{}' } }) + assembler.push({ type: 'tool-call-delta', index: 0, id: ToolCallId('c1'), name: 'echo', argumentsDelta: '{}' }) + assembler.push({ type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('c1'), name: 'echo', arguments: '{}' } }) // straggler after block-end — partial.block is set, so early return - assembler.push({ type: 'tool-call-delta', index: 0, id: CallId('c1'), name: 'evil', argumentsDelta: 'oops' }) - expect(assembler.blocks()).toEqual([{ type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{}' }]) + assembler.push({ type: 'tool-call-delta', index: 0, id: ToolCallId('c1'), name: 'evil', argumentsDelta: 'oops' }) + expect(assembler.blocks()).toEqual([{ type: 'tool-call', id: ToolCallId('c1'), name: 'echo', arguments: '{}' }]) }) it('assembles tool-call with generated id fallback when no id provided', () => { @@ -106,7 +106,7 @@ describe('BlockAssembler', () => { // No id and no name provided — uses fallback id `call-{index}` and empty name const blocks = assembler.blocks() expect(blocks).toEqual([ - { type: 'tool-call', id: CallId('call-0'), name: '', arguments: '{}' }, + { type: 'tool-call', id: ToolCallId('call-0'), name: '', arguments: '{}' }, ]) }) @@ -127,7 +127,7 @@ describe('BlockAssembler replay metadata', () => { assembler.push({ type: 'block-end', index: 1, - block: { type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{"text":' }, + block: { type: 'tool-call', id: ToolCallId('c1'), name: 'echo', arguments: '{"text":' }, }) assembler.push({ type: 'block-end', index: 2, block: { type: 'reasoning', text: 'tail' } }) assembler.push({ @@ -164,7 +164,7 @@ describe('BlockAssembler replay metadata', () => { assembler.push({ type: 'block-end', index: 1, - block: { type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{}' }, + block: { type: 'tool-call', id: ToolCallId('c1'), name: 'echo', arguments: '{}' }, }) assembler.push({ type: 'finish', reason: { kind: 'tool-calls' }, replayState }) @@ -178,7 +178,7 @@ describe('BlockAssembler replay metadata', () => { assembler.push({ type: 'block-end', index: 1, - block: { type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{"text":' }, + block: { type: 'tool-call', id: ToolCallId('c1'), name: 'echo', arguments: '{"text":' }, }) assembler.push({ type: 'finish', reason: { kind: 'max-tokens' }, replayState }) diff --git a/packages/llm/llm/tests/invariant.spec.ts b/packages/llm/llm/tests/invariant.spec.ts index 2d5744d22f..fdb538f5dc 100644 --- a/packages/llm/llm/tests/invariant.spec.ts +++ b/packages/llm/llm/tests/invariant.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import LlmRuntime, { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { ToolCallId, LlmAdapter } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import * as LlmInvariant from '@deepseek-ai/dsh-llm/invariant' import InvariantRegistry from '@deepseek-ai/dsh-invariants' @@ -38,8 +38,8 @@ describe('LLM stream invariants', () => { { type: 'block-end', index: 1, block: { type: 'reasoning', text: 'b' } }, { type: 'block-end', index: 0, block: { type: 'text', text: 'a' } }, { type: 'block-start', index: 2, blockType: 'tool-call' }, - { type: 'tool-call-delta', index: 2, id: CallId('c1'), name: 'echo', argumentsDelta: '{}' }, - { type: 'block-end', index: 2, block: { type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{}' } }, + { type: 'tool-call-delta', index: 2, id: ToolCallId('c1'), name: 'echo', argumentsDelta: '{}' }, + { type: 'block-end', index: 2, block: { type: 'tool-call', id: ToolCallId('c1'), name: 'echo', arguments: '{}' } }, { type: 'usage', usage: { inputTokens: 1, outputTokens: 1 } }, finish, ] diff --git a/packages/llm/llm/tests/message.spec.ts b/packages/llm/llm/tests/message.spec.ts index 846d37c9df..3d458611d2 100644 --- a/packages/llm/llm/tests/message.spec.ts +++ b/packages/llm/llm/tests/message.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { - CallId, + ToolCallId, createAssistantMessage, createToolResultMessage, createUserMessage, @@ -75,7 +75,7 @@ describe('message construction', () => { }) it('couples tool-result content and its cited call seq to one call identity', () => { - const callId = CallId('call-1') + const callId = ToolCallId('call-1') const message = createToolResultMessage({ callId, content: [{ type: 'text', text: 'result' }], diff --git a/packages/llm/llm/tests/properties.spec.ts b/packages/llm/llm/tests/properties.spec.ts index 31d07c1a47..22f854b378 100644 --- a/packages/llm/llm/tests/properties.spec.ts +++ b/packages/llm/llm/tests/properties.spec.ts @@ -11,7 +11,7 @@ import { describe, expect, it } from 'vitest' import fc from 'fast-check' import { BlockAssembler } from '@deepseek-ai/dsh-llm' import type { StreamChunk } from '@deepseek-ai/dsh-llm' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' // A small pool of indices so collisions (duplicate-index bugs) are common. const indexArb = fc.integer({ min: 0, max: 4 }) @@ -24,7 +24,7 @@ const blockEndArb = (index: number): fc.Arbitrary => fc.oneof( { type: 'block-end', index, block: { type: 'reasoning', text: r.text } } )), fc.record({ id: fc.string({ minLength: 1 }), name: fc.string(), args: fc.string() }).map((r): StreamChunk => ( - { type: 'block-end', index, block: { type: 'tool-call', id: CallId(r.id), name: r.name, arguments: r.args } } + { type: 'block-end', index, block: { type: 'tool-call', id: ToolCallId(r.id), name: r.name, arguments: r.args } } )), ) @@ -36,7 +36,7 @@ const chunkArb: fc.Arbitrary = indexArb.chain(index => fc.oneof( fc.string().map((text): StreamChunk => ({ type: 'text-delta', index, text })), fc.string().map((text): StreamChunk => ({ type: 'reasoning-delta', index, text })), fc.record({ id: fc.string({ minLength: 1 }), argumentsDelta: fc.string() }) - .map((r): StreamChunk => ({ type: 'tool-call-delta', index, id: CallId(r.id), argumentsDelta: r.argumentsDelta })), + .map((r): StreamChunk => ({ type: 'tool-call-delta', index, id: ToolCallId(r.id), argumentsDelta: r.argumentsDelta })), blockEndArb(index), fc.constant({ type: 'usage', usage: { inputTokens: 1, outputTokens: 1 } }), fc.constant({ type: 'finish', reason: { kind: 'stop' } }), diff --git a/packages/llm/token-meter/tests/token-meter.spec.ts b/packages/llm/token-meter/tests/token-meter.spec.ts index 074f18fb76..e08a36b75a 100644 --- a/packages/llm/token-meter/tests/token-meter.spec.ts +++ b/packages/llm/token-meter/tests/token-meter.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, expectTypeOf, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { createUserMessage, CallId, createMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, createMessage } from '@deepseek-ai/dsh-llm' import type { ContentBlock, Message, TokenUsage } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, SessionId, canonicalHeader } from '@deepseek-ai/dsh-session' import type { EpochHeader, SessionEvent } from '@deepseek-ai/dsh-session' @@ -134,10 +134,10 @@ describe('TokenMeter pricing', () => { const blocks: ContentBlock[] = [ { type: 'text', text: 'abcd' }, { type: 'reasoning', text: 'ab' }, - { type: 'tool-call', id: CallId('c'), name: 'read', arguments: '{"x":1}' }, + { type: 'tool-call', id: ToolCallId('c'), name: 'read', arguments: '{"x":1}' }, { type: 'tool-result', - toolCallId: CallId('c'), + toolCallId: ToolCallId('c'), content: [{ type: 'text', text: 'xy' }], isError: false, }, diff --git a/packages/mcp/mcp-client/tests/mcp-client.e2e.ts b/packages/mcp/mcp-client/tests/mcp-client.e2e.ts index cec7451db2..ee3db70a77 100644 --- a/packages/mcp/mcp-client/tests/mcp-client.e2e.ts +++ b/packages/mcp/mcp-client/tests/mcp-client.e2e.ts @@ -22,7 +22,7 @@ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' import LocalAttachmentStore from '@deepseek-ai/dsh-attachment-local' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime from '@deepseek-ai/dsh-tools' -import { CallId, LlmAdapter, LlmRuntime } from '@deepseek-ai/dsh-llm' +import { ToolCallId, LlmAdapter, LlmRuntime } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm' import { apply } from '@deepseek-ai/dsh-mcp-client/src/index.ts' import { publicToolName } from '@deepseek-ai/dsh-mcp-client/src/tools.ts' @@ -87,8 +87,8 @@ function textOf(block: unknown): string { } let callSeq = 0 -function nextCallId(): CallId { - return CallId(`e2e-${++callSeq}`) +function nextCallId(): ToolCallId { + return ToolCallId(`e2e-${++callSeq}`) } // ---- Fixture server tests ---- diff --git a/packages/mcp/mcp-client/tests/mcp-client.spec.ts b/packages/mcp/mcp-client/tests/mcp-client.spec.ts index 164c230c56..94a6573c91 100644 --- a/packages/mcp/mcp-client/tests/mcp-client.spec.ts +++ b/packages/mcp/mcp-client/tests/mcp-client.spec.ts @@ -4,7 +4,7 @@ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js' import { Context } from '@deepseek-ai/cordis' import AttachmentStore, { AttachmentError, AttachmentId } from '@deepseek-ai/dsh-attachment' import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, StoredImageAttachment } from '@deepseek-ai/dsh-attachment' -import { CallId, LlmAdapter, LlmRuntime } from '@deepseek-ai/dsh-llm' +import { ToolCallId, LlmAdapter, LlmRuntime } from '@deepseek-ai/dsh-llm' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' @@ -227,7 +227,7 @@ describe('syncTools', () => { expect(ctx.tools.get('search')).toBeDefined() expect(ctx.tools.get('mcp__srv__search')).toBeDefined() - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'search', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'search', arguments: {} }) expect(result.content[0]).toEqual({ type: 'text', text: 'native' }) }) @@ -364,14 +364,14 @@ describe('syncTools', () => { const missing = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('missing'), name: 'mcp__srv__supported', arguments: {}, + callId: ToolCallId('missing'), name: 'mcp__srv__supported', arguments: {}, }) expect(missing.error).toMatchObject({ info: { code: 'INVALID_TOOL_OUTPUT' } }) expect(missing.error?.message).toContain('structuredContent') const fallback = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('fallback'), name: 'mcp__srv__future-schema', arguments: {}, + callId: ToolCallId('fallback'), name: 'mcp__srv__future-schema', arguments: {}, }) if (fallback.isError) throw new Error('unsupported schema must use the bridge fallback') expect(fallback.value).toEqual({ @@ -398,7 +398,7 @@ describe('tool execution', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__echo', arguments: { msg: 'hi' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__echo', arguments: { msg: 'hi' } }) expect(result.isError).toBe(false) expect(result.content).toEqual([{ type: 'text', text: 'hello world' }]) @@ -420,7 +420,7 @@ describe('tool execution', () => { await syncTools(client as never, ctx, defaultOpts, new Map()) const publicName = publicToolName('srv', 'admin.reset') - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: publicName, arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: publicName, arguments: {} }) expect(result.isError).toBe(false) expect(client.callTool).toHaveBeenCalledWith( @@ -437,7 +437,7 @@ describe('tool execution', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__multi', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__multi', arguments: {} }) expect(result.content).toEqual([{ type: 'text', text: 'line1\nline2' }]) }) @@ -459,7 +459,7 @@ describe('tool execution', () => { await syncTools(client as never, rich.ctx, defaultOpts, new Map()) const result = await rich.ctx.tools.execute({ signal: testToolSignal, - callId: CallId('c1'), + callId: ToolCallId('c1'), name: 'mcp__srv__img', arguments: {}, agent: agentOn() as never, @@ -493,7 +493,7 @@ describe('tool execution', () => { await syncTools(client as never, ctx, defaultOpts, new Map()) const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('no-store'), + callId: ToolCallId('no-store'), name: 'mcp__srv__img', arguments: {}, agent: agentOn() as never, @@ -521,7 +521,7 @@ describe('tool execution', () => { await syncTools(client as never, rich.ctx, defaultOpts, new Map()) const result = await rich.ctx.tools.execute({ signal: testToolSignal, - callId: CallId('bad-batch'), + callId: ToolCallId('bad-batch'), name: 'mcp__srv__img', arguments: {}, agent: agentOn() as never, @@ -547,7 +547,7 @@ describe('tool execution', () => { await syncTools(client as never, rich.ctx, defaultOpts, new Map()) const result = await rich.ctx.tools.execute({ signal: testToolSignal, - callId: CallId('strict-batch'), + callId: ToolCallId('strict-batch'), name: 'mcp__srv__img', arguments: {}, agent: agentOn() as never, @@ -570,7 +570,7 @@ describe('tool execution', () => { await syncTools(client as never, rich.ctx, defaultOpts, new Map()) const result = await rich.ctx.tools.execute({ signal: testToolSignal, - callId: CallId('text-route'), + callId: ToolCallId('text-route'), name: 'mcp__srv__img', arguments: {}, agent: agentOn('text') as never, @@ -590,7 +590,7 @@ describe('tool execution', () => { const noProvider = await rich.ctx.tools.execute({ signal: testToolSignal, - callId: CallId('no-provider'), + callId: ToolCallId('no-provider'), name: 'mcp__srv__img', arguments: {}, agent: { options: { model: 'vision' }, session: { requestHeader: () => undefined } } as never, @@ -599,7 +599,7 @@ describe('tool execution', () => { const noModel = await rich.ctx.tools.execute({ signal: testToolSignal, - callId: CallId('no-model'), + callId: ToolCallId('no-model'), name: 'mcp__srv__img', arguments: {}, agent: { options: { provider: 'visual' }, session: { requestHeader: () => undefined } } as never, @@ -611,7 +611,7 @@ describe('tool execution', () => { await syncTools(client as never, noLlmCtx, defaultOpts, new Map()) const noLlm = await noLlmCtx.tools.execute({ signal: testToolSignal, - callId: CallId('no-llm'), + callId: ToolCallId('no-llm'), name: 'mcp__srv__img', arguments: {}, agent: agentOn() as never, @@ -621,7 +621,7 @@ describe('tool execution', () => { vi.spyOn(rich.ctx.llm, 'resolveModelInfo').mockRejectedValueOnce(new Error('catalog down')) const unverified = await rich.ctx.tools.execute({ signal: testToolSignal, - callId: CallId('unverified'), + callId: ToolCallId('unverified'), name: 'mcp__srv__img', arguments: {}, agent: agentOn() as never, @@ -633,7 +633,7 @@ describe('tool execution', () => { }) const unknown = await rich.ctx.tools.execute({ signal: testToolSignal, - callId: CallId('unknown-modalities'), + callId: ToolCallId('unknown-modalities'), name: 'mcp__srv__img', arguments: {}, agent: agentOn() as never, @@ -647,7 +647,7 @@ describe('tool execution', () => { }) const canceled = await rich.ctx.tools.execute({ signal: controller.signal, - callId: CallId('canceled'), + callId: ToolCallId('canceled'), name: 'mcp__srv__img', arguments: {}, agent: agentOn() as never, @@ -668,7 +668,7 @@ describe('tool execution', () => { await syncTools(client as never, rich.ctx, defaultOpts, new Map()) const result = await rich.ctx.tools.execute({ signal: testToolSignal, - callId: CallId('store-rejected'), + callId: ToolCallId('store-rejected'), name: 'mcp__srv__img', arguments: {}, agent: agentOn() as never, @@ -690,7 +690,7 @@ describe('tool execution', () => { await syncTools(client as never, rich.ctx, defaultOpts, new Map()) const result = await rich.ctx.tools.execute({ signal: testToolSignal, - callId: CallId('policy-rejected'), + callId: ToolCallId('policy-rejected'), name: 'mcp__srv__img', arguments: {}, agent: agentOn() as never, @@ -714,7 +714,7 @@ describe('tool execution', () => { await syncTools(client as never, rich.ctx, defaultOpts, new Map()) const result = await rich.ctx.tools.execute({ signal: testToolSignal, - callId: CallId('replaced'), + callId: ToolCallId('replaced'), name: 'mcp__srv__img', arguments: {}, agent: agentOn() as never, @@ -737,7 +737,7 @@ describe('tool execution', () => { await syncTools(valueClient as never, valueRich.ctx, defaultOpts, new Map()) const replaced = await valueRich.ctx.tools.execute({ signal: testToolSignal, - callId: CallId('value-replaced'), + callId: ToolCallId('value-replaced'), name: 'mcp__srv__img', arguments: {}, agent: agentOn() as never, @@ -756,7 +756,7 @@ describe('tool execution', () => { await syncTools(blockedClient as never, blockedRich.ctx, defaultOpts, new Map()) const blocked = await blockedRich.ctx.tools.execute({ signal: testToolSignal, - callId: CallId('blocked'), + callId: ToolCallId('blocked'), name: 'mcp__srv__img', arguments: {}, agent: agentOn() as never, @@ -775,7 +775,7 @@ describe('tool execution', () => { await syncTools(client as never, ctx, defaultOpts, new Map()) const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('primitive'), name: 'mcp__srv__primitive-blocks', arguments: {}, + callId: ToolCallId('primitive'), name: 'mcp__srv__primitive-blocks', arguments: {}, }) expect(result.content[0]).toEqual({ @@ -798,7 +798,7 @@ describe('tool execution', () => { { content: [{ type: 'text', text: '42' }], structuredContent: { answer: 42 } }, ) await syncTools(valid as never, ctx, defaultOpts, new Map()) - const success = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('valid'), name: 'mcp__srv__structured', arguments: {} }) + const success = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('valid'), name: 'mcp__srv__structured', arguments: {} }) if (success.isError) throw new Error('expected supported structuredContent to validate') expect(success.value).toEqual({ content: [{ type: 'text', text: '42' }], structuredContent: { answer: 42 } }) @@ -808,7 +808,7 @@ describe('tool execution', () => { { content: [{ type: 'text', text: 'wrong' }], structuredContent: { answer: 'forty-two' } }, ) await syncTools(invalid as never, invalidCtx, defaultOpts, new Map()) - const failure = await invalidCtx.tools.execute({ signal: testToolSignal, callId: CallId('invalid'), name: 'mcp__srv__structured', arguments: {} }) + const failure = await invalidCtx.tools.execute({ signal: testToolSignal, callId: ToolCallId('invalid'), name: 'mcp__srv__structured', arguments: {} }) expect(failure.error).toMatchObject({ info: { code: 'INVALID_TOOL_OUTPUT' } }) expect(failure.content[0]?.type === 'text' ? failure.content[0].text : '') .toContain('value.structuredContent.answer') @@ -824,7 +824,7 @@ describe('tool execution', () => { { content: [], structuredContent: ['kept', { nested: true }] }, ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('fallback'), name: 'mcp__srv__future-schema', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('fallback'), name: 'mcp__srv__future-schema', arguments: {} }) if (result.isError) throw new Error('unsupported MCP output schemas must fall back') expect(result.value).toEqual({ content: [], structuredContent: ['kept', { nested: true }] }) }) @@ -836,7 +836,7 @@ describe('tool execution', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__fail', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__fail', arguments: {} }) expect(result.isError).toBe(true) expect(result.content[0]).toEqual({ type: 'text', text: 'Error: something went wrong' }) @@ -851,7 +851,7 @@ describe('tool execution', () => { await syncTools(client as never, ctx, defaultOpts, new Map()) const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('task-only'), name: 'mcp__srv__task-only', arguments: {}, + callId: ToolCallId('task-only'), name: 'mcp__srv__task-only', arguments: {}, }) expect(result.isError).toBe(true) @@ -867,7 +867,7 @@ describe('tool execution', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - await ctx.tools.execute({ callId: CallId('c1'), name: 'mcp__srv__slow', arguments: {}, signal: controller.signal }) + await ctx.tools.execute({ callId: ToolCallId('c1'), name: 'mcp__srv__slow', arguments: {}, signal: controller.signal }) expect(client.callTool).toHaveBeenCalledWith( expect.anything(), @@ -883,7 +883,7 @@ describe('tool execution', () => { client.callTool.mockResolvedValue({ toolResult: { key: 'value' } }) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__legacy', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__legacy', arguments: {} }) expect(result.isError).toBe(false) expect(result.content[0]).toEqual({ type: 'text', text: '{"key":"value"}' }) @@ -899,7 +899,7 @@ describe('tool execution', () => { await syncTools(client as never, ctx, defaultOpts, new Map()) const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('legacy-structured'), name: 'mcp__srv__legacy-structured', arguments: {}, + callId: ToolCallId('legacy-structured'), name: 'mcp__srv__legacy-structured', arguments: {}, }) if (result.isError) throw new Error('expected legacy structured result success') @@ -916,7 +916,7 @@ describe('tool execution', () => { await syncTools(client as never, ctx, defaultOpts, new Map()) const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('legacy-error'), name: 'mcp__srv__legacy-error', arguments: {}, + callId: ToolCallId('legacy-error'), name: 'mcp__srv__legacy-error', arguments: {}, }) expect(result.isError).toBe(true) @@ -938,7 +938,7 @@ describe('tool execution edge cases', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__audio_tool', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__audio_tool', arguments: {} }) expect(result.content[0]).toEqual({ type: 'text', @@ -953,7 +953,7 @@ describe('tool execution edge cases', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__res_tool', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__res_tool', arguments: {} }) expect(result.content[0]).toEqual({ type: 'text', @@ -968,7 +968,7 @@ describe('tool execution edge cases', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__link_tool', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__link_tool', arguments: {} }) expect(result.content[0]).toEqual({ type: 'text', text: 'Resource link: Design (https://example.test/design)' }) }) @@ -980,7 +980,7 @@ describe('tool execution edge cases', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('missing-link'), name: 'mcp__srv__link_tool', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('missing-link'), name: 'mcp__srv__link_tool', arguments: {} }) expect(result.content[0]).toEqual({ type: 'text', text: '[resource link unavailable: the MCP block is missing its name or URI]', @@ -994,7 +994,7 @@ describe('tool execution edge cases', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__unknown_tool', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__unknown_tool', arguments: {} }) expect(result.content[0]).toEqual({ type: 'text', text: '[unsupported MCP content type: video]' }) }) @@ -1006,7 +1006,7 @@ describe('tool execution edge cases', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__img2', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__img2', arguments: {} }) expect(result.content[0]).toEqual({ type: 'text', @@ -1021,7 +1021,7 @@ describe('tool execution edge cases', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__audio_no_mime', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__audio_no_mime', arguments: {} }) expect(result.content[0]).toEqual({ type: 'text', @@ -1036,7 +1036,7 @@ describe('tool execution edge cases', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__notext', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__notext', arguments: {} }) expect(result.content[0]).toEqual({ type: 'text', text: '(notext returned no model-visible content)' }) }) @@ -1048,7 +1048,7 @@ describe('tool execution edge cases', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__empty_tool', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__empty_tool', arguments: {} }) expect(result.content[0]).toEqual({ type: 'text', text: '(empty_tool returned no model-visible content)' }) }) @@ -1061,7 +1061,7 @@ describe('tool execution edge cases', () => { client.callTool.mockResolvedValue({ toolResult: undefined, structuredContent: undefined }) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__legacy2', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__legacy2', arguments: {} }) expect(result.content[0]).toEqual({ type: 'text', text: '(no output)' }) }) @@ -1073,7 +1073,7 @@ describe('tool execution edge cases', () => { client.callTool.mockResolvedValue({}) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('legacy-empty'), name: 'mcp__srv__legacy-empty', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('legacy-empty'), name: 'mcp__srv__legacy-empty', arguments: {} }) expect(result.content[0]).toEqual({ type: 'text', text: '(no output)' }) }) @@ -1085,7 +1085,7 @@ describe('tool execution edge cases', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__err_notext', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__err_notext', arguments: {} }) expect(result.isError).toBe(true) expect(result.content[0]).toEqual({ @@ -1227,7 +1227,7 @@ describe('tool execution — non-object args fallback', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__coerce', arguments: null }) + await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__coerce', arguments: null }) expect(client.callTool).toHaveBeenCalledWith( { name: 'coerce', arguments: {} }, @@ -1243,7 +1243,7 @@ describe('tool execution — non-object args fallback', () => { ) await syncTools(client as never, ctx, defaultOpts, new Map()) - await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'mcp__srv__coerce2', arguments: 'bad' }) + await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'mcp__srv__coerce2', arguments: 'bad' }) expect(client.callTool).toHaveBeenCalledWith( { name: 'coerce2', arguments: {} }, diff --git a/packages/mcp/mcp-client/tests/reconnect.spec.ts b/packages/mcp/mcp-client/tests/reconnect.spec.ts index b7277ccb60..186d4dc345 100644 --- a/packages/mcp/mcp-client/tests/reconnect.spec.ts +++ b/packages/mcp/mcp-client/tests/reconnect.spec.ts @@ -8,7 +8,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest' import { Context } from '@deepseek-ai/cordis' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime from '@deepseek-ai/dsh-tools' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import type { Config } from '@deepseek-ai/dsh-mcp-client' // ---- Mock MCP SDK ---- @@ -115,8 +115,8 @@ function listing(...names: string[]): { tools: { name: string; inputSchema: { ty } let callSeq = 0 -function nextCallId(): CallId { - return CallId(`reconnect-${++callSeq}`) +function nextCallId(): ToolCallId { + return ToolCallId(`reconnect-${++callSeq}`) } // ---- Tests ---- diff --git a/packages/plan/plan-mode/tests/plan-mode.spec.ts b/packages/plan/plan-mode/tests/plan-mode.spec.ts index 8285147953..4e2a1960ce 100644 --- a/packages/plan/plan-mode/tests/plan-mode.spec.ts +++ b/packages/plan/plan-mode/tests/plan-mode.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { RUN_CODE_NAME, defineContentToolFixture } from '@deepseek-ai/dsh-tools' import { Session, SessionId, type UserMessage } from '@deepseek-ai/dsh-session' @@ -144,7 +144,7 @@ function expectPlanCodeSdkBindings(sdk: string): void { let callCounter = 0 function execute(ctx: Context, name: string, agent?: Agent) { return ctx.tools.execute({ - callId: CallId(`call-${++callCounter}`), + callId: ToolCallId(`call-${++callCounter}`), name, arguments: {}, signal: new AbortController().signal, @@ -747,7 +747,7 @@ describe('exit_plan_mode', () => { function callExit(ctx: Context, agent: Agent | undefined, plan = '# The plan\n\ndo things') { return ctx.tools.execute({ - callId: CallId(`call-exit-${++callCounter}`), + callId: ToolCallId(`call-exit-${++callCounter}`), name: EXIT_PLAN_MODE, arguments: { plan }, signal: new AbortController().signal, @@ -875,7 +875,7 @@ describe('exit_plan_mode', () => { const agent = await agentWithSession(ctx, 'code-mode-exit', { active: true }) const result = await ctx.tools.execute({ - callId: CallId(`call-exit-${++callCounter}`), + callId: ToolCallId(`call-exit-${++callCounter}`), name: RUN_CODE_NAME, arguments: { code: `return await tools.${EXIT_PLAN_MODE}({ plan: ${JSON.stringify(plan)} })`, description: 'Submit the plan for review' }, signal: new AbortController().signal, @@ -1023,7 +1023,7 @@ describe('exit_plan_mode', () => { const { ctx, agent, asked } = await setupWithReview({ selected: ['Approve'] }) const controller = new AbortController() const result = await ctx.tools.execute({ - callId: CallId(`call-exit-${++callCounter}`), + callId: ToolCallId(`call-exit-${++callCounter}`), name: EXIT_PLAN_MODE, arguments: { plan: '# P' }, agent, diff --git a/packages/sandbox/sandbox/src/escalation.ts b/packages/sandbox/sandbox/src/escalation.ts index e0b9a2ce63..e60b18daf6 100644 --- a/packages/sandbox/sandbox/src/escalation.ts +++ b/packages/sandbox/sandbox/src/escalation.ts @@ -97,7 +97,7 @@ export type EscalationOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'una * structurally the approval seam's `ApprovalService`, generic over the agent * type `A` and call-id type `C` so this package resolves escalations through * `ctx.approval` without importing the approval or agent packages (the tool - * layer infers `A`/`C` as its own `Agent`/`CallId`). + * layer infers `A`/`C` as its own `Agent`/`ToolCallId`). */ export interface EscalationApprover { /** diff --git a/packages/schedule/schedule/tests/plugin.spec.ts b/packages/schedule/schedule/tests/plugin.spec.ts index 2932ffa657..d4fa7e236a 100644 --- a/packages/schedule/schedule/tests/plugin.spec.ts +++ b/packages/schedule/schedule/tests/plugin.spec.ts @@ -4,7 +4,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader' import { agentEvents } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import * as toolSchedule from '../src/index.ts' @@ -51,7 +51,7 @@ describe('Schedule plugin composition', () => { const created = await ctx.agents.withInitiator(root.agent, () => ctx.tools.execute({ signal: new AbortController().signal, - callId: CallId('schedule-plugin-create'), + callId: ToolCallId('schedule-plugin-create'), name: 'schedule_create', arguments: { prompt: 'future reminder', after_seconds: 3_600 }, agent: root.agent, diff --git a/packages/schedule/schedule/tests/tools.spec.ts b/packages/schedule/schedule/tests/tools.spec.ts index 63f080a678..b8ef0be6f8 100644 --- a/packages/schedule/schedule/tests/tools.spec.ts +++ b/packages/schedule/schedule/tests/tools.spec.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' import type { Agent, AgentCancelCause, InboxTarget } from '@deepseek-ai/dsh-agent' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import type { UserMessage } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' @@ -73,7 +73,7 @@ async function execute( ): Promise { return test.ctx.agents.withInitiator(agent, () => test.ctx.tools.execute({ signal: executionSignal, - callId: CallId(`call-${Math.random()}`), + callId: ToolCallId(`call-${Math.random()}`), name, arguments: args, agent, @@ -111,7 +111,7 @@ describe('Schedule tool protocol', () => { schema.properties?.code?.const === 'persistence_uncertain') expect(persistenceError?.properties?.operation?.enum).toEqual(['create', 'list', 'delete']) for (const name of ['schedule_create', 'schedule_list', 'schedule_delete']) { - expect(test.ctx.tools.executionMode({ signal, callId: CallId(name), name, arguments: {}, agent: test.agent })) + expect(test.ctx.tools.executionMode({ signal, callId: ToolCallId(name), name, arguments: {}, agent: test.agent })) .toEqual({ kind: 'exclusive' }) } expect(test.ctx.tools.get('schedule_create')?.presentCall?.({ prompt: 'x', after_seconds: 1 })) diff --git a/packages/session-query/session-query/tests/search-helpers.spec.ts b/packages/session-query/session-query/tests/search-helpers.spec.ts index eb82890646..bcfef07739 100644 --- a/packages/session-query/session-query/tests/search-helpers.spec.ts +++ b/packages/session-query/session-query/tests/search-helpers.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { createUserMessage, CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' import SessionStore, { SESSION_FORMAT_VERSION, SessionId, @@ -31,7 +31,7 @@ function expectCode(code: SessionQueryErrorCode): Error { describe('session-query semantic extraction', () => { it('extracts first-party message, tool, todo, and failure detail', () => { - const callId = CallId('call') + const callId = ToolCallId('call') const messageContent: SessionEvent<'user/message'>['data']['content'] = [ { type: 'text', text: ' visible ' }, { type: 'reasoning', text: 'thought' }, diff --git a/packages/session-query/tool-session-query/tests/sqlite-integration.spec.ts b/packages/session-query/tool-session-query/tests/sqlite-integration.spec.ts index 500d7de3a6..eab3ff50db 100644 --- a/packages/session-query/tool-session-query/tests/sqlite-integration.spec.ts +++ b/packages/session-query/tool-session-query/tests/sqlite-integration.spec.ts @@ -4,7 +4,7 @@ import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import type { Agent } from '@deepseek-ai/dsh-agent' -import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' import SessionStore, { SESSION_FORMAT_VERSION, SessionId, @@ -78,7 +78,7 @@ describe('tool-session-query with the real SQLite provider', () => { const execute = (name: string, args: unknown) => ctx.tools.execute({ name, arguments: args, - callId: CallId(`integration-${++call}`), + callId: ToolCallId(`integration-${++call}`), signal: new AbortController().signal, agent: fakeAgent(caller), }) @@ -170,7 +170,7 @@ describe('tool-session-query with the real SQLite provider', () => { const execute = (args: unknown) => ctx.tools.execute({ name: 'session_event_search', arguments: args, - callId: CallId(`fractional-integration-${++call}`), + callId: ToolCallId(`fractional-integration-${++call}`), signal: new AbortController().signal, agent: fakeAgent(caller), }) diff --git a/packages/session-query/tool-session-query/tests/tool-session-query.spec.ts b/packages/session-query/tool-session-query/tests/tool-session-query.spec.ts index 7d213f9591..07969d19cf 100644 --- a/packages/session-query/tool-session-query/tests/tool-session-query.spec.ts +++ b/packages/session-query/tool-session-query/tests/tool-session-query.spec.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Context, type Fiber } from '@deepseek-ai/cordis' import type { Agent } from '@deepseek-ai/dsh-agent' -import { createUserMessage, CallId, HarnessError , createMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, HarnessError , createMessage } from '@deepseek-ai/dsh-llm' import { MAX_TIMER_DELAY_MS, TimeoutReason } from '@deepseek-ai/dsh-timeout' import * as TimeoutPolicy from '@deepseek-ai/dsh-tool-call-timeout-policy' import SessionStore, { @@ -218,7 +218,7 @@ async function mount( call: (toolName, args, options = {}) => ctx.tools.execute({ name: toolName, arguments: args, - callId: CallId(`call-${++calls}`), + callId: ToolCallId(`call-${++calls}`), signal: options.signal ?? new AbortController().signal, ...options.agent === undefined ? { agent: fakeAgent(caller) } : { agent: options.agent }, }), @@ -301,7 +301,7 @@ describe('registration and schemas', () => { expect(mounted.ctx.tools.executionMode({ name, arguments: args, - callId: CallId(`mode-${name}`), + callId: ToolCallId(`mode-${name}`), signal: new AbortController().signal, agent: fakeAgent(mounted.caller), })).toEqual({ kind }) @@ -579,7 +579,7 @@ describe('workspace authority and lineage redaction', () => { const missing = await mounted.ctx.tools.execute({ name: 'session_trace', arguments: {}, - callId: CallId('missing-agent'), + callId: ToolCallId('missing-agent'), signal: new AbortController().signal, }) expect(errorCode(missing)).toBe('SESSION_QUERY_TOOL_MISSING_AGENT') diff --git a/packages/session/session-checkpoint-policy/tests/fixtures/crash-child.ts b/packages/session/session-checkpoint-policy/tests/fixtures/crash-child.ts index 9557746db9..83826090dd 100644 --- a/packages/session/session-checkpoint-policy/tests/fixtures/crash-child.ts +++ b/packages/session/session-checkpoint-policy/tests/fixtures/crash-child.ts @@ -2,7 +2,7 @@ import { writeFile } from 'node:fs/promises' import { Context } from '@deepseek-ai/cordis' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' -import { createUserMessage, CallId, type GenerateOptions, LlmAdapter, type StreamChunk } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, type GenerateOptions, LlmAdapter, type StreamChunk } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import * as checkpointPolicy from '../../src/index.ts' @@ -29,7 +29,7 @@ class CrashAdapter extends LlmAdapter { yield { type: 'block-end', index: 0, - block: { type: 'tool-call', id: CallId('crash-call'), name: 'crash_tool', arguments: '{}' }, + block: { type: 'tool-call', id: ToolCallId('crash-call'), name: 'crash_tool', arguments: '{}' }, } yield { type: 'finish', reason: { kind: 'tool-calls' } } } diff --git a/packages/session/session-checkpoint-policy/tests/session-checkpoint-policy.spec.ts b/packages/session/session-checkpoint-policy/tests/session-checkpoint-policy.spec.ts index 21a04c90c2..88387e5b33 100644 --- a/packages/session/session-checkpoint-policy/tests/session-checkpoint-policy.spec.ts +++ b/packages/session/session-checkpoint-policy/tests/session-checkpoint-policy.spec.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent' -import LlmRuntime, { CallId, type GenerateOptions, LlmAdapter, type StreamChunk } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { ToolCallId, type GenerateOptions, LlmAdapter, type StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' import SessionPersistence from '@deepseek-ai/dsh-session-persistence' @@ -135,7 +135,7 @@ describe('session-checkpoint-policy tool and step boundaries', () => { }) const pending = ctx.tools.execute({ - callId: CallId('write-1'), name: 'write', arguments: {}, agent, + callId: ToolCallId('write-1'), name: 'write', arguments: {}, agent, signal: new AbortController().signal, }) await Promise.resolve() @@ -164,7 +164,7 @@ describe('session-checkpoint-policy tool and step boundaries', () => { }) const pending = ctx.tools.execute({ - callId: CallId('write-cancelled'), name: 'write', arguments: {}, agent, + callId: ToolCallId('write-cancelled'), name: 'write', arguments: {}, agent, signal: controller.signal, }) await Promise.resolve() @@ -195,7 +195,7 @@ describe('session-checkpoint-policy tool and step boundaries', () => { execute: async () => { ran = true; return null }, }) const result = await ctx.tools.execute({ - callId: CallId('write-2'), name: 'write', arguments: {}, agent, + callId: ToolCallId('write-2'), name: 'write', arguments: {}, agent, signal: new AbortController().signal, }) expect(result.isError).toBe(true) @@ -215,7 +215,7 @@ describe('session-checkpoint-policy tool and step boundaries', () => { execute: async () => null, }) await ctx.tools.execute({ - callId: CallId('nested-1'), name: 'nested', arguments: {}, agent, + callId: ToolCallId('nested-1'), name: 'nested', arguments: {}, agent, parent: Symbol('outer') as never, signal: new AbortController().signal, }) diff --git a/packages/session/session-persistence/tests/contract.ts b/packages/session/session-persistence/tests/contract.ts index a672884e46..5be7076bd7 100644 --- a/packages/session/session-persistence/tests/contract.ts +++ b/packages/session/session-persistence/tests/contract.ts @@ -11,7 +11,7 @@ import { describe, expect, it } from 'vitest' import { SESSION_FORMAT_VERSION, Session, SessionId, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionHeader, SurfaceEventType, SurfaceIntent } from '@deepseek-ai/dsh-session' -import { CallId, MessageId, createMessage, freezeMessage } from '@deepseek-ai/dsh-llm' +import { ToolCallId, MessageId, createMessage, freezeMessage } from '@deepseek-ai/dsh-llm' import type { SessionPersistence } from '../src/index.ts' /** A backend under test plus its teardown. */ @@ -184,7 +184,7 @@ export function runPersistenceContract(name: string, make: () => Promise Promise e.type === 'tool/result') expect(synthetic?.type === 'tool/result' && synthetic.data).toMatchObject({ message: { - source: { kind: 'tool', callId: CallId('call-x') }, - content: [{ type: 'tool-result', toolCallId: CallId('call-x'), isError: true }], + source: { kind: 'tool', callId: ToolCallId('call-x') }, + content: [{ type: 'tool-result', toolCallId: ToolCallId('call-x'), isError: true }], }, error: { code: TOOL_NOT_STARTED }, }) @@ -215,7 +215,7 @@ export function runPersistenceContract(name: string, make: () => Promise e.type === 'assistant/message') const callId = call?.type === 'assistant/message' && call.data.message.content.find(b => b.type === 'tool-call') - expect(callId && callId.type === 'tool-call' && callId.id).toBe(CallId('call-x')) + expect(callId && callId.type === 'tool-call' && callId.id).toBe(ToolCallId('call-x')) } finally { await dispose() } @@ -234,7 +234,7 @@ export function runPersistenceContract(name: string, make: () => Promise Promise Promise message.content.some(block => block.type === 'tool-result')) expect(resumedResult?.content[0]).toMatchObject({ - type: 'tool-result', toolCallId: CallId('call-risk'), isError: true, + type: 'tool-result', toolCallId: ToolCallId('call-risk'), isError: true, }) } finally { await dispose() diff --git a/packages/session/session-title-llm/tests/llm.spec.ts b/packages/session/session-title-llm/tests/llm.spec.ts index 7b9bade991..f635c921e8 100644 --- a/packages/session/session-title-llm/tests/llm.spec.ts +++ b/packages/session/session-title-llm/tests/llm.spec.ts @@ -1,6 +1,6 @@ import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' -import LlmRuntime, { createUserMessage, CallId, isAgentLoopRequest, LlmAdapter } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { createUserMessage, ToolCallId, isAgentLoopRequest, LlmAdapter } from '@deepseek-ai/dsh-llm' import type { FinishReason, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import { SessionTitleProviderId } from '@deepseek-ai/dsh-session-title' @@ -283,7 +283,7 @@ describe('generateSessionTitleWithLlm', () => { it('rejects tool-call blocks and a successful response with no text', async () => { const toolScript: StreamChunk[] = [ { type: 'block-start', index: 0, blockType: 'tool-call' }, - { type: 'tool-call-delta', index: 0, id: CallId('title-tool'), name: 'unexpected', argumentsDelta: '{}' }, + { type: 'tool-call-delta', index: 0, id: ToolCallId('title-tool'), name: 'unexpected', argumentsDelta: '{}' }, { type: 'finish', reason: { kind: 'stop' } }, ] const tool = await withScript(toolScript) diff --git a/packages/shell/shell-env/tests/shell-env.spec.ts b/packages/shell/shell-env/tests/shell-env.spec.ts index d923e7d236..4df99a41a4 100644 --- a/packages/shell/shell-env/tests/shell-env.spec.ts +++ b/packages/shell/shell-env/tests/shell-env.spec.ts @@ -8,7 +8,7 @@ import { homedir } from 'node:os' import { join, resolve } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import type { Agent } from '@deepseek-ai/dsh-agent' import type { ToolExecution } from '@deepseek-ai/dsh-tools' import { ShellEnvRegistry } from '@deepseek-ai/dsh-shell-env' @@ -22,8 +22,8 @@ function execution(sessionId?: string): ToolExecution { return { signal: testToolSignal, token: Symbol('bash-env-test') as ToolExecution['token'], - callId: CallId('bash-env-call'), - rootCallId: CallId('bash-env-call'), + callId: ToolCallId('bash-env-call'), + rootCallId: ToolCallId('bash-env-call'), name: 'bash', arguments: { command: 'true' }, ...(sessionId === undefined diff --git a/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts b/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts index 6d6affdb44..67551e45c4 100644 --- a/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts +++ b/packages/shell/tool-bash-persistent/tests/loader-composition.spec.ts @@ -6,7 +6,7 @@ import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -127,7 +127,7 @@ suite('persistent Bash through a real cordis.yml Loader composition', () => { const signal = new AbortController().signal const execute = (id: string, command: string) => context!.tools.execute({ signal, - callId: CallId(id), + callId: ToolCallId(id), name: 'bash', arguments: { command }, agent: owner, diff --git a/packages/shell/tool-bash-persistent/tests/tools.spec.ts b/packages/shell/tool-bash-persistent/tests/tools.spec.ts index 4cdebbbdce..921ff1f265 100644 --- a/packages/shell/tool-bash-persistent/tests/tools.spec.ts +++ b/packages/shell/tool-bash-persistent/tests/tools.spec.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -66,7 +66,7 @@ function call( ) { return ctx.tools.execute({ signal, - callId: CallId(`persistent-bash-${++callNumber}`), + callId: ToolCallId(`persistent-bash-${++callNumber}`), name: 'bash', arguments: { command }, ...owner === undefined ? {} : { agent: owner }, diff --git a/packages/shell/tool-bash/tests/tools.spec.ts b/packages/shell/tool-bash/tests/tools.spec.ts index 0f2e4d5a20..62258c205b 100644 --- a/packages/shell/tool-bash/tests/tools.spec.ts +++ b/packages/shell/tool-bash/tests/tools.spec.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { ShellExecutor } from '@deepseek-ai/dsh-shell' import type { ShellExecRequest, ShellExecSpec, ShellProcess, ShellProcessRead, ShellRunResult } from '@deepseek-ai/dsh-shell' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' @@ -76,7 +76,7 @@ function registerFakeAgent(ctx: Context, sessionId: string, inject: (...args: un } let callCounter = 0 function call(ctx: Context, name: string, args: unknown, agent?: Agent) { - return ctx.tools.execute({ signal: testToolSignal, callId: CallId(`call-${++callCounter}`), name, arguments: args, ...agent ? { agent } : {} }) + return ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId(`call-${++callCounter}`), name, arguments: args, ...agent ? { agent } : {} }) } function text(result: { content: { type: string; text?: string }[] }): string { @@ -309,7 +309,7 @@ describe('bash tool', () => { const ctx = await setup() const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('call-abort'), + callId: ToolCallId('call-abort'), name: 'bash', arguments: { command: 'sleep 60', description: 'test command' }, signal: controller.signal, @@ -509,7 +509,7 @@ describe('background execution through the job runtime', () => { const controller = new AbortController() controller.abort() const result = await ctx.tools.execute({ - callId: CallId('call-pre-aborted'), + callId: ToolCallId('call-pre-aborted'), name: 'bash', arguments: { command: 'sleep 60', description: 'test command', run_in_background: true }, signal: controller.signal, @@ -646,7 +646,7 @@ describe('sandbox escalation through the generic task producer', () => { const agent = sandboxAgent(undefined, ctx) ctx.agents.register(agent) const foreground = await ctx.tools.execute({ - callId: CallId('sandbox-signal'), + callId: ToolCallId('sandbox-signal'), name: 'bash', arguments: escalate, agent, @@ -669,7 +669,7 @@ describe('sandbox escalation through the generic task producer', () => { const start = vi.spyOn(bash, 'start') const result = await ctx.tools.execute({ - callId: CallId('cancelled-escalation-background'), + callId: ToolCallId('cancelled-escalation-background'), name: 'bash', arguments: { ...escalate, run_in_background: true }, agent, @@ -833,7 +833,7 @@ describe('session-cwd routing (per-session workdir)', () => { it('falls back to the executor default when the agent has no session cwd', async () => { const ctx = await setup() // No exec.agent at all → executor uses its config/process.cwd() default. - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('cwd-noagent'), name: 'bash', arguments: { command: 'pwd', description: 'pwd' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('cwd-noagent'), name: 'bash', arguments: { command: 'pwd', description: 'pwd' } }) expect(result.isError).toBe(false) expect(text(result).trim().length).toBeGreaterThan(0) }) @@ -1131,7 +1131,7 @@ describe('the model-facing bash tool builds its request from named args only (no await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('session-env-fg'), + callId: ToolCallId('session-env-fg'), name: 'bash', arguments: { command: 'true', description: 'run command' }, agent, @@ -1152,7 +1152,7 @@ describe('the model-facing bash tool builds its request from named args only (no await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('session-env-bg'), + callId: ToolCallId('session-env-bg'), name: 'bash', arguments: { command: 'sleep 1', @@ -1179,7 +1179,7 @@ describe('the model-facing bash tool builds its request from named args only (no await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('session-env-id-only'), + callId: ToolCallId('session-env-id-only'), name: 'bash', arguments: { command: 'true', description: 'run command' }, agent, @@ -1201,7 +1201,7 @@ describe('the model-facing bash tool builds its request from named args only (no for (const [callId, agent] of [['parent', parent], ['child', child]] as const) { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId(`session-env-${callId}`), + callId: ToolCallId(`session-env-${callId}`), name: 'bash', arguments: { command: 'true', description: 'run command' }, agent, @@ -1232,7 +1232,7 @@ describe('the model-facing bash tool builds its request from named args only (no // already set environment variables or feed stdin. await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('no-forward-1'), + callId: ToolCallId('no-forward-1'), name: 'bash', arguments: { command: 'echo hi', @@ -1254,7 +1254,7 @@ describe('the model-facing bash tool builds its request from named args only (no const { ctx, bash } = await setupRecording() const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('no-forward-2'), + callId: ToolCallId('no-forward-2'), name: 'bash', arguments: { command: 'sleep 1', diff --git a/packages/shell/tool-pwsh/tests/integration.spec.ts b/packages/shell/tool-pwsh/tests/integration.spec.ts index 022223a393..84d308e1b3 100644 --- a/packages/shell/tool-pwsh/tests/integration.spec.ts +++ b/packages/shell/tool-pwsh/tests/integration.spec.ts @@ -15,7 +15,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { spawnSync } from 'node:child_process' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { TOOL_ABORTED } from '@deepseek-ai/dsh-tools' import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local' @@ -41,7 +41,7 @@ let callCounter = 0 function call(name: string, args: unknown, agentObj?: object, signal?: AbortSignal) { return ctx.tools.execute({ signal: signal ?? testToolSignal, - callId: CallId(`it-${++callCounter}`), + callId: ToolCallId(`it-${++callCounter}`), name, arguments: args, ...agentObj ? { agent: agentObj as never } : {}, diff --git a/packages/shell/tool-pwsh/tests/tools.spec.ts b/packages/shell/tool-pwsh/tests/tools.spec.ts index ecd9f5bde5..69ecafe2d4 100644 --- a/packages/shell/tool-pwsh/tests/tools.spec.ts +++ b/packages/shell/tool-pwsh/tests/tools.spec.ts @@ -15,7 +15,7 @@ import { Context } from '@deepseek-ai/cordis' import { mkdtempSync, realpathSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve as resolvePath } from 'node:path' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools' import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local' @@ -271,7 +271,7 @@ let callCounter = 0 function call(ctx: Context, name: string, args: unknown, agent?: Agent) { return ctx.tools.execute({ signal: testToolSignal, - callId: CallId(`call-${++callCounter}`), + callId: ToolCallId(`call-${++callCounter}`), name, arguments: args, ...agent ? { agent } : {}, @@ -403,7 +403,7 @@ describe('execution through the bash seam', () => { bash.handler = () => runResult('ok\n') await ctx.tools.execute({ signal: controller.signal, - callId: CallId('call-signal'), + callId: ToolCallId('call-signal'), name: 'pwsh', arguments: { command: 'Write-Output ok', description: 'ok' }, }) @@ -626,7 +626,7 @@ describe('sandbox escalation through ctx.approval', () => { const agent = sandboxAgent(undefined, ctx) ctx.agents.register(agent) const foreground = await ctx.tools.execute({ - callId: CallId('sandbox-signal'), + callId: ToolCallId('sandbox-signal'), name: 'pwsh', arguments: escalate, agent, @@ -649,7 +649,7 @@ describe('sandbox escalation through ctx.approval', () => { const start = vi.spyOn(bash, 'start') const result = await ctx.tools.execute({ - callId: CallId('cancelled-escalation-background'), + callId: ToolCallId('cancelled-escalation-background'), name: 'pwsh', arguments: { ...escalate, run_in_background: true }, agent, @@ -752,7 +752,7 @@ describe('background execution through the job runtime', () => { const controller = new AbortController() controller.abort() const result = await ctx.tools.execute({ - callId: CallId('call-pre-aborted'), + callId: ToolCallId('call-pre-aborted'), name: 'pwsh', arguments: { command: 'Start-Sleep -Seconds 60', description: 'test command', run_in_background: true }, signal: controller.signal, diff --git a/packages/skill/tool-skill/tests/tool-skill.spec.ts b/packages/skill/tool-skill/tests/tool-skill.spec.ts index d8dd9df2f4..134fc85a62 100644 --- a/packages/skill/tool-skill/tests/tool-skill.spec.ts +++ b/packages/skill/tool-skill/tests/tool-skill.spec.ts @@ -3,7 +3,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { tmpdir } from 'node:os' import { Context } from '@deepseek-ai/cordis' -import { createUserMessage, CallId, type Message } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, type Message } from '@deepseek-ai/dsh-llm' import { createScope, type Scope } from '@deepseek-ai/dsh-scope' import { Session, SessionId, type SessionEvent, type UserMessage } from '@deepseek-ai/dsh-session' import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt' @@ -630,7 +630,7 @@ describe('dsh-tool-skill', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('body-refresh'), + callId: ToolCallId('body-refresh'), name: 'skill', arguments: { name: 'body-skill' }, agent, @@ -658,7 +658,7 @@ describe('dsh-tool-skill', () => { const scoped = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('scoped-load'), + callId: ToolCallId('scoped-load'), name: 'skill', arguments: { name: 'preset-only-skill' }, agent, @@ -668,7 +668,7 @@ describe('dsh-tool-skill', () => { const foreign = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('foreign-load'), + callId: ToolCallId('foreign-load'), name: 'skill', arguments: { name: 'preset-only-skill' }, agent: agentForCwd('/workspace/other'), @@ -766,7 +766,7 @@ describe('dsh-tool-skill', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('c1'), + callId: ToolCallId('c1'), name: 'skill', arguments: { name: 'project-skill' }, agent: { session: { header: { cwd: project } } } as never, @@ -825,9 +825,9 @@ describe('dsh-tool-skill', () => { content: 'Provider instructions.', }) - const opaque = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c2'), name: 'skill', arguments: { name: 'opaque-skill' } }) - const url = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c3'), name: 'skill', arguments: { name: 'url-skill' } }) - const provider = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c4'), name: 'skill', arguments: { name: 'provider-skill' } }) + const opaque = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c2'), name: 'skill', arguments: { name: 'opaque-skill' } }) + const url = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c3'), name: 'skill', arguments: { name: 'url-skill' } }) + const provider = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c4'), name: 'skill', arguments: { name: 'provider-skill' } }) if (opaque.content[0]?.type !== 'text' || url.content[0]?.type !== 'text' || provider.content[0]?.type !== 'text') { throw new Error('expected text tool results') @@ -849,7 +849,7 @@ describe('dsh-tool-skill', () => { content: 'Rogue instructions.', }) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c5'), name: 'skill', arguments: { name: 'rogue-resource-skill' } }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c5'), name: 'skill', arguments: { name: 'rogue-resource-skill' } }) expect(result.isError).toBe(true) expect(result.error?.info?.code).toBe('INVALID_TOOL_OUTPUT') @@ -871,10 +871,10 @@ describe('dsh-tool-skill', () => { content: 'Model-only instructions.', }) - const unknown = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c1'), name: 'skill', arguments: { name: 'missing' } }) - const invalid = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c2'), name: 'skill', arguments: { name: 'Bad_Name' } }) - const disabled = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c3'), name: 'skill', arguments: { name: 'hidden-skill' } }) - const modelOnly = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c4'), name: 'skill', arguments: { name: 'model-only-skill' } }) + const unknown = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c1'), name: 'skill', arguments: { name: 'missing' } }) + const invalid = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c2'), name: 'skill', arguments: { name: 'Bad_Name' } }) + const disabled = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c3'), name: 'skill', arguments: { name: 'hidden-skill' } }) + const modelOnly = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c4'), name: 'skill', arguments: { name: 'model-only-skill' } }) expect(unknown.isError).toBe(true) expect(invalid.isError).toBe(true) @@ -933,9 +933,9 @@ describe('dsh-tool-skill', () => { }, })) - const denied = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c6'), name: 'skill', arguments: { name: 'denied-skill' } }) - const raced = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c7'), name: 'skill', arguments: { name: 'policy-race-skill' } }) - const vanished = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c8'), name: 'skill', arguments: { name: 'vanishing-skill' } }) + const denied = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c6'), name: 'skill', arguments: { name: 'denied-skill' } }) + const raced = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c7'), name: 'skill', arguments: { name: 'policy-race-skill' } }) + const vanished = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c8'), name: 'skill', arguments: { name: 'vanishing-skill' } }) expect(denied.isError).toBe(true) expect(raced.isError).toBe(true) diff --git a/packages/spill/spill-local/tests/spill-local.spec.ts b/packages/spill/spill-local/tests/spill-local.spec.ts index fd01babeff..c929f18450 100644 --- a/packages/spill/spill-local/tests/spill-local.spec.ts +++ b/packages/spill/spill-local/tests/spill-local.spec.ts @@ -11,7 +11,7 @@ import { Context } from '@deepseek-ai/cordis' import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs' import { tmpdir } from 'node:os' import { basename, dirname, isAbsolute, join, normalize } from 'node:path' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import type { SaveTextSpill } from '@deepseek-ai/dsh-spill' import LocalSpillStore, { encodeSegment, privateRoot, saveTextFile, sessionDir } from '@deepseek-ai/dsh-spill-local' @@ -28,7 +28,7 @@ afterEach(() => { function request(overrides: Partial = {}): SaveTextSpill { return { owner: { sessionId: SessionId('sess-1') }, - source: { toolName: 'web_fetch', callId: CallId('call-1'), label: 'result' }, + source: { toolName: 'web_fetch', callId: ToolCallId('call-1'), label: 'result' }, suggestedName: 'web_fetch.txt', content: 'the full body', ...overrides, diff --git a/packages/spill/spill-policy/src/index.ts b/packages/spill/spill-policy/src/index.ts index d3cef074eb..d6ebb1b664 100644 --- a/packages/spill/spill-policy/src/index.ts +++ b/packages/spill/spill-policy/src/index.ts @@ -50,7 +50,7 @@ import { TextRetainer, describeOmitted } from '@deepseek-ai/dsh-output-retention import type { Omitted } from '@deepseek-ai/dsh-output-retention' import type { SaveTextSpill, SpillRef } from '@deepseek-ai/dsh-spill' import type { SessionId } from '@deepseek-ai/dsh-session' -import type { CallId } from '@deepseek-ai/dsh-llm' +import type { ToolCallId } from '@deepseek-ai/dsh-llm' import type { PostToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools' import type { SpillPolicyExec } from './types.ts' @@ -132,7 +132,7 @@ export function apply(ctx: Context, config: Config): void { totalBytes: number, sessionId: SessionId | undefined, toolName: string, - callId: CallId, + callId: ToolCallId, label: 'result' | 'dispatch', ): Promise { if (sessionId === undefined) { diff --git a/packages/spill/spill-policy/tests/spill-policy.spec.ts b/packages/spill/spill-policy/tests/spill-policy.spec.ts index e28ec1522d..433d2c1797 100644 --- a/packages/spill/spill-policy/tests/spill-policy.spec.ts +++ b/packages/spill/spill-policy/tests/spill-policy.spec.ts @@ -11,7 +11,7 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' -import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' @@ -58,7 +58,7 @@ function textTool(name: string, text: string) { function exec(name: string, session = 's1'): ToolExecution { // Only agent.session.header.id is read by the policy; a structural stub suffices. const agent = { session: { header: { id: SessionId(session) } } } - return { callId: CallId(`call-${name}`), name, arguments: {}, agent, signal: testToolSignal } as unknown as ToolExecution + return { callId: ToolCallId(`call-${name}`), name, arguments: {}, agent, signal: testToolSignal } as unknown as ToolExecution } /** @@ -204,7 +204,7 @@ describe('outer Code Mode failure capture', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('code-output-limit'), + callId: ToolCallId('code-output-limit'), name: 'run_code', arguments: { code: 'console.log("HEAD-" + "x".repeat(300)); console.log("TAIL-" + "y".repeat(300)); return "unreachable";', @@ -255,7 +255,7 @@ describe('the durable dispatch-log arm', () => { for (const tool of extraTools) ctx.tools.register(tool) const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('parent-1'), + callId: ToolCallId('parent-1'), name: 'run_code', arguments: { code: program, description: 'Drive dispatch-log spilling' }, agent: agent as never, @@ -338,7 +338,7 @@ describe('the durable dispatch-log arm', () => { let smallAfterHuge = false const runPromise = ctx.tools.execute({ signal: testToolSignal, - callId: CallId('parent-3'), + callId: ToolCallId('parent-3'), name: 'run_code', arguments: { // The program takes BOTH values while the spill backend hangs: the @@ -396,7 +396,7 @@ describe('the durable dispatch-log arm', () => { && (event.data as { subCallId: string }).subCallId.endsWith(`:code:${n}`)) const runPromise = ctx.tools.execute({ signal: testToolSignal, - callId: CallId('parent-bound'), + callId: ToolCallId('parent-bound'), name: 'run_code', arguments: { code: 'await tools.huge_read({}); await tools.huge_read({}); await tools.huge_read({}); return "done"', @@ -446,7 +446,7 @@ describe('the durable dispatch-log arm', () => { ctx.tools.register(textTool('huge_read', 'H'.repeat(2_000))) const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('parent-2'), + callId: ToolCallId('parent-2'), name: 'run_code', arguments: { code: 'return (await tools.huge_read({}))[0].text.length', description: 'Fail the spill backend' }, agent: agent as never, @@ -498,7 +498,7 @@ describe('best-effort fallback', () => { const { ctx, spill } = await setup({ maxInlineBytes: 10 }) const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {}) ctx.tools.register(textTool('big', 'x'.repeat(1000))) - const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c'), name: 'big', arguments: {} }) + const result = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c'), name: 'big', arguments: {} }) expect(textOf(result.content)).toBe('x'.repeat(1000)) expect(spill?.saves).toHaveLength(0) expect(warn).toHaveBeenCalled() diff --git a/packages/spill/spill/src/types.ts b/packages/spill/spill/src/types.ts index f07e13f4ef..8091fffce9 100644 --- a/packages/spill/spill/src/types.ts +++ b/packages/spill/spill/src/types.ts @@ -7,7 +7,7 @@ */ import type { Branded } from '@deepseek-ai/dsh-brand' -import type { CallId } from '@deepseek-ai/dsh-llm' +import type { ToolCallId } from '@deepseek-ai/dsh-llm' import type { SessionId } from '@deepseek-ai/dsh-session' /** @@ -47,7 +47,7 @@ export interface SpillSource { /** The tool whose result was spilled (e.g. `web_fetch`). */ toolName: string /** The model-issued call id the result belongs to. */ - callId: CallId + callId: ToolCallId /** A short human label for the artifact (e.g. `result`). */ label: string } diff --git a/packages/spill/spill/tests/service.spec.ts b/packages/spill/spill/tests/service.spec.ts index c17a77b273..b812b3e4e2 100644 --- a/packages/spill/spill/tests/service.spec.ts +++ b/packages/spill/spill/tests/service.spec.ts @@ -7,7 +7,7 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import { SpillLocator, SpillStore } from '@deepseek-ai/dsh-spill' import type { SaveTextSpill, SpillRef } from '@deepseek-ai/dsh-spill' @@ -29,7 +29,7 @@ class StubStore extends SpillStore { function request(content: string): SaveTextSpill { return { owner: { sessionId: SessionId('s1') }, - source: { toolName: 'web_fetch', callId: CallId('c1'), label: 'result' }, + source: { toolName: 'web_fetch', callId: ToolCallId('c1'), label: 'result' }, suggestedName: 'web_fetch.txt', content, } diff --git a/packages/subagent/subagent-in-process-driver/tests/structured.spec.ts b/packages/subagent/subagent-in-process-driver/tests/structured.spec.ts index 8840e8b101..e34f900209 100644 --- a/packages/subagent/subagent-in-process-driver/tests/structured.spec.ts +++ b/packages/subagent/subagent-in-process-driver/tests/structured.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { createUserMessage, CallId, type ContentBlock, type GenerateOptions } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId, type ContentBlock, type GenerateOptions } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' @@ -132,7 +132,7 @@ describe('in-process structured output', () => { const response = [ ...toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 5 }).slice(0, -2), { type: 'block-start', index: 1, blockType: 'tool-call' }, - { type: 'block-end', index: 1, block: { type: 'tool-call', id: CallId('c2'), name: 'side_effect', arguments: '{}' } }, + { type: 'block-end', index: 1, block: { type: 'tool-call', id: ToolCallId('c2'), name: 'side_effect', arguments: '{}' } }, { type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } }, { type: 'finish', reason: { kind: 'tool-calls' } }, ] as Script[number] @@ -160,7 +160,7 @@ describe('in-process structured output', () => { const response = [ ...toolCallResponse('c1', STRUCTURED_OUTPUT_TOOL, { answer: 5 }).slice(0, -2), { type: 'block-start', index: 1, blockType: 'tool-call' }, - { type: 'block-end', index: 1, block: { type: 'tool-call', id: CallId('c2'), name: 'side_effect', arguments: '{}' } }, + { type: 'block-end', index: 1, block: { type: 'tool-call', id: ToolCallId('c2'), name: 'side_effect', arguments: '{}' } }, { type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } }, { type: 'finish', reason: { kind: 'tool-calls' } }, ] as Script[number] @@ -197,7 +197,7 @@ describe('in-process structured output', () => { it('leaves tool calls that PRECEDE the capture in the same response untouched', async () => { const response = [ { type: 'block-start', index: 0, blockType: 'tool-call' }, - { type: 'block-end', index: 0, block: { type: 'tool-call', id: CallId('c1'), name: 'side_effect', arguments: '{}' } }, + { type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('c1'), name: 'side_effect', arguments: '{}' } }, ...toolCallResponse('c2', STRUCTURED_OUTPUT_TOOL, { answer: 6 }).map(chunk => 'index' in chunk ? { ...chunk, index: 1 } : chunk), ] as Script[number] @@ -432,7 +432,7 @@ describe('in-process structured output', () => { expect(adapter.requests).toHaveLength(2) const child = ctx.agents.get(run.id)! const outer = child.session.events.find(event => - event.type === 'tool/result' && event.data.message.source.callId === CallId('c1')) + event.type === 'tool/result' && event.data.message.source.callId === ToolCallId('c1')) expect(outer?.type === 'tool/result' && outer.data.message.content[0].isError).toBe(true) await run.dispose() }) diff --git a/packages/subagent/subagent-in-process-driver/tests/subagent-in-process-driver.spec.ts b/packages/subagent/subagent-in-process-driver/tests/subagent-in-process-driver.spec.ts index 823890d934..da43c58fb1 100644 --- a/packages/subagent/subagent-in-process-driver/tests/subagent-in-process-driver.spec.ts +++ b/packages/subagent/subagent-in-process-driver/tests/subagent-in-process-driver.spec.ts @@ -1,4 +1,4 @@ -import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm' +import { ToolCallId, createUserMessage } from '@deepseek-ai/dsh-llm' import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { type Agent, type AgentOptions } from '@deepseek-ai/dsh-agent' @@ -177,8 +177,8 @@ describe('startInProcessRun', () => { toolCallResponse('t1', 'noop', {}, 'partial one'), [ { type: 'block-start', index: 0, blockType: 'tool-call' }, - { type: 'tool-call-delta', index: 0, id: CallId('t2'), name: 'noop', argumentsDelta: '{}' }, - { type: 'block-end', index: 0, block: { type: 'tool-call', id: CallId('t2'), name: 'noop', arguments: '{}' } }, + { type: 'tool-call-delta', index: 0, id: ToolCallId('t2'), name: 'noop', argumentsDelta: '{}' }, + { type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('t2'), name: 'noop', arguments: '{}' } }, { type: 'usage', usage: { inputTokens: 20, outputTokens: 5 } }, { type: 'finish', reason: { kind: 'max-tokens' } }, ], diff --git a/packages/subagent/subagent/tests/continuation.spec.ts b/packages/subagent/subagent/tests/continuation.spec.ts index 6cf1aea5a8..4370f93b87 100644 --- a/packages/subagent/subagent/tests/continuation.spec.ts +++ b/packages/subagent/subagent/tests/continuation.spec.ts @@ -12,7 +12,7 @@ import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process' import type { GenerateOptions, MessageId, StreamChunk } from '@deepseek-ai/dsh-llm' -import { CallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm' +import { ToolCallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm' import { defineTool } from '@deepseek-ai/dsh-tools' import InvariantRegistry from '@deepseek-ai/dsh-invariants' import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' @@ -1351,8 +1351,8 @@ describe('continuable review regressions', () => { toolCallResponse('t1', 'noop', {}, 'partial one'), [ { type: 'block-start', index: 0, blockType: 'tool-call' }, - { type: 'tool-call-delta', index: 0, id: CallId('t2'), name: 'noop', argumentsDelta: '{}' }, - { type: 'block-end', index: 0, block: { type: 'tool-call', id: CallId('t2'), name: 'noop', arguments: '{}' } }, + { type: 'tool-call-delta', index: 0, id: ToolCallId('t2'), name: 'noop', argumentsDelta: '{}' }, + { type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('t2'), name: 'noop', arguments: '{}' } }, { type: 'usage', usage: { inputTokens: 20, outputTokens: 5 } }, { type: 'finish', reason: { kind: 'max-tokens' } }, ], diff --git a/packages/subagent/tool-subagent-control/tests/list-agents.spec.ts b/packages/subagent/tool-subagent-control/tests/list-agents.spec.ts index 4eb8008c3b..684e82a3e9 100644 --- a/packages/subagent/tool-subagent-control/tests/list-agents.spec.ts +++ b/packages/subagent/tool-subagent-control/tests/list-agents.spec.ts @@ -3,7 +3,7 @@ import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { SessionId } from '@deepseek-ai/dsh-session' @@ -86,7 +86,7 @@ function callTool( ) { return ctx.tools.execute({ signal, - callId: CallId(`call-${++calls}`), + callId: ToolCallId(`call-${++calls}`), name, arguments: args, ...agent !== undefined ? { agent: agent as never } : {}, diff --git a/packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts b/packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts index 0636254841..007c45a4cb 100644 --- a/packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts +++ b/packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts @@ -3,7 +3,7 @@ import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { SessionId } from '@deepseek-ai/dsh-session' @@ -85,7 +85,7 @@ function callTool( ) { return ctx.tools.execute({ signal, - callId: CallId(`call-${++calls}`), + callId: ToolCallId(`call-${++calls}`), name, arguments: args, ...agent !== undefined ? { agent: agent as never } : {}, diff --git a/packages/subagent/tool-subagent-report/tests/tool-subagent-report.spec.ts b/packages/subagent/tool-subagent-report/tests/tool-subagent-report.spec.ts index 28d6068757..e8bb53925d 100644 --- a/packages/subagent/tool-subagent-report/tests/tool-subagent-report.spec.ts +++ b/packages/subagent/tool-subagent-report/tests/tool-subagent-report.spec.ts @@ -7,7 +7,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import { assembleContextFor } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' -import { CallId, LlmAdapter, createUserMessage } from '@deepseek-ai/dsh-llm' +import { ToolCallId, LlmAdapter, createUserMessage } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import type { SessionEvent } from '@deepseek-ai/dsh-session' @@ -118,7 +118,7 @@ let calls = 0 function callReport(ctx: Context, child: Agent, output: string, signal = testSignal) { return ctx.tools.execute({ signal, - callId: CallId(`report-${++calls}`), + callId: ToolCallId(`report-${++calls}`), name: 'report', arguments: { output }, agent: child, diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index 1ee5e40228..c336bc408d 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os' import path from 'node:path' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools' import { assembleContextFor, type Agent } from '@deepseek-ai/dsh-agent' @@ -55,7 +55,7 @@ function callSubagent(ctx: Context, args: unknown, over: { agent?: Agent | undef const agent = 'agent' in over ? over.agent : fakeAgent() return ctx.tools.execute({ signal: testToolSignal, - callId: CallId(`call-${++callCounter}`), + callId: ToolCallId(`call-${++callCounter}`), name: 'subagent', arguments: args, ...agent ? { agent } : {}, @@ -136,13 +136,13 @@ describe('dsh-tool-subagent', () => { const ctx = await setup({ provider: 'mock' }) expect(ctx.tools.executionMode({ signal: testToolSignal, - callId: CallId('subagent-foreground'), + callId: ToolCallId('subagent-foreground'), name: 'subagent', arguments: { description: 'do work', prompt: 'Reply OK' }, })).toEqual({ kind: 'parallel' }) expect(ctx.tools.executionMode({ signal: testToolSignal, - callId: CallId('subagent-background'), + callId: ToolCallId('subagent-background'), name: 'subagent', arguments: { description: 'do work', prompt: 'Reply OK', run_in_background: true }, })).toEqual({ kind: 'parallel' }) @@ -217,8 +217,8 @@ describe('dsh-tool-subagent', () => { const names = ctx.tools.schemas().map(s => s.name).filter(n => n.startsWith('subagent')).sort() expect(names).toEqual(['subagent', 'subagent_acp']) - const viaSpawn = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c-spawn'), name: 'subagent', arguments: { description: 'd', prompt: 'p' }, agent: fakeAgent() }) - const viaAcp = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('c-acp'), name: 'subagent_acp', arguments: { description: 'd', prompt: 'p' }, agent: fakeAgent() }) + const viaSpawn = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c-spawn'), name: 'subagent', arguments: { description: 'd', prompt: 'p' }, agent: fakeAgent() }) + const viaAcp = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('c-acp'), name: 'subagent_acp', arguments: { description: 'd', prompt: 'p' }, agent: fakeAgent() }) expect(text(viaSpawn)).toBe('from spawn') expect(text(viaAcp)).toBe('from acp') }) @@ -828,7 +828,7 @@ describe('dsh-tool-subagent background mode', () => { const started = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('resumable-one-shot'), + callId: ToolCallId('resumable-one-shot'), name: 'subagent_resumable', arguments: { description: 'work', prompt: 'go', run_in_background: true }, agent: parent, @@ -850,7 +850,7 @@ describe('dsh-tool-subagent background mode', () => { const collected = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('collect-1'), + callId: ToolCallId('collect-1'), name: 'job_output', arguments: { job_id: 'subagent-1', wait: true }, agent: parent, @@ -860,7 +860,7 @@ describe('dsh-tool-subagent background mode', () => { // Final-output reads are idempotent (not consumed). const again = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('collect-2'), + callId: ToolCallId('collect-2'), name: 'job_output', arguments: { job_id: 'subagent-1' }, agent: parent, @@ -878,7 +878,7 @@ describe('dsh-tool-subagent background mode', () => { const started = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('diagnostic-background-start'), + callId: ToolCallId('diagnostic-background-start'), name: 'subagent', arguments: { description: 'd', prompt: 'p', run_in_background: true }, agent: parent, @@ -887,7 +887,7 @@ describe('dsh-tool-subagent background mode', () => { const output = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('diagnostic-background-output'), + callId: ToolCallId('diagnostic-background-output'), name: 'job_output', arguments: { job_id: 'subagent-1', wait: true }, agent: parent, @@ -932,7 +932,7 @@ describe('dsh-tool-subagent background mode', () => { const started = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('broken-start'), + callId: ToolCallId('broken-start'), name: 'subagent_broken', arguments: { description: 'broken', prompt: 'p', run_in_background: true }, agent: parent, @@ -940,7 +940,7 @@ describe('dsh-tool-subagent background mode', () => { expect(text(started)).toBe('started background subagent job subagent-1') const output = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('broken-output'), + callId: ToolCallId('broken-output'), name: 'job_output', arguments: { job_id: 'subagent-1', wait: true }, agent: parent, @@ -963,21 +963,21 @@ describe('dsh-tool-subagent background mode', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('pending-start'), + callId: ToolCallId('pending-start'), name: 'subagent_pending', arguments: { description: 'pending', prompt: 'p', run_in_background: true }, agent: parent, }) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('pending-kill'), + callId: ToolCallId('pending-kill'), name: 'job_kill', arguments: { job_id: 'subagent-1', reason: 'no longer needed' }, agent: parent, }) const output = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('pending-output'), + callId: ToolCallId('pending-output'), name: 'job_output', arguments: { job_id: 'subagent-1', wait: true }, agent: parent, @@ -1005,21 +1005,21 @@ describe('dsh-tool-subagent background mode', () => { await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('broken-rollback-start'), + callId: ToolCallId('broken-rollback-start'), name: 'subagent_broken_rollback', arguments: { description: 'broken rollback', prompt: 'p', run_in_background: true }, agent: parent, }) await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('broken-rollback-kill'), + callId: ToolCallId('broken-rollback-kill'), name: 'job_kill', arguments: { job_id: 'subagent-1' }, agent: parent, }) const output = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('broken-rollback-output'), + callId: ToolCallId('broken-rollback-output'), name: 'job_output', arguments: { job_id: 'subagent-1', wait: true }, agent: parent, @@ -1056,19 +1056,19 @@ describe('dsh-tool-subagent background mode', () => { // Direct apply preserves omitted agentOptions instead of applying schema defaults. tool.apply(ctx, { provider: 'hanging', toolName: 'subagent_hang' }) - const startOne = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('h1'), name: 'subagent_hang', arguments: { description: 'one', prompt: 'p', run_in_background: true }, agent: parent }) - const startTwo = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('h2'), name: 'subagent_hang', arguments: { description: 'two', prompt: 'p', run_in_background: true }, agent: parent }) + const startOne = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('h1'), name: 'subagent_hang', arguments: { description: 'one', prompt: 'p', run_in_background: true }, agent: parent }) + const startTwo = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('h2'), name: 'subagent_hang', arguments: { description: 'two', prompt: 'p', run_in_background: true }, agent: parent }) expect(text(startOne)).toBe('started background subagent job subagent-1') expect(text(startTwo)).toBe('started background subagent job subagent-2') - const withReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k1'), name: 'job_kill', arguments: { job_id: 'subagent-1', reason: 'superseded' }, agent: parent }) - const withoutReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k2'), name: 'job_kill', arguments: { job_id: 'subagent-2' }, agent: parent }) + const withReason = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('k1'), name: 'job_kill', arguments: { job_id: 'subagent-1', reason: 'superseded' }, agent: parent }) + const withoutReason = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('k2'), name: 'job_kill', arguments: { job_id: 'subagent-2' }, agent: parent }) expect(text(withReason)).toBe('requested cancellation of job subagent-1') expect(text(withoutReason)).toBe('requested cancellation of job subagent-2') expect(cancels).toEqual(['superseded', 'background subagent task killed']) // The aborted children settle as killed tasks. - const killed = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('w1'), name: 'job_output', arguments: { job_id: 'subagent-1', wait: true }, agent: parent }) + const killed = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('w1'), name: 'job_output', arguments: { job_id: 'subagent-1', wait: true }, agent: parent }) expect(text(killed)).toBe('(no new output)\n[status: killed]') }) @@ -1104,7 +1104,7 @@ describe('dsh-tool-subagent continuable background mode', () => { const { ctx } = await continuableSetup() expect(ctx.tools.executionMode({ signal: testToolSignal, - callId: CallId('subagent-continuable'), + callId: ToolCallId('subagent-continuable'), name: 'subagent', arguments: { description: 'do work', prompt: 'Reply OK' }, })).toEqual({ kind: 'parallel' }) @@ -1203,7 +1203,7 @@ describe('dsh-tool-subagent continuable background mode', () => { const execute = (callId: string, description: string, signal: AbortSignal) => ctx.tools.execute({ signal, - callId: CallId(callId), + callId: ToolCallId(callId), name: 'subagent_gated', arguments: { description, prompt: 'work', run_in_background: true }, agent: parent, @@ -1273,7 +1273,7 @@ describe('background preflight failure (no orphaned child, by construction)', () const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('probe-1'), + callId: ToolCallId('probe-1'), name: 'subagent_probe', arguments: { description: 'd', prompt: 'p', run_in_background: true }, agent: parent, diff --git a/packages/terminal/tool-terminal/tests/loader-composition.spec.ts b/packages/terminal/tool-terminal/tests/loader-composition.spec.ts index ada46ef78f..577850407c 100644 --- a/packages/terminal/tool-terminal/tests/loader-composition.spec.ts +++ b/packages/terminal/tool-terminal/tests/loader-composition.spec.ts @@ -6,7 +6,7 @@ import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -114,15 +114,15 @@ suite('terminal real Loader composition through cordis.yml', () => { const owner = agent(context) const signal = new AbortController().signal const spawn = await context.tools.execute({ - signal, callId: CallId('spawn'), name: 'terminal_open', arguments: { type: 'shell', name: 'main', cwd: root }, agent: owner, + signal, callId: ToolCallId('spawn'), name: 'terminal_open', arguments: { type: 'shell', name: 'main', cwd: root }, agent: owner, }) expect(resultText(spawn)).toContain('started terminal session pty-1 (main)') await context.tools.execute({ - signal, callId: CallId('state'), name: 'terminal_send', arguments: { sessionId: 'pty-1', text: 'export KEEP=loader; cd /' }, agent: owner, + signal, callId: ToolCallId('state'), name: 'terminal_send', arguments: { sessionId: 'pty-1', text: 'export KEEP=loader; cd /' }, agent: owner, }) const read = await context.tools.execute({ - signal, callId: CallId('read'), name: 'terminal_send', arguments: { sessionId: 'pty-1', text: 'printf "cwd=%s keep=%s\\n" "$PWD" "$KEEP"' }, agent: owner, + signal, callId: ToolCallId('read'), name: 'terminal_send', arguments: { sessionId: 'pty-1', text: 'printf "cwd=%s keep=%s\\n" "$PWD" "$KEEP"' }, agent: owner, }) expect(resultText(read)).toContain('cwd=/ keep=loader') expect(context.terminals.list(owner)).toHaveLength(1) diff --git a/packages/terminal/tool-terminal/tests/tools.spec.ts b/packages/terminal/tool-terminal/tests/tools.spec.ts index 9feaf1e7b0..406ae59778 100644 --- a/packages/terminal/tool-terminal/tests/tools.spec.ts +++ b/packages/terminal/tool-terminal/tests/tools.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -122,11 +122,11 @@ let callNumber = 0 const TOOL_NAMES = ['terminal_open', 'terminal_send', 'terminal_read', 'terminal_signal', 'terminal_close', 'terminal_list'] as const const testToolSignal = new AbortController().signal function call(ctx: Context, name: string, args: unknown, agent?: Agent) { - return ctx.tools.execute({ signal: testToolSignal, callId: CallId(`pty-call-${++callNumber}`), name, arguments: args, ...agent ? { agent } : {} }) + return ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId(`pty-call-${++callNumber}`), name, arguments: args, ...agent ? { agent } : {} }) } function callWithSignal(ctx: Context, name: string, args: unknown, agent: Agent, signal: AbortSignal) { - return ctx.tools.execute({ callId: CallId(`pty-call-${++callNumber}`), name, arguments: args, agent, signal }) + return ctx.tools.execute({ callId: ToolCallId(`pty-call-${++callNumber}`), name, arguments: args, agent, signal }) } function text(result: { content: { type: string; text?: string }[] }): string { diff --git a/packages/test-support/llm-replay/tests/llm-replay.spec.ts b/packages/test-support/llm-replay/tests/llm-replay.spec.ts index 16244c5ce5..915b4332c7 100644 --- a/packages/test-support/llm-replay/tests/llm-replay.spec.ts +++ b/packages/test-support/llm-replay/tests/llm-replay.spec.ts @@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import type { SessionEvent } from '@deepseek-ai/dsh-session' import { CompactionId } from '@deepseek-ai/dsh-compaction' -import LlmRuntime, { CallId, createUserMessage, GenerateOptions, LlmAdapter, StreamChunk } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { ToolCallId, createUserMessage, GenerateOptions, LlmAdapter, StreamChunk } from '@deepseek-ai/dsh-llm' import { type Config, type ReplayEntry, @@ -532,8 +532,8 @@ describe('installLlmReplay (through the real LlmRuntime)', () => { function scriptedCall(argumentsDelta: string): StreamChunk[] { return [ { type: 'block-start', index: 0, blockType: 'tool-call' }, - { type: 'tool-call-delta', index: 0, id: CallId('c1'), name: 'update_goal', argumentsDelta }, - { type: 'block-end', index: 0, block: { type: 'tool-call', id: CallId('c1'), name: 'update_goal', arguments: argumentsDelta } }, + { type: 'tool-call-delta', index: 0, id: ToolCallId('c1'), name: 'update_goal', argumentsDelta }, + { type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('c1'), name: 'update_goal', arguments: argumentsDelta } }, { type: 'finish', reason: { kind: 'tool-calls' } }, ] } diff --git a/packages/todo/tool-todo/tests/loader-composition.spec.ts b/packages/todo/tool-todo/tests/loader-composition.spec.ts index 945b2e84e5..a3f5bd68fb 100644 --- a/packages/todo/tool-todo/tests/loader-composition.spec.ts +++ b/packages/todo/tool-todo/tests/loader-composition.spec.ts @@ -9,7 +9,7 @@ import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -101,7 +101,7 @@ describe('tool-todo real Loader composition through cordis.yml', () => { const owner = agent(ctx) const result = await ctx.tools.execute({ signal: new AbortController().signal, - callId: CallId('parallel'), + callId: ToolCallId('parallel'), name: 'todo_write', arguments: { todos: PARALLEL_TODOS }, agent: owner, @@ -119,7 +119,7 @@ describe('tool-todo real Loader composition through cordis.yml', () => { const owner = agent(ctx) const result = await ctx.tools.execute({ signal: new AbortController().signal, - callId: CallId('parallel-enabled'), + callId: ToolCallId('parallel-enabled'), name: 'todo_write', arguments: { todos: PARALLEL_TODOS }, agent: owner, diff --git a/packages/todo/tool-todo/tests/tool-todo.spec.ts b/packages/todo/tool-todo/tests/tool-todo.spec.ts index dd959f2f9f..76f49f0228 100644 --- a/packages/todo/tool-todo/tests/tool-todo.spec.ts +++ b/packages/todo/tool-todo/tests/tool-todo.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime from '@deepseek-ai/dsh-tools' import { Session, SessionId } from '@deepseek-ai/dsh-session' @@ -39,7 +39,7 @@ function callTodo(ctx: Context, args: unknown, over: { agent?: Agent | undefined const agent = 'agent' in over ? over.agent : agentWithSession() return ctx.tools.execute({ signal: testToolSignal, - callId: CallId(`call-${++callCounter}`), + callId: ToolCallId(`call-${++callCounter}`), name: 'todo_write', arguments: args, ...agent ? { agent } : {}, diff --git a/packages/util/brand/README.i18n.yaml b/packages/util/brand/README.i18n.yaml index 7a31df8f6b..369a2713e9 100644 --- a/packages/util/brand/README.i18n.yaml +++ b/packages/util/brand/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/util/brand/README.md -README.md: ce05a652e2f863f737bf71fc473224fc16e25ab9 -README.zh.md: 07d1279b601d38d4dcf9ca10ddeccb3c3ed3547b +README.md: ef2fe2b2d05d621ca0e316b968b9ea7ddf7dca77 +README.zh.md: 602ee1ceda21e6aab40eb98a394806cc9eb64f92 diff --git a/packages/util/brand/README.md b/packages/util/brand/README.md index ce05a652e2..ef2fe2b2d0 100644 --- a/packages/util/brand/README.md +++ b/packages/util/brand/README.md @@ -6,7 +6,7 @@ The `Branded` nominal-typing primitive — a tiny, **type-only** package (no ## What `Branded` is -A brand makes structurally-identical strings non-interchangeable at the type level: a `SessionId` cannot be passed where a `CallId` is expected, even though both are plain `string`s at runtime. +A brand makes structurally-identical strings non-interchangeable at the type level: a `SessionId` cannot be passed where a `ToolCallId` is expected, even though both are plain `string`s at runtime. ```ts import type { Branded } from '@deepseek-ai/dsh-brand' @@ -23,6 +23,6 @@ Construction goes through the per-id factory in the owning package. Comparison, ## Policy: brand ids that cross package boundaries -A package brands the ids it owns — `CallId` in `dsh-llm`, the shared agent/session `SessionId` in `dsh-session`, and `JobId` in `dsh-jobs`. Brand cross-package ids that could plausibly be confused; not every string needs one. +A package brands the ids it owns — `ToolCallId` in `dsh-llm`, the shared agent/session `SessionId` in `dsh-session`, and `JobId` in `dsh-jobs`. Brand cross-package ids that could plausibly be confused; not every string needs one. This package owns only the primitive. Keeping it dependency-free lets `dsh-jobs`, for example, brand `JobId` without importing an unrelated capability package merely to reach `Branded`. diff --git a/packages/util/brand/README.zh.md b/packages/util/brand/README.zh.md index 07d1279b60..602ee1ceda 100644 --- a/packages/util/brand/README.zh.md +++ b/packages/util/brand/README.zh.md @@ -6,7 +6,7 @@ ## `Branded` 是什么 -品牌使 `SessionId` 和 `CallId` 这样结构相同的字符串在类型层面不可互换,尽管两者在运行时都是普通 `string`。 +品牌使 `SessionId` 和 `ToolCallId` 这样结构相同的字符串在类型层面不可互换,尽管两者在运行时都是普通 `string`。 ```ts import type { Branded } from '@deepseek-ai/dsh-brand' @@ -23,6 +23,6 @@ export function SessionId(id: string): SessionId { ## 策略:为跨包边界的 id 添加品牌 -包为自己拥有的 id 添加品牌:`CallId` 位于 `dsh-llm`,共享的 agent/会话 `SessionId` 位于 `dsh-session`,`JobId` 位于 `dsh-jobs`。为可能被混淆的跨包 id 添加品牌,但无需为每个字符串都添加。 +包为自己拥有的 id 添加品牌:`ToolCallId` 位于 `dsh-llm`,共享的 agent/会话 `SessionId` 位于 `dsh-session`,`JobId` 位于 `dsh-jobs`。为可能被混淆的跨包 id 添加品牌,但无需为每个字符串都添加。 该包只负责这一原语。保持无依赖意味着,例如 `dsh-jobs` 可以为 `JobId` 使用品牌类型,而无需仅为使用 `Branded` 而导入不相关的功能包。 diff --git a/packages/util/brand/src/index.ts b/packages/util/brand/src/index.ts index 379a769e14..942f6dc9d2 100644 --- a/packages/util/brand/src/index.ts +++ b/packages/util/brand/src/index.ts @@ -4,12 +4,12 @@ * cross-boundary id. * * A brand makes structurally-identical strings non-interchangeable at the type - * level: a `SessionId` cannot be passed where a `CallId` is expected, even + * level: a `SessionId` cannot be passed where a `ToolCallId` is expected, even * though both are plain strings at runtime. Construction goes through a per-id * factory in the OWNING package (a plain cast inside — zero runtime cost); * comparison, logging, and serialization all behave as ordinary strings. * - * Policy: a package brands the ids it owns — `CallId` in dsh-llm (tool-call + * Policy: a package brands the ids it owns — `ToolCallId` in dsh-llm (tool-call * correlation), the shared agent/session `SessionId` in dsh-session, and * `JobId` in dsh-jobs. Branding is for ids that cross package boundaries and * could plausibly be confused; not every string needs a brand. diff --git a/packages/web/tool-web/tests/integration.spec.ts b/packages/web/tool-web/tests/integration.spec.ts index 1aa1fa6416..9dbe616626 100644 --- a/packages/web/tool-web/tests/integration.spec.ts +++ b/packages/web/tool-web/tests/integration.spec.ts @@ -10,7 +10,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' import { AddressInfo } from 'node:net' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { type ToolExecutionResult } from '@deepseek-ai/dsh-tools' import WebRuntime from '@deepseek-ai/dsh-web' @@ -57,7 +57,7 @@ afterEach(async () => { let counter = 0 function call(name: string, args: unknown): Promise { - return ctx.tools.execute({ signal: testToolSignal, callId: CallId(`call-${++counter}`), name, arguments: args }) + return ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId(`call-${++counter}`), name, arguments: args }) } describe('web_fetch integration over the real backend', () => { @@ -149,7 +149,7 @@ describe('tool-call timeout returns TOOL_TIMEOUT (deadline wins over a slow fetc }) it('returns a structured TOOL_TIMEOUT (not the provider WEB_FETCH_TIMEOUT) when the tool-call budget wins', async () => { - const out = await tctx.tools.execute({ signal: testToolSignal, callId: CallId('slow-1'), name: 'web_fetch', arguments: { url: slowBase } }) + const out = await tctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('slow-1'), name: 'web_fetch', arguments: { url: slowBase } }) expect(out.isError).toBe(true) // The outer tool-call deadline won: TOOL_TIMEOUT, owned by dsh-tool-call-timeout-policy, // NOT the provider's own WEB_FETCH_TIMEOUT (its 30s backstop never fired). diff --git a/packages/web/tool-web/tests/spill.spec.ts b/packages/web/tool-web/tests/spill.spec.ts index 9a7cce5844..80888ad9dd 100644 --- a/packages/web/tool-web/tests/spill.spec.ts +++ b/packages/web/tool-web/tests/spill.spec.ts @@ -14,7 +14,7 @@ import { mkdtempSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime from '@deepseek-ai/dsh-tools' @@ -65,7 +65,7 @@ afterEach(async () => { /** A web_fetch call carrying a session owner (so the policy can scope the spill). */ function fetchCall(): Promise<{ isError: boolean; content: { type: string; text?: string }[] }> { const agent = { session: { header: { id: SessionId('web-sess') } } } - const exec = { callId: CallId('call-1'), name: 'web_fetch', arguments: { url: base }, agent, signal: testToolSignal } as unknown as ToolExecution + const exec = { callId: ToolCallId('call-1'), name: 'web_fetch', arguments: { url: base }, agent, signal: testToolSignal } as unknown as ToolExecution return ctx.tools.execute(exec) } diff --git a/packages/web/tool-web/tests/tool-web.spec.ts b/packages/web/tool-web/tests/tool-web.spec.ts index 2adad8b79c..711ea6427e 100644 --- a/packages/web/tool-web/tests/tool-web.spec.ts +++ b/packages/web/tool-web/tests/tool-web.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import TurndownService from 'turndown' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { type ToolExecutionResult } from '@deepseek-ai/dsh-tools' import WebRuntime from '@deepseek-ai/dsh-web' @@ -49,7 +49,7 @@ async function mountTools(opts: { if (opts.fetchProvider) ctx.web.registerFetchProvider(opts.fetchProvider) const fiber = await ctx.plugin(ToolWeb, opts.config ?? {}) let counter = 0 - const call = (name: string, args: unknown) => ctx.tools.execute({ signal: testToolSignal, callId: CallId(`call-${++counter}`), name, arguments: args }) + const call = (name: string, args: unknown) => ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId(`call-${++counter}`), name, arguments: args }) return { ctx, fiber, call } } @@ -451,9 +451,9 @@ describe('tool-web registration', () => { const names = ctx.tools.schemas().map(s => s.name) expect(names).toContain('web_search') expect(names).toContain('web_fetch') - expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('search-safe'), name: 'web_search', arguments: { queries: ['q'] } })) + expect(ctx.tools.executionMode({ signal: testToolSignal, callId: ToolCallId('search-safe'), name: 'web_search', arguments: { queries: ['q'] } })) .toEqual({ kind: 'parallel' }) - expect(ctx.tools.executionMode({ signal: testToolSignal, callId: CallId('fetch-safe'), name: 'web_fetch', arguments: { url: 'https://a.test' } })) + expect(ctx.tools.executionMode({ signal: testToolSignal, callId: ToolCallId('fetch-safe'), name: 'web_fetch', arguments: { url: 'https://a.test' } })) .toEqual({ kind: 'parallel' }) await fiber.dispose() expect(ctx.tools.schemas().map(s => s.name)).not.toContain('web_search') @@ -723,7 +723,7 @@ describe('tool-web execution through the real registry', () => { } const { ctx, fiber } = await mountTools({ webConfig: { fetchProvider: 'stub-fetch' }, fetchProvider }) const controller = new AbortController() - const out = await ctx.tools.execute({ callId: CallId('fetch-1'), name: 'web_fetch', arguments: { url: 'https://a.test' }, signal: controller.signal }) + const out = await ctx.tools.execute({ callId: ToolCallId('fetch-1'), name: 'web_fetch', arguments: { url: 'https://a.test' }, signal: controller.signal }) expect(out.isError).toBe(false) expect(out.value).toEqual({ url: 'https://a.test', @@ -750,7 +750,7 @@ describe('tool-web execution through the real registry', () => { }, } const { ctx, fiber } = await mountTools({ webConfig: { fetchProvider: 'stub-fetch' }, fetchProvider }) - const out = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('fetch-2'), name: 'web_fetch', arguments: { url: 'https://a.test' } }) + const out = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('fetch-2'), name: 'web_fetch', arguments: { url: 'https://a.test' } }) expect(out.isError).toBe(false) expect(out.value).toEqual({ url: 'https://a.test', @@ -772,7 +772,7 @@ describe('tool-web execution through the real registry', () => { } const { ctx, fiber } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: provider }) const controller = new AbortController() - await ctx.tools.execute({ callId: CallId('search-1'), name: 'web_search', arguments: { queries: ['q'] }, signal: controller.signal }) + await ctx.tools.execute({ callId: ToolCallId('search-1'), name: 'web_search', arguments: { queries: ['q'] }, signal: controller.signal }) expect(seen.signal).toBe(controller.signal) await fiber.dispose() }) @@ -791,7 +791,7 @@ describe('tool-web execution through the real registry', () => { } const { ctx, fiber } = await mountTools({ webConfig: { searchProvider: 'stub-search' }, search: provider }) const controller = new AbortController() - const pending = ctx.tools.execute({ callId: CallId('search-multi-1'), name: 'web_search', arguments: { queries: ['one', 'two'] }, signal: controller.signal }) + const pending = ctx.tools.execute({ callId: ToolCallId('search-multi-1'), name: 'web_search', arguments: { queries: ['one', 'two'] }, signal: controller.signal }) await vi.waitFor(() => { expect(signals).toHaveLength(2) }) expect(signals[0]).toBe(signals[1]) expect(signals[0]).not.toBe(controller.signal) diff --git a/packages/workflow/tool-ralph/tests/integration.spec.ts b/packages/workflow/tool-ralph/tests/integration.spec.ts index e6de6c38ef..636f320729 100644 --- a/packages/workflow/tool-ralph/tests/integration.spec.ts +++ b/packages/workflow/tool-ralph/tests/integration.spec.ts @@ -3,7 +3,7 @@ import { Context } from '@deepseek-ai/cordis' import type { Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' -import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import SubagentRuntime from '@deepseek-ai/dsh-subagent' import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-in-process-driver' @@ -83,7 +83,7 @@ describe('dsh-tool-ralph over the real spawn and worker-thread stack', () => { }) const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('ralph-integration'), + callId: ToolCallId('ralph-integration'), name: 'ralph', arguments: { objective: 'Complete both migration slices.', maxRounds: 2 }, agent: parent, @@ -135,7 +135,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'), + callId: ToolCallId('ralph-child-failure'), name: 'ralph', arguments: { objective: 'Complete both migration slices.', maxRounds: 2 }, agent: parent, @@ -224,7 +224,7 @@ describe('dsh-tool-ralph over the real spawn and worker-thread stack', () => { const result = await ctx.tools.execute({ signal: testToolSignal, - callId: CallId('ralph-script-enforcement'), + callId: ToolCallId('ralph-script-enforcement'), name: 'ralph', arguments: { objective: 'Complete the scoped work.', maxRounds: config.maxRounds }, agent: parent, @@ -251,7 +251,7 @@ describe('dsh-tool-ralph over the real spawn and worker-thread stack', () => { ctx.on('workflow/agent-end', (_run, child) => { outcomes.push(child.outcome) }) const controller = new AbortController() const pending = ctx.tools.execute({ - callId: CallId('ralph-real-cancel'), + callId: ToolCallId('ralph-real-cancel'), name: 'ralph', arguments: { objective: 'Keep working until cancelled.', maxRounds: 2 }, agent: parent, diff --git a/packages/workflow/tool-ralph/tests/tool-ralph.spec.ts b/packages/workflow/tool-ralph/tests/tool-ralph.spec.ts index 7c6f02e36c..d552f33fb0 100644 --- a/packages/workflow/tool-ralph/tests/tool-ralph.spec.ts +++ b/packages/workflow/tool-ralph/tests/tool-ralph.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import type { Agent } from '@deepseek-ai/dsh-agent' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import SubagentRuntime from '@deepseek-ai/dsh-subagent' import type { SubagentCapabilities, SubagentProvider, SubagentRun, SubagentStartRequest } from '@deepseek-ai/dsh-subagent' @@ -99,7 +99,7 @@ function execute( ): Promise { return ctx.tools.execute({ signal: extra?.signal ?? testToolSignal, - callId: CallId('ralph-call'), + callId: ToolCallId('ralph-call'), name: 'ralph', arguments: args, ...extra?.agent === undefined ? {} : { agent: extra.agent }, diff --git a/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts b/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts index 938f9a5502..408767fae2 100644 --- a/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts +++ b/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts @@ -10,7 +10,7 @@ import type { WorkflowAgentEndInfo, WorkflowAgentInfo, WorkflowResult, WorkflowRun, WorkflowRunId as WorkflowRunIdType, WorkflowStartRequest, } from '@deepseek-ai/dsh-workflow' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import SubagentRuntime from '@deepseek-ai/dsh-subagent' import WorkerThreadWorkflowEngine from '@deepseek-ai/dsh-workflow-worker-thread' import * as toolWorkflow from '../src/index.ts' @@ -96,7 +96,7 @@ function execute(ctx: Context, args: unknown, extra?: { }): Promise { return ctx.tools.execute({ signal: testToolSignal, - callId: CallId('call-1'), + callId: ToolCallId('call-1'), name: 'workflow', arguments: args, ...extra?.agent ? { agent: extra.agent } : {}, diff --git a/scripts/gen-persistence-catalog.ts b/scripts/gen-persistence-catalog.ts index 248951def8..af5de495e6 100644 --- a/scripts/gen-persistence-catalog.ts +++ b/scripts/gen-persistence-catalog.ts @@ -38,7 +38,7 @@ type EventEnvelopeTypeName = typeof EVENT_ENVELOPE_TYPE_NAMES[number] /** Primary subsystems page for linked payload types. */ const LINK_MAP: Record = { - CallId: 'core.md', + ToolCallId: 'core.md', ContentBlock: 'core.md', MessageSource: 'core.md', ScheduleChange: 'schedule.md', From aa5bc532d31d447f5438b71855eac04832500688 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 19 Aug 2026 22:35:31 +0800 Subject: [PATCH 02/13] docs(notes): rename CallId to ToolCallId in agent notes Keep the active implemented and proposed Agent Notes current with the renamed ToolCallId brand: branded-ids, content-block-vocabulary, approval-seam, tool-output-spill-files, and task-surface, in both languages, with re-recorded .i18n.yaml pairing records. --- .../2026-06-11-content-block-vocabulary.i18n.yaml | 4 ++-- .../architecture/2026-06-11-content-block-vocabulary.md | 2 +- .../2026-06-11-content-block-vocabulary.zh.md | 2 +- .../architecture/2026-06-20-branded-ids.i18n.yaml | 4 ++-- .../implemented/architecture/2026-06-20-branded-ids.md | 8 ++++---- .../implemented/architecture/2026-06-20-branded-ids.zh.md | 8 ++++---- .../2026-07-08-tool-output-spill-files.i18n.yaml | 4 ++-- .../architecture/2026-07-08-tool-output-spill-files.md | 2 +- .../architecture/2026-07-08-tool-output-spill-files.zh.md | 2 +- .../feature/2026-07-06-approval-seam.i18n.yaml | 4 ++-- .../notes/implemented/feature/2026-07-06-approval-seam.md | 2 +- .../implemented/feature/2026-07-06-approval-seam.zh.md | 2 +- .../proposed/feature/2026-08-04-task-surface.i18n.yaml | 4 ++-- .agents/notes/proposed/feature/2026-08-04-task-surface.md | 6 +++--- .../notes/proposed/feature/2026-08-04-task-surface.zh.md | 6 +++--- 15 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml index dac9ce6b5c..98850aed4c 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md -2026-06-11-content-block-vocabulary.md: a31df6a7d16ea7cba649702fdb474dab34533c1b -2026-06-11-content-block-vocabulary.zh.md: da387b179816cda64791e71ca7affa1fbdfd195b +2026-06-11-content-block-vocabulary.md: d7d3f6b43a3f65d1421f026e5b6c2cc1ba1eadd2 +2026-06-11-content-block-vocabulary.zh.md: ed4f915dff6dcb6dbc91400f9bfa5384253aea7b diff --git a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md index a31df6a7d1..d7d3f6b43a 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md @@ -25,4 +25,4 @@ In-session context injection (`context/message`) and mid-turn steering originall - Multimodal blocks return only with coordinated adapter, UI, and compaction support; see [the drop-image Agent Note](../../archived/simplification/2026-07-04-drop-image-content-block.md). - Cache hints and assistant prefill remain absent until a shipping adapter can honor them; see the [producer-less variants](../../archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md) and [inert request knobs](../../archived/simplification/2026-07-04-drop-inert-request-knobs.md) Agent Notes. - Every adapter pays a translation cost; the first real adapters have since validated the streaming protocol, and new adapters should continue proving their provider-specific mapping in adapter-local tests. -- IDs that cross package boundaries are branded (`CallId`, the shared agent/session `SessionId`) — nominal typing at zero runtime cost. +- IDs that cross package boundaries are branded (`ToolCallId`, the shared agent/session `SessionId`) — nominal typing at zero runtime cost. diff --git a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md index da387b1798..ed4f915dff 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md @@ -25,4 +25,4 @@ harness 需要一套统一的内部消息语言,供 agent loop(智能体循 - 多模态块只有在适配器、UI 和上下文压缩(context compaction)三方协同支持后才会回归;见 [drop-image Agent Note](../../archived/simplification/2026-07-04-drop-image-content-block.md)。 - 缓存提示与 assistant prefill 在有实际适配器能兑现之前保持缺席;见[无生产者的词汇变体](../../archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md)与[无端到端可用路径的请求旋钮](../../archived/simplification/2026-07-04-drop-inert-request-knobs.md) Agent Note。 - 每个适配器都需承担翻译成本;首批真实适配器已验证了流式输出协议,新适配器应继续在适配器本地测试中验证其提供方特有的映射。 -- 跨包边界的 ID 使用品牌类型(`CallId`、agent 与会话共享的 `SessionId`)——零运行时开销的名义类型。 +- 跨包边界的 ID 使用品牌类型(`ToolCallId`、agent 与会话共享的 `SessionId`)——零运行时开销的名义类型。 diff --git a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml index ed11425f4b..8cad585a9b 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-branded-ids.md -2026-06-20-branded-ids.md: 29b258b21240c92e74051339f0939a8e70933099 -2026-06-20-branded-ids.zh.md: 2f960bac3172f1e83161f1af8f4e9d2cc0cda7bd +2026-06-20-branded-ids.md: 68558071c47fb2b74e559b6b9936366911eb6548 +2026-06-20-branded-ids.zh.md: 5ea2547b8ab45534188a6a4a9e61f3458358beb3 diff --git a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md index 29b258b212..68558071c4 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md +++ b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md @@ -6,13 +6,13 @@ English | [中文](2026-06-20-branded-ids.zh.md) ## Problem -The harness brands `CallId` (`packages/llm/llm/src/brand.ts`) and the shared agent/session `SessionId` (`packages/core/session/src/types.ts`) using the `Branded = string & { readonly [BRAND]: B }` machinery (owned by the type-only `@deepseek-ai/dsh-brand` package at `packages/util/brand/` — see its [README](../../../../packages/util/brand/README.md)) and a zero-cost cast factory per type. `dsh-brand` also states the governing policy: *"Branding is for ids that cross package boundaries and could plausibly be confused; not every string needs a brand."* That policy is right; the problem is that it is only half-applied. Two gaps let a structurally-identical-but-semantically-wrong string slip through the type checker today. +The harness brands `ToolCallId` (`packages/llm/llm/src/brand.ts`) and the shared agent/session `SessionId` (`packages/core/session/src/types.ts`) using the `Branded = string & { readonly [BRAND]: B }` machinery (owned by the type-only `@deepseek-ai/dsh-brand` package at `packages/util/brand/` — see its [README](../../../../packages/util/brand/README.md)) and a zero-cost cast factory per type. `dsh-brand` also states the governing policy: *"Branding is for ids that cross package boundaries and could plausibly be confused; not every string needs a brand."* That policy is right; the problem is that it is only half-applied. Two gaps let a structurally-identical-but-semantically-wrong string slip through the type checker today. **Gap 1 — unbranded cross-boundary IDs in the bash seam.** The background-job id is a plain `string`: `BashTask.id: string` (`packages/shell/shell/src/types.ts`), carried as `string` through the whole executor seam (`ShellExecutor.get`/`ownerOf`/`readOutput`/`kill(id: string)` in `packages/shell/shell/src/index.ts`) and validated/passed as `string` by the model-facing tools (`validateJobId`, `assertTaskAccess`, the `job_id` schema arg in `packages/shell/tool-bash/src/index.ts`). It is generated by a per-executor counter — `` `bash-${this.nextTaskId++}` `` in `packages/shell/bash-local/src/index.ts` — which gives it **exactly the same `name-N` shape as `SessionId`'s default** (`` `session-${++counter}` `` in `packages/core/session/src/index.ts`). A bash job id and a session id are trivially swappable at a call site and the compiler says nothing. It is a model-facing id (the model passes `job_id` back to `bash_output`/`bash_kill`), so a confusion here is reachable from untrusted input. The bash **owner token** is the related sub-case: `ShellExecRequest.owner?: string` and `ShellExecSpec.owner: string | undefined` (`packages/shell/shell/src/types.ts`) are documented as a deliberately *opaque* isolation key, but in every live caller the value IS the owning agent's shared `Agent.id`/`SessionId` (`callerToken = (exec) => exec.agent?.id` in `packages/shell/tool-bash/src/index.ts`) wearing a different seam-local name. It is compared for access control (`owner !== callerToken(exec)`), so a mismatched-but-well-typed string here is a cross-session isolation bug the type system currently cannot catch. This is the shared id alias covered by the [unified agent/session identity decision](../simplification/2026-06-20-unify-agent-and-session-id.md). -**Gap 2 — brand erosion at the boundaries of the *already-branded* IDs.** Even `CallId` and `SessionId` decay back to bare `string` at exactly the places confusion is most likely: registry/store key types and public method params. Representative sites include the session store, the agent registry (both keyed by the shared `SessionId`), tool-presentation call-id maps, ACP's session records, and the persistence coordinator. A brand that is dropped at a collection key buys nothing on lookups — the value of the existing brands is partly unrealized. +**Gap 2 — brand erosion at the boundaries of the *already-branded* IDs.** Even `ToolCallId` and `SessionId` decay back to bare `string` at exactly the places confusion is most likely: registry/store key types and public method params. Representative sites include the session store, the agent registry (both keyed by the shared `SessionId`), tool-presentation call-id maps, ACP's session records, and the persistence coordinator. A brand that is dropped at a collection key buys nothing on lookups — the value of the existing brands is partly unrealized. ## Decision @@ -22,7 +22,7 @@ A type-only change. Brands are zero-cost casts; nothing about runtime behavior, - **Mint a distinct `OwnerToken` brand.** Add `OwnerToken = Branded<'OwnerToken'>` in `packages/shell/shell/src/types.ts`; type `ShellExecRequest.owner` / `ShellExecSpec.owner` / `ShellExecutor.ownerOf` as `OwnerToken | undefined`. The `dsh-tool-bash` consumer casts the agent's shared `id` (`SessionId`) into an `OwnerToken` at the boundary — the one place the two vocabularies meet. The bash Service Definition never imports `dsh-session`. (Rationale in the next section.) -- **Stop the brand erosion.** Propagate the existing brands to the `Map` key types and public method params listed under Gap 2 — `Map`, `Map`, `get(id: SessionId)`, `Map`, ACP's `SessionId` surface, and the coordinator's `Map`. This is the larger mechanical share of the change and the part that makes the *existing* brands actually load-bearing on lookups, not just on struct fields. +- **Stop the brand erosion.** Propagate the existing brands to the `Map` key types and public method params listed under Gap 2 — `Map`, `Map`, `get(id: SessionId)`, `Map`, ACP's `SessionId` surface, and the coordinator's `Map`. This is the larger mechanical share of the change and the part that makes the *existing* brands actually load-bearing on lookups, not just on struct fields. Illustrative shape (the factory pattern is identical to the three existing brands): @@ -60,7 +60,7 @@ Kept deliberately narrow per the "not every string needs a brand" policy. Each o ## Verification -The landed invariants: `BashTaskId` and `OwnerToken` are defined in `dsh-shell` and threaded end-to-end (Service Definition, the `dsh-bash-local` generation site, the `dsh-tool-bash` model-facing tool) with no `dsh-shell` dependency on `dsh-session`; no collection keyed by an in-scope branded id (`CallId`/`SessionId`/`BashTaskId`) is keyed by bare `string`; public method params and exported signatures keep the brand; and brands are constructed via the cast factory at each boundary where a raw string enters (provider call id, ACP session id, model-supplied `job_id`), never as scattered `as` casts. +The landed invariants: `BashTaskId` and `OwnerToken` are defined in `dsh-shell` and threaded end-to-end (Service Definition, the `dsh-bash-local` generation site, the `dsh-tool-bash` model-facing tool) with no `dsh-shell` dependency on `dsh-session`; no collection keyed by an in-scope branded id (`ToolCallId`/`SessionId`/`BashTaskId`) is keyed by bare `string`; public method params and exported signatures keep the brand; and brands are constructed via the cast factory at each boundary where a raw string enters (provider call id, ACP session id, model-supplied `job_id`), never as scattered `as` casts. ## Consequences diff --git a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md index 2f960bac31..5ea2547b8a 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md @@ -6,13 +6,13 @@ Status: implemented ## 问题 -harness 使用 `Branded = string & { readonly [BRAND]: B }` 机制,为 `CallId`(`packages/llm/llm/src/brand.ts`)和 agent(智能体)/会话共享的 `SessionId`(`packages/core/session/src/types.ts`)做 brand 处理;该机制由纯类型包 `@deepseek-ai/dsh-brand` 拥有,位于 `packages/util/brand/`,见其 [README](../../../../packages/util/brand/README.zh.md),并为每个类型提供零开销的 cast 工厂。`dsh-brand` 还声明了治理策略:*「Branding 用于跨包边界且可能被混淆的 id;不是每个 string 都需要 brand。」* 这条策略是正确的;问题在于它只落实了一半。两处缺口使得结构相同但语义错误的 string 今天仍能通过类型检查器。 +harness 使用 `Branded = string & { readonly [BRAND]: B }` 机制,为 `ToolCallId`(`packages/llm/llm/src/brand.ts`)和 agent(智能体)/会话共享的 `SessionId`(`packages/core/session/src/types.ts`)做 brand 处理;该机制由纯类型包 `@deepseek-ai/dsh-brand` 拥有,位于 `packages/util/brand/`,见其 [README](../../../../packages/util/brand/README.zh.md),并为每个类型提供零开销的 cast 工厂。`dsh-brand` 还声明了治理策略:*「Branding 用于跨包边界且可能被混淆的 id;不是每个 string 都需要 brand。」* 这条策略是正确的;问题在于它只落实了一半。两处缺口使得结构相同但语义错误的 string 今天仍能通过类型检查器。 **缺口 1:bash seam 中未 brand 的跨边界 ID。** 后台 job id 是普通 `string`:`BashTask.id: string`(`packages/shell/shell/src/types.ts`),作为 `string` 贯穿整个执行器 seam(`packages/shell/shell/src/index.ts` 中的 `ShellExecutor.get`/`ownerOf`/`readOutput`/`kill(id: string)`),再由面向模型的工具以 `string` 校验并传递(`validateJobId`、`assertTaskAccess`、`packages/shell/tool-bash/src/index.ts` 中 `job_id` 的 schema 参数)。它由每执行器计数器生成——`packages/shell/bash-local/src/index.ts` 中的 `` `bash-${this.nextTaskId++}` ``——其形状与 `SessionId` 的默认值**完全相同,都是 `name-N`**(`packages/core/session/src/index.ts` 中的 `` `session-${++counter}` ``)。bash job id 和会话 id 在调用点轻易就能互换,而编译器毫无反应。它是面向模型的 id(模型会把 `job_id` 传回 `bash_output`/`bash_kill`),所以该混淆可由不受信任的输入触达。 bash **owner token** 是相关的子情形:`ShellExecRequest.owner?: string` 和 `ShellExecSpec.owner: string | undefined`(`packages/shell/shell/src/types.ts`)被文档描述为刻意*不透明*的隔离键,但在所有实际调用方中,该值就是所属 agent 共享的 `Agent.id`/`SessionId`(`callerToken = (exec) => exec.agent?.id`,位于 `packages/shell/tool-bash/src/index.ts`),只是披着另一个 seam 本地名称。它被用于访问控制比较(`owner !== callerToken(exec)`),因此一个不匹配但类型正确的 string 在此处就是跨会话隔离 bug,而当前类型系统无法捕获。这正是[统一 agent/session 标识决策](../simplification/2026-06-20-unify-agent-and-session-id.zh.md)覆盖的共享 id 别名。 -**缺口 2:*已经 brand* 的 ID 在边界处被侵蚀。** 就连 `CallId` 和 `SessionId` 也恰好在最容易混淆的地方退化为裸 `string`:注册表/store 键类型和公开方法参数。代表性位置包括会话存储、agent 注册表(二者都以共享的 `SessionId` 为键)、工具展示层的 call-id map、ACP(Agent Client Protocol)的会话记录,以及持久化协调器。在集合键处丢弃 brand,会让既有 brand 在查找时毫无价值;它们的价值只实现了一部分。 +**缺口 2:*已经 brand* 的 ID 在边界处被侵蚀。** 就连 `ToolCallId` 和 `SessionId` 也恰好在最容易混淆的地方退化为裸 `string`:注册表/store 键类型和公开方法参数。代表性位置包括会话存储、agent 注册表(二者都以共享的 `SessionId` 为键)、工具展示层的 call-id map、ACP(Agent Client Protocol)的会话记录,以及持久化协调器。在集合键处丢弃 brand,会让既有 brand 在查找时毫无价值;它们的价值只实现了一部分。 ## 决策 @@ -22,7 +22,7 @@ bash **owner token** 是相关的子情形:`ShellExecRequest.owner?: string` - **铸造独立的 `OwnerToken` brand。** 在 `packages/shell/shell/src/types.ts` 中添加 `OwnerToken = Branded<'OwnerToken'>`;将 `ShellExecRequest.owner` / `ShellExecSpec.owner` / `ShellExecutor.ownerOf` 的类型标注为 `OwnerToken | undefined`。`dsh-tool-bash` 消费方在边界处将 agent 共享的 `id`(`SessionId`)cast 为 `OwnerToken`——这是两套词汇唯一交汇的地方。bash Service Definition 从不导入 `dsh-session`。(理由见下一节。) -- **阻止 brand 侵蚀。** 将既有 brand 传播到缺口 2 列出的 `Map` 键类型和公开方法参数中:`Map`、`Map`、`get(id: SessionId)`、`Map`、ACP 的 `SessionId` surface、协调器的 `Map`。这是变更中机械量最大的部分,也是让*既有* brand 在查找处真正发挥作用(而不仅仅标注在结构体字段上)的关键。 +- **阻止 brand 侵蚀。** 将既有 brand 传播到缺口 2 列出的 `Map` 键类型和公开方法参数中:`Map`、`Map`、`get(id: SessionId)`、`Map`、ACP 的 `SessionId` surface、协调器的 `Map`。这是变更中机械量最大的部分,也是让*既有* brand 在查找处真正发挥作用(而不仅仅标注在结构体字段上)的关键。 示意形状(工厂模式与已有的三个 brand 完全一致): @@ -60,7 +60,7 @@ export function OwnerToken(id: string): OwnerToken { ## 验证 -已落地的不变式如下:`BashTaskId` 和 `OwnerToken` 定义在 `dsh-shell` 中,并端到端贯穿 Service Definition、`dsh-bash-local` 生成点与 `dsh-tool-bash` 面向模型的工具,且 `dsh-shell` 未添加对 `dsh-session` 的依赖;没有任何以范围内 brand id(`CallId`/`SessionId`/`BashTaskId`)为键的集合使用裸 `string`;公开方法参数和导出签名保留 brand;每个原始 string 进入的边界(提供方 call id、ACP 会话 id、模型提供的 `job_id`)都通过 cast 工厂构造 brand,而不是散落的 `as` cast。 +已落地的不变式如下:`BashTaskId` 和 `OwnerToken` 定义在 `dsh-shell` 中,并端到端贯穿 Service Definition、`dsh-bash-local` 生成点与 `dsh-tool-bash` 面向模型的工具,且 `dsh-shell` 未添加对 `dsh-session` 的依赖;没有任何以范围内 brand id(`ToolCallId`/`SessionId`/`BashTaskId`)为键的集合使用裸 `string`;公开方法参数和导出签名保留 brand;每个原始 string 进入的边界(提供方 call id、ACP 会话 id、模型提供的 `job_id`)都通过 cast 工厂构造 brand,而不是散落的 `as` cast。 ## 后果 diff --git a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.i18n.yaml index 15abdc91f0..7f42f584f8 100644 --- a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md -2026-07-08-tool-output-spill-files.md: 4d1d4b7b665f34b362df2f8c8aeb06d96bf2668f -2026-07-08-tool-output-spill-files.zh.md: 3e7ce4f57a0078c6a4b919436946be8e172fa7cb +2026-07-08-tool-output-spill-files.md: 341cb1ce131f627eed0f76ca6eb6a26fbb9dc689 +2026-07-08-tool-output-spill-files.zh.md: bd7c5dc058a8c753cefff3dd81802802426529ec diff --git a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md index 4d1d4b7b66..341cb1ce13 100644 --- a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md +++ b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md @@ -35,7 +35,7 @@ interface SpillStore { interface SpillSource { toolName: string - callId: CallId + callId: ToolCallId label: string } diff --git a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md index 3e7ce4f57a..bd7c5dc058 100644 --- a/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md @@ -35,7 +35,7 @@ interface SpillStore { interface SpillSource { toolName: string - callId: CallId + callId: ToolCallId label: string } diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml b/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml index bb470cfea4..38d6e0a7aa 100644 --- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-06-approval-seam.md -2026-07-06-approval-seam.md: ace41ebbb94cc24c2fdd3e7ae2d3a69b28b169af -2026-07-06-approval-seam.zh.md: 63b7478b0903c149fc87fd944c95e94cc8de2359 +2026-07-06-approval-seam.md: 92bdf38918c0b4e4da0cdd302e9e0aebf46fcc76 +2026-07-06-approval-seam.zh.md: 9b6bdf6f6416599258d1c177a1dc0f7479fbf523 diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.md b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md index ace41ebbb9..92bdf38918 100644 --- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.md +++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.md @@ -55,7 +55,7 @@ After validation and a successful `approval/asked` append, the service resolves Answerers are `approval/request` waterfall listeners. Zero listeners fall through to `unavailable`; a recognizing listener occupies the first-wins decision slot, while an unrecognized agent must delegate with `next()`. Listeners dispose with their fibers, so an unloaded channel fails closed. Because sibling registration order is not deterministic, a deployment composes one terminal answerer and reserves `prepend` for decide-or-delegate gates. -`ApprovalRequest` carries the asking `agent`, `toolName`, optional exact `callId`, human-readable `reason`, and optional `signal`. It uses the `CallId` brand without importing `dsh-tools`, which depends on this seam. Channel adapters correlate any richer call state by `callId`; the approval request does not duplicate tool arguments. +`ApprovalRequest` carries the asking `agent`, `toolName`, optional exact `callId`, human-readable `reason`, and optional `signal`. It uses the `ToolCallId` brand without importing `dsh-tools`, which depends on this seam. Channel adapters correlate any richer call state by `callId`; the approval request does not duplicate tool arguments. #### Ask routing in dsh-tools diff --git a/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md b/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md index 63b7478b09..9b6bdf6f64 100644 --- a/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md +++ b/.agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md @@ -55,7 +55,7 @@ tool/result "escalated" — this one call ran under the wider mode; the gra 应答者是 `approval/request` waterfall 监听器。零监听器会直接落到 `unavailable`;识别该 agent 的监听器占用先到先得的决策槽,而不识别的监听器必须调用 `next()` 委派。监听器会随其 fiber 一同 dispose(资源释放),因此卸载通道后,请求会在故障时默认被拒绝。由于兄弟插件的注册顺序不确定,部署应组合一个终端应答者,并保留 `prepend` 给「决策或委派」门禁。 -`ApprovalRequest` 携带发起请求的 `agent`、`toolName`、可选的精确 `callId`、人类可读的 `reason` 和可选的 `signal`。它使用 `CallId` brand 而不导入依赖本 seam 的 `dsh-tools`。通道适配器可按 `callId` 关联任何更丰富的调用状态;审批请求本身不重复携带工具参数。 +`ApprovalRequest` 携带发起请求的 `agent`、`toolName`、可选的精确 `callId`、人类可读的 `reason` 和可选的 `signal`。它使用 `ToolCallId` brand 而不导入依赖本 seam 的 `dsh-tools`。通道适配器可按 `callId` 关联任何更丰富的调用状态;审批请求本身不重复携带工具参数。 #### dsh-tools 中的 Ask 路由 diff --git a/.agents/notes/proposed/feature/2026-08-04-task-surface.i18n.yaml b/.agents/notes/proposed/feature/2026-08-04-task-surface.i18n.yaml index e45bb53e99..e8f45bb052 100644 --- a/.agents/notes/proposed/feature/2026-08-04-task-surface.i18n.yaml +++ b/.agents/notes/proposed/feature/2026-08-04-task-surface.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-08-04-task-surface.md -2026-08-04-task-surface.md: d2c75d30f47076577534c239d4f1ed222851c45f -2026-08-04-task-surface.zh.md: a428175422f853072750487e5463fb614e8f1d00 +2026-08-04-task-surface.md: 13edd1c89514363e43753f366b7b38b6810bd969 +2026-08-04-task-surface.zh.md: ac3fdc48073c7d63d1f0f33bdf7861064d4449c6 diff --git a/.agents/notes/proposed/feature/2026-08-04-task-surface.md b/.agents/notes/proposed/feature/2026-08-04-task-surface.md index d2c75d30f4..13edd1c895 100644 --- a/.agents/notes/proposed/feature/2026-08-04-task-surface.md +++ b/.agents/notes/proposed/feature/2026-08-04-task-surface.md @@ -142,7 +142,7 @@ type SubmitTaskSurfaceResult = type GetActiveTaskSurfaceResult = | { active: true - callId: CallId + callId: ToolCallId surfaceId: TaskSurfaceId model: TaskSurfaceModelV1 pending: TaskSurfacePendingSubmission | null @@ -166,7 +166,7 @@ The Host resolves the exact successful `show_task_surface` occurrence, revalidat interface TaskSurfaceCorrelation { version: 1 submissionId: TaskSurfaceSubmissionId - callId: CallId + callId: ToolCallId surfaceId: TaskSurfaceId values: Record } @@ -210,7 +210,7 @@ The Session log is the authority. A small `taskSurface` unit in the existing [Se ```ts ignore-check interface TaskSurfaceProjection { - active: { callId: CallId; surfaceId: TaskSurfaceId } | null + active: { callId: ToolCallId; surfaceId: TaskSurfaceId } | null } ``` diff --git a/.agents/notes/proposed/feature/2026-08-04-task-surface.zh.md b/.agents/notes/proposed/feature/2026-08-04-task-surface.zh.md index a428175422..ac3fdc4807 100644 --- a/.agents/notes/proposed/feature/2026-08-04-task-surface.zh.md +++ b/.agents/notes/proposed/feature/2026-08-04-task-surface.zh.md @@ -142,7 +142,7 @@ type SubmitTaskSurfaceResult = type GetActiveTaskSurfaceResult = | { active: true - callId: CallId + callId: ToolCallId surfaceId: TaskSurfaceId model: TaskSurfaceModelV1 pending: TaskSurfacePendingSubmission | null @@ -166,7 +166,7 @@ Host 解析出 `show_task_surface` 的确切成功调用实例,依据其已持 interface TaskSurfaceCorrelation { version: 1 submissionId: TaskSurfaceSubmissionId - callId: CallId + callId: ToolCallId surfaceId: TaskSurfaceId values: Record } @@ -210,7 +210,7 @@ Task Surface 服务将已接受提交的协调状态记录为 `pending.phase: 'q ```ts ignore-check interface TaskSurfaceProjection { - active: { callId: CallId; surfaceId: TaskSurfaceId } | null + active: { callId: ToolCallId; surfaceId: TaskSurfaceId } | null } ``` From 82c34463fc98c52bae8d9f6e31f46c8a36cfe9ad Mon Sep 17 00:00:00 2001 From: _Kerman Date: Fri, 21 Aug 2026 17:11:33 +0800 Subject: [PATCH 03/13] fix(tests): complete ToolCallId rename --- packages/core/agent-loop/tests/cancel.spec.ts | 6 +++--- packages/llm/llm/tests/assembler.spec.ts | 6 +++--- packages/llm/llm/tests/content.spec.ts | 8 ++++---- .../tests/compression.spec.ts | 8 ++++---- .../tests/differential.spec.ts | 10 +++++----- .../tests/loader-composition.spec.ts | 4 ++-- .../shell/tool-pwsh-persistent/tests/tools.spec.ts | 4 ++-- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/core/agent-loop/tests/cancel.spec.ts b/packages/core/agent-loop/tests/cancel.spec.ts index 992f4b6284..0ac3637120 100644 --- a/packages/core/agent-loop/tests/cancel.spec.ts +++ b/packages/core/agent-loop/tests/cancel.spec.ts @@ -1,4 +1,4 @@ -import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm' +import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' /** * Tests for the queue-aware `Agent.cancel()` primitive. The default clears * queued and steering work, while `keepInbox` preserves pending input for a @@ -543,7 +543,7 @@ describe('Agent.cancel()', () => { { type: 'text-delta', index: 0, text: 'reading the file' }, { type: 'block-end', index: 0, block: { type: 'text', text: 'reading the file' } }, { type: 'block-start', index: 1, blockType: 'tool-call' }, - { type: 'tool-call-delta', index: 1, id: CallId('c1'), name: 'read', argumentsDelta: '{"pa' }, + { type: 'tool-call-delta', index: 1, id: ToolCallId('c1'), name: 'read', argumentsDelta: '{"pa' }, ], }]) const ctx = await harness(adapter) @@ -620,7 +620,7 @@ describe('Agent.cancel()', () => { const adapter = new MockAdapter([{ hangAfter: [ { type: 'block-start', index: 0, blockType: 'tool-call' }, - { type: 'tool-call-delta', index: 0, id: CallId('c1'), name: 'read', argumentsDelta: '{"pa' }, + { type: 'tool-call-delta', index: 0, id: ToolCallId('c1'), name: 'read', argumentsDelta: '{"pa' }, ], }]) const ctx = await harness(adapter) diff --git a/packages/llm/llm/tests/assembler.spec.ts b/packages/llm/llm/tests/assembler.spec.ts index 5aff02d41b..4a4a73324e 100644 --- a/packages/llm/llm/tests/assembler.spec.ts +++ b/packages/llm/llm/tests/assembler.spec.ts @@ -245,10 +245,10 @@ describe('BlockAssembler.interruptedBlocks', () => { assembler.push({ type: 'text-delta', index: 0, text: 'calling' }) assembler.push({ type: 'block-end', index: 0, block: { type: 'text', text: 'calling' } }) assembler.push({ type: 'block-start', index: 1, blockType: 'tool-call' }) - assembler.push({ type: 'tool-call-delta', index: 1, id: CallId('c1'), name: 'read', argumentsDelta: '{"a":1}' }) - assembler.push({ type: 'block-end', index: 1, block: { type: 'tool-call', id: CallId('c1'), name: 'read', arguments: '{"a":1}' } }) + assembler.push({ type: 'tool-call-delta', index: 1, id: ToolCallId('c1'), name: 'read', argumentsDelta: '{"a":1}' }) + assembler.push({ type: 'block-end', index: 1, block: { type: 'tool-call', id: ToolCallId('c1'), name: 'read', arguments: '{"a":1}' } }) assembler.push({ type: 'block-start', index: 2, blockType: 'tool-call' }) - assembler.push({ type: 'tool-call-delta', index: 2, id: CallId('c2'), name: 'read', argumentsDelta: '{"pa' }) + assembler.push({ type: 'tool-call-delta', index: 2, id: ToolCallId('c2'), name: 'read', argumentsDelta: '{"pa' }) expect(assembler.interruptedBlocks()).toEqual([{ type: 'text', text: 'calling' }]) }) diff --git a/packages/llm/llm/tests/content.spec.ts b/packages/llm/llm/tests/content.spec.ts index ffb5a586bf..2a7de895b7 100644 --- a/packages/llm/llm/tests/content.spec.ts +++ b/packages/llm/llm/tests/content.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { AttachmentId } from '@deepseek-ai/dsh-attachment' -import { CallId, createUserMessage, OFFLOADED_IMAGE_TEXT, offloadRequestImages } from '../src/index.ts' +import { createUserMessage, OFFLOADED_IMAGE_TEXT, offloadRequestImages, ToolCallId } from '../src/index.ts' import type { ContentBlock } from '../src/index.ts' const source = { kind: 'plugin' as const, plugin: 'test' } @@ -49,7 +49,7 @@ describe('offloadRequestImages', () => { createUserMessage({ content: [{ type: 'tool-result', - toolCallId: CallId('shot'), + toolCallId: ToolCallId('shot'), content: [shared], }], source, @@ -61,7 +61,7 @@ describe('offloadRequestImages', () => { expect(fitted).not.toBe(messages) expect(fitted[0]?.content).toEqual([{ type: 'tool-result', - toolCallId: CallId('shot'), + toolCallId: ToolCallId('shot'), content: [{ type: 'text', text: OFFLOADED_IMAGE_TEXT }], }]) expect(fitted[1]?.content).toEqual([shared, image(3)]) @@ -77,7 +77,7 @@ describe('offloadRequestImages', () => { it('keeps unchanged nested content while replacing a later image', () => { const nested = { type: 'tool-result' as const, - toolCallId: CallId('text-only'), + toolCallId: ToolCallId('text-only'), content: [{ type: 'text' as const, text: 'kept' }], } const messages = [createUserMessage({ content: [nested, image(3)], source })] diff --git a/packages/session/session-persistence-sqlite/tests/compression.spec.ts b/packages/session/session-persistence-sqlite/tests/compression.spec.ts index 93f4da380f..c638b70b2f 100644 --- a/packages/session/session-persistence-sqlite/tests/compression.spec.ts +++ b/packages/session/session-persistence-sqlite/tests/compression.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { zstdCompressSync } from 'node:zlib' import type { SessionEvent } from '@deepseek-ai/dsh-session' -import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm' +import { ToolCallId, type StreamChunk } from '@deepseek-ai/dsh-llm' import { decodeStorageRecord, MAX_PACKED_DATA_BYTES, @@ -85,10 +85,10 @@ describe('SQLite compression', () => { const events = [ ...[0, 1, 2].map(seq => event(seq, seq, { type: 'reasoning-delta', index: 1, text: `${seq}` })), ...[3, 4, 5].map(seq => event(seq, seq, { - type: 'tool-call-delta', index: 2, id: CallId('named'), name: 'write', argumentsDelta: `${seq}`, + type: 'tool-call-delta', index: 2, id: ToolCallId('named'), name: 'write', argumentsDelta: `${seq}`, })), ...[6, 7, 8].map(seq => event(seq, seq, { - type: 'tool-call-delta', index: 3, id: CallId('unnamed'), argumentsDelta: `${seq}`, + type: 'tool-call-delta', index: 3, id: ToolCallId('unnamed'), argumentsDelta: `${seq}`, })), ] const records = packChunkRuns(events) @@ -128,7 +128,7 @@ describe('SQLite compression', () => { event(2, Number.MAX_SAFE_INTEGER, { type: 'text-delta', index: 0, text: 'c' }), ] const toolName = [0, 1, 2].map(seq => event(seq, seq, { - type: 'tool-call-delta', index: 0, id: CallId('id'), + type: 'tool-call-delta', index: 0, id: ToolCallId('id'), ...seq === 2 ? {} : { name: 'write' }, argumentsDelta: 'x', })) for (const events of [gap, step, block, unsafeTime, toolName]) { diff --git a/packages/session/session-persistence-sqlite/tests/differential.spec.ts b/packages/session/session-persistence-sqlite/tests/differential.spec.ts index 45ec734992..deb80e8c56 100644 --- a/packages/session/session-persistence-sqlite/tests/differential.spec.ts +++ b/packages/session/session-persistence-sqlite/tests/differential.spec.ts @@ -5,7 +5,7 @@ import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { DatabaseSync } from 'node:sqlite' -import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm' +import { ToolCallId, type StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { type SessionEvent } from '@deepseek-ai/dsh-session' import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence' import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl' @@ -86,7 +86,7 @@ function packingMatrixLog(): SessionEvent[] { chunk: { type: 'tool-call-delta' as const, index: 2, - id: CallId('named-call'), + id: ToolCallId('named-call'), name: 'write', argumentsDelta: `{${index}`, }, @@ -96,7 +96,7 @@ function packingMatrixLog(): SessionEvent[] { chunk: { type: 'tool-call-delta' as const, index: 3, - id: CallId('unnamed-call'), + id: ToolCallId('unnamed-call'), argumentsDelta: `${index}}`, }, time: 3_000 + index, @@ -176,13 +176,13 @@ const streamChunkArbitrary: fc.Arbitrary = fc.oneof( fc.record({ type: fc.constant<'tool-call-delta'>('tool-call-delta'), index: fc.nat(2), - id: fc.constantFrom(CallId('call-1'), CallId('call-2')), + id: fc.constantFrom(ToolCallId('call-1'), ToolCallId('call-2')), argumentsDelta: fc.string(), }), fc.record({ type: fc.constant<'tool-call-delta'>('tool-call-delta'), index: fc.nat(2), - id: fc.constantFrom(CallId('call-1'), CallId('call-2')), + id: fc.constantFrom(ToolCallId('call-1'), ToolCallId('call-2')), name: fc.constantFrom('read', 'write'), argumentsDelta: fc.string(), }), diff --git a/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts b/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts index 1a95d7fe23..48a894a8bd 100644 --- a/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts +++ b/packages/shell/tool-pwsh-persistent/tests/loader-composition.spec.ts @@ -7,7 +7,7 @@ import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -130,7 +130,7 @@ describe.skipIf(!hasPwsh)('persistent pwsh through a real cordis.yml Loader comp const signal = new AbortController().signal const execute = (id: string, command: string) => context!.tools.execute({ signal, - callId: CallId(id), + callId: ToolCallId(id), name: 'pwsh', arguments: { command }, agent: owner, diff --git a/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts b/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts index 8d76abd3a6..8d9c55e39c 100644 --- a/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts +++ b/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { CallId } from '@deepseek-ai/dsh-llm' +import { ToolCallId } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -66,7 +66,7 @@ function call( ) { return ctx.tools.execute({ signal, - callId: CallId(`persistent-pwsh-${++callNumber}`), + callId: ToolCallId(`persistent-pwsh-${++callNumber}`), name: 'pwsh', arguments: { command }, ...owner === undefined ? {} : { agent: owner }, From e8c2423f5bc73fa2ac00c4dd34c62e8dc6548bf3 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 26 Aug 2026 11:12:37 +0800 Subject: [PATCH 04/13] chore: regenerate third-party notices for claude-agent-sdk 0.3.241 --- THIRD_PARTY_NOTICES.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index e079fa8c57..03c01e88af 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -113,18 +113,18 @@ pnpm applies local patches to the following packages at install time, so shipped The project owner authorizes distribution of every version of the official `@anthropic-ai/claude-agent-sdk` package and the official Claude Code CLI/platform payloads that each version declares through `optionalDependencies`. This identity-scoped authorization does not classify their declared terms as permissive and does not cover any unrelated runtime package; version, declared-license, and payload-set changes still require the ordinary dependency, lockfile, compatibility, terms, and notices review. -The installed SDK 0.3.220 declares the following optional platform packages. Each carries the official Claude Code 2.1.220 executable; the package identities and versions come from the SDK manifest, while the declared license field is verified against the platform payload installed for the current host. +The installed SDK 0.3.241 declares the following optional platform packages. Each carries the official Claude Code 2.1.241 executable; the package identities and versions come from the SDK manifest, while the declared license field is verified against the platform payload installed for the current host. | Optional platform package | Version | Declared license | | --- | --- | --- | -| [`@anthropic-ai/claude-agent-sdk-darwin-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-darwin-arm64) | 0.3.220 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-darwin-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-darwin-x64) | 0.3.220 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-linux-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-arm64) | 0.3.220 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-linux-arm64-musl`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-arm64-musl) | 0.3.220 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-linux-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-x64) | 0.3.220 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-linux-x64-musl`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-x64-musl) | 0.3.220 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-win32-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-win32-arm64) | 0.3.220 | SEE LICENSE IN LICENSE.md | -| [`@anthropic-ai/claude-agent-sdk-win32-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-win32-x64) | 0.3.220 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-darwin-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-darwin-arm64) | 0.3.241 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-darwin-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-darwin-x64) | 0.3.241 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-linux-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-arm64) | 0.3.241 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-linux-arm64-musl`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-arm64-musl) | 0.3.241 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-linux-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-x64) | 0.3.241 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-linux-x64-musl`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-linux-x64-musl) | 0.3.241 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-win32-arm64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-win32-arm64) | 0.3.241 | SEE LICENSE IN LICENSE.md | +| [`@anthropic-ai/claude-agent-sdk-win32-x64`](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk-win32-x64) | 0.3.241 | SEE LICENSE IN LICENSE.md | ## Development-only npm dependencies From 75428c7f472aa1a4e16816abfb4ef5578f96098f Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:29:13 +0800 Subject: [PATCH 05/13] perf(ci) --- .github/workflows/ci.yml | 4 ++-- scripts/ci-workflow.spec.ts | 4 ++-- scripts/coverage-exempt.ts | 8 ++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb9f01026e..c5afaf5fca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,7 +159,7 @@ jobs: || 'dsh-ubuntu-24-04-16core' }} name: node 24 / snapshots and artifacts env: - DSH_GATE_CONCURRENCY: '8' + DSH_GATE_CONCURRENCY: '10' DSH_NODE_COMPAT_SKIP_TYPECHECK: '1' DSH_OXLINT_THREADS: '8' DSH_PUBLINT_CONCURRENCY: '8' @@ -445,7 +445,7 @@ jobs: timeout-minutes: 120 env: DSH_COVERAGE_MAX_WORKERS: '6' - DSH_COVERAGE_PARTITIONS: '4' + DSH_COVERAGE_PARTITIONS: '6' DSH_COVERAGE_TEST_TIMEOUT_MS: '30000' DSH_GATE_CONCURRENCY: '3' steps: diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index 40a665d667..084e30c754 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -98,9 +98,9 @@ describe('CI workflow', () => { )) expect(buildCommands.map(step => step.run)).toContain('pnpm run check:ci:windows-blocking') - // windows-coverage uses the lower 4-partition profile. + // windows-coverage runs the 6-partition profile. expect(windowsCoverage.name).toBe('windows node 24 / coverage') - expect(windowsCoverage.env).toMatchObject({ DSH_COVERAGE_PARTITIONS: '4' }) + expect(windowsCoverage.env).toMatchObject({ DSH_COVERAGE_PARTITIONS: '6' }) const coverageSteps = windowsCoverage.steps as unknown[] const coverageCommands = coverageSteps.filter((step): step is Record & { run: string } => ( isRecord(step) && typeof step.run === 'string' diff --git a/scripts/coverage-exempt.ts b/scripts/coverage-exempt.ts index c6e69b2d93..843193e3bb 100644 --- a/scripts/coverage-exempt.ts +++ b/scripts/coverage-exempt.ts @@ -34,6 +34,14 @@ export const coverageExemptHeavySuites: readonly CoverageExemptSuite[] = [ filter: 'packages/typert/generator/tests/', exclude: 'packages/typert/generator/tests/**', }, + // Spawns the full-corpus transform gate in a child process (Node's ESM + // loader is its oracle), so no measured file executes in-process; the + // sibling in-process suites carry the package's src coverage. As a single + // 499–513 s case it dominated one native Windows coverage partition. + { + filter: 'packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts', + exclude: 'packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts', + }, // Real child-process fixtures over scripts/ sources, which coverage never measures. { filter: 'scripts/install-lefthook.spec.ts', exclude: 'scripts/install-lefthook.spec.ts' }, { filter: 'scripts/oxlint-contract.spec.ts', exclude: 'scripts/oxlint-contract.spec.ts' }, From 54ef0f315a52440c372b06be063d2c6702a3e3f4 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 25 Aug 2026 22:28:47 +0800 Subject: [PATCH 06/13] perf(typert): skip re-verified diagnostics and share analyzer caches in the tsdown plugin --- .../typert/generator/src/tsdown-plugin.ts | 9 +++++-- packages/typert/generator/src/workspace.ts | 26 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/typert/generator/src/tsdown-plugin.ts b/packages/typert/generator/src/tsdown-plugin.ts index eca5ad47d2..dedfd366e9 100644 --- a/packages/typert/generator/src/tsdown-plugin.ts +++ b/packages/typert/generator/src/tsdown-plugin.ts @@ -22,6 +22,11 @@ interface TypertPlugin { const DECORATOR_SYNTAX = /^\s*@[A-Za-z_$][\w$]*/m +// This plugin consumes tsc-emitted `lib/types` output, so every project it +// would re-diagnose has already passed the workspace tsc build in the same +// orchestration; the generator skips its per-package diagnostic pass here. +const TSC_VERIFIED_INPUT = { checkDiagnostics: false } as const + /** Generation scope selected by a tsdown build phase. */ export interface TypertPluginOptions { /** Package mode emits only the package being bundled; workspace mode emits every explicit contributor once. */ @@ -78,7 +83,7 @@ export function typertPlugin(pluginOptions: TypertPluginOptions = {}): TypertPlu if (manifest.name === undefined || !hasTypertExport(manifest.exports)) return let artifacts = artifactsByRoot.get(root) if (artifacts === undefined) { - const generator = new WorkspaceTypertGenerator(root) + const generator = new WorkspaceTypertGenerator(root, TSC_VERIFIED_INPUT) artifacts = pluginOptions.faces === undefined ? generator.generate() : generator.generate(undefined, pluginOptions.faces) @@ -89,7 +94,7 @@ export function typertPlugin(pluginOptions: TypertPluginOptions = {}): TypertPlu } function emitWorkspace(root: string, faces: readonly TypertFace[] | undefined): void { - const generator = new WorkspaceTypertGenerator(root) + const generator = new WorkspaceTypertGenerator(root, TSC_VERIFIED_INPUT) const packages = generator.discover(faces) .filter(candidate => hasTypertExport(readManifest(join(root, candidate.root)).exports)) .map(candidate => candidate.package) diff --git a/packages/typert/generator/src/workspace.ts b/packages/typert/generator/src/workspace.ts index 94d1d937e4..50866dc8b1 100644 --- a/packages/typert/generator/src/workspace.ts +++ b/packages/typert/generator/src/workspace.ts @@ -5,7 +5,7 @@ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' -import { TypertAnalysisError, WorkspaceAnalyzer } from './analyzer.ts' +import { TypertAnalysisError, WorkspaceAnalyzer, WorkspaceCaches } from './analyzer.ts' import type { DiscoveredTypertPackage } from './analyzer.ts' import { FaceModelEmitter } from './emitter.ts' import type { ModelEmitResult } from './emitter.ts' @@ -16,13 +16,32 @@ export interface WorkspaceEmitResult extends ModelEmitResult { readonly packageRoot: string } +/** Behavior switches for one {@link WorkspaceTypertGenerator}. */ +export interface WorkspaceTypertGeneratorOptions { + /** + * Run the per-package syntactic/semantic diagnostic pass before analysis + * (default true). Pass false only when the same orchestration already + * verified the workspace with tsc; the Typert-specific analysis checks + * (annotation coverage, private cross-package references, unretainable + * merges) run regardless. + */ + readonly checkDiagnostics?: boolean +} + /** Discover, analyze, and emit package reflection from independent faces. */ export class WorkspaceTypertGenerator { + /** Parsed-config and program-host state shared by every analyzer this generator creates. */ + private readonly caches = new WorkspaceCaches() + /** * Bind generation to one workspace root. * @param root - directory containing face aggregate tsconfigs. + * @param options - behavior switches applied to every pass of this generator. */ - constructor(private readonly root: string) {} + constructor( + private readonly root: string, + private readonly options: WorkspaceTypertGeneratorOptions = {}, + ) {} /** * Find public package faces that contribute Cordis services/events or @@ -33,6 +52,7 @@ export class WorkspaceTypertGenerator { discover(faces?: readonly TypertFace[]): DiscoveredTypertPackage[] { return new WorkspaceAnalyzer({ root: this.root, + caches: this.caches, ...(faces === undefined ? {} : { faces }), }).discoverPackages() } @@ -48,7 +68,9 @@ export class WorkspaceTypertGenerator { const workspace = new WorkspaceAnalyzer({ root: this.root, packages: selected, + caches: this.caches, ...(faces === undefined ? {} : { faces }), + ...(this.options.checkDiagnostics === undefined ? {} : { checkDiagnostics: this.options.checkDiagnostics }), }).analyze() const artifacts: WorkspaceEmitResult[] = [] for (const face of workspace.faces) { From 6d9cc6ab96cb0b99eb7d8aabd13f3ee19829132e Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 25 Aug 2026 22:52:32 +0800 Subject: [PATCH 07/13] test(webworker-runtime): drop coverage requirement and compile transform suites --- .../tests/compile/transform-corpus-check.ts | 417 ----------- .../tests/compile/transform-corpus.spec.ts | 33 - .../tests/compile/transform.spec.ts | 704 ------------------ scripts/coverage-exempt.ts | 11 +- vitest.config.ts | 4 + 5 files changed, 9 insertions(+), 1160 deletions(-) delete mode 100644 packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts delete mode 100644 packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts delete mode 100644 packages/experimental/webworker-runtime/tests/compile/transform.spec.ts diff --git a/packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts b/packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts deleted file mode 100644 index e9849ef256..0000000000 --- a/packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts +++ /dev/null @@ -1,417 +0,0 @@ -/** - * Full-corpus regression for the worker module transform: every built bundle in - * the workspace is transformed, executed through the real wrapper contract, and - * its export shape compared against what Node's own ESM loader produces for the - * same file. - * - * This is the harness that answers "does the transform hold on real output", - * which no hand-written case can: the corpus is whatever the build currently - * emits, so a rolldown upgrade that starts emitting an unseen module form shows - * up here first. - * - * Module-syntax statistics are counted from the acorn AST, so the check has no - * separate lexer dependency. Baseline exemptions are a pinned list, not a count: - * four files cannot be imported by Node in this repository for reasons unrelated - * to the transform, and an unexpected member fails the run. - * - * Cost: this walks the whole build output and imports every bundle, so it takes - * tens of seconds and needs `pnpm run build:lib:host` to have run. It is a - * heavyweight suite, not part of a default aggregator run. - * - * Run: tsx tests/compile/transform-corpus-check.ts [files...] - * With no arguments it discovers the corpus itself. - */ -import { readdirSync, readFileSync, statSync } from 'node:fs' -import { createRequire } from 'node:module' -import { join } from 'node:path' -import { fileURLToPath, pathToFileURL } from 'node:url' -import { parse } from 'acorn' -import { createAlsRuntime } from '../../src/polyfill/async-context/als-runtime.ts' -import { lowerModuleSource } from '../../src/compile/transform.ts' -import { WRAPPER_PARAMS } from '../../src/image-layout.ts' - -const repositoryRoot = fileURLToPath(new URL('../../../../../', import.meta.url)) - -/** - * Files Node's ESM loader cannot import in this repository, so no baseline - * export shape exists to compare against. None is a transform failure: each is - * checked to still TRANSFORM cleanly, only the comparison is skipped. - * - * Named rather than counted: an unlisted baseline failure is a real finding - * (a bundle that stopped being importable), and it must not hide inside a total. - * A listed file that becomes importable also fails, so the list cannot rot. - */ -const BASELINE_EXEMPT: ReadonlyMap = new Map([ - ['packages/client/ui-primitives/lib/index.js', 'imports .css, which bare Node cannot load'], - ['packages/client/web/lib/index.js', 'imports .css, which bare Node cannot load'], - ['packages/subprocess/win32-process/lib/index.js', 'koffi type-name collision on a second load'], - ['packages/test-support/client-runtime/lib/index.js', "needs vitest's internal state"], -]) - -/** - * Bundles whose own SOURCE contains the double-lowering sentinels, so the - * transform's guard refuses them by design. - * - * This package is the only such case and the refusal is correct: its bundle - * carries `transform.ts`'s own template literals (`` `__als$${n}` `` from - * `alsTemp`, and the `${ALS}.pause(` fragments), which is exactly the text the - * guard looks for. A self-referential false positive is the right trade: the - * guard exists because a mis-wired image manifest would otherwise show up only - * as "slower", and no roster row transforms this package. - * - * Listed rather than skipped silently, and asserted to keep refusing: if the - * guard stopped tripping here, either the guard or this bundle's contents - * changed, and both are worth knowing about. - */ -const DOUBLE_LOWERING_SENTINEL: ReadonlySet = new Set([ - 'packages/experimental/webworker-runtime/lib/index.js', -]) - -let failures = 0 -const report: string[] = [] -const log = (line: string): void => { - report.push(line) - process.stdout.write(`${line}\n`) -} -const fail = (line: string): void => { - failures += 1 - log(line) -} - -/** @returns Built bundles under a two-level package directory, in stable order. */ -function discover(): string[] { - const found: string[] = [] - /** @returns Sorted subdirectory names, or none when the path is not a readable directory. */ - const subdirectories = (path: string): string[] => { - try { - return readdirSync(path, { withFileTypes: true }) - .filter(entry => entry.isDirectory()) - .map(entry => entry.name) - .sort() - } catch { - return [] - } - } - for (const group of ['packages', 'vendor']) { - const groupDirectory = join(repositoryRoot, group) - for (const entry of subdirectories(groupDirectory)) { - // `packages///lib/index.js`, `vendor//lib/index.js`. - const candidates = group === 'vendor' - ? [join(groupDirectory, entry, 'lib', 'index.js')] - : subdirectories(join(groupDirectory, entry)) - .map(child => join(groupDirectory, entry, child, 'lib', 'index.js')) - for (const candidate of candidates) { - try { - if (statSync(candidate).isFile()) found.push(candidate) - } catch { - // No bundle for this package: it may not build a runtime artifact. - } - } - } - } - return found -} - -/** - * @returns Path relative to the repository root, for stable diagnostics. - * Always POSIX-separated: the exemption table and the recorded findings key - * on one form, and a win32 walk would otherwise miss every entry. - */ -const relative = (path: string): string => path.slice(repositoryRoot.length).replaceAll('\\', '/') - -/** - * Present a Node ESM namespace the way the worker loader hands one over, so a - * real dependency and a transformed one look the same to the module body. - * @param value - A module namespace, or whatever `require` returned. - * @returns The value, or an `__esModule`-marked projection of a Module namespace. - */ -function asLoaderExports(value: unknown): unknown { - if (value === null || typeof value !== 'object') return value - if ((value as { [Symbol.toStringTag]?: string })[Symbol.toStringTag] !== 'Module') return value - const out: Record = {} - Object.defineProperty(out, '__esModule', { value: true }) - for (const key of Object.keys(value)) { - Object.defineProperty(out, key, { enumerable: true, get: () => (value as Record)[key] }) - } - return out -} - -/** - * Specifiers a transformed body will request, read straight out of the emitted - * code. The transform emits every static import as `require()` - * (`transform.ts` builds them with `JSON.stringify`), so a literal scan finds - * exactly the set that must be resolvable before the body runs. A dynamic - * `import(expr)` is not found and does not need to be: it resolves lazily, - * after the body has already produced its exports. - * @param code - Emitted CommonJS body. - * @returns The requested specifiers, deduplicated. - */ -function requestedSpecifiers(code: string): string[] { - const found = new Set() - for (const match of code.matchAll(/require\("((?:[^"\\]|\\.)*)"\)/g)) { - const raw = match[1] - if (raw !== undefined) found.add(JSON.parse(`"${raw}"`) as string) - } - return [...found] -} - -/** - * Load a dependency through the same loader that produces this check's baseline. - * - * This matters more than it looks. The baseline every file is compared against is - * `await import(file)` — Node's ESM loader. A dependency fetched with - * `createRequire` instead goes through the CommonJS resolver, which selects the - * `require` condition of a package's `exports` map: for a dual-build package that - * is a DIFFERENT ARTIFACT with a different interop shape. `@deepseek-ai/schemastery` - * is the case that exposed it — `require` yields `lib/index.cjs`, whose - * `module.exports` is the `Schema` function with no `default` and no `__esModule`, - * while `import` yields `lib/index.mjs`, a namespace with `default`. A body - * written against the second shape misbehaves when handed the first. - * - * That divergence also made the whole check runner-dependent: under the `tsx` CLI - * `require` was patched to return the ESM view and all 228 passed, while under - * `node --import tsx/esm` three files failed. A gate whose verdict depends on how - * it was launched is not a gate, so dependencies now come from `import()` and the - * CommonJS path is only a fallback. - * @param specifier - Module specifier as the transformed body requests it. - * @param path - Absolute path of the importing bundle. - * @returns The dependency in loader-facing form, or undefined when neither loader can supply it. - */ -async function loadDependency(specifier: string, path: string): Promise { - const real = createRequire(pathToFileURL(path)) - try { - // Resolve through the importer so relative and bare specifiers both work, then - // import the resolved file: resolution is CommonJS's, delivery is ESM's. - const resolved = specifier.startsWith('node:') ? specifier : pathToFileURL(real.resolve(specifier)).href - return asLoaderExports(await import(resolved)) - } catch { - // Not importable as ESM (a genuine CommonJS-only dependency, or unresolvable). - try { - return asLoaderExports(real(specifier)) - } catch { - return undefined - } - } -} - -/** A stand-in for a dependency Node cannot load here: every access answers something callable. */ -function fakeModule(): unknown { - const target: Record = {} - return new Proxy(target, { - get: (holder, key) => { - if (key === '__esModule') return true - if (key === 'default') return function fakeDefault() {} - if (typeof key === 'symbol') return undefined - if (!(key in holder)) holder[key] = function fakeNamed() {} - return holder[key] - }, - has: () => true, - }) -} - -const als = createAlsRuntime() - -/** - * Execute a transformed body under the real wrapper contract. - * - * Dependencies are loaded BEFORE the body runs, because the body's `require` is - * synchronous while faithful delivery ({@link loadDependency}) is not. A - * dependency neither loader can supply falls back to a permissive stand-in: the - * subject under test is this file's own export shape, not its dependencies'. - * @param code - Emitted CommonJS body. - * @param path - Absolute path of the bundle, used for resolution and diagnostics. - * @returns The populated `exports` object. - */ -async function runTransformed(code: string, path: string): Promise> { - const exports: Record = {} - const module = { exports } - const loaded = new Map() - await Promise.all(requestedSpecifiers(code).map(async (specifier) => { - const delivered = await loadDependency(specifier, path) - if (delivered !== undefined) loaded.set(specifier, delivered) - })) - const fakes = new Map() - const require = (specifier: string): unknown => { - const delivered = loaded.get(specifier) - if (delivered !== undefined) return delivered - if (!fakes.has(specifier)) fakes.set(specifier, fakeModule()) - return fakes.get(specifier) - } - // eslint-disable-next-line @typescript-eslint/no-implied-eval -- the wrapper contract under test is a `new Function` body - const factory = new Function(...WRAPPER_PARAMS, code) as (...args: unknown[]) => void - const metaRequire = createRequire(pathToFileURL(path)) - factory(exports, require, module, path, path.replace(/\/[^/]*$/, ''), { - url: pathToFileURL(path).href, - // Path-anchored like the worker loader; an import-only export face falls - // back to this check file's own resolver. - resolve: (specifier: string) => { - try { - return pathToFileURL(metaRequire.resolve(specifier)).href - } catch { - return import.meta.resolve(specifier) - } - }, - }, als) - return exports -} - -/** Module-syntax counts read from the AST. */ -interface Counts { - staticImports: number - dynamicImports: number - importMeta: number - awaitExpressions: number -} - -/** @returns Occurrence counts of the forms the transform rewrites. */ -function countForms(source: string, _path: string): Counts { - const counts: Counts = { staticImports: 0, dynamicImports: 0, importMeta: 0, awaitExpressions: 0 } - let program: unknown - try { - program = parse(source, { ecmaVersion: 'latest', sourceType: 'module', allowAwaitOutsideFunction: true }) - } catch { - // Counting is reporting only; a parse failure is the transform's to report. - return counts - } - const walk = (node: unknown): void => { - if (node === null || typeof node !== 'object') return - if (Array.isArray(node)) { - for (const child of node) walk(child) - return - } - const record = node as Record - if (typeof record.type !== 'string') return - if (record.type === 'ImportDeclaration') counts.staticImports += 1 - if (record.type === 'ImportExpression') counts.dynamicImports += 1 - if (record.type === 'AwaitExpression') counts.awaitExpressions += 1 - if (record.type === 'MetaProperty' && (record.meta as { name?: string } | undefined)?.name === 'import') { - counts.importMeta += 1 - } - for (const [key, value] of Object.entries(record)) { - if (key === 'type' || key === 'start' || key === 'end') continue - walk(value) - } - } - walk(program) - return counts -} - -const files = process.argv.slice(2).length > 0 - ? process.argv.slice(2).map(path => (path.startsWith('/') ? path : join(process.cwd(), path))) - : discover() - -if (files.length === 0) { - process.stdout.write('transform-corpus-check: no built bundles found; run `pnpm run build:lib:host` first\n') - process.exitCode = 1 -} else { - const verdicts = { - ok: 0, mismatch: 0, transformFailed: 0, execFailed: 0, exempt: 0, unexpectedBaseline: 0, sentinelRefused: 0, - } - const totals = { bytesIn: 0, bytesOut: 0, lowered: 0, unchanged: 0, lineDrift: 0 } - const counts: Counts = { staticImports: 0, dynamicImports: 0, importMeta: 0, awaitExpressions: 0 } - - for (const file of files) { - const key = relative(file) - const source = readFileSync(file, 'utf8') - const observed = countForms(source, file) - counts.staticImports += observed.staticImports - counts.dynamicImports += observed.dynamicImports - counts.importMeta += observed.importMeta - counts.awaitExpressions += observed.awaitExpressions - totals.bytesIn += source.length - - let code: string - try { - code = lowerModuleSource({ filename: file, source }).code - } catch (reason) { - const message = (reason as Error).message - if (DOUBLE_LOWERING_SENTINEL.has(key)) { - // Expected: this bundle's own text contains the sentinels the guard - // matches. Assert it is really the guard talking, not some other refusal. - if (message.includes('already lowered')) { - verdicts.sentinelRefused += 1 - } else { - fail(`- WRONG REFUSAL ${key}: expected the double-lowering guard, got: ${message}`) - } - continue - } - fail(`- TRANSFORM FAILED ${key}: ${message}`) - verdicts.transformFailed += 1 - continue - } - if (DOUBLE_LOWERING_SENTINEL.has(key)) { - fail(`- STALE SENTINEL ${key}: the double-lowering guard no longer refuses it; ` - + 'remove it from DOUBLE_LOWERING_SENTINEL or check whether the guard still works') - } - totals.bytesOut += code.length - if (code === source) totals.unchanged += 1 - else totals.lowered += 1 - - // The debugging contract, over the whole corpus: a transformed body has the - // same line count as its source, so a stack frame still points at the right - // line. - const sourceLines = source.split('\n').length - const codeLines = code.split('\n').length - if (sourceLines !== codeLines) { - fail(`- LINE DRIFT ${key}: source ${String(sourceLines)} lines, transformed ${String(codeLines)}`) - totals.lineDrift += 1 - } - - const exemption = BASELINE_EXEMPT.get(key) - let expected: string[] - try { - expected = Object.keys(await import(pathToFileURL(file).href) as object).sort() - } catch (reason) { - if (exemption === undefined) { - // A bundle that stopped being importable is a real finding, so it fails - // rather than joining a tolerated total. - fail(`- UNEXPECTED BASELINE FAILURE ${key}: ${(reason as Error).message.split('\n')[0]}`) - verdicts.unexpectedBaseline += 1 - } else { - verdicts.exempt += 1 - } - continue - } - if (exemption !== undefined) { - // The exemption list must stay honest in the other direction too: a file - // that became importable should leave the list. - fail(`- STALE EXEMPTION ${key}: imports fine now (${exemption}); remove it from BASELINE_EXEMPT`) - } - - let actual: string[] - try { - actual = Object.keys(await runTransformed(code, file)).sort() - } catch (reason) { - fail(`- EXEC FAILED ${key}: ${(reason as Error).message.split('\n')[0]}`) - verdicts.execFailed += 1 - continue - } - - const missing = expected.filter(name => !actual.includes(name)) - const extra = actual.filter(name => !expected.includes(name)) - if (missing.length === 0 && extra.length === 0) { - verdicts.ok += 1 - continue - } - fail(`- EXPORT MISMATCH ${key}: missing=[${missing.join(',')}] extra=[${extra.join(',')}]`) - verdicts.mismatch += 1 - } - - const growth = totals.bytesIn === 0 ? 0 : ((totals.bytesOut - totals.bytesIn) / totals.bytesIn) * 100 - log('') - log(`files=${String(files.length)} ok=${String(verdicts.ok)} exportMismatch=${String(verdicts.mismatch)} ` - + `transformFailed=${String(verdicts.transformFailed)} execFailed=${String(verdicts.execFailed)} ` - + `lineDrift=${String(totals.lineDrift)} baselineExempt=${String(verdicts.exempt)} ` - + `sentinelRefused=${String(verdicts.sentinelRefused)} ` - + `unexpectedBaselineFailure=${String(verdicts.unexpectedBaseline)}`) - log(`lowered=${String(totals.lowered)} packedAsIs=${String(totals.unchanged)} ` - + `bytes ${String(totals.bytesIn)} -> ${String(totals.bytesOut)} (${growth.toFixed(1)}%)`) - log(`forms: staticImport=${String(counts.staticImports)} dynamicImport=${String(counts.dynamicImports)} ` - + `importMeta=${String(counts.importMeta)} await=${String(counts.awaitExpressions)}`) - - process.stdout.write(failures === 0 - ? `\ntransform-corpus-check: ${String(verdicts.ok)} bundles match their ESM baseline, ` - + `${String(verdicts.exempt)} exempt, ${String(verdicts.sentinelRefused)} sentinel-refused, no drift\n` - : `\ntransform-corpus-check: ${String(failures)} finding(s)\n`) - process.exitCode = failures === 0 ? 0 : 1 -} diff --git a/packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts b/packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts deleted file mode 100644 index 8f1d745ea9..0000000000 --- a/packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Runs the full-corpus transform gate (`transform-corpus-check.ts`) in the - * launcher it is written for, and reports its findings as this suite's failure. - * - * Spawned rather than imported, because the gate's oracle is NODE's ESM loader: - * every built bundle's transformed export shape is compared against what - * `await import(file)` produces there. Vitest replaces that loader with vite's - * module runner, which imports files Node cannot — a `.css` import resolves, and - * koffi loads a second time — so an in-process corpus run measures the transform - * against a different loader and reports three of the four pinned baseline - * exemptions as stale. The gate's own note applies to itself: a gate whose - * verdict depends on how it was launched is not a gate. - * - * The corpus is the build output, so this skips on a tree that has none. - */ -import { spawnSync } from 'node:child_process' -import { fileURLToPath } from 'node:url' -import { expect, test } from 'vitest' - -const runner = fileURLToPath(new URL('./transform-corpus-check.ts', import.meta.url)) - -test('every built bundle transforms to the export shape Node loads', (context) => { - const finished = spawnSync(process.execPath, ['--import', 'tsx/esm', runner], { encoding: 'utf8' }) - const output = `${finished.stdout}${finished.stderr}` - if (output.includes('no built bundles found')) { - context.skip('the workspace has no build output to sweep') - return - } - // The runner prefixes every finding with '- ', so a failure reads as the - // findings themselves rather than as a diff of its whole report. - expect(output.split('\n').filter(line => line.startsWith('- ')).join('\n')).toBe('') - expect(finished.status, output).toBe(0) -}, 900_000) diff --git a/packages/experimental/webworker-runtime/tests/compile/transform.spec.ts b/packages/experimental/webworker-runtime/tests/compile/transform.spec.ts deleted file mode 100644 index 6140910ad3..0000000000 --- a/packages/experimental/webworker-runtime/tests/compile/transform.spec.ts +++ /dev/null @@ -1,704 +0,0 @@ -/** - * Semantic check of the worker module transform (`src/compile/transform.ts`): what the - * emitted CommonJS body looks like for each module form, how suspension points - * are rewritten, that line numbers survive, which forms are refused, and that - * every covered trap form stays fixed. - * - * Scope boundary: this file checks the transform itself; the image collector's - * loop around it is covered by the packer's `transform-image.spec.ts`. - * Emitted-code assertions are deliberately written against substrings - * of the real output rather than whole-file goldens: a golden would fail on every - * helper reordering, which is not the contract. The contract is the observable - * one — the code parses as script, publishes the right bindings, keeps line - * count, and routes suspension through `__als`. - * - * The trap cases are module forms that break a boot when the transform - * mishandles them. Five traps cannot recur while the AST pass is the parser, - * but they stay checked because a future parser swap could reintroduce them. - */ -import { expect, test } from 'vitest' -import { parse } from 'acorn' -import { lowerModuleSource } from '../../src/compile/transform.ts' -import { LOWERING_VERSION, WRAPPER_PARAMS } from '../../src/image-layout.ts' - -/** - * Lower one probe module the way the packer does — the transform's only caller. - * @param source - Module source under test. - * @param path - Path the diagnostics name. - * @returns The emitted body. - */ -const transformModule = (source: string, path = 'probe.js'): string => - lowerModuleSource({ filename: path, source }).code - -/** Register one comparison as its own case, serialized at call time. */ -const check = (label: string, actual: unknown, expected: unknown): void => { - const [seen, wanted] = [JSON.stringify(actual), JSON.stringify(expected)] - test(label, () => { expect(seen).toBe(wanted) }) -} - -/** Assert a substring is present in an emitted body. */ -const contains = (label: string, code: string, needle: string): void => { - test(label, () => { expect(code).toContain(needle) }) -} - -/** Assert a substring is absent (used for "must survive untouched" cases). */ -const lacks = (label: string, code: string, needle: string): void => { - test(label, () => { expect(code).not.toContain(needle) }) -} - -/** @returns The error message of a refused transform, or undefined when it succeeded. */ -const refusal = (source: string, path = 'probe.js'): string | undefined => { - try { - transformModule(source, path) - return undefined - } catch (reason) { - return (reason as Error).message - } -} - -/** Assert the transform refuses a source and names the reason. */ -const refuses = (label: string, source: string, fragment: string): void => { - const message = refusal(source) - test(label, () => { expect(message).toContain(fragment) }) -} - -/** - * The wrapper contract, applied for real: compile the body with the declared - * parameters and run it. This is the same `new Function` shape the loader uses - * (module-loader.ts), so a body that compiles here compiles there. - * @param code - Emitted CommonJS body. - * @param require - Module resolver the body's `require` calls reach. - * @param als - Suspension runtime bound to `__als`. - * @returns The populated `exports` object. - */ -function runBody( - code: string, - require: (specifier: string) => unknown = () => ({}), - als?: unknown, -): Record { - const exports: Record = {} - const module = { exports } - // eslint-disable-next-line @typescript-eslint/no-implied-eval -- the wrapper contract under test is a `new Function` body - const factory = new Function(...WRAPPER_PARAMS, code) as (...args: unknown[]) => void - factory(exports, require, module, '/vfs/probe.js', '/vfs', { url: 'file:///vfs/probe.js' }, als) - return exports -} - -/** Every emitted body must parse as a script — the transform's own exit gate, re-checked here. */ -const parsesAsScript = (label: string, code: string): void => { - test(label, () => { - expect(() => parse(code, { ecmaVersion: 'latest', sourceType: 'script', allowAwaitOutsideFunction: false })).not.toThrow() - }) -} - -// --------------------------------------------------------------------------- -// 1. The published contract: the three names the packer and loader share. -// --------------------------------------------------------------------------- - -check('LOWERING_VERSION is a non-empty string', typeof LOWERING_VERSION === 'string' && LOWERING_VERSION.length > 0, true) -check('WRAPPER_PARAMS is the frozen 7-parameter shape', [...WRAPPER_PARAMS], [ - 'exports', 'require', 'module', '__filename', '__dirname', '__dsh$meta', '__als', -]) -// The wrapper signature is a contract with the loader's `new Function`, so the -// parameters must be valid identifiers in that position. -check( - 'every wrapper parameter is a usable identifier', - (() => { - try { - // eslint-disable-next-line @typescript-eslint/no-implied-eval -- proves the parameter names compile where the loader uses them - new Function(...WRAPPER_PARAMS, 'return 0') - return true - } catch { - return false - } - })(), - true, -) - -// --------------------------------------------------------------------------- -// 2. lowerModuleSource: the packer face. `lowered` is the pack-time decision. -// --------------------------------------------------------------------------- - -{ - const esm = lowerModuleSource({ filename: 'node_modules/p/index.js', source: 'export const a = 1\n' }) - check('lowered=true for a module that needed rewriting', esm.lowered, true) - check('lowered code differs from source', esm.code !== 'export const a = 1\n', true) - - // Plain CommonJS with no suspension point is the "pack as-is" case: the - // collector relies on this to leave 1693-odd entries untouched. - const plain = 'module.exports = 1\n' - const cjs = lowerModuleSource({ filename: 'node_modules/p/legacy.cjs', source: plain }) - check('lowered=false for plain CommonJS', cjs.lowered, false) - check('unlowered code is the input verbatim', cjs.code, plain) - - // A CommonJS body that still contains a suspension point must be rewritten: - // `await` inside a function is the ALS protocol's business even with no ESM. - const cjsAwait = lowerModuleSource({ - filename: 'node_modules/p/async.cjs', - source: 'module.exports = async () => { await 1 }\n', - }) - check('lowered=true for CommonJS carrying a suspension point', cjsAwait.lowered, true) - contains('CommonJS await still routes through __als', cjsAwait.code, '__als.pause(') - - // `lowered` must agree with the code/source comparison by construction. - check('lowered mirrors code !== source', cjsAwait.lowered, cjsAwait.code !== 'module.exports = async () => { await 1 }\n') -} - -// --------------------------------------------------------------------------- -// 3. Import forms. -// --------------------------------------------------------------------------- - -{ - // Side-effect import: a bare require, nothing bound. - const code = transformModule("import './side-effect.js'\n", 'probe.js') - contains('side-effect import becomes a bare require', code, 'require("./side-effect.js")') - parsesAsScript('side-effect import', code) - - const requested: string[] = [] - runBody(code, (specifier) => { - requested.push(specifier) - return {} - }) - check('side-effect import actually requires at run time', requested, ['./side-effect.js']) -} - -{ - // Named imports are snapshots (CommonJS destructuring semantics), which is the - // documented, accepted divergence from ESM live bindings on the import side. - const code = transformModule("import { a, b as c } from 'p'\nexport const out = [a, c]\n", 'probe.js') - parsesAsScript('named imports', code) - const exports = runBody(code, () => ({ a: 1, b: 2 })) - check('named import binds by imported name, honouring the alias', exports.out, [1, 2]) -} - -{ - // Default and namespace imports go through the two interop helpers, which must - // agree with `Loader.unwrapExports` on the `__esModule` convention. - const code = transformModule("import d from 'p'\nimport * as ns from 'q'\nexport const seen = [d, ns.x, ns.default]\n", 'probe.js') - parsesAsScript('default and namespace imports', code) - - // An `__esModule` module: default comes from `.default`, namespace passes through. - const esModule = { __esModule: true, default: 'D', x: 'X' } - const withEsm = runBody(code, () => esModule) - check('default import of an __esModule module reads .default', (withEsm.seen as unknown[])[0], 'D') - - // A plain CommonJS module: the module object *is* the default, and the - // namespace gains a `default` key pointing at it. - const plain = { x: 'X' } - const withCjs = runBody(code, () => plain) - check('default import of plain CommonJS is the module object', (withCjs.seen as unknown[])[0], plain) - check('namespace of plain CommonJS keeps the named key', (withCjs.seen as unknown[])[1], 'X') - check('namespace of plain CommonJS synthesizes default', (withCjs.seen as unknown[])[2], plain) -} - -// --------------------------------------------------------------------------- -// 4. Export forms, including the live-binding contract. -// --------------------------------------------------------------------------- - -{ - const code = transformModule('export const a = 1\nexport function f() {}\nexport class K {}\n', 'probe.js') - parsesAsScript('exported declarations', code) - contains('module bodies get the __esModule marker', code, '__esModule') - contains('use strict is part of the prologue', code, '"use strict"') - const exports = runBody(code) - check('exported const is published', exports.a, 1) - check('exported function is published', typeof exports.f, 'function') - check('exported class is published', typeof exports.K, 'function') -} - -{ - // Local exports are getters, so a later assignment is observable through - // `exports` — the ESM live-binding property. - const code = transformModule('export let counter = 0\nexport function bump() { counter += 1 }\n', 'probe.js') - parsesAsScript('live binding', code) - const exports = runBody(code) - check('live binding starts at its initializer', exports.counter, 0) - ;(exports.bump as () => void)() - check('live binding observes a later assignment', exports.counter, 1) - // A getter, not a data property: this is what makes the above work. - check( - 'exported local is an accessor', - typeof Object.getOwnPropertyDescriptor(exports, 'counter')?.get, - 'function', - ) -} - -{ - // Trap 4: a multi-declarator export publishes every binding. - const code = transformModule('export const a = 1, b = 2\n', 'probe.js') - parsesAsScript('multi-declarator export', code) - const exports = runBody(code) - check('multi-declarator export publishes every binding', [exports.a, exports.b], [1, 2]) -} - -{ - // Destructuring exports exercise the pattern walker (object, array, rest, - // default) — every branch of `declaredBindings`. - const code = transformModule( - 'export const { p, q: renamed, ...restObj } = { p: 1, q: 2, z: 3 }\n' - + 'export const [first, , third = 30, ...restArr] = [10, 20, undefined, 40, 50]\n', - 'probe.js', - ) - parsesAsScript('destructuring exports', code) - const exports = runBody(code) - check('object pattern export', [exports.p, exports.renamed], [1, 2]) - check('object rest export', exports.restObj, { z: 3 }) - check('array pattern export with hole', [exports.first, exports.third], [10, 30]) - check('array rest export', exports.restArr, [40, 50]) - // The renamed target is what is published; the source key is not a binding. - check('object pattern publishes the local name, not the source key', 'q' in exports, false) -} - -{ - const code = transformModule('const x = 1\nexport { x as y }\n', 'probe.js') - parsesAsScript('local export clause', code) - const exports = runBody(code) - check('local export clause publishes under the exported name', exports.y, 1) - check('local export clause does not publish the local name', 'x' in exports, false) -} - -{ - // Re-export clause: a getter onto the required module, so it also stays live. - const module: Record = { a: 1 } - const code = transformModule("export { a, a as aliased } from 'p'\n", 'probe.js') - parsesAsScript('re-export clause', code) - const exports = runBody(code, () => module) - check('re-export publishes the name', exports.a, 1) - check('re-export publishes the alias', exports.aliased, 1) - module.a = 2 - check('re-export is live against the source module', exports.a, 2) -} - -{ - // `export *` copies enumerable keys, skips `default`, and must not clobber an - // existing local export. - const code = transformModule("export const own = 'local'\nexport * from 'p'\n", 'probe.js') - parsesAsScript('export all', code) - const exports = runBody(code, () => ({ extra: 'E', default: 'D', own: 'theirs' })) - check('export * copies named keys', exports.extra, 'E') - check('export * skips default', 'default' in exports, false) - check('export * does not overwrite an existing export', exports.own, 'local') -} - -{ - const code = transformModule("export * as ns from 'p'\n", 'probe.js') - parsesAsScript('export all as namespace', code) - const exports = runBody(code, () => ({ x: 1 })) - check('export * as ns publishes a namespace object', (exports.ns as Record).x, 1) -} - -{ - const code = transformModule('export default 42\n', 'probe.js') - parsesAsScript('default export value', code) - check('default export lands on exports.default', runBody(code).default, 42) -} - -{ - // Documented cost: the function name stops being a module-scope binding, but - // the named function expression can still refer to itself. - const code = transformModule('export default function self(n) { return n <= 0 ? 0 : self(n - 1) }\n', 'probe.js') - parsesAsScript('default export function', code) - const fn = runBody(code).default as (n: number) => number - check('default-exported function keeps self-reference', fn(3), 0) -} - -{ - const code = transformModule("export { x as default } from 'p'\n", 'probe.js') - parsesAsScript('re-export as default', code) - check('re-export as default publishes default', runBody(code, () => ({ x: 'D' })).default, 'D') -} - -// --------------------------------------------------------------------------- -// 5. import.meta and dynamic import. -// --------------------------------------------------------------------------- - -{ - const code = transformModule('export const here = import.meta.url\n', 'probe.js') - parsesAsScript('import.meta', code) - contains('import.meta becomes the wrapper parameter', code, '__dsh$meta') - check('import.meta.url resolves through the wrapper', runBody(code).here, 'file:///vfs/probe.js') -} - -{ - // Dynamic import routes through the same require chain (which is what makes - // typert-loader's absolute-path `import()` land on the VFS resolver), and the - // result is namespace-shaped. - const code = transformModule("export const load = () => import('p')\n", 'probe.js') - parsesAsScript('dynamic import', code) - contains('dynamic import becomes the helper call', code, '__dsh$dynImport') - const load = runBody(code, () => ({ x: 1 })).load as () => Promise> - const namespace = await load() - check('dynamic import resolves to a namespace object', namespace.x, 1) - check('dynamic import namespace has a default', 'default' in namespace, true) -} - -// --------------------------------------------------------------------------- -// 6. Suspension points. Behaviour is checked against a recording runtime, so -// these assert the protocol shape rather than re-testing als-runtime. -// --------------------------------------------------------------------------- - -/** A recording stand-in for the ALS runtime: proves the emitted calls happen in order. */ -function recordingAls(): { als: Record; calls: string[] } { - const calls: string[] = [] - const als = { - pause: (value: unknown) => { - calls.push('pause') - return Promise.resolve(value).then( - settled => ({ ok: true, value: settled, snapshot: 'S' }), - (error: unknown) => ({ ok: false, error, snapshot: 'S' }), - ) - }, - resume: (token: { ok: boolean; value?: unknown; error?: unknown }) => { - calls.push('resume') - if (token.ok) return token.value - throw token.error - }, - snapshot: () => { - calls.push('snapshot') - return 'S' - }, - afterYield: (_snapshot: unknown, sent: unknown) => { - calls.push('afterYield') - return sent - }, - iterator: (value: unknown) => { - calls.push('iterator') - const source = value as Record - const asyncFactory = source[Symbol.asyncIterator] as (() => AsyncIterator) | undefined - if (typeof asyncFactory === 'function') return asyncFactory.call(source) - const syncFactory = source[Symbol.iterator] as () => Iterator - const inner = syncFactory.call(source) - return { - next: async (...args: unknown[]) => { - const step = inner.next(...args as [unknown]) - return { done: step.done ?? false, value: await step.value } - }, - return: async (sent?: unknown) => { - const step = inner.return?.(sent) ?? { done: true, value: undefined } - return { done: step.done ?? true, value: await step.value } - }, - } - }, - close: async (iterator: AsyncIterator) => { - calls.push('close') - return iterator.return?.(undefined) - }, - } - return { als, calls } -} - -{ - const code = transformModule('export const run = async () => await 7\n', 'probe.js') - parsesAsScript('await rewrite', code) - contains('await is wrapped in resume(await pause(', code, '__als.resume(await __als.pause(') - const { als, calls } = recordingAls() - const run = runBody(code, () => ({}), als).run as () => Promise - check('await still yields its value', await run(), 7) - check('await goes pause-then-resume', calls, ['pause', 'resume']) -} - -{ - // The rejection path is the half that a naive "snapshot on success" rewrite - // gets wrong, so it is checked as its own case. - const code = transformModule( - "export const run = async () => { try { await Promise.reject(new Error('boom')) } catch (reason) { return `caught:${reason.message}` } }\n", - 'probe.js', - ) - parsesAsScript('await rejection', code) - const { als, calls } = recordingAls() - const run = runBody(code, () => ({}), als).run as () => Promise - check('rejection surfaces through resume', await run(), 'caught:boom') - check('rejection path also goes pause-then-resume', calls, ['pause', 'resume']) -} - -{ - // for-await desugars to an explicit loop; `return()` must run only on abrupt - // completion, which is the language rule. The two - // completion paths need two different loop bodies, so they are separate cases. - const plain = 'export const run = async (src) => { const seen = []\n' - + 'for await (const item of src) { seen.push(item) }\n' - + 'return seen }\n' - const code = transformModule(plain, 'probe.js') - parsesAsScript('for-await', code) - contains('for-await uses the iterator helper', code, '__als.iterator(') - contains('for-await closes on abrupt completion', code, '__als.close(') - - /** An async iterable counting up to `n`, rebuilt per case so state cannot leak. */ - const counting = (n: number): unknown => ({ - [Symbol.asyncIterator]: () => { - let emitted = 0 - return { - next: () => Promise.resolve( - emitted < n ? { done: false, value: ++emitted } : { done: true, value: undefined }, - ), - } - }, - }) - - // Normal completion: the iterator is exhausted, so `return()` must NOT run. - const { als, calls } = recordingAls() - const run = runBody(code, () => ({}), als).run as (src: unknown) => Promise - check('for-await over an async source collects values', await run(counting(2)), [1, 2]) - check('normal completion does not close the iterator', calls.includes('close'), false) - - // Abrupt completion (break), and a sync source whose values are promises - // (async-from-sync): close must run exactly once. - const breaking = 'export const run = async (src) => { const seen = []\n' - + 'for await (const item of src) { seen.push(item); if (item === 2) break }\n' - + 'return seen }\n' - const breakingCode = transformModule(breaking, 'probe.js') - parsesAsScript('for-await with break', breakingCode) - const { als: als2, calls: calls2 } = recordingAls() - const run2 = runBody(breakingCode, () => ({}), als2).run as (src: unknown) => Promise - const syncSource = { - [Symbol.iterator]: () => [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)][Symbol.iterator](), - } - check('for-await accepts a sync source of promises', await run2(syncSource), [1, 2]) - check('break closes the iterator exactly once', calls2.filter(name => name === 'close').length, 1) -} - -{ - // Destructuring in the loop head goes through the same binding path. - const code = transformModule( - 'export const run = async (src) => { const seen = []\nfor await (const { v } of src) seen.push(v)\nreturn seen }\n', - 'probe.js', - ) - parsesAsScript('for-await destructuring', code) - const { als } = recordingAls() - const run = runBody(code, () => ({}), als).run as (src: unknown) => Promise - check('for-await destructures each step', await run([{ v: 1 }, { v: 2 }]), [1, 2]) -} - -{ - // A non-block body must still be wrapped, or the emitted loop would swallow - // the following statement. - const code = transformModule( - 'export const run = async (src) => { let sum = 0\nfor await (const n of src) sum += n\nreturn sum }\n', - 'probe.js', - ) - parsesAsScript('for-await single-statement body', code) - const { als } = recordingAls() - const run = runBody(code, () => ({}), als).run as (src: unknown) => Promise - check('for-await with a non-block body runs correctly', await run([1, 2, 3]), 6) -} - -{ - // `yield` in an async generator: the snapshot is taken before suspending and - // the consumer's sent value comes back through afterYield. - const code = transformModule( - 'export async function* gen() { const got = yield 1\nyield got * 2 }\n', - 'probe.js', - ) - parsesAsScript('yield rewrite', code) - contains('yield is wrapped in afterYield(snapshot(), yield ...)', code, '__als.afterYield(__als.snapshot(),yield ') - const { als, calls } = recordingAls() - const gen = runBody(code, () => ({}), als).gen as () => AsyncGenerator - const iterator = gen() - check('first yield produces its value', (await iterator.next(0)).value, 1) - check('sent value returns through afterYield', (await iterator.next(21)).value, 42) - check('yield recorded snapshot and afterYield', calls.filter(name => name === 'afterYield').length >= 1, true) -} - -{ - // Statement-position `yield*` desugars into a forwarding loop. - const code = transformModule( - 'export async function* outer(inner) { yield* inner\nyield "tail" }\n', - 'probe.js', - ) - parsesAsScript('yield* rewrite', code) - const { als } = recordingAls() - const outer = runBody(code, () => ({}), als).outer as (inner: unknown) => AsyncGenerator - const collected: unknown[] = [] - for await (const value of outer(['a', 'b'])) collected.push(value) - check('yield* forwards inner values then continues', collected, ['a', 'b', 'tail']) -} - -// --------------------------------------------------------------------------- -// 7. Line numbers. The debugging contract: a stack frame in a transformed body -// points at the same line as the artifact it came from. -// --------------------------------------------------------------------------- - -/** @returns Line count of a string, counting a trailing newline's line as the last. */ -const lineCount = (text: string): number => text.split('\n').length - -{ - // The prologue is emitted without a trailing newline, so a transformed body - // has exactly as many lines as its source. Anything else is line drift. - const cases: Array<{ readonly label: string; readonly source: string }> = [ - { label: 'imports and exports', source: "import { a } from 'p'\n\nexport const b = a\n\nexport default b\n" }, - { label: 'await in a function', source: 'export const f = async () => {\n const v = await g()\n return v\n}\n' }, - { - label: 'for-await (body re-emitted)', - source: 'export const f = async (src) => {\n for await (const x of src) {\n use(x)\n }\n done()\n}\n', - }, - { - label: 'yield* (statement desugared)', - source: 'export async function* f(inner) {\n yield* inner\n after()\n}\n', - }, - { label: 'export * with following lines', source: "export * from 'p'\nconst tail = 1\nexport { tail }\n" }, - { label: 'multi-line import clause', source: "import {\n a,\n b,\n} from 'p'\nexport const out = [a, b]\n" }, - ] - for (const { label, source } of cases) { - const code = transformModule(source, 'probe.js') - check(`line count survives: ${label}`, lineCount(code), lineCount(source)) - } -} - -// --------------------------------------------------------------------------- -// 8. Refusals. Every one of these is a form the transform must reject loudly -// rather than emit something that breaks later. -// --------------------------------------------------------------------------- - -refuses('top-level await is refused', 'export const a = 1\nawait boot()\n', 'top-level await') -refuses('top-level for-await is refused', 'for await (const x of src) use(x)\n', 'top-level for-await') -refuses( - 'labeled for-await is refused', - 'export const f = async (src) => { outer: for await (const x of src) { break outer } }\n', - 'labeled for-await', -) -refuses( - 'import attributes are refused', - "import data from './d.json' with { type: 'json' }\n", - 'import attributes', -) -refuses( - 'value-position yield* is refused', - 'export async function* f(inner) { const v = yield* inner\nuse(v) }\n', - 'yield* is only supported as a statement', -) -refuses( - 'assignment around yield* is refused, never silently dropped', - 'export async function* f(inner) { let v\nv = yield* inner\nuse(v) }\n', - 'yield* is only supported as the whole statement expression', -) -refuses( - 'a call around yield* is refused, never silently dropped', - 'export async function* f(inner) { use(yield* inner) }\n', - 'yield* is only supported as the whole statement expression', -) -refuses( - 'already-lowered source is refused', - 'const x = __als.pause(1)\n', - 'already lowered', -) -refuses('unparseable source is refused', 'export const = \n', 'parse failed') - -{ - // A refusal must name the file and the line, which is what makes a build - // failure actionable. - const message = refusal('export const a = 1\n\n\nawait boot()\n', 'node_modules/p/index.js') - check('refusal names the file', message?.includes('node_modules/p/index.js'), true) - check('refusal names the offending line', message?.includes(':4'), true) -} - -// --------------------------------------------------------------------------- -// 9. Trap regressions. Each case is a module form that breaks a boot when the -// transform mishandles it; the AST pass must keep them fixed. -// --------------------------------------------------------------------------- - -{ - // Trap 1: a file with no module syntax can still contain a dynamic import. A - // transform that skips such files would leave it unrewritten, and it would - // escape to the host engine's parser. - const code = transformModule("module.exports = () => import('./x.js')\n", 'probe.js') - contains('trap 1: dynamic import in a CommonJS file is still rewritten', code, '__dsh$dynImport') - parsesAsScript('trap 1', code) -} - -{ - // Trap 2: `export {}` is a bundler module marker and must be removed before - // `new Function` parses the body. The needle is the keyword in statement - // position, since `exports.` in the prologue legitimately contains the same - // letters. - const code = transformModule('export {};\n', 'probe.js') - lacks('trap 2: bare export {} is removed', code, 'export {') - lacks('trap 2: no export keyword survives', code, 'export;') - parsesAsScript('trap 2', code) - check('trap 2: emitted body still marks __esModule', '__esModule' in runBody(code), true) -} - -{ - // Trap 3/4: every declarator is published, including declarations without an - // initializer. - const code = transformModule('export let x, y\nexport const set = () => { x = 1; y = 2 }\n', 'probe.js') - parsesAsScript('trap 3', code) - const exports = runBody(code) - ;(exports.set as () => void)() - check('trap 3: every declarator is exported, initializer or not', [exports.x, exports.y], [1, 2]) -} - -{ - // Trap 6: a block comment before a class member named `import` must not be - // treated as a dynamic import. Renaming `EntryTree.prototype.import` breaks - // the loading chain at `Entry._init` with - // "this.parent.tree.import is not a function". - const source = 'export class A {\n /** doc */ import(name) { return name }\n}\n' - const code = transformModule(source, 'probe.js') - lacks('trap 6: a method named import is not rewritten', code, '__dsh$dynImport') - parsesAsScript('trap 6', code) - const A = runBody(code).A as new () => { import: (name: string) => string } - check('trap 6: the method is still callable under its own name', new A().import('kept'), 'kept') -} - -{ - // Trap 7: a comment between `export` and the declaration keyword must not - // hide the declaration; refusing zod's - // `export /*@__NO_SIDE_EFFECTS__*/ function` takes 30-odd roster rows down - // with it. - const code = transformModule('export /*@__NO_SIDE_EFFECTS__*/ function $constructor(x) { return x }\n', 'probe.js') - parsesAsScript('trap 7', code) - check('trap 7: export with an interposed comment still publishes', typeof runBody(code).$constructor, 'function') -} - -{ - // `new.target` is also a MetaProperty. Replacing every MetaProperty would - // make `new.target === Cls` permanently false, silently disabling - // abstract-seam guards in `jobs` and `llm`. - const source = 'export class Base {\n constructor() { this.direct = new.target === Base }\n}\n' - const code = transformModule(source, 'probe.js') - contains('trap 8: new.target survives verbatim', code, 'new.target') - lacks('trap 8: new.target is not replaced by the meta parameter', code, '__dsh$meta') - parsesAsScript('trap 8', code) - const Base = runBody(code).Base as new () => { direct: boolean } - class Derived extends Base {} - check('trap 8: new.target compares true for a direct construction', new Base().direct, true) - check('trap 8: new.target compares false for a subclass', new Derived().direct, false) -} - -{ - // Shebang handling: `#!` is only legal at offset 0, which the prologue - // occupies. It is commented out in place so both offsets and the line count - // stay put. - const source = '#!/usr/bin/env node\nexport const main = 1\n' - const code = transformModule(source, 'probe.js') - lacks('shebang is not left in the emitted body', code, '#!') - parsesAsScript('shebang', code) - check('shebang: line count still survives', lineCount(code), lineCount(source)) - check('shebang: the module still works', runBody(code).main, 1) -} - -{ - // The exit gate itself: the transform re-parses its own output as a script. - // Any leftover module syntax or mis-spliced interval fails there, not at load. - // Re-checked here over a source that exercises several edits at once. - const source = "import a from 'p'\nexport * from 'q'\nexport const f = async () => { for await (const x of a) { await x } }\n" - parsesAsScript('exit gate over combined edits', transformModule(source, 'probe.js')) -} - -// --------------------------------------------------------------------------- -// 10. Caching: the transform memoizes by source text, and the cache must not -// leak a different file's result. -// --------------------------------------------------------------------------- - -{ - const source = 'export const cached = 1\n' - const first = transformModule(source, 'a.js') - const second = transformModule(source, 'b.js') - check('identical sources return the identical cached body', first === second, true) - // Distinct sources must not collide. - check( - 'distinct sources produce distinct bodies', - transformModule('export const other = 2\n', 'c.js') !== first, - true, - ) -} diff --git a/scripts/coverage-exempt.ts b/scripts/coverage-exempt.ts index 843193e3bb..38b295512c 100644 --- a/scripts/coverage-exempt.ts +++ b/scripts/coverage-exempt.ts @@ -34,13 +34,12 @@ export const coverageExemptHeavySuites: readonly CoverageExemptSuite[] = [ filter: 'packages/typert/generator/tests/', exclude: 'packages/typert/generator/tests/**', }, - // Spawns the full-corpus transform gate in a child process (Node's ESM - // loader is its oracle), so no measured file executes in-process; the - // sibling in-process suites carry the package's src coverage. As a single - // 499–513 s case it dominated one native Windows coverage partition. + // The webworker-runtime package is outside the coverage requirement by + // decision: vitest.config.ts threshold-excludes its src, so every suite + // runs uninstrumented. { - filter: 'packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts', - exclude: 'packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts', + filter: 'packages/experimental/webworker-runtime/tests/', + exclude: 'packages/experimental/webworker-runtime/tests/**', }, // Real child-process fixtures over scripts/ sources, which coverage never measures. { filter: 'scripts/install-lefthook.spec.ts', exclude: 'scripts/install-lefthook.spec.ts' }, diff --git a/vitest.config.ts b/vitest.config.ts index 133089e651..7374c355a6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -306,6 +306,10 @@ export default defineConfig({ // would put whole-workspace compiler analysis under v8 // instrumentation — the coverage lane's longest tail. 'packages/typert/generator/src/*.ts', + // Experimental webworker-runtime is outside the coverage requirement + // by decision: its correctness signal is its uninstrumented suite and + // the packer's end-to-end image spec. + 'packages/experimental/webworker-runtime/src/**/*.ts', 'packages/host/apiproxy/src/index.ts', 'packages/host/apiproxy/src/invariant.ts', 'packages/host/apiproxy/src/api-proxy.ts', From 8793cd477ba2be85edfa47c8f0a7062d329c1437 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:07:34 +0800 Subject: [PATCH 08/13] test(webworker-runtime): keep the corpus gate as a Node import sweep --- .../tests/compile/transform-corpus-check.ts | 139 ++++++++++++++++++ .../tests/compile/transform-corpus.spec.ts | 32 ++++ scripts/coverage-exempt.ts | 14 +- 3 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts create mode 100644 packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts diff --git a/packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts b/packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts new file mode 100644 index 0000000000..6c041b93c1 --- /dev/null +++ b/packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts @@ -0,0 +1,139 @@ +/** + * Full-corpus import gate: every built bundle in the workspace — + * `packages///lib/index.js` and `vendor//lib/index.js` + * — must be importable by Node's ESM loader. A bundle that stops importing (a + * stray `.css` import, an emitted module Node cannot parse, a dependency that + * throws at module scope) is reported by name. + * + * Baseline exemptions are a pinned list, not a count: an unlisted import + * failure is a real finding (a bundle that stopped being importable), and it + * must not hide inside a total. A listed file that becomes importable also + * fails, so the list cannot rot. + * + * Cost: this walks the whole build output and imports every bundle serially in + * one process, so it takes minutes on loaded runners and needs + * `pnpm run build:lib:host` to have run. It is a heavyweight suite, not part + * of a default aggregator run. + * + * Run: tsx tests/compile/transform-corpus-check.ts [files...] + * With no arguments it discovers the corpus itself. + */ +import { readdirSync, statSync } from 'node:fs' +import { join } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const repositoryRoot = fileURLToPath(new URL('../../../../../', import.meta.url)) + +/** + * Files Node's ESM loader cannot import in this repository. None is a finding: + * each is listed with the reason the import fails, and the run refuses a + * listed file that imports cleanly so the list stays current in both + * directions. The koffi entry depends on corpus order: sandbox-windows-acl + * imports the win32-process package earlier in the serial sweep (a distinct + * module instance under its node_modules URL), so win32-process's own file-URL + * import re-registers koffi's type names and fails as the second load. + */ +const BASELINE_EXEMPT: ReadonlyMap = new Map([ + ['packages/client/ui-primitives/lib/index.js', 'imports .css, which bare Node cannot load'], + ['packages/client/web/lib/index.js', 'imports .css, which bare Node cannot load'], + ['packages/subprocess/win32-process/lib/index.js', 'koffi type-name collision on a second load'], + ['packages/test-support/client-runtime/lib/index.js', "needs vitest's internal state"], +]) + +let failures = 0 +const report: string[] = [] +const log = (line: string): void => { + report.push(line) + process.stdout.write(`${line}\n`) +} +const fail = (line: string): void => { + failures += 1 + log(line) +} + +/** @returns Built bundles under a two-level package directory, in stable order. */ +function discover(): string[] { + const found: string[] = [] + /** @returns Sorted subdirectory names, or none when the path is not a readable directory. */ + const subdirectories = (path: string): string[] => { + try { + return readdirSync(path, { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .map(entry => entry.name) + .sort() + } catch { + return [] + } + } + for (const group of ['packages', 'vendor']) { + const groupDirectory = join(repositoryRoot, group) + for (const entry of subdirectories(groupDirectory)) { + // `packages///lib/index.js`, `vendor//lib/index.js`. + const candidates = group === 'vendor' + ? [join(groupDirectory, entry, 'lib', 'index.js')] + : subdirectories(join(groupDirectory, entry)) + .map(child => join(groupDirectory, entry, child, 'lib', 'index.js')) + for (const candidate of candidates) { + try { + if (statSync(candidate).isFile()) found.push(candidate) + } catch { + // No bundle for this package: it may not build a runtime artifact. + } + } + } + } + return found +} + +/** + * @returns Path relative to the repository root, for stable diagnostics. + * Always POSIX-separated: the exemption table keys on one form, and a win32 + * walk would otherwise miss every entry. + */ +const relative = (path: string): string => path.slice(repositoryRoot.length).replaceAll('\\', '/') + +const files = process.argv.slice(2).length > 0 + ? process.argv.slice(2).map(path => (path.startsWith('/') ? path : join(process.cwd(), path))) + : discover() + +if (files.length === 0) { + process.stdout.write('transform-corpus-check: no built bundles found; run `pnpm run build:lib:host` first\n') + process.exitCode = 1 +} else { + const verdicts = { ok: 0, exempt: 0, unexpectedBaseline: 0 } + + for (const file of files) { + const key = relative(file) + const exemption = BASELINE_EXEMPT.get(key) + try { + await import(pathToFileURL(file).href) + } catch (reason) { + if (exemption === undefined) { + // A bundle that stopped being importable is a real finding, so it + // fails rather than joining a tolerated total. + fail(`- UNEXPECTED BASELINE FAILURE ${key}: ${(reason as Error).message.split('\n')[0]}`) + verdicts.unexpectedBaseline += 1 + } else { + verdicts.exempt += 1 + } + continue + } + if (exemption !== undefined) { + // The exemption list must stay honest in the other direction too: a file + // that became importable should leave the list. + fail(`- STALE EXEMPTION ${key}: imports fine now (${exemption}); remove it from BASELINE_EXEMPT`) + continue + } + verdicts.ok += 1 + } + + log('') + log(`files=${String(files.length)} ok=${String(verdicts.ok)} baselineExempt=${String(verdicts.exempt)} ` + + `unexpectedBaselineFailure=${String(verdicts.unexpectedBaseline)}`) + + process.stdout.write(failures === 0 + ? `\ntransform-corpus-check: ${String(verdicts.ok)} bundles import under Node, ` + + `${String(verdicts.exempt)} exempt\n` + : `\ntransform-corpus-check: ${String(failures)} finding(s)\n`) + process.exitCode = failures === 0 ? 0 : 1 +} diff --git a/packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts b/packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts new file mode 100644 index 0000000000..43d5f3dd66 --- /dev/null +++ b/packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts @@ -0,0 +1,32 @@ +/** + * Runs the full-corpus import gate (`transform-corpus-check.ts`) in the + * launcher it is written for, and reports its findings as this suite's failure. + * + * Spawned rather than imported, because the gate's oracle is NODE's ESM loader: + * whether a built bundle imports is judged by `await import(file)` there. + * Vitest replaces that loader with vite's module runner, which imports files + * Node cannot — a `.css` import resolves, and koffi loads a second time — so an + * in-process run measures a different loader and reports the pinned baseline + * exemptions as stale. A gate whose verdict depends on how it was launched is + * not a gate. + * + * The corpus is the build output, so this skips on a tree that has none. + */ +import { spawnSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' +import { expect, test } from 'vitest' + +const runner = fileURLToPath(new URL('./transform-corpus-check.ts', import.meta.url)) + +test('every built bundle imports under Node', (context) => { + const finished = spawnSync(process.execPath, ['--import', 'tsx/esm', runner], { encoding: 'utf8' }) + const output = `${finished.stdout}${finished.stderr}` + if (output.includes('no built bundles found')) { + context.skip('the workspace has no build output to sweep') + return + } + // The runner prefixes every finding with '- ', so a failure reads as the + // findings themselves rather than as a diff of its whole report. + expect(output.split('\n').filter(line => line.startsWith('- ')).join('\n')).toBe('') + expect(finished.status, output).toBe(0) +}, 900_000) diff --git a/scripts/coverage-exempt.ts b/scripts/coverage-exempt.ts index 38b295512c..e7dc90bb29 100644 --- a/scripts/coverage-exempt.ts +++ b/scripts/coverage-exempt.ts @@ -36,7 +36,10 @@ export const coverageExemptHeavySuites: readonly CoverageExemptSuite[] = [ }, // The webworker-runtime package is outside the coverage requirement by // decision: vitest.config.ts threshold-excludes its src, so every suite - // runs uninstrumented. + // runs uninstrumented. This tree includes the full-corpus import gate, a + // single 900s-budget case that spawns a child sweep over every built + // bundle; inside an instrumented partition it exceeds the Windows + // partition budget under load. { filter: 'packages/experimental/webworker-runtime/tests/', exclude: 'packages/experimental/webworker-runtime/tests/**', @@ -46,15 +49,6 @@ export const coverageExemptHeavySuites: readonly CoverageExemptSuite[] = [ { filter: 'scripts/oxlint-contract.spec.ts', exclude: 'scripts/oxlint-contract.spec.ts' }, { filter: 'scripts/change-scope.spec.ts', exclude: 'scripts/change-scope.spec.ts' }, { filter: 'scripts/translation-pairing-merge.spec.ts', exclude: 'scripts/translation-pairing-merge.spec.ts' }, - // Spawns the full-corpus transform gate in a child process (Node's ESM - // loader is its oracle), so no measured file executes in-process; the - // package src is threshold-excluded in vitest.config.ts. A single - // 900s-budget case; running it inside an instrumented partition exceeds - // the Windows partition budget under load. - { - filter: 'packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts', - exclude: 'packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts', - }, // Built-artifact proof. Packer/runtime src is threshold-excluded, and the // native Windows aggregate makes this uninstrumented gate wait for build so // the suite never observes a partially emitted workspace closure. From 2d89a76b943ec0eb43c7f1fb6323d0a724eaecd9 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:33:10 +0800 Subject: [PATCH 09/13] test(webworker-runtime): restore the transform semantic spec --- .../tests/compile/transform.spec.ts | 704 ++++++++++++++++++ 1 file changed, 704 insertions(+) create mode 100644 packages/experimental/webworker-runtime/tests/compile/transform.spec.ts diff --git a/packages/experimental/webworker-runtime/tests/compile/transform.spec.ts b/packages/experimental/webworker-runtime/tests/compile/transform.spec.ts new file mode 100644 index 0000000000..bf8b74a9bb --- /dev/null +++ b/packages/experimental/webworker-runtime/tests/compile/transform.spec.ts @@ -0,0 +1,704 @@ +/** + * Semantic check of the worker module transform (`src/compile/transform.ts`): what the + * emitted CommonJS body looks like for each module form, how suspension points + * are rewritten, that line numbers survive, which forms are refused, and that + * every covered trap form stays fixed. + * + * Scope boundary: this file checks the transform itself; the pack-time loop + * around it is covered end-to-end by the packer's `image-loadable.spec.ts`. + * Emitted-code assertions are deliberately written against substrings + * of the real output rather than whole-file goldens: a golden would fail on every + * helper reordering, which is not the contract. The contract is the observable + * one — the code parses as script, publishes the right bindings, keeps line + * count, and routes suspension through `__als`. + * + * The trap cases are module forms that break a boot when the transform + * mishandles them. Five traps cannot recur while the AST pass is the parser, + * but they stay checked because a future parser swap could reintroduce them. + */ +import { expect, test } from 'vitest' +import { parse } from 'acorn' +import { lowerModuleSource } from '../../src/compile/transform.ts' +import { LOWERING_VERSION, WRAPPER_PARAMS } from '../../src/image-layout.ts' + +/** + * Lower one probe module the way the packer does — the transform's only caller. + * @param source - Module source under test. + * @param path - Path the diagnostics name. + * @returns The emitted body. + */ +const transformModule = (source: string, path = 'probe.js'): string => + lowerModuleSource({ filename: path, source }).code + +/** Register one comparison as its own case, serialized at call time. */ +const check = (label: string, actual: unknown, expected: unknown): void => { + const [seen, wanted] = [JSON.stringify(actual), JSON.stringify(expected)] + test(label, () => { expect(seen).toBe(wanted) }) +} + +/** Assert a substring is present in an emitted body. */ +const contains = (label: string, code: string, needle: string): void => { + test(label, () => { expect(code).toContain(needle) }) +} + +/** Assert a substring is absent (used for "must survive untouched" cases). */ +const lacks = (label: string, code: string, needle: string): void => { + test(label, () => { expect(code).not.toContain(needle) }) +} + +/** @returns The error message of a refused transform, or undefined when it succeeded. */ +const refusal = (source: string, path = 'probe.js'): string | undefined => { + try { + transformModule(source, path) + return undefined + } catch (reason) { + return (reason as Error).message + } +} + +/** Assert the transform refuses a source and names the reason. */ +const refuses = (label: string, source: string, fragment: string): void => { + const message = refusal(source) + test(label, () => { expect(message).toContain(fragment) }) +} + +/** + * The wrapper contract, applied for real: compile the body with the declared + * parameters and run it. This is the same `new Function` shape the loader uses + * (module-loader.ts), so a body that compiles here compiles there. + * @param code - Emitted CommonJS body. + * @param require - Module resolver the body's `require` calls reach. + * @param als - Suspension runtime bound to `__als`. + * @returns The populated `exports` object. + */ +function runBody( + code: string, + require: (specifier: string) => unknown = () => ({}), + als?: unknown, +): Record { + const exports: Record = {} + const module = { exports } + // eslint-disable-next-line @typescript-eslint/no-implied-eval -- the wrapper contract under test is a `new Function` body + const factory = new Function(...WRAPPER_PARAMS, code) as (...args: unknown[]) => void + factory(exports, require, module, '/vfs/probe.js', '/vfs', { url: 'file:///vfs/probe.js' }, als) + return exports +} + +/** Every emitted body must parse as a script — the transform's own exit gate, re-checked here. */ +const parsesAsScript = (label: string, code: string): void => { + test(label, () => { + expect(() => parse(code, { ecmaVersion: 'latest', sourceType: 'script', allowAwaitOutsideFunction: false })).not.toThrow() + }) +} + +// --------------------------------------------------------------------------- +// 1. The published contract: the three names the packer and loader share. +// --------------------------------------------------------------------------- + +check('LOWERING_VERSION is a non-empty string', typeof LOWERING_VERSION === 'string' && LOWERING_VERSION.length > 0, true) +check('WRAPPER_PARAMS is the frozen 7-parameter shape', [...WRAPPER_PARAMS], [ + 'exports', 'require', 'module', '__filename', '__dirname', '__dsh$meta', '__als', +]) +// The wrapper signature is a contract with the loader's `new Function`, so the +// parameters must be valid identifiers in that position. +check( + 'every wrapper parameter is a usable identifier', + (() => { + try { + // eslint-disable-next-line @typescript-eslint/no-implied-eval -- proves the parameter names compile where the loader uses them + new Function(...WRAPPER_PARAMS, 'return 0') + return true + } catch { + return false + } + })(), + true, +) + +// --------------------------------------------------------------------------- +// 2. lowerModuleSource: the packer face. `lowered` is the pack-time decision. +// --------------------------------------------------------------------------- + +{ + const esm = lowerModuleSource({ filename: 'node_modules/p/index.js', source: 'export const a = 1\n' }) + check('lowered=true for a module that needed rewriting', esm.lowered, true) + check('lowered code differs from source', esm.code !== 'export const a = 1\n', true) + + // Plain CommonJS with no suspension point is the "pack as-is" case: the + // collector relies on this to leave 1693-odd entries untouched. + const plain = 'module.exports = 1\n' + const cjs = lowerModuleSource({ filename: 'node_modules/p/legacy.cjs', source: plain }) + check('lowered=false for plain CommonJS', cjs.lowered, false) + check('unlowered code is the input verbatim', cjs.code, plain) + + // A CommonJS body that still contains a suspension point must be rewritten: + // `await` inside a function is the ALS protocol's business even with no ESM. + const cjsAwait = lowerModuleSource({ + filename: 'node_modules/p/async.cjs', + source: 'module.exports = async () => { await 1 }\n', + }) + check('lowered=true for CommonJS carrying a suspension point', cjsAwait.lowered, true) + contains('CommonJS await still routes through __als', cjsAwait.code, '__als.pause(') + + // `lowered` must agree with the code/source comparison by construction. + check('lowered mirrors code !== source', cjsAwait.lowered, cjsAwait.code !== 'module.exports = async () => { await 1 }\n') +} + +// --------------------------------------------------------------------------- +// 3. Import forms. +// --------------------------------------------------------------------------- + +{ + // Side-effect import: a bare require, nothing bound. + const code = transformModule("import './side-effect.js'\n", 'probe.js') + contains('side-effect import becomes a bare require', code, 'require("./side-effect.js")') + parsesAsScript('side-effect import', code) + + const requested: string[] = [] + runBody(code, (specifier) => { + requested.push(specifier) + return {} + }) + check('side-effect import actually requires at run time', requested, ['./side-effect.js']) +} + +{ + // Named imports are snapshots (CommonJS destructuring semantics), which is the + // documented, accepted divergence from ESM live bindings on the import side. + const code = transformModule("import { a, b as c } from 'p'\nexport const out = [a, c]\n", 'probe.js') + parsesAsScript('named imports', code) + const exports = runBody(code, () => ({ a: 1, b: 2 })) + check('named import binds by imported name, honouring the alias', exports.out, [1, 2]) +} + +{ + // Default and namespace imports go through the two interop helpers, which must + // agree with `Loader.unwrapExports` on the `__esModule` convention. + const code = transformModule("import d from 'p'\nimport * as ns from 'q'\nexport const seen = [d, ns.x, ns.default]\n", 'probe.js') + parsesAsScript('default and namespace imports', code) + + // An `__esModule` module: default comes from `.default`, namespace passes through. + const esModule = { __esModule: true, default: 'D', x: 'X' } + const withEsm = runBody(code, () => esModule) + check('default import of an __esModule module reads .default', (withEsm.seen as unknown[])[0], 'D') + + // A plain CommonJS module: the module object *is* the default, and the + // namespace gains a `default` key pointing at it. + const plain = { x: 'X' } + const withCjs = runBody(code, () => plain) + check('default import of plain CommonJS is the module object', (withCjs.seen as unknown[])[0], plain) + check('namespace of plain CommonJS keeps the named key', (withCjs.seen as unknown[])[1], 'X') + check('namespace of plain CommonJS synthesizes default', (withCjs.seen as unknown[])[2], plain) +} + +// --------------------------------------------------------------------------- +// 4. Export forms, including the live-binding contract. +// --------------------------------------------------------------------------- + +{ + const code = transformModule('export const a = 1\nexport function f() {}\nexport class K {}\n', 'probe.js') + parsesAsScript('exported declarations', code) + contains('module bodies get the __esModule marker', code, '__esModule') + contains('use strict is part of the prologue', code, '"use strict"') + const exports = runBody(code) + check('exported const is published', exports.a, 1) + check('exported function is published', typeof exports.f, 'function') + check('exported class is published', typeof exports.K, 'function') +} + +{ + // Local exports are getters, so a later assignment is observable through + // `exports` — the ESM live-binding property. + const code = transformModule('export let counter = 0\nexport function bump() { counter += 1 }\n', 'probe.js') + parsesAsScript('live binding', code) + const exports = runBody(code) + check('live binding starts at its initializer', exports.counter, 0) + ;(exports.bump as () => void)() + check('live binding observes a later assignment', exports.counter, 1) + // A getter, not a data property: this is what makes the above work. + check( + 'exported local is an accessor', + typeof Object.getOwnPropertyDescriptor(exports, 'counter')?.get, + 'function', + ) +} + +{ + // Trap 4: a multi-declarator export publishes every binding. + const code = transformModule('export const a = 1, b = 2\n', 'probe.js') + parsesAsScript('multi-declarator export', code) + const exports = runBody(code) + check('multi-declarator export publishes every binding', [exports.a, exports.b], [1, 2]) +} + +{ + // Destructuring exports exercise the pattern walker (object, array, rest, + // default) — every branch of `declaredBindings`. + const code = transformModule( + 'export const { p, q: renamed, ...restObj } = { p: 1, q: 2, z: 3 }\n' + + 'export const [first, , third = 30, ...restArr] = [10, 20, undefined, 40, 50]\n', + 'probe.js', + ) + parsesAsScript('destructuring exports', code) + const exports = runBody(code) + check('object pattern export', [exports.p, exports.renamed], [1, 2]) + check('object rest export', exports.restObj, { z: 3 }) + check('array pattern export with hole', [exports.first, exports.third], [10, 30]) + check('array rest export', exports.restArr, [40, 50]) + // The renamed target is what is published; the source key is not a binding. + check('object pattern publishes the local name, not the source key', 'q' in exports, false) +} + +{ + const code = transformModule('const x = 1\nexport { x as y }\n', 'probe.js') + parsesAsScript('local export clause', code) + const exports = runBody(code) + check('local export clause publishes under the exported name', exports.y, 1) + check('local export clause does not publish the local name', 'x' in exports, false) +} + +{ + // Re-export clause: a getter onto the required module, so it also stays live. + const module: Record = { a: 1 } + const code = transformModule("export { a, a as aliased } from 'p'\n", 'probe.js') + parsesAsScript('re-export clause', code) + const exports = runBody(code, () => module) + check('re-export publishes the name', exports.a, 1) + check('re-export publishes the alias', exports.aliased, 1) + module.a = 2 + check('re-export is live against the source module', exports.a, 2) +} + +{ + // `export *` copies enumerable keys, skips `default`, and must not clobber an + // existing local export. + const code = transformModule("export const own = 'local'\nexport * from 'p'\n", 'probe.js') + parsesAsScript('export all', code) + const exports = runBody(code, () => ({ extra: 'E', default: 'D', own: 'theirs' })) + check('export * copies named keys', exports.extra, 'E') + check('export * skips default', 'default' in exports, false) + check('export * does not overwrite an existing export', exports.own, 'local') +} + +{ + const code = transformModule("export * as ns from 'p'\n", 'probe.js') + parsesAsScript('export all as namespace', code) + const exports = runBody(code, () => ({ x: 1 })) + check('export * as ns publishes a namespace object', (exports.ns as Record).x, 1) +} + +{ + const code = transformModule('export default 42\n', 'probe.js') + parsesAsScript('default export value', code) + check('default export lands on exports.default', runBody(code).default, 42) +} + +{ + // Documented cost: the function name stops being a module-scope binding, but + // the named function expression can still refer to itself. + const code = transformModule('export default function self(n) { return n <= 0 ? 0 : self(n - 1) }\n', 'probe.js') + parsesAsScript('default export function', code) + const fn = runBody(code).default as (n: number) => number + check('default-exported function keeps self-reference', fn(3), 0) +} + +{ + const code = transformModule("export { x as default } from 'p'\n", 'probe.js') + parsesAsScript('re-export as default', code) + check('re-export as default publishes default', runBody(code, () => ({ x: 'D' })).default, 'D') +} + +// --------------------------------------------------------------------------- +// 5. import.meta and dynamic import. +// --------------------------------------------------------------------------- + +{ + const code = transformModule('export const here = import.meta.url\n', 'probe.js') + parsesAsScript('import.meta', code) + contains('import.meta becomes the wrapper parameter', code, '__dsh$meta') + check('import.meta.url resolves through the wrapper', runBody(code).here, 'file:///vfs/probe.js') +} + +{ + // Dynamic import routes through the same require chain (which is what makes + // typert-loader's absolute-path `import()` land on the VFS resolver), and the + // result is namespace-shaped. + const code = transformModule("export const load = () => import('p')\n", 'probe.js') + parsesAsScript('dynamic import', code) + contains('dynamic import becomes the helper call', code, '__dsh$dynImport') + const load = runBody(code, () => ({ x: 1 })).load as () => Promise> + const namespace = await load() + check('dynamic import resolves to a namespace object', namespace.x, 1) + check('dynamic import namespace has a default', 'default' in namespace, true) +} + +// --------------------------------------------------------------------------- +// 6. Suspension points. Behaviour is checked against a recording runtime, so +// these assert the protocol shape rather than re-testing als-runtime. +// --------------------------------------------------------------------------- + +/** A recording stand-in for the ALS runtime: proves the emitted calls happen in order. */ +function recordingAls(): { als: Record; calls: string[] } { + const calls: string[] = [] + const als = { + pause: (value: unknown) => { + calls.push('pause') + return Promise.resolve(value).then( + settled => ({ ok: true, value: settled, snapshot: 'S' }), + (error: unknown) => ({ ok: false, error, snapshot: 'S' }), + ) + }, + resume: (token: { ok: boolean; value?: unknown; error?: unknown }) => { + calls.push('resume') + if (token.ok) return token.value + throw token.error + }, + snapshot: () => { + calls.push('snapshot') + return 'S' + }, + afterYield: (_snapshot: unknown, sent: unknown) => { + calls.push('afterYield') + return sent + }, + iterator: (value: unknown) => { + calls.push('iterator') + const source = value as Record + const asyncFactory = source[Symbol.asyncIterator] as (() => AsyncIterator) | undefined + if (typeof asyncFactory === 'function') return asyncFactory.call(source) + const syncFactory = source[Symbol.iterator] as () => Iterator + const inner = syncFactory.call(source) + return { + next: async (...args: unknown[]) => { + const step = inner.next(...args as [unknown]) + return { done: step.done ?? false, value: await step.value } + }, + return: async (sent?: unknown) => { + const step = inner.return?.(sent) ?? { done: true, value: undefined } + return { done: step.done ?? true, value: await step.value } + }, + } + }, + close: async (iterator: AsyncIterator) => { + calls.push('close') + return iterator.return?.(undefined) + }, + } + return { als, calls } +} + +{ + const code = transformModule('export const run = async () => await 7\n', 'probe.js') + parsesAsScript('await rewrite', code) + contains('await is wrapped in resume(await pause(', code, '__als.resume(await __als.pause(') + const { als, calls } = recordingAls() + const run = runBody(code, () => ({}), als).run as () => Promise + check('await still yields its value', await run(), 7) + check('await goes pause-then-resume', calls, ['pause', 'resume']) +} + +{ + // The rejection path is the half that a naive "snapshot on success" rewrite + // gets wrong, so it is checked as its own case. + const code = transformModule( + "export const run = async () => { try { await Promise.reject(new Error('boom')) } catch (reason) { return `caught:${reason.message}` } }\n", + 'probe.js', + ) + parsesAsScript('await rejection', code) + const { als, calls } = recordingAls() + const run = runBody(code, () => ({}), als).run as () => Promise + check('rejection surfaces through resume', await run(), 'caught:boom') + check('rejection path also goes pause-then-resume', calls, ['pause', 'resume']) +} + +{ + // for-await desugars to an explicit loop; `return()` must run only on abrupt + // completion, which is the language rule. The two + // completion paths need two different loop bodies, so they are separate cases. + const plain = 'export const run = async (src) => { const seen = []\n' + + 'for await (const item of src) { seen.push(item) }\n' + + 'return seen }\n' + const code = transformModule(plain, 'probe.js') + parsesAsScript('for-await', code) + contains('for-await uses the iterator helper', code, '__als.iterator(') + contains('for-await closes on abrupt completion', code, '__als.close(') + + /** An async iterable counting up to `n`, rebuilt per case so state cannot leak. */ + const counting = (n: number): unknown => ({ + [Symbol.asyncIterator]: () => { + let emitted = 0 + return { + next: () => Promise.resolve( + emitted < n ? { done: false, value: ++emitted } : { done: true, value: undefined }, + ), + } + }, + }) + + // Normal completion: the iterator is exhausted, so `return()` must NOT run. + const { als, calls } = recordingAls() + const run = runBody(code, () => ({}), als).run as (src: unknown) => Promise + check('for-await over an async source collects values', await run(counting(2)), [1, 2]) + check('normal completion does not close the iterator', calls.includes('close'), false) + + // Abrupt completion (break), and a sync source whose values are promises + // (async-from-sync): close must run exactly once. + const breaking = 'export const run = async (src) => { const seen = []\n' + + 'for await (const item of src) { seen.push(item); if (item === 2) break }\n' + + 'return seen }\n' + const breakingCode = transformModule(breaking, 'probe.js') + parsesAsScript('for-await with break', breakingCode) + const { als: als2, calls: calls2 } = recordingAls() + const run2 = runBody(breakingCode, () => ({}), als2).run as (src: unknown) => Promise + const syncSource = { + [Symbol.iterator]: () => [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)][Symbol.iterator](), + } + check('for-await accepts a sync source of promises', await run2(syncSource), [1, 2]) + check('break closes the iterator exactly once', calls2.filter(name => name === 'close').length, 1) +} + +{ + // Destructuring in the loop head goes through the same binding path. + const code = transformModule( + 'export const run = async (src) => { const seen = []\nfor await (const { v } of src) seen.push(v)\nreturn seen }\n', + 'probe.js', + ) + parsesAsScript('for-await destructuring', code) + const { als } = recordingAls() + const run = runBody(code, () => ({}), als).run as (src: unknown) => Promise + check('for-await destructures each step', await run([{ v: 1 }, { v: 2 }]), [1, 2]) +} + +{ + // A non-block body must still be wrapped, or the emitted loop would swallow + // the following statement. + const code = transformModule( + 'export const run = async (src) => { let sum = 0\nfor await (const n of src) sum += n\nreturn sum }\n', + 'probe.js', + ) + parsesAsScript('for-await single-statement body', code) + const { als } = recordingAls() + const run = runBody(code, () => ({}), als).run as (src: unknown) => Promise + check('for-await with a non-block body runs correctly', await run([1, 2, 3]), 6) +} + +{ + // `yield` in an async generator: the snapshot is taken before suspending and + // the consumer's sent value comes back through afterYield. + const code = transformModule( + 'export async function* gen() { const got = yield 1\nyield got * 2 }\n', + 'probe.js', + ) + parsesAsScript('yield rewrite', code) + contains('yield is wrapped in afterYield(snapshot(), yield ...)', code, '__als.afterYield(__als.snapshot(),yield ') + const { als, calls } = recordingAls() + const gen = runBody(code, () => ({}), als).gen as () => AsyncGenerator + const iterator = gen() + check('first yield produces its value', (await iterator.next(0)).value, 1) + check('sent value returns through afterYield', (await iterator.next(21)).value, 42) + check('yield recorded snapshot and afterYield', calls.filter(name => name === 'afterYield').length >= 1, true) +} + +{ + // Statement-position `yield*` desugars into a forwarding loop. + const code = transformModule( + 'export async function* outer(inner) { yield* inner\nyield "tail" }\n', + 'probe.js', + ) + parsesAsScript('yield* rewrite', code) + const { als } = recordingAls() + const outer = runBody(code, () => ({}), als).outer as (inner: unknown) => AsyncGenerator + const collected: unknown[] = [] + for await (const value of outer(['a', 'b'])) collected.push(value) + check('yield* forwards inner values then continues', collected, ['a', 'b', 'tail']) +} + +// --------------------------------------------------------------------------- +// 7. Line numbers. The debugging contract: a stack frame in a transformed body +// points at the same line as the artifact it came from. +// --------------------------------------------------------------------------- + +/** @returns Line count of a string, counting a trailing newline's line as the last. */ +const lineCount = (text: string): number => text.split('\n').length + +{ + // The prologue is emitted without a trailing newline, so a transformed body + // has exactly as many lines as its source. Anything else is line drift. + const cases: Array<{ readonly label: string; readonly source: string }> = [ + { label: 'imports and exports', source: "import { a } from 'p'\n\nexport const b = a\n\nexport default b\n" }, + { label: 'await in a function', source: 'export const f = async () => {\n const v = await g()\n return v\n}\n' }, + { + label: 'for-await (body re-emitted)', + source: 'export const f = async (src) => {\n for await (const x of src) {\n use(x)\n }\n done()\n}\n', + }, + { + label: 'yield* (statement desugared)', + source: 'export async function* f(inner) {\n yield* inner\n after()\n}\n', + }, + { label: 'export * with following lines', source: "export * from 'p'\nconst tail = 1\nexport { tail }\n" }, + { label: 'multi-line import clause', source: "import {\n a,\n b,\n} from 'p'\nexport const out = [a, b]\n" }, + ] + for (const { label, source } of cases) { + const code = transformModule(source, 'probe.js') + check(`line count survives: ${label}`, lineCount(code), lineCount(source)) + } +} + +// --------------------------------------------------------------------------- +// 8. Refusals. Every one of these is a form the transform must reject loudly +// rather than emit something that breaks later. +// --------------------------------------------------------------------------- + +refuses('top-level await is refused', 'export const a = 1\nawait boot()\n', 'top-level await') +refuses('top-level for-await is refused', 'for await (const x of src) use(x)\n', 'top-level for-await') +refuses( + 'labeled for-await is refused', + 'export const f = async (src) => { outer: for await (const x of src) { break outer } }\n', + 'labeled for-await', +) +refuses( + 'import attributes are refused', + "import data from './d.json' with { type: 'json' }\n", + 'import attributes', +) +refuses( + 'value-position yield* is refused', + 'export async function* f(inner) { const v = yield* inner\nuse(v) }\n', + 'yield* is only supported as a statement', +) +refuses( + 'assignment around yield* is refused, never silently dropped', + 'export async function* f(inner) { let v\nv = yield* inner\nuse(v) }\n', + 'yield* is only supported as the whole statement expression', +) +refuses( + 'a call around yield* is refused, never silently dropped', + 'export async function* f(inner) { use(yield* inner) }\n', + 'yield* is only supported as the whole statement expression', +) +refuses( + 'already-lowered source is refused', + 'const x = __als.pause(1)\n', + 'already lowered', +) +refuses('unparseable source is refused', 'export const = \n', 'parse failed') + +{ + // A refusal must name the file and the line, which is what makes a build + // failure actionable. + const message = refusal('export const a = 1\n\n\nawait boot()\n', 'node_modules/p/index.js') + check('refusal names the file', message?.includes('node_modules/p/index.js'), true) + check('refusal names the offending line', message?.includes(':4'), true) +} + +// --------------------------------------------------------------------------- +// 9. Trap regressions. Each case is a module form that breaks a boot when the +// transform mishandles it; the AST pass must keep them fixed. +// --------------------------------------------------------------------------- + +{ + // Trap 1: a file with no module syntax can still contain a dynamic import. A + // transform that skips such files would leave it unrewritten, and it would + // escape to the host engine's parser. + const code = transformModule("module.exports = () => import('./x.js')\n", 'probe.js') + contains('trap 1: dynamic import in a CommonJS file is still rewritten', code, '__dsh$dynImport') + parsesAsScript('trap 1', code) +} + +{ + // Trap 2: `export {}` is a bundler module marker and must be removed before + // `new Function` parses the body. The needle is the keyword in statement + // position, since `exports.` in the prologue legitimately contains the same + // letters. + const code = transformModule('export {};\n', 'probe.js') + lacks('trap 2: bare export {} is removed', code, 'export {') + lacks('trap 2: no export keyword survives', code, 'export;') + parsesAsScript('trap 2', code) + check('trap 2: emitted body still marks __esModule', '__esModule' in runBody(code), true) +} + +{ + // Trap 3/4: every declarator is published, including declarations without an + // initializer. + const code = transformModule('export let x, y\nexport const set = () => { x = 1; y = 2 }\n', 'probe.js') + parsesAsScript('trap 3', code) + const exports = runBody(code) + ;(exports.set as () => void)() + check('trap 3: every declarator is exported, initializer or not', [exports.x, exports.y], [1, 2]) +} + +{ + // Trap 6: a block comment before a class member named `import` must not be + // treated as a dynamic import. Renaming `EntryTree.prototype.import` breaks + // the loading chain at `Entry._init` with + // "this.parent.tree.import is not a function". + const source = 'export class A {\n /** doc */ import(name) { return name }\n}\n' + const code = transformModule(source, 'probe.js') + lacks('trap 6: a method named import is not rewritten', code, '__dsh$dynImport') + parsesAsScript('trap 6', code) + const A = runBody(code).A as new () => { import: (name: string) => string } + check('trap 6: the method is still callable under its own name', new A().import('kept'), 'kept') +} + +{ + // Trap 7: a comment between `export` and the declaration keyword must not + // hide the declaration; refusing zod's + // `export /*@__NO_SIDE_EFFECTS__*/ function` takes 30-odd roster rows down + // with it. + const code = transformModule('export /*@__NO_SIDE_EFFECTS__*/ function $constructor(x) { return x }\n', 'probe.js') + parsesAsScript('trap 7', code) + check('trap 7: export with an interposed comment still publishes', typeof runBody(code).$constructor, 'function') +} + +{ + // `new.target` is also a MetaProperty. Replacing every MetaProperty would + // make `new.target === Cls` permanently false, silently disabling + // abstract-seam guards in `jobs` and `llm`. + const source = 'export class Base {\n constructor() { this.direct = new.target === Base }\n}\n' + const code = transformModule(source, 'probe.js') + contains('trap 8: new.target survives verbatim', code, 'new.target') + lacks('trap 8: new.target is not replaced by the meta parameter', code, '__dsh$meta') + parsesAsScript('trap 8', code) + const Base = runBody(code).Base as new () => { direct: boolean } + class Derived extends Base {} + check('trap 8: new.target compares true for a direct construction', new Base().direct, true) + check('trap 8: new.target compares false for a subclass', new Derived().direct, false) +} + +{ + // Shebang handling: `#!` is only legal at offset 0, which the prologue + // occupies. It is commented out in place so both offsets and the line count + // stay put. + const source = '#!/usr/bin/env node\nexport const main = 1\n' + const code = transformModule(source, 'probe.js') + lacks('shebang is not left in the emitted body', code, '#!') + parsesAsScript('shebang', code) + check('shebang: line count still survives', lineCount(code), lineCount(source)) + check('shebang: the module still works', runBody(code).main, 1) +} + +{ + // The exit gate itself: the transform re-parses its own output as a script. + // Any leftover module syntax or mis-spliced interval fails there, not at load. + // Re-checked here over a source that exercises several edits at once. + const source = "import a from 'p'\nexport * from 'q'\nexport const f = async () => { for await (const x of a) { await x } }\n" + parsesAsScript('exit gate over combined edits', transformModule(source, 'probe.js')) +} + +// --------------------------------------------------------------------------- +// 10. Caching: the transform memoizes by source text, and the cache must not +// leak a different file's result. +// --------------------------------------------------------------------------- + +{ + const source = 'export const cached = 1\n' + const first = transformModule(source, 'a.js') + const second = transformModule(source, 'b.js') + check('identical sources return the identical cached body', first === second, true) + // Distinct sources must not collide. + check( + 'distinct sources produce distinct bodies', + transformModule('export const other = 2\n', 'c.js') !== first, + true, + ) +} From e1c49dab59767acb3a609a3c364c15f4d05bf3ab Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:34:29 +0800 Subject: [PATCH 10/13] ci(windows): clear stale pnpm setup state before install --- .github/workflows/ci.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5afaf5fca..182d66d6fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -421,6 +421,13 @@ jobs: run: >- reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" + # Best-effort: a torn-down job on this self-hosted pool can leave a + # locked @reflink native module under the action's install destination, + # and pnpm/action-setup's self-installer then fails its unlink with + # EPERM. Clearing the destination gives every attempt fresh state. + - name: Clear stale pnpm setup state + shell: pwsh + run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue } - uses: pnpm/action-setup@v4 with: dest: ${{ runner.temp }}/setup-pnpm-js @@ -457,6 +464,10 @@ jobs: run: >- reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" + # Best-effort stale pnpm-destination cleanup; rationale on windows-build's copy. + - name: Clear stale pnpm setup state + shell: pwsh + run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue } - uses: pnpm/action-setup@v4 with: dest: ${{ runner.temp }}/setup-pnpm-js @@ -491,6 +502,10 @@ jobs: run: >- reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" + # Best-effort stale pnpm-destination cleanup; rationale on windows-build's copy. + - name: Clear stale pnpm setup state + shell: pwsh + run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue } - uses: pnpm/action-setup@v4 with: dest: ${{ runner.temp }}/setup-pnpm-js @@ -533,6 +548,10 @@ jobs: run: >- reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" + # Best-effort stale pnpm-destination cleanup; rationale on windows-build's copy. + - name: Clear stale pnpm setup state + shell: pwsh + run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue } - uses: pnpm/action-setup@v4 with: dest: ${{ runner.temp }}/setup-pnpm-js From f18ab429e42a6c917329cb71e9c43a9673d91161 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:46:17 +0800 Subject: [PATCH 11/13] ci(release): pack rehearsal tarballs concurrently --- .github/workflows/release-vendor.yml | 4 ++- .github/workflows/release.yml | 6 ++-- scripts/release/pack.ts | 44 ++++++++++++++++++++++------ scripts/release/process.ts | 22 +++++++++++++- 4 files changed, 63 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release-vendor.yml b/.github/workflows/release-vendor.yml index 34290bb7a2..8e1d531a03 100644 --- a/.github/workflows/release-vendor.yml +++ b/.github/workflows/release-vendor.yml @@ -71,8 +71,10 @@ jobs: - name: Build run: pnpm run build:lib:host + # Concurrency here is rehearsal-only: the credentialed publish workflows + # invoke release:pack without the flag and keep the strictly serial path. - name: Pack release tarballs - run: pnpm run release:pack --family vendor --out dist/npm-vendor + run: pnpm run release:pack --family vendor --out dist/npm-vendor --concurrency 8 - name: Verify packed install run: pnpm run release:verify-packed-install --family vendor --from dist/npm-vendor diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b84a299b9..4ae1940c0c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,8 +68,10 @@ jobs: - name: Build run: pnpm run build:official + # Concurrency here is rehearsal-only: the credentialed publish workflows + # invoke release:pack without the flag and keep the strictly serial path. - name: Pack release tarballs - run: pnpm run release:pack --family dsh --out dist/npm + run: pnpm run release:pack --family dsh --out dist/npm --concurrency 8 # The harness packages declare the vendored framework as a peer, and this # verification must not depend on the registry already carrying matching @@ -77,7 +79,7 @@ jobs: # publishes — so it installs that family's pack output too. Only dist/npm # is published. - name: Pack the vendored framework for verification - run: pnpm run release:pack --family vendor --out dist/npm-vendor + run: pnpm run release:pack --family vendor --out dist/npm-vendor --concurrency 8 # dsh-sandbox-local declares the Landlock entry as a runtime dependency, so # the verification needs its tarball. Its platform packages stay out: they diff --git a/scripts/release/pack.ts b/scripts/release/pack.ts index 3b68a1e49c..0742eedcda 100644 --- a/scripts/release/pack.ts +++ b/scripts/release/pack.ts @@ -11,7 +11,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' import { join, resolve } from 'node:path' import { parseArgs } from 'node:util' import { releaseFamily, tarballName, type ReleaseFamily, type ReleaseMember } from './families.ts' -import { isEntry, run } from './process.ts' +import { isEntry, runConcurrent } from './process.ts' import { PUBLISH_ORDER_FILE, tarballFiles } from './tarball.ts' /** Where pack output lands when `--out` is omitted. */ @@ -24,8 +24,8 @@ const DEFAULT_OUTPUT = 'dist/npm' * @param destination - absolute output directory. * @returns The tarball filename. */ -function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): string { - run('pnpm', ['--dir', member.directory, 'pack', '--pack-destination', destination]) +async function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): Promise { + await runConcurrent('pnpm', ['--dir', member.directory, 'pack', '--pack-destination', destination]) const filename = tarballName(member) const tarball = join(destination, filename) @@ -34,13 +34,27 @@ function packMember(family: ReleaseFamily, member: ReleaseMember, destination: s return filename } +/** + * @returns The validated `--concurrency` value; 1 (the default) packs the + * members one at a time, exactly as the credentialed publish workflows run it. + */ +function parseConcurrency(raw: string | undefined): number { + if (raw === undefined) return 1 + const parsed = Number.parseInt(raw, 10) + if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) { + throw new Error(`--concurrency must be a positive integer, got ${JSON.stringify(raw)}`) + } + return parsed +} + /** Pack the family named by `--family` into `--out`. */ -function main(): void { +async function main(): Promise { const { values } = parseArgs({ - options: { family: { type: 'string' }, out: { type: 'string' } }, + options: { family: { type: 'string' }, out: { type: 'string' }, concurrency: { type: 'string' } }, allowPositionals: false, }) - if (values.family === undefined) throw new Error('usage: pack.ts --family [--out dist/npm]') + if (values.family === undefined) throw new Error('usage: pack.ts --family [--out dist/npm] [--concurrency 1]') + const concurrency = parseConcurrency(values.concurrency) const family = releaseFamily(values.family) const root = process.cwd() @@ -52,11 +66,23 @@ function main(): void { rmSync(destination, { recursive: true, force: true }) mkdirSync(destination, { recursive: true }) - const order: string[] = [] - for (const member of members) order.push(packMember(family, member, destination)) + // Members pack in a bounded pool; the recorded publish order stays the + // members' order regardless of completion order, because each worker writes + // its result at the member's own position. + const order = new Array(members.length) + let cursor = 0 + await Promise.all(Array.from({ length: Math.min(concurrency, members.length) }, async () => { + while (cursor < members.length) { + const index = cursor + cursor += 1 + const member = members[index] + if (member === undefined) break + order[index] = await packMember(family, member, destination) + } + })) writeFileSync(join(destination, PUBLISH_ORDER_FILE), `${order.join('\n')}\n`) console.log(`release pack: family ${family.id}, ${String(order.length)} tarball(s) in ${values.out ?? DEFAULT_OUTPUT}`) } -if (isEntry(import.meta.url)) main() +if (isEntry(import.meta.url)) await main() diff --git a/scripts/release/process.ts b/scripts/release/process.ts index f73d8d53b8..3147be4d4a 100644 --- a/scripts/release/process.ts +++ b/scripts/release/process.ts @@ -3,7 +3,7 @@ * `pnpm`, `npm`, and `tar`, and each needs one of three failure behaviours. */ -import { spawnSync } from 'node:child_process' +import { spawn, spawnSync } from 'node:child_process' import { realpathSync } from 'node:fs' import { fileURLToPath } from 'node:url' @@ -83,6 +83,26 @@ export function run(command: string, args: readonly string[], options: RunOption if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${String(result.status)}`) } +/** + * Run a command with inherited streams without blocking the event loop, so a + * caller can hold several commands in flight, and fail on a non-zero exit. + * Concurrent children interleave their output at line granularity. + * @param command - executable name. + * @param args - command arguments. + * @param options - working directory and environment. + * @returns Resolves when the command exits with status zero. + */ +export function runConcurrent(command: string, args: readonly string[], options: RunOptions = {}): Promise { + return new Promise((resolveRun, rejectRun) => { + const child = spawn(command, [...args], { cwd: options.cwd, env: options.env, stdio: 'inherit' }) + child.once('error', rejectRun) + child.once('close', (status, signal) => { + if (status === 0) resolveRun() + else rejectRun(new Error(`${command} ${args.join(' ')} exited with ${String(status ?? signal)}`)) + }) + }) +} + /** * Return whether Node started the given module as the process entry point. * @param moduleUrl - the caller's `import.meta.url`. From b342b09401c27ce6f4dc8036b333d834aeab609a Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:23:41 +0800 Subject: [PATCH 12/13] chore(release): drop the unused synchronous runner --- scripts/release/process.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/scripts/release/process.ts b/scripts/release/process.ts index 3147be4d4a..c85ca2c719 100644 --- a/scripts/release/process.ts +++ b/scripts/release/process.ts @@ -70,19 +70,6 @@ export function capture(command: string, args: readonly string[], options: RunOp return result.stdout.trim() } -/** - * Run a command with inherited streams, so its progress reaches the log, and - * fail on a non-zero exit. - * @param command - executable name. - * @param args - command arguments. - * @param options - working directory and environment. - */ -export function run(command: string, args: readonly string[], options: RunOptions = {}): void { - const result = spawnSync(command, [...args], { cwd: options.cwd, env: options.env, stdio: 'inherit' }) - if (result.error !== undefined) throw result.error - if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${String(result.status)}`) -} - /** * Run a command with inherited streams without blocking the event loop, so a * caller can hold several commands in flight, and fail on a non-zero exit. From 2a1a2605dc62e3c11661a8e5df7aa7b7fb190e77 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:28:44 +0800 Subject: [PATCH 13/13] test(workflow-worker-thread): budget startup waits for the contended Windows pool --- .../tests/workflow-worker-thread.spec.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts b/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts index 6b819aa367..acd2702dab 100644 --- a/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts +++ b/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts @@ -23,11 +23,13 @@ function fakeParent(): Agent { vi.setConfig({ testTimeout: 30_000 }) /** - * Wait up to 10 seconds for CPU-bound worker startup or cross-thread delivery on contended CI. - * Host reactions after an observed event use explicit tight overrides, so this generous startup + * Wait up to 60 seconds for CPU-bound worker startup or cross-thread delivery on contended CI: + * startup is the only environment-sensitive phase of a same-process worker exchange, and the + * loaded self-hosted Windows pool stretches the tsx-in-worker boot past 10 seconds. Host + * reactions after an observed event use explicit tight overrides, so this generous startup * allowance cannot hide multi-second reap regressions. */ -function waitFor(assertion: () => void, timeout = 10_000): Promise { +function waitFor(assertion: () => void, timeout = 60_000): Promise { return vi.waitFor(assertion, { timeout, interval: 50 }) } @@ -181,7 +183,9 @@ async function run(ctx: Context, parent: Agent, source: { script: string; meta: } } -describe('dsh-workflow-worker-thread', () => { +// The per-test cap leaves room for one generous startup wait plus the tight +// post-event assertions; explicit narrower timeouts inside stay authoritative. +describe('dsh-workflow-worker-thread', { timeout: 120_000 }, () => { describe('script execution over a real worker thread', () => { it('runs a script end-to-end: agent() text results, phases, log, args, return value, events', async () => { const { ctx, parent, provider } = await setup({ reply: (_request, index) => text(`answer-${index}`) })