Merge remote-tracking branch 'origin/master' into worktree/3116-docs-mpa-idempotence

This commit is contained in:
Yichen Jiang
2026-08-26 13:10:09 +08:00
215 changed files with 1080 additions and 1080 deletions
@@ -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
@@ -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.
@@ -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,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
@@ -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
@@ -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 仍能通过类型检查器。
**缺口 1bash 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、ACPAgent Client Protocol)的会话记录,以及持久化协调器。在集合键处丢弃 brand,会让既有 brand 在查找时毫无价值;它们的价值只实现了一部分。
**缺口 2*已经 brand* 的 ID 在边界处被侵蚀。** 就连 `ToolCallId` 和 `SessionId` 也恰好在最容易混淆的地方退化为裸 `string`:注册表/store 键类型和公开方法参数。代表性位置包括会话存储、agent 注册表(二者都以共享的 `SessionId` 为键)、工具展示层的 call-id map、ACPAgent 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,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
@@ -35,7 +35,7 @@ interface SpillStore {
interface SpillSource {
toolName: string
callId: CallId
callId: ToolCallId
label: string
}
@@ -35,7 +35,7 @@ interface SpillStore {
interface SpillSource {
toolName: string
callId: CallId
callId: ToolCallId
label: string
}
@@ -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
@@ -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
@@ -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,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
@@ -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
}
```
@@ -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
}
```
+2 -2
View File
@@ -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,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' } }
@@ -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,
@@ -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' },
@@ -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
View File
@@ -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
View File
@@ -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,
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,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
View File
@@ -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,
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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' }],
}],
@@ -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,
}),
@@ -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}'],
},
@@ -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,
}),
@@ -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 = [
@@ -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. */
@@ -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>)
@@ -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
@@ -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
View File
@@ -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,
@@ -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: {
@@ -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}` } }
@@ -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. */
@@ -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),
)
@@ -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,
}),
@@ -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,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,
}),
@@ -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,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: {},
})
@@ -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)
@@ -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'))
}
})
})
@@ -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'),
@@ -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
View File
@@ -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' },
@@ -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
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,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
View File
@@ -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', {
+14 -14
View File
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { createScope, scopeTarget } from '@deepseek-ai/dsh-scope'
import { createUserMessage, CallId, createMessage, createToolResultMessage, freezeMessage } from '@deepseek-ai/dsh-llm'
import { createUserMessage, ToolCallId, createMessage, createToolResultMessage, freezeMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId, TOOL_NOT_STARTED } from '@deepseek-ai/dsh-session'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import InvariantRegistry, { InvariantError } from '@deepseek-ai/dsh-invariants'
@@ -46,18 +46,18 @@ describe('session-log invariants', () => {
step: 1,
message: createMessage({
role: 'assistant',
content: [{ type: 'tool-call', id: CallId('c1'), name: 'echo', arguments: '{}' }],
content: [{ type: 'tool-call', id: ToolCallId('c1'), name: 'echo', arguments: '{}' }],
source: {
kind: 'model',
...{ provider: 'mock', model: 'mock' },
},
}),
}, { surfaceOp: 'append' })
session.append('tool/call', { turn: 1, step: 1, callId: CallId('c1'), name: 'echo', arguments: '{}' })
session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c1'), name: 'echo', arguments: '{}' })
session.append('tool/result', {
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('c1'),
callId: ToolCallId('c1'),
content: [],
isError: false,
}),
@@ -222,7 +222,7 @@ describe('session-log invariants', () => {
turn: 1,
step: 1,
message: createToolResultMessage({
callId: CallId('ghost'),
callId: ToolCallId('ghost'),
content: [],
isError: false,
}),
@@ -237,7 +237,7 @@ describe('session-log invariants', () => {
turn: 1,
step: 1,
message: createToolResultMessage({
callId: CallId('closed'),
callId: ToolCallId('closed'),
content: [],
isError: false,
}),
@@ -252,7 +252,7 @@ describe('session-log invariants', () => {
session.append('tool/call', {
turn: 1,
step: 1,
callId: CallId('rewrite'),
callId: ToolCallId('rewrite'),
name: 'echo',
arguments: '{}',
})
@@ -260,7 +260,7 @@ describe('session-log invariants', () => {
turn: 1,
step: 1,
message: createToolResultMessage({
callId: CallId('rewrite'),
callId: ToolCallId('rewrite'),
content: [{ type: 'text', text: 'original' }],
isError: false,
}),
@@ -292,7 +292,7 @@ describe('session-log invariants', () => {
session.append('tool/call', {
turn: 1,
step: 1,
callId: CallId('rewrite'),
callId: ToolCallId('rewrite'),
name: 'echo',
arguments: '{}',
})
@@ -300,7 +300,7 @@ describe('session-log invariants', () => {
turn: 1,
step: 1,
message: createToolResultMessage({
callId: CallId('rewrite'),
callId: ToolCallId('rewrite'),
content: [{ type: 'text', text: 'original' }],
isError: false,
}),
@@ -332,7 +332,7 @@ describe('session-log invariants', () => {
turn: 1,
step: 1,
message: createToolResultMessage({
callId: CallId('crashed'),
callId: ToolCallId('crashed'),
content: [],
isError: true,
}),
@@ -346,7 +346,7 @@ describe('session-log invariants', () => {
expect(() => {
unresolved.append('turn/start', { turn: 1 })
unresolved.append('step/start', { turn: 1, step: 1 })
unresolved.append('tool/call', { turn: 1, step: 1, callId: CallId('c1'), name: 'echo', arguments: '{}' })
unresolved.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c1'), name: 'echo', arguments: '{}' })
unresolved.append('step/end', { turn: 1, step: 1 })
unresolved.append('turn/end', { turn: 1, reason: { kind: 'error', error: { message: 'boom', code: 'UNKNOWN' } } })
}).not.toThrow()
@@ -357,14 +357,14 @@ describe('session-log invariants', () => {
const session = ctx.sessions.create()
session.append('turn/start', { turn: 1 })
session.append('step/start', { turn: 1, step: 1 })
session.append('tool/call', { turn: 1, step: 1, callId: CallId('c1'), name: 'echo', arguments: '{}' })
session.append('tool/call', { turn: 1, step: 1, callId: ToolCallId('c1'), name: 'echo', arguments: '{}' })
session.append('step/end', { turn: 1, step: 1 })
session.append('step/start', { turn: 1, step: 2 })
expect(() => session.append('tool/result', {
turn: 1,
step: 2,
message: createToolResultMessage({
callId: CallId('c1'),
callId: ToolCallId('c1'),
content: [],
isError: false,
}),
@@ -9,7 +9,7 @@
import { describe, expect, it } from 'vitest'
import fc from 'fast-check'
import { createUserMessage, CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import { createUserMessage, ToolCallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEventMap, SessionEventType, SurfaceIntent } from '@deepseek-ai/dsh-session'
@@ -61,7 +61,7 @@ const messageEventArb: fc.Arbitrary<Appendable> = fc.oneof(
.map((r): Appendable => ({ type: 'tool/result', data: {
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId(r.id),
callId: ToolCallId(r.id),
content: r.content,
isError: r.isError,
}),
+15 -15
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import { ToolCallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '../src/index.ts'
import type { SessionEvent, SurfaceEvent } from '../src/index.ts'
@@ -57,7 +57,7 @@ describe('interruptedTurnClosers', () => {
role: 'assistant',
content: [
{ type: 'text', text: 'calling a tool' },
{ type: 'tool-call', id: CallId('call-1'), name: 'bash', arguments: '{}' },
{ type: 'tool-call', id: ToolCallId('call-1'), name: 'bash', arguments: '{}' },
],
source: {
kind: 'model',
@@ -75,7 +75,7 @@ describe('interruptedTurnClosers', () => {
turn: 2,
step: 1,
message: {
source: { callId: CallId('call-1') },
source: { callId: ToolCallId('call-1') },
content: [{ isError: true }],
},
error: { code: TOOL_NOT_STARTED },
@@ -94,7 +94,7 @@ describe('interruptedTurnClosers', () => {
message: createMessage({
role: 'assistant',
content: [
{ type: 'tool-call', id: CallId('call-1'), name: 'bash', arguments: '{}' },
{ type: 'tool-call', id: ToolCallId('call-1'), name: 'bash', arguments: '{}' },
],
source: {
kind: 'model',
@@ -105,7 +105,7 @@ describe('interruptedTurnClosers', () => {
{ type: 'tool/result', seq: 3, time: 3, data: {
turn: 2, step: 1,
message: createToolResultMessage({
callId: CallId('call-1'),
callId: ToolCallId('call-1'),
content: [{ type: 'text', text: 'ok' }],
isError: false,
}),
@@ -125,7 +125,7 @@ describe('interruptedTurnClosers', () => {
message: createMessage({
role: 'assistant',
content: [
{ type: 'tool-call', id: CallId('call-1'), name: 'bash', arguments: '{}' },
{ type: 'tool-call', id: ToolCallId('call-1'), name: 'bash', arguments: '{}' },
],
source: {
kind: 'model',
@@ -152,7 +152,7 @@ describe('interruptedTurnClosers', () => {
message: createMessage({
role: 'assistant',
content: [
{ type: 'tool-call', id: CallId('old-call'), name: 'bash', arguments: '{}' },
{ type: 'tool-call', id: ToolCallId('old-call'), name: 'bash', arguments: '{}' },
],
source: {
kind: 'model',
@@ -163,7 +163,7 @@ describe('interruptedTurnClosers', () => {
{ type: 'tool/result', seq: 3, time: 3, data: {
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('old-call'),
callId: ToolCallId('old-call'),
content: [],
isError: false,
}),
@@ -177,7 +177,7 @@ describe('interruptedTurnClosers', () => {
message: createMessage({
role: 'assistant',
content: [
{ type: 'tool-call', id: CallId('new-call'), name: 'bash', arguments: '{}' },
{ type: 'tool-call', id: ToolCallId('new-call'), name: 'bash', arguments: '{}' },
],
source: {
kind: 'model',
@@ -201,8 +201,8 @@ describe('interruptedTurnClosers', () => {
message: createMessage({
role: 'assistant',
content: [
{ type: 'tool-call', id: CallId('call-a'), name: 'bash', arguments: '{}' },
{ type: 'tool-call', id: CallId('call-b'), name: 'bash', arguments: '{}' },
{ type: 'tool-call', id: ToolCallId('call-a'), name: 'bash', arguments: '{}' },
{ type: 'tool-call', id: ToolCallId('call-b'), name: 'bash', arguments: '{}' },
],
source: {
kind: 'model',
@@ -214,7 +214,7 @@ describe('interruptedTurnClosers', () => {
{ type: 'tool/result', seq: 3, time: 3, data: {
turn: 1, step: 1,
message: createToolResultMessage({
callId: CallId('call-a'),
callId: ToolCallId('call-a'),
content: [],
isError: false,
}),
@@ -235,7 +235,7 @@ describe('interruptedTurnClosers', () => {
message: createMessage({
role: 'assistant',
content: [
{ type: 'tool-call', id: CallId('call-1'), name: 'bash', arguments: '{}' },
{ type: 'tool-call', id: ToolCallId('call-1'), name: 'bash', arguments: '{}' },
],
source: {
kind: 'model',
@@ -243,7 +243,7 @@ describe('interruptedTurnClosers', () => {
},
}),
} },
{ type: 'tool/call', seq: 3, time: 3, data: { turn: 1, step: 1, callId: CallId('call-1'), name: 'bash', arguments: '{}' } },
{ type: 'tool/call', seq: 3, time: 3, data: { turn: 1, step: 1, callId: ToolCallId('call-1'), name: 'bash', arguments: '{}' } },
]
const closers = interruptedTurnClosers(events)
expect(closers.map(e => e.type)).toEqual(['tool/result', 'step/end', 'turn/end'])
@@ -266,7 +266,7 @@ describe('interruptedTurnClosers', () => {
const events: SessionEvent[] = [
userTurnStart(1, 0),
{ type: 'step/start', seq: 1, time: 1, data: { turn: 1, step: 1 } },
{ type: 'tool/call', seq: 2, time: 2, data: { turn: 1, step: 1, callId: CallId('orphan'), name: 'bash', arguments: '{}' } },
{ type: 'tool/call', seq: 2, time: 2, data: { turn: 1, step: 1, callId: ToolCallId('orphan'), name: 'bash', arguments: '{}' } },
]
const closers = interruptedTurnClosers(events)
// No pending calls → no synthetic tool/result, just step/end + turn/end.

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