Bladeren bron

Merge remote-tracking branch 'origin/master' into worktree/fix-question-drafts-session-switch

Yichen Jiang 2 weken geleden
bovenliggende
commit
2cb1a323b7
100 gewijzigde bestanden met toevoegingen van 411 en 388 verwijderingen
  1. 2 2
      .agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml
  2. 1 1
      .agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md
  3. 1 1
      .agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml
  5. 4 4
      .agents/notes/implemented/architecture/2026-06-20-branded-ids.md
  6. 4 4
      .agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md
  7. 2 2
      .agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.i18n.yaml
  8. 1 1
      .agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md
  9. 1 1
      .agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md
  10. 2 2
      .agents/notes/implemented/feature/2026-07-06-approval-seam.i18n.yaml
  11. 1 1
      .agents/notes/implemented/feature/2026-07-06-approval-seam.md
  12. 1 1
      .agents/notes/implemented/feature/2026-07-06-approval-seam.zh.md
  13. 2 2
      .agents/notes/proposed/feature/2026-08-04-task-surface.i18n.yaml
  14. 3 3
      .agents/notes/proposed/feature/2026-08-04-task-surface.md
  15. 3 3
      .agents/notes/proposed/feature/2026-08-04-task-surface.zh.md
  16. 21 2
      .github/workflows/ci.yml
  17. 3 1
      .github/workflows/release-vendor.yml
  18. 4 2
      .github/workflows/release.yml
  19. 2 2
      apps/cli/tests/fixtures/dsh-badge/snapshot.ts
  20. 2 2
      apps/cli/tests/profiles/acp/tests/fixtures/control-surface/control-surface-llm.ts
  21. 2 2
      apps/cli/tests/profiles/headless/tests/code-mode.e2e.ts
  22. 2 2
      apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs
  23. 3 3
      apps/cli/tests/profiles/headless/tests/semantic-checkpoint.expected.e2e.ts
  24. 2 2
      apps/cli/tests/web-agent-presets.e2e.ts
  25. 2 2
      apps/web/tests/background-job-list.e2e.ts
  26. 3 3
      apps/web/tests/chat-continuous-conversation.e2e.ts
  27. 2 2
      apps/web/tests/chat-scroll-contract.e2e.ts
  28. 2 2
      apps/web/tests/chat-scroll-fixture.ts
  29. 3 3
      apps/web/tests/complex-history.perf.ts
  30. 4 4
      apps/web/tests/minimal-preset.snapshot.ts
  31. 2 2
      apps/web/tests/produced-file-mentions.e2e.ts
  32. 2 2
      apps/web/tests/produced-files.e2e.ts
  33. 2 2
      apps/web/tests/replay-round-trip.e2e.ts
  34. 4 4
      apps/web/tests/schedule-after.e2e.ts
  35. 4 4
      apps/web/tests/shipped-composition.e2e.ts
  36. 2 2
      docs/cordis-tutorial/07-into-the-harness.i18n.yaml
  37. 3 3
      docs/cordis-tutorial/07-into-the-harness.md
  38. 3 3
      docs/cordis-tutorial/07-into-the-harness.zh.md
  39. 2 2
      docs/persistence-catalog.i18n.yaml
  40. 4 4
      docs/persistence-catalog.md
  41. 4 4
      docs/persistence-catalog.zh.md
  42. 2 2
      docs/subsystems/approval.i18n.yaml
  43. 1 1
      docs/subsystems/approval.md
  44. 1 1
      docs/subsystems/approval.zh.md
  45. 2 2
      docs/subsystems/compaction.i18n.yaml
  46. 1 1
      docs/subsystems/compaction.md
  47. 1 1
      docs/subsystems/compaction.zh.md
  48. 2 2
      docs/subsystems/core.i18n.yaml
  49. 2 2
      docs/subsystems/core.md
  50. 2 2
      docs/subsystems/core.zh.md
  51. 2 2
      docs/subsystems/llm-streaming.i18n.yaml
  52. 2 2
      docs/subsystems/llm-streaming.md
  53. 2 2
      docs/subsystems/llm-streaming.zh.md
  54. 2 2
      docs/subsystems/session.i18n.yaml
  55. 1 1
      docs/subsystems/session.md
  56. 1 1
      docs/subsystems/session.zh.md
  57. 2 2
      docs/subsystems/spill.i18n.yaml
  58. 1 1
      docs/subsystems/spill.md
  59. 1 1
      docs/subsystems/spill.zh.md
  60. 2 2
      docs/subsystems/tools.i18n.yaml
  61. 4 4
      docs/subsystems/tools.md
  62. 4 4
      docs/subsystems/tools.zh.md
  63. 2 2
      docs/user/develop/practice/llm-adapter.i18n.yaml
  64. 3 3
      docs/user/develop/practice/llm-adapter.md
  65. 3 3
      docs/user/develop/practice/llm-adapter.zh.md
  66. 4 4
      packages/acp/acp/tests/approval.spec.ts
  67. 3 3
      packages/acp/acp/tests/bridge.spec.ts
  68. 3 3
      packages/acp/acp/tests/edges.spec.ts
  69. 5 5
      packages/acp/acp/tests/updates.spec.ts
  70. 2 2
      packages/api/session-controller/tests/event-script.client.ts
  71. 2 2
      packages/api/session-controller/tests/history-records.client.spec.ts
  72. 10 10
      packages/api/session-controller/tests/session-history-journal.host.spec.ts
  73. 2 2
      packages/client/connection/src/client/fixture.ts
  74. 4 4
      packages/client/ui-approval/src/client/contract/slots.ts
  75. 3 3
      packages/client/ui-approval/tests/ui-approval.client.spec.tsx
  76. 3 3
      packages/client/ui-chat/src/client/contract/slots.ts
  77. 2 2
      packages/client/ui-chat/src/client/contract/store.ts
  78. 1 1
      packages/client/ui-chat/src/client/index.ts
  79. 8 8
      packages/compaction/compaction-basic/tests/compaction-basic.spec.ts
  80. 2 2
      packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts
  81. 2 2
      packages/compaction/compaction-tool-result-pruner/src/types.ts
  82. 6 6
      packages/compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts
  83. 16 16
      packages/compaction/compaction/tests/tool-pairing.spec.ts
  84. 92 92
      packages/context/agent-instructions/tests/agent-instructions.spec.ts
  85. 2 2
      packages/context/session-reference/tests/session-reference.spec.ts
  86. 2 2
      packages/context/time-context/tests/time-context.spec.ts
  87. 2 2
      packages/core/agent-loop/tests/agent-initiator.spec.ts
  88. 3 3
      packages/core/agent-loop/tests/cancel.spec.ts
  89. 10 10
      packages/core/agent-loop/tests/contract-regressions.spec.ts
  90. 3 3
      packages/core/agent-loop/tests/coverage-edges.spec.ts
  91. 3 3
      packages/core/agent-loop/tests/interception.spec.ts
  92. 4 4
      packages/core/agent-loop/tests/loop.spec.ts
  93. 2 2
      packages/core/agent-loop/tests/mock-adapter.ts
  94. 27 27
      packages/core/agent-loop/tests/tool-calls.spec.ts
  95. 3 3
      packages/core/session/src/chunk-rows.ts
  96. 3 3
      packages/core/session/src/invariant.ts
  97. 2 2
      packages/core/session/src/repair.ts
  98. 2 2
      packages/core/session/src/types.ts
  99. 6 6
      packages/core/session/tests/chunk-rows.spec.ts
  100. 2 2
      packages/core/session/tests/fork.spec.ts

+ 2 - 2
.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

+ 1 - 1
.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.

+ 1 - 1
.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`)——零运行时开销的名义类型。

+ 2 - 2
.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: dda97bbf546ef99083cbe3bd2c7da39070407e04
-2026-06-20-branded-ids.zh.md: 82b79dff9d5018e2ea9f9969148eb25225f6d727
+2026-06-20-branded-ids.md: 6443608c76fe42be74a2b8fe8a27669b09951a49
+2026-06-20-branded-ids.zh.md: f13d999aadf4dba7f2c7d31bb2739deae4a0991f

+ 4 - 4
.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<B> = 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.
+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<B> = 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.
 
 **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<SessionId, Session>`, `Map<SessionId, Agent>`, `get(id: SessionId)`, `Map<CallId, …>`, ACP's `SessionId` surface, and the coordinator's `Map<SessionId, …>`. 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<SessionId, Session>`, `Map<SessionId, Agent>`, `get(id: SessionId)`, `Map<ToolCallId, …>`, ACP's `SessionId` surface, and the coordinator's `Map<SessionId, …>`. 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
 

+ 4 - 4
.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md

@@ -6,13 +6,13 @@ Status: implemented
 
 ## 问题
 
-harness 使用 `Branded<B> = 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<B> = 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<SessionId, Session>`、`Map<SessionId, Agent>`、`get(id: SessionId)`、`Map<CallId, …>`、ACP 的 `SessionId` surface、协调器的 `Map<SessionId, …>`。这是变更中机械量最大的部分,也是让*既有* brand 在查找处真正发挥作用(而不仅仅标注在结构体字段上)的关键。
+- **阻止 brand 侵蚀。** 将既有 brand 传播到缺口 2 列出的 `Map` 键类型和公开方法参数中:`Map<SessionId, Session>`、`Map<SessionId, Agent>`、`get(id: SessionId)`、`Map<ToolCallId, …>`、ACP 的 `SessionId` surface、协调器的 `Map<SessionId, …>`。这是变更中机械量最大的部分,也是让*既有* 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。
 
 ## 后果
 

+ 2 - 2
.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: e14607e388c634c4e2679c993c1b720be0a3a9f3
-2026-07-08-tool-output-spill-files.zh.md: 372c9c6cadf3cd64c3de97a8c305b8909f03caab
+2026-07-08-tool-output-spill-files.md: 915e22f1245adb6f7cfc7d358e9d5802531bab63
+2026-07-08-tool-output-spill-files.zh.md: 8d08b05483a302f4188506531da6f507931bea9c

+ 1 - 1
.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
 }
 

+ 1 - 1
.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
 }
 

+ 2 - 2
.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: cbb1b1cb1fef0f6ed1db8e3e0eb1ea01a5834f53
-2026-07-06-approval-seam.zh.md: 29e23f81827200cbbbe0d6a78216ff1f1d4b52ae
+2026-07-06-approval-seam.md: 6e94ed945b4c568bd3c82c8e590736d8d1c27606
+2026-07-06-approval-seam.zh.md: 26447522ecf1d93a09944c0d2eb9574f1e0f9cb2

+ 1 - 1
.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
 

+ 1 - 1
.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 路由
 

+ 2 - 2
.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: 03f79dbc7d40957603885c97883a2de1bb5405d7
-2026-08-04-task-surface.zh.md: 63bb286bd8095f6eb0d3b0d366e62a838ae8bdc9
+2026-08-04-task-surface.md: dbf73976b606a4a45202c1b29b79f5cfbd77ac1c
+2026-08-04-task-surface.zh.md: ecf8764b7b1da7a4a874dd78c6f69df80fb36f36

+ 3 - 3
.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<string, JsonValue>
 }
@@ -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
 }
 ```
 

+ 3 - 3
.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<string, JsonValue>
 }
@@ -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
 }
 ```
 

+ 21 - 2
.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'
@@ -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
@@ -445,7 +452,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:
@@ -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

+ 3 - 1
.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

+ 4 - 2
.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

+ 2 - 2
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,

+ 2 - 2
apps/cli/tests/profiles/acp/tests/fixtures/control-surface/control-surface-llm.ts

@@ -2,7 +2,7 @@
 
 import type { Context } from '@deepseek-ai/cordis'
 import {
-  CallId,
+  ToolCallId,
   LlmAdapter,
   ReasoningEffortId,
   type GenerateOptions,
@@ -50,7 +50,7 @@ class ControlSurfaceAdapter extends LlmAdapter {
       .join('')
     const hasToolResult = current.some(message => message.content.some(block => block.type === 'tool-result'))
     if (!hasToolResult) {
-      const callId = CallId(userText.includes('cancel') ? 'control-cancel-add' : 'control-add')
+      const callId = ToolCallId(userText.includes('cancel') ? 'control-cancel-add' : 'control-add')
       yield { type: 'block-start', index: 0, blockType: 'reasoning' }
       yield { type: 'reasoning-delta', index: 0, text: 'checking the attached tool' }
       yield { type: 'block-end', index: 0, block: { type: 'reasoning', text: 'checking the attached tool' } }

+ 2 - 2
apps/cli/tests/profiles/headless/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<ToolExecutionResult> {
   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,

+ 2 - 2
apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs

@@ -1,6 +1,6 @@
 /** Deterministic keyless Agent Teams adapter shared by profile snapshot and CLI e2e. */
 
-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' },

+ 3 - 3
apps/cli/tests/profiles/headless/tests/semantic-checkpoint.expected.e2e.ts

@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'
 import { Context } from '@deepseek-ai/cordis'
 import { normalizeSessionSnapshot, type NormalizeContext } from '@deepseek-ai/dsh-session-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<string
         step: 1,
         message: createMessage({
           role: 'assistant',
-          content: [{ type: 'tool-call', id: CallId('unknown-outcome-call'), name: 'write_remote', arguments: '{"value":1}' }],
+          content: [{ type: 'tool-call', id: ToolCallId('unknown-outcome-call'), name: 'write_remote', arguments: '{"value":1}' }],
           source: {
             kind: 'model',
             ...{ provider: 'deepseek-official', model: 'deepseek-v4-flash' },
@@ -62,7 +62,7 @@ async function seedInterruptedSession(root: string, cwd: string): Promise<string
       data: {
         turn: 1,
         step: 1,
-        callId: CallId('unknown-outcome-call'),
+        callId: ToolCallId('unknown-outcome-call'),
         name: 'write_remote',
         arguments: '{"value":1}',
       },

+ 2 - 2
apps/cli/tests/web-agent-presets.e2e.ts

@@ -14,7 +14,7 @@ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
 import { SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-tool-subagent/model-selection-settings'
 import { SETTINGS_NAMESPACE, SHIPPED_PRESET_ROOT } 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'
@@ -429,7 +429,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,

+ 2 - 2
apps/web/tests/background-job-list.e2e.ts

@@ -7,7 +7,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 {
@@ -85,7 +85,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,

+ 3 - 3
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<typeof CallId>
+  readonly callId?: ReturnType<typeof ToolCallId>
   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}`,
   }
 }

+ 2 - 2
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'
@@ -35,7 +35,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'

+ 2 - 2
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,

+ 3 - 3
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)}`,

+ 4 - 4
apps/web/tests/minimal-preset.snapshot.ts

@@ -5,7 +5,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 } 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'
@@ -85,14 +85,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,
@@ -101,7 +101,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,

+ 2 - 2
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', {

+ 2 - 2
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', {

+ 2 - 2
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,
@@ -99,7 +99,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"',

+ 4 - 4
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,

+ 4 - 4
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'
@@ -207,7 +207,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',
@@ -225,7 +225,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,
@@ -239,7 +239,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,

+ 2 - 2
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: acd13d6d06a86378ee13557ea4e3e172670bf1af
-07-into-the-harness.zh.md: b6a732f6e37217ba317a0187b499dbbdcae949da
+07-into-the-harness.md: 28e6c2009eec736c92987e0da282f04db249e466
+07-into-the-harness.zh.md: 603b9cac19e05c997073ad0747622b29790bc555

+ 3 - 3
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,

+ 3 - 3
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,

+ 2 - 2
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: 593fb073022e139a13f1d642c6f12a94400b0446
-persistence-catalog.zh.md: 59acfa23c81687f23498d598cb0aead3e669ceb2
+persistence-catalog.md: b9839833308afbd0d561bc227a95b236a10238a1
+persistence-catalog.zh.md: 7b5fa938b23ea9112e370133bf7575c8d689806d

+ 4 - 4
docs/persistence-catalog.md

@@ -142,12 +142,12 @@ Source: [`packages/preset/agent-presets/src/session.ts:28`](../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/types.ts:44`](../packages/interaction/user-approval/src/types.ts)
 
@@ -840,10 +840,10 @@ Source: [`packages/todo/tool-todo/src/types.ts:31`](../packages/todo/tool-todo/s
  * 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:268`](../packages/core/session/src/types.ts)
 

+ 4 - 4
docs/persistence-catalog.zh.md

@@ -144,12 +144,12 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
 '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/types.ts:44`](../packages/interaction/user-approval/src/types.ts)
 
@@ -842,10 +842,10 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
  * 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:266`](../packages/core/session/src/types.ts)
 

+ 2 - 2
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: d9f1169b52e427cd37e7bc54fa37da59d48aecce
-approval.zh.md: abc2361db3d84517c7e7497cfb39b4548160c259
+approval.md: 89130232b45da2982d0da40c8fad217b364758b2
+approval.zh.md: e8047cd5a8bf8dfa23b9e80ad97db00d0e25a061

+ 1 - 1
docs/subsystems/approval.md

@@ -70,7 +70,7 @@ interface ApprovalRequest extends ApprovalRequestEvent {
    * 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
   /**

+ 1 - 1
docs/subsystems/approval.zh.md

@@ -70,7 +70,7 @@ interface ApprovalRequest extends ApprovalRequestEvent {
    * 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
   /**

+ 2 - 2
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: 019912531636848404a33d772fec3131cafa0287
-compaction.zh.md: 9acc4d8250d0274be525d826e4391dce1a606392
+compaction.md: 03642b32cc3cb3caf743d908e9c988c1a3c8fa1e
+compaction.zh.md: af37c4824cc4b01515400049525fbd7b91d018f3

+ 1 - 1
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. */

+ 1 - 1
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. */

+ 2 - 2
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: 6490712bc3c90686ba2cbd787a9f21f59b9f9f3d
-core.zh.md: 79a2b5d1e9090fe56ea3807d4cf42d0f87c6a48b
+core.md: dbe12dfc9bfdafe0f59d7e52eefc5695b1c8d063
+core.zh.md: 50fd2e4e76ee72653c02c2ffeca06042191aa9c4

+ 2 - 2
docs/subsystems/core.md

@@ -301,7 +301,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<B>` 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.
 
@@ -312,7 +312,7 @@ Source: [`packages/util/brand/src/index.ts`](../../packages/util/brand/src/index
 type Branded<B extends string> = 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).
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 

+ 2 - 2
docs/subsystems/core.zh.md

@@ -311,7 +311,7 @@ declare module '@deepseek-ai/dsh-llm' {
 
 ### 品牌化 ID
 
-在包之间传递的 ID 都经过**品牌化**——结构上是字符串,但在类型层面不可互换(不能把 `SessionId` 传给需要 `CallId` 的位置)。每种类型通过各自的工厂构造;比较、日志记录和 JSON 行为与普通字符串相同。
+在包之间传递的 ID 都经过**品牌化**——结构上是字符串,但在类型层面不可互换(不能把 `SessionId` 传给需要 `ToolCallId` 的位置)。每种类型通过各自的工厂构造;比较、日志记录和 JSON 行为与普通字符串相同。
 
 `Branded<B>` 原语位于独立的纯类型包 [dsh-brand](../../packages/util/brand) 中(没有运行时代码,也不依赖 harness 包),因此任何包都能品牌化其拥有的 id,而无需依赖无关的能力包。
 
@@ -322,7 +322,7 @@ declare module '@deepseek-ai/dsh-llm' {
 type Branded<B extends string> = 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`。
 
 <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
 

+ 2 - 2
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: 8db1d95d91095d09eeb4a0399e1449688cab52cc
-llm-streaming.zh.md: fd2d483cbbecac9b1015d67e0e14aa67b67ef08e
+llm-streaming.md: 331c30e1039741462df7ae3e7a9e6497281a5df0
+llm-streaming.zh.md: f05c67bc7552a4bc5a9359416d98b62f238f1c65

+ 2 - 2
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.
 
 Image access belongs to request serialization rather than the durable attachment or deterministic request-image version. `resolveImageAttachmentAccess()` combines the attachment provider's optional host object path with a mapping supplied by the consumer for the current tool execution filesystem. The result is available only for that request and does not participate in `variantId`.
 
@@ -205,7 +205,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 }
   | {

+ 2 - 2
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。
 
 图片访问方式属于请求序列化,不属于持久附件或确定性请求图片版本。`resolveImageAttachmentAccess()` 把附件提供方可选的宿主对象路径,与消费方为当前工具执行文件系统提供的映射组合起来。结果只适用于本次请求,不参与 `variantId`。
 
@@ -205,7 +205,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 }
   | {

+ 2 - 2
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: ef24802d7ea567681f935fea86efaab2eb2b80c0
-session.zh.md: bee235657d1c1d1a6009eb23d51b8c88482c0748
+session.md: 87e8b33dec8eda6f7a716d71f5c4afa69a420d35
+session.zh.md: d30b7d24412a04521300c657c1b07daa1620ca5f

+ 1 - 1
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

+ 1 - 1
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

+ 2 - 2
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: 1ea23747ddb42c7b36d71e8125dff059ba985b3c
-spill.zh.md: a10512f0c6ff26be3dc2637cd8c19606ba4fbdc6
+spill.md: 366cacbef06e18e79d593e946536b062d8d83d50
+spill.zh.md: 82e2ad9efe418175642c3523614c2601b17e4450

+ 1 - 1
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
 }

+ 1 - 1
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
 }

+ 2 - 2
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

+ 4 - 4
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 (`<parent>:code:<n>`). */
-  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
 }

+ 4 - 4
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 (`<parent>:code:<n>`). */
-  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
 }

+ 2 - 2
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

+ 3 - 3
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<StreamChunk> {
   // 1. Start each content block with block-start.
@@ -76,7 +76,7 @@ async function* exampleChunks(): AsyncIterable<StreamChunk> {
   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<StreamChunk> {
     index: 1,
     block: {
       type: 'tool-call',
-      id: CallId('call-123'),
+      id: ToolCallId('call-123'),
       name: 'bash',
       arguments: '{"command":"ls"}',
     },

+ 3 - 3
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<StreamChunk> {
   // 1. Start each content block with block-start.
@@ -76,7 +76,7 @@ async function* exampleChunks(): AsyncIterable<StreamChunk> {
   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<StreamChunk> {
     index: 1,
     block: {
       type: 'tool-call',
-      id: CallId('call-123'),
+      id: ToolCallId('call-123'),
       name: 'bash',
       arguments: '{"command":"ls"}',
     },

+ 4 - 4
packages/acp/acp/tests/approval.spec.ts

@@ -1,6 +1,6 @@
 import { afterEach, describe, expect, it } from 'vitest'
 import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
-import { CallId } from '@deepseek-ai/dsh-llm'
+import { ToolCallId } from '@deepseek-ai/dsh-llm'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import { SessionId } from '@deepseek-ai/dsh-session'
 import ApprovalService, { type ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
@@ -22,8 +22,8 @@ describe('ACP machine permission policy', () => {
     const agent = harness.ctx.agents.get(SessionId(sessionId))!
     agent.session.append('turn/start', { turn: 1 })
     agent.session.append('step/start', { turn: 1, step: 1 })
-    agent.session.append('tool/call', { turn: 1, step: 1, callId: CallId('call-9'), name: 'bash', arguments: '{}' })
-    return { agent, toolName: 'bash', callId: CallId('call-9'), ...overrides }
+    agent.session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('call-9'), name: 'bash', arguments: '{}' })
+    return { agent, toolName: 'bash', callId: ToolCallId('call-9'), ...overrides }
   }
 
   it('maps the two advertised one-shot choices', async () => {
@@ -71,7 +71,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)
   })

+ 3 - 3
packages/acp/acp/tests/bridge.spec.ts

@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { AttachmentError } from '@deepseek-ai/dsh-attachment'
-import { CallId, type StreamChunk } from '@deepseek-ai/dsh-llm'
+import { 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'
@@ -14,11 +14,11 @@ import { startHttpMcpFixture } from '../../../mcp/mcp-client/tests/http-fixture.
 function oneToolCall(): StreamChunk[] {
   return [
     { type: 'block-start', index: 0, blockType: 'tool-call' },
-    { type: 'tool-call-delta', index: 0, id: CallId('call-switch'), name: 'switch_model', argumentsDelta: '{}' },
+    { type: 'tool-call-delta', index: 0, id: ToolCallId('call-switch'), name: 'switch_model', argumentsDelta: '{}' },
     {
       type: 'block-end',
       index: 0,
-      block: { type: 'tool-call', id: CallId('call-switch'), name: 'switch_model', arguments: '{}' },
+      block: { type: 'tool-call', id: ToolCallId('call-switch'), name: 'switch_model', arguments: '{}' },
     },
     { type: 'finish', reason: { kind: 'tool-calls' } },
   ]

+ 3 - 3
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'
@@ -11,8 +11,8 @@ function toolCallResponse(): StreamChunk[] {
     { type: 'reasoning-delta', index: 0, text: 'inspect first' },
     { type: 'block-end', index: 0, block: { type: 'reasoning', text: 'inspect first' } },
     { type: 'block-start', index: 1, blockType: 'tool-call' },
-    { type: 'tool-call-delta', index: 1, id: CallId('call-1'), name: 'echo', argumentsDelta: '{}' },
-    { type: 'block-end', index: 1, block: { type: 'tool-call', id: CallId('call-1'), name: 'echo', arguments: '{}' } },
+    { type: 'tool-call-delta', index: 1, id: ToolCallId('call-1'), name: 'echo', argumentsDelta: '{}' },
+    { type: 'block-end', index: 1, block: { type: 'tool-call', id: ToolCallId('call-1'), name: 'echo', arguments: '{}' } },
     { type: 'usage', usage: { inputTokens: 8, outputTokens: 2, reasoningTokens: 1 } },
     { type: 'finish', reason: { kind: 'tool-calls' } },
   ]

+ 5 - 5
packages/acp/acp/tests/updates.spec.ts

@@ -1,6 +1,6 @@
 import { describe, expect, it, vi } from 'vitest'
 import type { Context } from '@deepseek-ai/cordis'
-import { CallId, MessageId } from '@deepseek-ai/dsh-llm'
+import { ToolCallId, MessageId } from '@deepseek-ai/dsh-llm'
 import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
 import { assistantUpdates, toolCallUpdate, toolResultUpdate } from '../src/updates.ts'
 
@@ -33,7 +33,7 @@ describe('standard ACP update projection', () => {
     const session = { requestContext: () => undefined } as unknown as Session
     const event = assistantEvent([
       { type: 'reasoning', text: '' },
-      { type: 'tool-call', id: CallId('call-hidden'), name: 'hidden', arguments: '{}' },
+      { type: 'tool-call', id: ToolCallId('call-hidden'), name: 'hidden', arguments: '{}' },
     ])
 
     await expect(assistantUpdates(ctx, session, event)).resolves.toEqual([])
@@ -59,7 +59,7 @@ describe('standard ACP update projection', () => {
       type: 'tool/call',
       seq: 0,
       time: 0,
-      data: { turn: 1, step: 1, callId: CallId('call-bad'), name: 'broken', arguments: '{' },
+      data: { turn: 1, step: 1, callId: ToolCallId('call-bad'), name: 'broken', arguments: '{' },
     })
     const result = await toolResultUpdate({ get: () => undefined } as unknown as Context, {
       type: 'tool/result',
@@ -71,10 +71,10 @@ describe('standard ACP update projection', () => {
         message: {
           id: MessageId('tool-message'),
           role: 'user',
-          source: { kind: 'tool', callId: CallId('call-bad') },
+          source: { kind: 'tool', callId: ToolCallId('call-bad') },
           content: [{
             type: 'tool-result',
-            toolCallId: CallId('call-bad'),
+            toolCallId: ToolCallId('call-bad'),
             isError: true,
             content: [{ type: 'reasoning', text: 'hidden' }],
           }],

+ 2 - 2
packages/api/session-controller/tests/event-script.client.ts

@@ -1,5 +1,5 @@
 import {
-  CallId, createMessage, createToolResultMessage, createUserMessage,
+  ToolCallId, createMessage, createToolResultMessage, createUserMessage,
 } from '@deepseek-ai/dsh-llm'
 // Minimal SessionEvent builders for orchestration tests (shape mirrors what the
 // host emits; only the fields the object layer reads).
@@ -52,7 +52,7 @@ export const ev = {
         turn,
         step,
         message: createToolResultMessage({
-          callId: CallId(callId),
+          callId: ToolCallId(callId),
           content: text(body),
           isError: false,
         }),

+ 2 - 2
packages/api/session-controller/tests/history-records.client.spec.ts

@@ -1,7 +1,7 @@
 /** Packed history records become one event-shaped Client value per wire record. */
 
 import { describe, expect, it } from 'vitest'
-import { CallId } from '@deepseek-ai/dsh-llm/brand'
+import { ToolCallId } from '@deepseek-ai/dsh-llm/brand'
 import type { SessionHistoryRecord } from '../src/types.ts'
 import {
   historyEntries,
@@ -59,7 +59,7 @@ describe('Session history record projection', () => {
           turn: 2,
           step: 4,
           index: 1,
-          id: CallId('call-1'),
+          id: ToolCallId('call-1'),
           dt: [2, 3],
           args: ['', '{"x":', '1}'],
         },

+ 10 - 10
packages/api/session-controller/tests/session-history-journal.host.spec.ts

@@ -5,7 +5,7 @@ import { Context } from '@deepseek-ai/cordis'
 import AgentRegistry from '@deepseek-ai/dsh-agent'
 import SessionStore from '@deepseek-ai/dsh-session'
 import { decodeStorageRecord, type ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
-import { CallId, createMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
+import { ToolCallId, createMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
 import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
 import { SessionHistoryController } from '@deepseek-ai/dsh-api-session-controller/src/history.ts'
 import type {
@@ -110,12 +110,12 @@ describe('Session history raw journal', () => {
     const stream = await openFollow(history, session.id, abort.signal)
     const collected = collect(stream, 2, abort)
     const call = session.append('tool/call', {
-      turn: 1, step: 1, callId: CallId('raw-call'), name: 'custom', arguments: '{malformed',
+      turn: 1, step: 1, callId: ToolCallId('raw-call'), name: 'custom', arguments: '{malformed',
     })
     const result = session.append('tool/result', {
       turn: 1, step: 1,
       message: createToolResultMessage({
-        callId: CallId('raw-call'),
+        callId: ToolCallId('raw-call'),
         content: [{ type: 'text', text: 'raw output' }],
         isError: false,
       }),
@@ -140,7 +140,7 @@ describe('Session history raw journal', () => {
     const iterator = stream[Symbol.asyncIterator]()
 
     session.append('tool/call', {
-      turn: 1, step: 1, callId: CallId('live-fast'), name: 'term', arguments: '{"cmd":"pwd"}',
+      turn: 1, step: 1, callId: ToolCallId('live-fast'), name: 'term', arguments: '{"cmd":"pwd"}',
     })
     await expect(iterator.next()).resolves.toMatchObject({
       value: { type: 'event', event: { type: 'tool/call', data: { callId: 'live-fast' } } },
@@ -153,7 +153,7 @@ describe('Session history raw journal', () => {
       session.append('tool/result', {
         turn: 1, step: 1,
         message: createToolResultMessage({
-          callId: CallId('live-fast'),
+          callId: ToolCallId('live-fast'),
           content: [{ type: 'text', text: 'ok' }],
           isError: false,
         }),
@@ -175,12 +175,12 @@ describe('Session history raw journal', () => {
     const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } })
     const start = session.append('turn/start', { turn: 1 })
     const call = session.append('tool/call', {
-      turn: 1, step: 1, callId: CallId('history-call'), name: 'custom', arguments: '{broken',
+      turn: 1, step: 1, callId: ToolCallId('history-call'), name: 'custom', arguments: '{broken',
     })
     const result = session.append('tool/result', {
       turn: 1, step: 1,
       message: createToolResultMessage({
-        callId: CallId('history-call'),
+        callId: ToolCallId('history-call'),
         content: [{ type: 'text', text: 'failed raw output' }],
         isError: true,
       }),
@@ -298,7 +298,7 @@ describe('Session history raw journal', () => {
       step: 1,
       chunk: { type: 'reasoning-delta', index: 0, text: `r${String(index)}` },
     }))
-    const callId = CallId('packed-call')
+    const callId = ToolCallId('packed-call')
     const toolCall = [0, 1, 2].map(index => session.append('assistant/chunk', {
       turn: 1,
       step: 1,
@@ -358,7 +358,7 @@ describe('Session history raw journal', () => {
     await expect(iterator.next()).resolves.toMatchObject({
       value: { type: 'event', event: { type: 'turn/start' } },
     })
-    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"}' })
     await expect(iterator.next()).resolves.toMatchObject({
       value: { type: 'event', event: { type: 'tool/call' } },
     })
@@ -373,7 +373,7 @@ describe('Session history raw journal', () => {
       const result = 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,
         }),

+ 2 - 2
packages/client/connection/src/client/fixture.ts

@@ -5,7 +5,7 @@ import {
   createToolResultMessage,
   createUserMessage,
 } from '@deepseek-ai/dsh-llm/message'
-import { CallId, type MessageId } from '@deepseek-ai/dsh-llm/brand'
+import { ToolCallId, type MessageId } from '@deepseek-ai/dsh-llm/brand'
 import type {
   AssistantMessage,
   ContentBlock,
@@ -341,7 +341,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 = [

+ 4 - 4
packages/client/ui-approval/src/client/contract/slots.ts

@@ -1,5 +1,5 @@
 /** Approval composer and optional correlated-detail contracts. */
-import type { CallId } from '@deepseek-ai/dsh-llm'
+import type { ToolCallId } from '@deepseek-ai/dsh-llm'
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
 import type {
   PropsLocale, PropsRenderSlots, PropsRuntime,
@@ -45,7 +45,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
 /** Stable identity handed to an optional approval-detail renderer. */
 export interface ApprovalDetailOwnerProps {
   /** Tool call correlated with the request. */
-  callId: CallId
+  callId: ToolCallId
 }
 
 /** Client-visible fields of an approval request projected through Remote Events. */
@@ -53,7 +53,7 @@ export interface ApprovalPresentationRequest {
   /** Tool requesting the decision. */
   readonly toolName: string
   /** Tool call correlated with the request. */
-  readonly callId?: CallId
+  readonly callId?: ToolCallId
   /** Human-readable reason supplied by the requester. */
   readonly reason?: string
   /** Cancellation projected from the Host waterfall. */
@@ -74,7 +74,7 @@ export class PendingApproval {
   /** Tool requesting the decision. */
   readonly toolName: string
   /** Correlated Tool call, when supplied by the asker. */
-  readonly callId: CallId | undefined
+  readonly callId: ToolCallId | undefined
   /** Human-readable reason supplied by the asker. */
   readonly reason: string | undefined
   /** Result returned by the Remote Event listener to the Host waterfall. */

+ 3 - 3
packages/client/ui-approval/tests/ui-approval.client.spec.tsx

@@ -2,7 +2,7 @@
 import { Context } from '@deepseek-ai/cordis'
 import { createScope, scopeOf } from '@deepseek-ai/dsh-api-session-controller/client'
 import InvariantRegistry from '@deepseek-ai/dsh-invariants'
-import type { CallId } from '@deepseek-ai/dsh-llm'
+import type { ToolCallId } from '@deepseek-ai/dsh-llm'
 import type { SessionId } from '@deepseek-ai/dsh-session/types'
 import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
 import { afterEach, describe, expect, it, vi } from 'vitest'
@@ -122,7 +122,7 @@ describe('PendingApproval', () => {
     const remove = vi.spyOn(controller.signal, 'removeEventListener')
     const pending = new PendingApproval(id('s1'), {
       toolName: 'bash',
-      callId: 'call-1' as CallId,
+      callId: 'call-1' as ToolCallId,
       reason: 'needs access',
       signal: controller.signal,
     })
@@ -343,7 +343,7 @@ describe('ApprovalPanel', () => {
   it('renders correlated detail and returns allow-once', async () => {
     const pending = new PendingApproval(id('s1'), {
       toolName: 'bash',
-      callId: 'call-1' as CallId,
+      callId: 'call-1' as ToolCallId,
       reason: 'Run this exact command',
     })
     const renderSlot = vi.fn(() => <code>pnpm test</code>)

+ 3 - 3
packages/client/ui-chat/src/client/contract/slots.ts

@@ -11,7 +11,7 @@ import type {
 import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives'
 import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
 import type { createChatStore } from '../stores.ts'
-import type { CallId, SelectionTarget } from './store.ts'
+import type { ToolCallId, SelectionTarget } from './store.ts'
 import type { ChatNode, ChatNodeKind } from './chat-nodes.ts'
 import type { ChatSnapshot, CommandNode, CompactionSummaryNode, ToolCallBlock } from './snapshot.ts'
 
@@ -59,10 +59,10 @@ export interface ChatNodeTurnDataInjected {
 
 /** Stable owner currency delivered to a keyed Chat renderer. */
 export interface ChatNodeOwnerProps {
-  selectedCallId?: CallId | undefined
+  selectedCallId?: ToolCallId | undefined
   cwd?: string | undefined
   openFile: (path: string) => void
-  inspectCall: (callId: CallId) => void
+  inspectCall: (callId: ToolCallId) => void
   forkAt: (seq: number) => void
   renderMessageImages: RenderMessageImages
   fileMentions: (owner: TurnTailOwnerProps) => MarkdownFileMentions | undefined

+ 2 - 2
packages/client/ui-chat/src/client/contract/store.ts

@@ -1,13 +1,13 @@
 /** Chat-owned selection state shared by the transcript and details panel. */
 
 /** Tool call identity as carried by Chat nodes. */
-export type CallId = string
+export type ToolCallId = string
 
 /** Selection target for the Chat details linkage channel. */
 export interface SelectionTarget {
   turnSeq: number
   stepSeq?: number
-  callId?: CallId
+  callId?: ToolCallId
   toolName?: string
 }
 

+ 1 - 1
packages/client/ui-chat/src/client/index.ts

@@ -25,7 +25,7 @@ export type {
   FinalAssistantChatData, ManualCompactionChatData, RetryChatData, ToolChatData,
   TurnTailChatData,
 } from './contract/chat-nodes.ts'
-export type { CallId, ChatStoreState, SelectionTarget } from './contract/store.ts'
+export type { ToolCallId, ChatStoreState, SelectionTarget } from './contract/store.ts'
 export type {
   AssistantActionOwnerProps, ChatFileMentions, ChatNodeOwnerProps, ChatNodeTurnDataInjected,
   ChatNodeViewProps, ChatScrollPosition, ChatStore, ChatViewInjected, ChatViewSlotProps,

+ 8 - 8
packages/compaction/compaction-basic/tests/compaction-basic.spec.ts

@@ -13,7 +13,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,
@@ -143,7 +143,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) }],
@@ -191,7 +191,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({
@@ -569,7 +569,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', {
@@ -735,7 +735,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', {
@@ -1184,7 +1184,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,
@@ -1200,7 +1200,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,
@@ -1418,7 +1418,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: {

+ 2 - 2
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}` } }

+ 2 - 2
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. */

+ 6 - 6
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<string, unknown> = {},
 ): 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),
     )

+ 16 - 16
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,
       }),

+ 92 - 92
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)
 

+ 2 - 2
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,
       }),

+ 2 - 2
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' } },
   ]

+ 2 - 2
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: {},
     })

+ 3 - 3
packages/core/agent-loop/tests/cancel.spec.ts

@@ -1,4 +1,4 @@
-import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
+import { ToolCallId, createUserMessage } 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)

+ 10 - 10
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<PostToolDecision> => {
-      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'))
     }
   })
 })

+ 3 - 3
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'),

+ 3 - 3
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 } },
     ]

+ 4 - 4
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, ReasoningEffortId, StreamChunk } from '@deepseek-ai/dsh-llm'
+import LlmRuntime, { createUserMessage, ToolCallId, LlmError, ReasoningEffortId, 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'
@@ -1081,7 +1081,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"}' },
@@ -1136,7 +1136,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"}' },
@@ -1214,7 +1214,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' },

+ 2 - 2
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

+ 27 - 27
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<PreToolDecision> => {
-      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<PostToolDecision> => {
@@ -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<PreToolDecision> => {
-      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' })

+ 3 - 3
packages/core/session/src/chunk-rows.ts

@@ -19,7 +19,7 @@
  * @module @deepseek-ai/dsh-session/chunk-rows
  */
 
-import { CallId } from '@deepseek-ai/dsh-llm/brand'
+import { ToolCallId } from '@deepseek-ai/dsh-llm/brand'
 import type { StreamChunk } from '@deepseek-ai/dsh-llm'
 import type { SessionEvent } from './types.ts'
 
@@ -51,7 +51,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[]
@@ -188,7 +188,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),
       },

+ 3 - 3
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<CallId>
+  pendingCalls: Set<ToolCallId>
 }
 
 /** One accepted event's deferred mutation of a committed session trace. */
@@ -34,7 +34,7 @@ interface SessionTraceTransition {
   scalars: Pick<SessionTrace, 'lastSeq' | 'openTurn' | 'openStep' | 'nextTurn' | 'nextStep'>
   pendingCalls:
     | { kind: 'none' }
-    | { kind: 'add' | 'delete'; callId: CallId }
+    | { kind: 'add' | 'delete'; callId: ToolCallId }
     | { kind: 'clear' }
 }
 

+ 2 - 2
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<CallId, { step: number; callSeq?: number }>()
+  const pendingCalls = new Map<ToolCallId, { step: number; callSeq?: number }>()
   for (const event of events) {
     switch (event.type) {
       case 'turn/start':

+ 2 - 2
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,
@@ -265,7 +265,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

+ 6 - 6
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 { chunkRowLength, isChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
@@ -47,7 +47,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' }
@@ -58,7 +58,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)
@@ -96,7 +96,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')]
@@ -191,13 +191,13 @@ const deltaChunkArb: fc.Arbitrary<StreamChunk> = 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(),
   }),

+ 2 - 2
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', {

Some files were not shown because too many files changed in this diff