From fe72ab42d11dc43738590e71730f6eda18dd20c1 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 22 Aug 2026 02:02:38 +0800 Subject: [PATCH 1/6] feat(deepseek): upload incremental session logs --- ...pseek-llm-api-request-extensions.i18n.yaml | 4 +- ...-21-deepseek-llm-api-request-extensions.md | 36 ++- ...-deepseek-llm-api-request-extensions.zh.md | 38 ++- apps/cli/composition.md | 3 + docs/capability-seams.i18n.yaml | 4 +- docs/capability-seams.md | 4 +- docs/capability-seams.zh.md | 4 +- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 16 ++ docs/config-catalog.zh.md | 16 ++ docs/event-producer-consumer.i18n.yaml | 4 +- docs/event-producer-consumer.md | 4 +- docs/event-producer-consumer.zh.md | 2 +- docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 5 + docs/module-graph.zh.md | 5 + docs/persistence-catalog.i18n.yaml | 4 +- docs/persistence-catalog.md | 18 ++ docs/persistence-catalog.zh.md | 18 ++ docs/subsystems/llm-streaming.i18n.yaml | 4 +- docs/subsystems/llm-streaming.md | 2 +- docs/subsystems/llm-streaming.zh.md | 2 +- examples/acp-agent/composition.md | 3 + examples/acp-agent/cordis.yml | 3 + examples/headless-agent/composition.md | 3 + examples/headless-agent/cordis.yml | 3 + examples/jsonrpc-agent/cordis.yml | 3 + examples/jsonrpc-agent/minimal.cordis.yml | 3 + examples/package.json | 1 + packages/bundle/base/cordis.patch.yml | 3 + packages/bundle/base/package.json | 1 + .../core/session/src/known-event-types.ts | 1 + .../README.i18n.yaml | 4 +- .../llm/deepseek-llm-api-extensions/README.md | 2 +- .../deepseek-llm-api-extensions/README.zh.md | 2 +- packages/llm/llm-deepseek/README.i18n.yaml | 4 +- packages/llm/llm-deepseek/README.md | 2 +- packages/llm/llm-deepseek/README.zh.md | 2 +- packages/llm/llm-deepseek/package.json | 1 + .../llm/llm-deepseek/tests/adapter.e2e.ts | 10 +- .../tests/loader-composition.spec.ts | 54 ++++- packages/llm/llm-pi-ai/tests/adapter.spec.ts | 1 + .../session-log-deepseek/README.i18n.yaml | 6 + .../session/session-log-deepseek/README.md | 49 ++++ .../session/session-log-deepseek/README.zh.md | 49 ++++ .../session/session-log-deepseek/package.json | 54 +++++ .../session/session-log-deepseek/src/codec.ts | 227 ++++++++++++++++++ .../session/session-log-deepseek/src/index.ts | 109 +++++++++ .../session-log-deepseek/src/invariant.ts | 58 +++++ .../session/session-log-deepseek/src/types.ts | 61 +++++ .../session-log-deepseek/tests/codec.spec.ts | 139 +++++++++++ .../tests/invariant.spec.ts | 95 ++++++++ .../session-log-deepseek/tests/upload.spec.ts | 181 ++++++++++++++ .../session-log-deepseek/tsconfig.json | 30 +++ pnpm-lock.yaml | 31 +++ python/sdk-runtime/package.json | 1 + .../runtime/cordis.yml | 3 + scripts/gen-doc-graphs.ts | 2 +- tsconfig.host.json | 1 + 59 files changed, 1345 insertions(+), 58 deletions(-) create mode 100644 packages/session/session-log-deepseek/README.i18n.yaml create mode 100644 packages/session/session-log-deepseek/README.md create mode 100644 packages/session/session-log-deepseek/README.zh.md create mode 100644 packages/session/session-log-deepseek/package.json create mode 100644 packages/session/session-log-deepseek/src/codec.ts create mode 100644 packages/session/session-log-deepseek/src/index.ts create mode 100644 packages/session/session-log-deepseek/src/invariant.ts create mode 100644 packages/session/session-log-deepseek/src/types.ts create mode 100644 packages/session/session-log-deepseek/tests/codec.spec.ts create mode 100644 packages/session/session-log-deepseek/tests/invariant.spec.ts create mode 100644 packages/session/session-log-deepseek/tests/upload.spec.ts create mode 100644 packages/session/session-log-deepseek/tsconfig.json diff --git a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml index a12ce16819..37e85824d7 100644 --- a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md -2026-08-21-deepseek-llm-api-request-extensions.md: d83b53adcfbf41f9addde0b7d4ac9ab1a8572d2f -2026-08-21-deepseek-llm-api-request-extensions.zh.md: 47d7ce574c189f8d71d28963910e948c86169198 +2026-08-21-deepseek-llm-api-request-extensions.md: 9353cfc7dcefff3156d1e0be446c6e0e85e94f73 +2026-08-21-deepseek-llm-api-request-extensions.zh.md: 77959c037b4658672639f15e0a1da69bb5a49a1f diff --git a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md index d83b53adcf..9353cfc7dc 100644 --- a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md +++ b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md @@ -1,4 +1,4 @@ -# Agent Note: DeepSeek LLM API request extensions for plugin package metadata +# Agent Note: DeepSeek LLM API request extensions for session logs and plugin packages Status: implemented @@ -6,17 +6,25 @@ English | [中文](2026-08-21-deepseek-llm-api-request-extensions.zh.md) ## Problem -Provider-side diagnosis needs the exact active plugin package versions that produced an official DeepSeek request. The existing browser-facing plugin inventory reports configured Loader rows and lifecycle phases but owns neither package-manifest resolution nor the requesting agent's standing preset composition. +The canonical Session log contains request boundaries, raw response chunks, assembled messages, tool activity, plugin events, and failure facts that the model message list does not preserve. The OTel session-telemetry path projects and batches that log independently of model requests, uses deployment-selected sharing modes, and intentionally drops most assistant chunks. DeepSeek's official API therefore cannot reconstruct the complete harness trajectory from its ordinary request messages or the telemetry feed. -This metadata belongs only on the official DeepSeek adapter path. Adding it to `GenerateOptions` or the provider-neutral LLM seam would expose a DeepSeek wire concept to pi-ai and every future adapter. +Provider-side diagnosis also needs the exact active plugin package versions that produced a request. The existing browser-facing plugin inventory reports configured Loader rows and lifecycle phases but owns neither package-manifest resolution nor the requesting agent's standing preset composition. -The adapter also needs one plugin-owned extension point. Importing Loader, preset, and package-manifest logic directly into `llm-deepseek` would make the transport own metadata discovery and prevent independent request fields from evolving as plugins. +Both values belong only on the official DeepSeek adapter path. Adding them to `GenerateOptions` or the provider-neutral LLM seam would expose DeepSeek wire concepts to pi-ai and every future adapter. ## Decision -`@deepseek-ai/dsh-deepseek-llm-api-extensions` registers `ctx.deepseekLlmApiExtensions`, an additive registry of top-level fields for `deepseek-official` request bodies. A contributor claims one declaration-merged field with `register()`. The adapter invokes `prepare()` after serializing the exact wire messages, passes the request cancellation signal, rejects preparation or base-field collision before HTTP, merges the detached fields, and calls the captured `accept()` transaction after HTTP 2xx. The registry stops awaiting preparation after cancellation even if a contributor ignores the signal. Acceptance failures remain request failures under `REQUEST_EXTENSION`; transport and non-2xx failures never accept a contribution. A composition without the registry retains the reusable base adapter. +`@deepseek-ai/dsh-deepseek-llm-api-extensions` registers `ctx.deepseekLlmApiExtensions`, an additive registry of top-level fields for `deepseek-official` request bodies. A contributor claims one declaration-merged field with `register()`. The adapter invokes `prepare()` after serializing the exact wire messages, passes the request cancellation signal, rejects preparation or base-field collision before HTTP, merges the detached fields, and calls the captured `accept()` transaction after HTTP 2xx. The registry stops awaiting preparation after cancellation even if a contributor ignores the signal. Acceptance failures remain request failures under `REQUEST_EXTENSION`; transport and non-2xx failures never accept a contribution. A composition without the registry retains the reusable base adapter. Shipped compositions mount the registry and both contributors: package metadata is enabled by default, while Session-log upload is disabled by default and requires `session-log-deepseek.enabled: true`. Keyless `deepseek-official` replay invokes preparation with a synthetic empty base body and the same acceptance transaction before its first recorded chunk, preserving post-2xx extension side effects rather than field bytes. -Shipped compositions mount the registry and the default-on plugin-package contributor. Keyless `deepseek-official` replay invokes preparation with a synthetic empty base body and the same acceptance transaction before its first recorded chunk, preserving post-2xx extension side effects rather than field bytes. The provider-neutral `llm` package and `llm-pi-ai` contain no extension type, service lookup, field merge, or acceptance call. +The provider-neutral `llm` package and `llm-pi-ai` contain no extension type, service lookup, field merge, or acceptance call. + +## Incremental session-log field + +`@deepseek-ai/dsh-session-log-deepseek` owns `dsh_session_log` as an explicit opt-in. When enabled, each request carrying a live Session id sends the contiguous canonical event suffix after the greatest durable `session-log-deepseek/accepted` watermark for that same Session identity. The field includes the immutable Session header and complete event envelopes. A 2xx appends a new watermark for the transmitted `throughSeq`; that event enters the following request's suffix. Forked logs retain parent watermark ids, so a child starts from sequence zero under its own identity. Concurrent acceptances may arrive out of order, and the maximum watermark remains authoritative. A process-local fold scans each Session event once and incrementally consumes later appends; a new Session object or HMR generation rebuilds the fold from durable history. + +The failure direction is at least once. A transport or provider rejection records no watermark. A crash after remote acceptance but before the watermark persists causes replay after resume, never a skipped sequence. Existing session checkpoints persist the event; the upload plugin owns no second store. + +Event strings are raw unless exact request-relative references reduce encoded bytes. A reference identifies one serialized DeepSeek message, a path to one parsed string value, and a half-open UTF-8 byte range. The encoder verifies that each candidate range decodes to the exact matched UTF-16 substring; surrogate-splitting and ill-formed UTF-16 candidates stay raw. The decoder reconstructs every literal and cited byte exactly and rejects invalid paths, ranges, or code-point splits. Fuzzy similarity, normalization, and lossy omission are absent. ## Plugin package field @@ -42,13 +50,17 @@ The process-lifetime manifest-identity cache remains separate because in-process ## Verification -Registry tests pin duplicate ownership, effect-scoped disposal, detached field values, concurrent and abortable preparation, receiver-preserving acceptance, one acceptance settlement, and failure aggregation. Package-inventory tests pin default-on and explicit-off policies, host and standing-preset discovery, conflicting Loader resolution bases, manifest resolution, lifecycle filtering, and exact name/version ordering. The direct adapter mock proves pre-HTTP preparation failure, cancellation, non-2xx non-acceptance, 2xx acceptance before a later stream failure, and field collision. Keyless replay pins post-2xx extension acceptance, real Loader composition inspects the default metadata field, one credentialed real-API request mounts the production contributor, and pi-ai tests retain their unchanged wire requests. +Registry tests pin duplicate ownership, effect-scoped disposal, detached field values, concurrent and abortable preparation, receiver-preserving acceptance, one acceptance settlement, and failure aggregation. Session tests pin the default-off policy, explicit full-first/suffix-later delivery, incremental watermark folding, persisted restart recovery, fork identity fencing, out-of-order acceptance, exact Unicode reconstruction, invalid references, surrogate-safe raw fallback, and late invariant loading. Package-inventory tests pin default-on and explicit-off policies, host and standing-preset discovery, conflicting Loader resolution bases, manifest resolution, lifecycle filtering, and exact name/version ordering. The direct adapter mock proves pre-HTTP preparation failure, cancellation, non-2xx non-acceptance, 2xx acceptance before a later stream failure, and field collision. Keyless replay pins post-2xx extension acceptance, real Loader composition pins default package metadata plus opt-in Session upload, one real-API request mounts both shipped extensions and proves the official endpoint accepts them, and pi-ai tests retain their unchanged wire requests. ## Alternatives considered -**Add generic metadata to `GenerateOptions` or `ctx.llm`.** Rejected because the value and acceptance timing are DeepSeek wire semantics; a provider-neutral request would make every adapter understand or ignore a foreign field. +**Add generic metadata to `GenerateOptions` or `ctx.llm`.** Rejected because the values and acceptance timing are DeepSeek wire semantics; a provider-neutral request would make every adapter understand or ignore foreign fields. -**Hard-wire package discovery into `llm-deepseek`.** Rejected because the adapter would import Loader, preset, and package-manifest logic. The registry keeps transport responsible only for field merge and HTTP acceptance. +**Hard-wire the two producers into `llm-deepseek`.** Rejected because the adapter would import Session, Loader, preset, package-manifest, and cursor logic. The registry keeps transport responsible only for field merge and HTTP acceptance. + +**Use fuzzy message similarity or omit overlapping event data.** Rejected because the receiver could not reconstruct the canonical log. Exact byte references with raw fallback preserve every value. + +**Keep the upload cursor only in memory.** Rejected because a normal process restart would resend the entire Session. A canonical acceptance event makes restart recovery best-effort durable without another storage backend; the remaining crash window produces allowed duplicates. **Inventory every live Cordis fiber.** Rejected because programmatic and in-memory fibers have no authoritative npm package provenance. Loader-backed host and preset entries provide exact resolvable package identity. @@ -58,6 +70,8 @@ Registry tests pin duplicate ownership, effect-scoped disposal, detached field v ## Consequences -Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. The field is model-hidden and adds no prompt tokens or KV-cache changes. Manifest resolution, field collision, acceptance handling, or provider schema rejection fails the model request rather than silently dropping metadata. +Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. An explicit Session-log opt-in also carries the complete newly unaccepted Session suffix. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Encoding, manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata. -Direct calls without a live Agent still carry the host package inventory. The [DeepSeek request-identity decision](../feature/2026-08-11-deepseek-request-user-id-header.md) continues to own user/session headers, which remain outside the body. +The accepted-watermark event becomes part of the canonical log and is itself delivered on a later request. Crash recovery can duplicate a suffix but does not infer acceptance from assistant output or create a second local cursor store. Direct calls without a live Session omit the session field; host package inventory remains available. + +The [DeepSeek request-identity decision](../feature/2026-08-11-deepseek-request-user-id-header.md) continues to own user/session headers, which remain outside the body. The [session-telemetry decision](../feature/2026-07-23-session-telemetry-otel-revival.md) remains current until a separate change removes that seam and backend; this request path does not alter OTel capture or sharing modes. diff --git a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md index 47d7ce574c..77959c037b 100644 --- a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md @@ -1,4 +1,4 @@ -# Agent Note: DeepSeek LLM API 插件包元数据请求扩展 +# Agent Note: DeepSeek LLM API 会话日志与插件包请求扩展 Status: implemented @@ -6,17 +6,25 @@ Status: implemented ## 问题 -提供方侧诊断需要产生一条 DeepSeek 官方请求的确切存活插件包版本。现有面向浏览器的插件清单会报告已配置 Loader 配置项与生命周期阶段,但既不拥有包 manifest(元数据清单)解析,也不拥有请求 Agent 的 standing preset 组合。 +权威会话日志包含请求边界、原始响应分片、组装后消息、工具活动、插件事件与失败事实,模型消息列表无法保留全部内容。OTel 会话遥测路径独立于模型请求投影和批处理该日志,使用部署方选择的共享模式,并刻意丢弃大多数 assistant 分片。因此,DeepSeek 官方 API 无法从普通请求消息或遥测流重建完整 harness 轨迹。 -该元数据只属于 DeepSeek 官方适配器路径。把它加入 `GenerateOptions` 或提供方无关的 LLM seam,会让 pi-ai 与未来每个适配器接触 DeepSeek 协议概念。 +提供方侧诊断还需要产生当前请求的确切存活插件包版本。现有面向浏览器的插件清单会报告已配置 Loader 配置项与生命周期阶段,但既不拥有包 manifest(元数据清单)解析,也不拥有请求 Agent 的 standing preset 组合。 -适配器还需要一个由插件拥有的扩展点。若 `llm-deepseek` 直接导入 Loader、preset 与包 manifest 逻辑,传输层就会拥有元数据发现,并阻止独立请求字段作为插件分别演进。 +两个值都只属于 DeepSeek 官方适配器路径。把它们加入 `GenerateOptions` 或提供方无关的 LLM seam,会让 pi-ai 与未来每个适配器接触 DeepSeek 协议概念。 ## 决策 -`@deepseek-ai/dsh-deepseek-llm-api-extensions` 注册 `ctx.deepseekLlmApiExtensions`,即 `deepseek-official` 请求正文顶层字段的增量注册表。贡献方通过 `register()` 认领一个经声明合并的字段。适配器在序列化确切协议消息后调用 `prepare()`、传入请求取消信号,在 HTTP 前拒绝准备失败或基础字段冲突,合并分离字段,并在 HTTP 2xx 后调用捕获的 `accept()` 事务。即使贡献方忽略信号,注册表也会在取消后停止等待准备。接受失败仍以 `REQUEST_EXTENSION` 使请求失败;传输失败与非 2xx 失败绝不会接受贡献。未挂载注册表的组合会保留可复用基础适配器。 +`@deepseek-ai/dsh-deepseek-llm-api-extensions` 注册 `ctx.deepseekLlmApiExtensions`,即 `deepseek-official` 请求正文顶层字段的增量注册表。贡献方通过 `register()` 认领一个经声明合并的字段。适配器在序列化确切协议消息后调用 `prepare()`、传入请求取消信号,在 HTTP 前拒绝准备失败或基础字段冲突,合并分离字段,并在 HTTP 2xx 后调用捕获的 `accept()` 事务。即使贡献方忽略信号,注册表也会在取消后停止等待准备。接受失败仍以 `REQUEST_EXTENSION` 使请求失败;传输失败与非 2xx 失败绝不会接受贡献。未挂载注册表的组合会保留可复用基础适配器。随附组合会挂载注册表与两个贡献方:插件包元数据默认开启,会话日志上传默认关闭,需要设置 `session-log-deepseek.enabled: true`。无密钥 `deepseek-official` 回放会使用合成的空基础正文执行准备,并在第一个已记录分片前调用同一接受事务;它保持的是 2xx 后扩展副作用,而非字段字节。 -随附组合会挂载注册表与默认开启的插件包贡献方。无密钥 `deepseek-official` 回放会使用合成的空基础正文执行准备,并在第一个已记录分片前调用同一接受事务;它保持的是 2xx 后扩展副作用,而非字段字节。提供方无关的 `llm` 包与 `llm-pi-ai` 不包含任何扩展类型、服务查找、字段合并或接受调用。 +提供方无关的 `llm` 包与 `llm-pi-ai` 不包含任何扩展类型、服务查找、字段合并或接受调用。 + +## 增量会话日志字段 + +`@deepseek-ai/dsh-session-log-deepseek` 以显式选择启用的方式拥有 `dsh_session_log`。启用后,每个携带存活会话 id 的请求都会发送该确切会话身份最大持久 `session-log-deepseek/accepted` 水位之后的连续权威事件后缀。该字段包含不可变会话 header 与完整事件信封。2xx 会为已发送的 `throughSeq` 追加新水位;该事件会进入下一次请求的后缀。Fork 日志会保留父级水位 id,因此子会话会在自己的身份下从序列零开始。并发接受可能乱序到达,最大水位仍保持权威。进程内 fold 会让每条会话事件只被扫描一次,并增量消费后续追加;新的会话对象或 HMR generation 会从持久历史重建该 fold。 + +失败方向为至少一次。传输失败或提供方拒绝不会记录水位。远端接受后、水位持久化前发生崩溃,会在恢复后触发重放,绝不会跳过序列。现有会话检查点会持久化该事件;上传插件不拥有第二份存储。 + +只有在确切的请求相对引用能够减少编码字节时,事件字符串才不使用原始形式。一个引用会标识一条已序列化 DeepSeek 消息、通向一个解析后字符串值的路径,以及半开 UTF-8 字节范围。编码器会验证每个候选范围都能解码为完全相同的已匹配 UTF-16 子串;拆分 surrogate 或非良构 UTF-16 的候选会保持原始形式。解码器会精确重建每个字面值与引用字节,并拒绝无效路径、范围或码点切分。系统不使用模糊相似度、规范化或有损省略。 ## 插件包字段 @@ -42,15 +50,19 @@ Status: implemented ## 验证 -注册表测试固定重复所有权、effect 作用域 dispose(资源释放)、分离字段值、并发且可取消的准备、保留接收者的接受操作、单次接受结算与失败聚合。插件包清单测试固定默认开启与显式关闭策略、宿主与 standing preset 发现、冲突的 Loader 解析基址、manifest 解析、生命周期过滤及确切名称/版本排序。直接适配器 mock 测试证明 HTTP 前准备失败、取消、非 2xx 不接受、2xx 在后续流失败前接受,以及字段冲突。无密钥回放固定 2xx 后扩展接受,真实 Loader 组合检查默认元数据字段,一个带凭据的真实 API 请求会挂载生产贡献方;pi-ai 测试保持其协议请求不变。 +注册表测试固定重复所有权、effect 作用域 dispose(资源释放)、分离字段值、并发且可取消的准备、保留接收者的接受操作、单次接受结算与失败聚合。会话测试固定默认关闭策略、显式启用后的首次完整/后续后缀交付、增量水位 fold、持久化重启恢复、fork 身份围栏、乱序接受、确切 Unicode 重建、无效引用、surrogate 安全原始回退与 invariant 延迟加载。插件包清单测试固定默认开启与显式关闭策略、宿主与 standing preset 发现、冲突的 Loader 解析基址、manifest 解析、生命周期过滤及确切名称/版本排序。直接适配器 mock 测试证明 HTTP 前准备失败、取消、非 2xx 不接受、2xx 在后续流失败前接受,以及字段冲突。无密钥回放会固定 2xx 后扩展接受,真实 Loader 组合会固定默认包元数据与显式启用的会话上传,一个真实 API 请求会挂载两个随附扩展并证明官方端点接受它们;pi-ai 测试保持其协议请求不变。 ## 考虑过的替代方案 -**向 `GenerateOptions` 或 `ctx.llm` 添加通用元数据。** 已否决,因为该值与接受时点属于 DeepSeek 协议语义;提供方无关请求会迫使每个适配器理解或忽略外来字段。 +**向 `GenerateOptions` 或 `ctx.llm` 添加通用元数据。** 已否决,因为这些值与接受时点属于 DeepSeek 协议语义;提供方无关请求会迫使每个适配器理解或忽略外来字段。 -**把包发现硬编码进 `llm-deepseek`。** 已否决,因为适配器将导入 Loader、preset 与包 manifest 逻辑。注册表让传输只负责字段合并与 HTTP 接受。 +**把两个提供方硬编码进 `llm-deepseek`。** 已否决,因为适配器将导入会话、Loader、preset、包 manifest 与游标逻辑。注册表让传输只负责字段合并与 HTTP 接受。 -**清点每个存活 Cordis fiber。** 已否决,因为编程式与内存 fiber 没有权威 npm 包来源。Loader 支撑的宿主与 preset 配置项能提供可精确解析的包身份。 +**使用模糊消息相似度,或省略重叠事件数据。** 已否决,因为接收方无法重建权威日志。带原始回退的确切字节引用会保留每个值。 + +**只在内存中保留上传游标。** 已否决,因为普通进程重启会重发完整会话。权威接受事件让重启恢复获得尽力而为的持久性,无需另一存储后端;剩余崩溃窗口只会产生允许的重复。 + +**清点每个存活 Cordis fiber。** 已否决,因为编程式与内存 fiber 没有权威 NPM 包来源。Loader 支撑的宿主与 preset 配置项能提供可精确解析的包身份。 **cache 一份全进程清单,或按 TTL 使其过期。** 已否决,因为单份不可变清单无法正确反映 Loader 生命周期与逐会话 preset,TTL 则允许元数据在过期边界之间保持陈旧。暂缓的 epoch 设计会根据权威存活状态转换执行失效。 @@ -58,6 +70,8 @@ Status: implemented ## 后果 -DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。该字段对模型不可见,不增加提示词 token,也不改变 KV Cache。manifest 解析、字段冲突、接受处理或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。 +DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。显式选择启用会话日志后,请求还会携带完整的未接受会话新后缀。这些字段对模型不可见,不增加提示词 token,也不改变 KV Cache,但可能显著增大 HTTP 正文。编码、manifest 解析、字段冲突、接受记录或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。 -缺少存活 Agent 的直接调用仍会携带宿主包清单。[DeepSeek 请求身份决策](../feature/2026-08-11-deepseek-request-user-id-header.zh.md)继续拥有 user/session header,且这些 header 仍位于正文之外。 +已接受水位事件会成为权威日志的一部分,并在后续请求中自行交付。崩溃恢复可能重复后缀,但不会根据 assistant 输出推断接受,也不会创建第二份本地游标存储。缺少存活会话的直接调用会省略会话字段;宿主包清单仍然可用。 + +[DeepSeek 请求身份决策](../feature/2026-08-11-deepseek-request-user-id-header.zh.md)继续拥有 user/session header,且这些 header 仍位于正文之外。[会话遥测决策](../feature/2026-07-23-session-telemetry-otel-revival.zh.md)在另一项变更删除该 seam 与后端之前仍保持当前有效;本请求路径不改变 OTel 捕获或共享模式。 diff --git a/apps/cli/composition.md b/apps/cli/composition.md index c5a6bbe534..9e119a6100 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -18,6 +18,8 @@ flowchart LR cfg --> plugin_dsh_base_deepseek_llm_api_extensions plugin_dsh_base_session["session
@deepseek-ai/dsh-session"] cfg --> plugin_dsh_base_session + plugin_dsh_base_session_log_deepseek["session-log-deepseek
@deepseek-ai/dsh-session-log-deepseek"] + cfg --> plugin_dsh_base_session_log_deepseek plugin_dsh_base_typert["typert
@deepseek-ai/dsh-typert-registry"] cfg --> plugin_dsh_base_typert plugin_dsh_base_typert_loader["typert-loader
@deepseek-ai/dsh-typert-loader"] @@ -177,6 +179,7 @@ flowchart LR | `llm` | `@deepseek-ai/dsh-llm` | | `deepseek-llm-api-extensions` | `@deepseek-ai/dsh-deepseek-llm-api-extensions` | | `session` | `@deepseek-ai/dsh-session` | +| `session-log-deepseek` | `@deepseek-ai/dsh-session-log-deepseek` | | `typert` | `@deepseek-ai/dsh-typert-registry` | | `typert-loader` | `@deepseek-ai/dsh-typert-loader` | | `typert-gateway` | `@deepseek-ai/dsh-api-gateway` | diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml index 848a3bc580..90c04debe0 100644 --- a/docs/capability-seams.i18n.yaml +++ b/docs/capability-seams.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/capability-seams.md -capability-seams.md: 44ee46a0bc6472c62154df9e4111ed6e6649bacd -capability-seams.zh.md: 62ae7ac460ac9fc1c3df62d38a9dd64956744cec +capability-seams.md: 86760ca4fc83d921bc489b03b5156ab692f04dff +capability-seams.zh.md: 2a44729c0b552bf26a6f51132ceb4ca31c256814 diff --git a/docs/capability-seams.md b/docs/capability-seams.md index 44ee46a0bc..86760ca4fc 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -20,6 +20,7 @@ flowchart LR pkg_compaction_basic["compaction-basic"] pkg_deepseek_llm_api_extensions["deepseek-llm-api-extensions"] svc_deepseekLlmApiExtensions["ctx.deepseekLlmApiExtensions
Official DeepSeek request extensions"] + pkg_session_log_deepseek["session-log-deepseek"] pkg_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek"] pkg_token_meter["token-meter"] svc_tokenMeter["ctx.tokenMeter
Replay token measurement"] @@ -259,6 +260,7 @@ flowchart LR pkg_sandbox_local --> svc_sandbox pkg_sandbox_policy --> svc_sandboxPolicy pkg_session --> svc_sessions + pkg_session_log_deepseek --> svc_deepseekLlmApiExtensions pkg_session_persistence --> svc_sessionPersistence pkg_session_persistence_jsonl --> svc_sessionPersistence pkg_session_persistence_sqlite --> svc_sessionPersistence @@ -433,7 +435,7 @@ flowchart LR | --- | --- | --- | --- | --- | --- | --- | | `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | `host-runtime`, [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | The host commits accepted images before session events; provider adapters resolve authorized durable references into provider-native content. | | `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/test-support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compaction-basic`](../packages/compaction/compaction-basic) | - | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. | -| `ctx.deepseekLlmApiExtensions` | `seam` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | [`llm-deepseek`](../packages/llm/llm-deepseek) | - | Plugins prepare independent top-level fields; the official adapter merges them and commits their delivery state after HTTP acceptance. | +| `ctx.deepseekLlmApiExtensions` | `seam` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | [`session-log-deepseek`](../packages/session/session-log-deepseek), [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | [`llm-deepseek`](../packages/llm/llm-deepseek) | - | Plugins prepare independent top-level fields; the official adapter merges them and commits their delivery state after HTTP acceptance. | | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Owns isolated per-session replay folds; pressure consumers share immutable revisioned measurements. | | `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. | | `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), `subagent-inprocess`, [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | Owns append-only Session instances and emits the durable session event feed. | diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index 62ae7ac460..2a44729c0b 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -22,6 +22,7 @@ flowchart LR pkg_compaction_basic["compaction-basic"] pkg_deepseek_llm_api_extensions["deepseek-llm-api-extensions"] svc_deepseekLlmApiExtensions["ctx.deepseekLlmApiExtensions
Official DeepSeek request extensions"] + pkg_session_log_deepseek["session-log-deepseek"] pkg_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek"] pkg_token_meter["token-meter"] svc_tokenMeter["ctx.tokenMeter
Replay token measurement"] @@ -261,6 +262,7 @@ flowchart LR pkg_sandbox_local --> svc_sandbox pkg_sandbox_policy --> svc_sandboxPolicy pkg_session --> svc_sessions + pkg_session_log_deepseek --> svc_deepseekLlmApiExtensions pkg_session_persistence --> svc_sessionPersistence pkg_session_persistence_jsonl --> svc_sessionPersistence pkg_session_persistence_sqlite --> svc_sessionPersistence @@ -435,7 +437,7 @@ flowchart LR | --- | --- | --- | --- | --- | --- | --- | | `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | `host-runtime`, [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | 宿主会在会话事件之前提交已接受的图片;提供方适配器将已授权的持久引用解析为提供方原生内容。 | | `ctx.llm` | `seam` | [`llm`](../packages/llm/llm) | [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-replay`](../packages/test-support/llm-replay) | [`agent-loop`](../packages/core/agent-loop), [`compaction-basic`](../packages/compaction/compaction-basic) | - | 适配器注册提供方实现;agent loop(智能体循环)与压缩功能调用提供方无关的流服务。 | -| `ctx.deepseekLlmApiExtensions` | `seam` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | [`llm-deepseek`](../packages/llm/llm-deepseek) | - | 插件准备彼此独立的顶层字段;官方适配器会合并这些字段,并在 HTTP 接受后提交其交付状态。 | +| `ctx.deepseekLlmApiExtensions` | `seam` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions) | [`session-log-deepseek`](../packages/session/session-log-deepseek), [`plugin-package-inventory-deepseek`](../packages/llm/plugin-package-inventory-deepseek) | [`llm-deepseek`](../packages/llm/llm-deepseek) | - | 插件准备彼此独立的顶层字段;官方适配器会合并这些字段,并在 HTTP 接受后提交其交付状态。 | | `ctx.tokenMeter` | `core` | [`token-meter`](../packages/llm/token-meter) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 拥有按会话隔离的回放折叠区;压力消费方共享不可变且带修订版本的测量结果。 | | `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 | | `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), `subagent-inprocess`, [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 | diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 822ffbb383..ce47897d2b 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 003777a66d598edea65796ef01f19540aa212253 -config-catalog.zh.md: 16d043b540b64e9ad8ee291bb9454d046c15dadd +config-catalog.md: 77af1ffed349e40f800d0ccd14610b2b2a609a0d +config-catalog.zh.md: d444f337a0a1a9a7419cda8a7eb8774b0666a961 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 003777a66d..77af1ffed3 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1724,6 +1724,22 @@ Depends on: `Readable` (`node:stream`) · `Writable` (`node:stream`) Source: [`packages/sdk/server/src/index.ts:25`](../packages/sdk/server/src/index.ts) + + +## `@deepseek-ai/dsh-session-log-deepseek` + +Requires: `deepseekLlmApiExtensions` · `sessions` + +```ts config-catalog +/** Session-log request contribution configuration. */ +export interface Config { + /** Contribute `dsh_session_log` to official DeepSeek requests. Defaults to `false`. */ + enabled?: boolean +} +``` + +Source: [`packages/session/session-log-deepseek/src/index.ts:25`](../packages/session/session-log-deepseek/src/index.ts) + ## `@deepseek-ai/dsh-session-persistence-jsonl` diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 16d043b540..d444f337a0 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -1726,6 +1726,22 @@ export interface JsonRpcConfig { 来源:[`packages/sdk/server/src/index.ts:29`](../packages/sdk/server/src/index.ts) + + +## `@deepseek-ai/dsh-session-log-deepseek` + +需要:`deepseekLlmApiExtensions` · `sessions` + +```ts config-catalog +/** Session-log request contribution configuration. */ +export interface Config { + /** Contribute `dsh_session_log` to official DeepSeek requests. Defaults to `false`. */ + enabled?: boolean +} +``` + +来源:[`packages/session/session-log-deepseek/src/index.ts:25`](../packages/session/session-log-deepseek/src/index.ts) + ## `@deepseek-ai/dsh-session-persistence-jsonl` diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index e360803627..ab3357cfc4 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/event-producer-consumer.md -event-producer-consumer.md: c3d476fed6e4878e10a81858f918368c75bf7523 -event-producer-consumer.zh.md: 82a754094d9bc2daa1fe585d7da5cb33d67aa0a6 +event-producer-consumer.md: ee1ab4fe3eba4b6585e980a096a4b50467a7d287 +event-producer-consumer.zh.md: f9873bff957e6d141221ff6f5af000a585d25f69 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index c3d476fed6..ee1ab4fe3e 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -40,7 +40,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `apiproxy`, [`llm`](../packages/llm/llm) | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:65`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | -| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | +| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) | | `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) | @@ -71,7 +71,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event string | Dispatchers | Listeners | | --- | --- | --- | -| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) | +| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) | | `internal/plugin` | - | `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), `modules`, `webserver` | | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` | | `internal/status` | - | [`agent`](../packages/core/agent) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 82a754094d..f9873bff95 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -42,7 +42,7 @@ | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `apiproxy`, [`llm`](../packages/llm/llm) | | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:65`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | -| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | +| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) | | `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) | diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index d5de17cf3f..8f5579ab74 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: 924ec71647a391172d3acb41f53351340330b501 -module-graph.zh.md: 594de1d2bd17744e5d611f8b5987d01e27cc1538 +module-graph.md: df9b8e1a1fb0a69c2e18ea23773e51a3fecd8a34 +module-graph.zh.md: 914daf784b21d14f44bf7001a160b81af1adbd48 diff --git a/docs/module-graph.md b/docs/module-graph.md index 924ec71647..df9b8e1a1f 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -272,6 +272,7 @@ flowchart TD end subgraph group_session["packages/session"] pkg_session_checkpoint_policy["session-checkpoint-policy"] + pkg_session_log_deepseek["session-log-deepseek"] pkg_session_persistence["session-persistence"] pkg_session_persistence_jsonl["session-persistence-jsonl"] pkg_session_persistence_sqlite["session-persistence-sqlite"] @@ -495,6 +496,9 @@ flowchart TD pkg_sandbox --> pkg_invariants pkg_sandbox --> pkg_llm pkg_sandbox --> pkg_session + pkg_session_log_deepseek --> pkg_deepseek_llm_api_extensions + pkg_session_log_deepseek --> pkg_invariants + pkg_session_log_deepseek --> pkg_session pkg_session_persistence --> pkg_brand pkg_session_persistence --> pkg_invariants pkg_session_persistence --> pkg_session @@ -1560,6 +1564,7 @@ flowchart TD | [`code-runtime-worker-thread`](../packages/code-runtime/code-runtime-worker-thread) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | | [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt) | | [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | +| [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | | [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`acp-snapshot`](../packages/test-support/acp-snapshot) | `test-support` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 594de1d2bd..914daf784b 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -274,6 +274,7 @@ flowchart TD end subgraph group_session["packages/session"] pkg_session_checkpoint_policy["session-checkpoint-policy"] + pkg_session_log_deepseek["session-log-deepseek"] pkg_session_persistence["session-persistence"] pkg_session_persistence_jsonl["session-persistence-jsonl"] pkg_session_persistence_sqlite["session-persistence-sqlite"] @@ -497,6 +498,9 @@ flowchart TD pkg_sandbox --> pkg_invariants pkg_sandbox --> pkg_llm pkg_sandbox --> pkg_session + pkg_session_log_deepseek --> pkg_deepseek_llm_api_extensions + pkg_session_log_deepseek --> pkg_invariants + pkg_session_log_deepseek --> pkg_session pkg_session_persistence --> pkg_brand pkg_session_persistence --> pkg_invariants pkg_session_persistence --> pkg_session @@ -1562,6 +1566,7 @@ flowchart TD | [`code-runtime-worker-thread`](../packages/code-runtime/code-runtime-worker-thread) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | | [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt) | | [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | +| [`session-log-deepseek`](../packages/session/session-log-deepseek) | `session` | [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | | [`session-projection`](../packages/session/session-projection) | `session` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | | [`acp-snapshot`](../packages/test-support/acp-snapshot) | `test-support` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 9f53c1e2a9..234cd56f74 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-catalog.md -persistence-catalog.md: b505bc4ce1c71198ffc316a4f06d3c00174f3fb7 -persistence-catalog.zh.md: 715ae6515f8e7c6df936229becc41d492425b1ef +persistence-catalog.md: 3969ec00068a0b40eb7362a1805313ad35c84e5e +persistence-catalog.zh.md: f0e58af4181c60e1d675f7f5b394accfee0d7cf0 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index b505bc4ce1..3969ec0006 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -667,6 +667,24 @@ Types: [SessionTitleLlmRequestEventData](subsystems/session-title.md) Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/session/session-title-llm/src/index.ts) +### `session-log-deepseek/*` + + + +#### `session-log-deepseek/accepted` — log-only + +```ts persistence-catalog +/** Records a confirmed HTTP acceptance watermark for restart-safe suffix selection. */ +'session-log-deepseek/accepted': { + /** Session identity the accepted request carried; inherited fork markers retain the parent's id. */ + sessionId: import('@deepseek-ai/dsh-session/types').SessionId + /** Last canonical event included in the accepted request. */ + throughSeq: number +} +``` + +Source: [`packages/session/session-log-deepseek/src/types.ts:54`](../packages/session/session-log-deepseek/src/types.ts) + ### `step/*` diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 715ae6515f..f0e58af418 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -669,6 +669,24 @@ export type SessionEvent = { 来源:[`packages/session/session-title-llm/src/index.ts:43`](../packages/session/session-title-llm/src/index.ts) +### `session-log-deepseek/*` + + + +#### `session-log-deepseek/accepted` — log-only + +```ts persistence-catalog +/** Records a confirmed HTTP acceptance watermark for restart-safe suffix selection. */ +'session-log-deepseek/accepted': { + /** Session identity the accepted request carried; inherited fork markers retain the parent's id. */ + sessionId: import('@deepseek-ai/dsh-session/types').SessionId + /** Last canonical event included in the accepted request. */ + throughSeq: number +} +``` + +来源:[`packages/session/session-log-deepseek/src/types.ts:54`](../packages/session/session-log-deepseek/src/types.ts) + ### `step/*` diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml index 485696c80e..d3f211267a 100644 --- a/docs/subsystems/llm-streaming.i18n.yaml +++ b/docs/subsystems/llm-streaming.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/llm-streaming.md -llm-streaming.md: a0d099bc43d1b6d17cd757e3c6bf9f9ac83f3d4f -llm-streaming.zh.md: 585134620e6b378418ac158af623388a0cfda53d +llm-streaming.md: 4ad7af5673f3894d86af72b04db365fcc0f36608 +llm-streaming.zh.md: ff62bccdac018ea57b58d5edec9b7dae448be76a diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md index a0d099bc43..4ad7af5673 100644 --- a/docs/subsystems/llm-streaming.md +++ b/docs/subsystems/llm-streaming.md @@ -666,7 +666,7 @@ interface LlmCallConfigAdapterDefaults { `ctx.deepseekLlmApiExtensions` is the provider-specific registry for additive top-level fields on `deepseek-official` requests. Contributor plugins use `register(field, provider)` to claim one field; the adapter calls `prepare(request)` after serializing its base body and merges the returned fields before HTTP. The prepared `accept()` transaction runs after 2xx, so a contributor can commit delivery state without treating a transport or provider rejection as acceptance. Preparation, collision, and acceptance failures use `REQUEST_EXTENSION` and fail the model request. -The [wire reference](../deepseek-llm-api-wire-extensions.md) defines the exact request headers, extension transaction, field versions, and receiver obligations. The shipped composition registers [`dsh_plugin_packages`](../../packages/llm/plugin-package-inventory-deepseek/README.md) as the complete active Loader-backed package set. The field remains outside model messages and is absent from the pi-ai adapter path. +The [wire reference](../deepseek-llm-api-wire-extensions.md) defines the exact request headers, extension transaction, field versions, and receiver obligations. The shipped composition registers [`dsh_session_log`](../../packages/session/session-log-deepseek/README.md) as a lossless incremental canonical-log suffix and [`dsh_plugin_packages`](../../packages/llm/plugin-package-inventory-deepseek/README.md) as the complete active Loader-backed package set. These fields remain outside model messages and are absent from the pi-ai adapter path. ## Service and provider contracts diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md index 585134620e..ff62bccdac 100644 --- a/docs/subsystems/llm-streaming.zh.md +++ b/docs/subsystems/llm-streaming.zh.md @@ -672,7 +672,7 @@ interface LlmCallConfigAdapterDefaults { `ctx.deepseekLlmApiExtensions` 是用于向 `deepseek-official` 请求添加顶层字段的提供方特定注册表。贡献插件通过 `register(field, provider)` 认领一个字段;适配器在序列化基础正文后调用 `prepare(request)`,并在 HTTP 前合并返回字段。已准备的 `accept()` 事务会在 2xx 后运行,因此贡献方可以提交交付状态,而不会把传输失败或提供方拒绝当作接受。准备、冲突与接受失败会使用 `REQUEST_EXTENSION`,并使模型请求失败。 -[协议参考](../deepseek-llm-api-wire-extensions.zh.md)定义确切的请求标头、扩展事务、字段版本和接收方义务。随附组合会将 [`dsh_plugin_packages`](../../packages/llm/plugin-package-inventory-deepseek/README.zh.md) 注册为完整存活 Loader 包集合。该字段仍位于模型消息之外,也不会进入 pi-ai 适配器路径。 +[协议参考](../deepseek-llm-api-wire-extensions.zh.md)定义确切的请求标头、扩展事务、字段版本和接收方义务。随附组合会将 [`dsh_session_log`](../../packages/session/session-log-deepseek/README.zh.md) 注册为无损增量权威日志后缀,并将 [`dsh_plugin_packages`](../../packages/llm/plugin-package-inventory-deepseek/README.zh.md) 注册为完整存活 Loader 包集合。这些字段仍位于模型消息之外,也不会进入 pi-ai 适配器路径。 ## 服务与提供方约定 diff --git a/examples/acp-agent/composition.md b/examples/acp-agent/composition.md index c081572b39..a9337541ac 100644 --- a/examples/acp-agent/composition.md +++ b/examples/acp-agent/composition.md @@ -10,6 +10,8 @@ flowchart LR cfg["examples/acp-agent
cordis.yml"] plugin_acp_deepseek_llm_api_extensions["deepseek-llm-api-extensions
@deepseek-ai/dsh-deepseek-llm-api-extensions"] cfg --> plugin_acp_deepseek_llm_api_extensions + plugin_acp_session_log_deepseek["session-log-deepseek
@deepseek-ai/dsh-session-log-deepseek"] + cfg --> plugin_acp_session_log_deepseek plugin_acp_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek
@deepseek-ai/dsh-plugin-package-inventory-deepseek"] cfg --> plugin_acp_plugin_package_inventory_deepseek plugin_acp_llm_deepseek["llm-deepseek
@deepseek-ai/dsh-llm-deepseek"] @@ -80,6 +82,7 @@ flowchart LR | Plugin id | Package / module | | --- | --- | | `deepseek-llm-api-extensions` | `@deepseek-ai/dsh-deepseek-llm-api-extensions` | +| `session-log-deepseek` | `@deepseek-ai/dsh-session-log-deepseek` | | `plugin-package-inventory-deepseek` | `@deepseek-ai/dsh-plugin-package-inventory-deepseek` | | `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` | | `sandbox` | `@deepseek-ai/dsh-sandbox-local` | diff --git a/examples/acp-agent/cordis.yml b/examples/acp-agent/cordis.yml index 1bff01b585..bdecdec5d5 100644 --- a/examples/acp-agent/cordis.yml +++ b/examples/acp-agent/cordis.yml @@ -7,6 +7,9 @@ - id: deepseek-llm-api-extensions name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' +- id: session-log-deepseek + name: '@deepseek-ai/dsh-session-log-deepseek' + - id: plugin-package-inventory-deepseek name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek' diff --git a/examples/headless-agent/composition.md b/examples/headless-agent/composition.md index 0492bb34d6..ddb20410e1 100644 --- a/examples/headless-agent/composition.md +++ b/examples/headless-agent/composition.md @@ -14,6 +14,8 @@ flowchart LR cfg --> plugin_headless_credentials plugin_headless_deepseek_llm_api_extensions["deepseek-llm-api-extensions
@deepseek-ai/dsh-deepseek-llm-api-extensions"] cfg --> plugin_headless_deepseek_llm_api_extensions + plugin_headless_session_log_deepseek["session-log-deepseek
@deepseek-ai/dsh-session-log-deepseek"] + cfg --> plugin_headless_session_log_deepseek plugin_headless_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek
@deepseek-ai/dsh-plugin-package-inventory-deepseek"] cfg --> plugin_headless_plugin_package_inventory_deepseek plugin_headless_llm_deepseek["llm-deepseek
@deepseek-ai/dsh-llm-deepseek"] @@ -69,6 +71,7 @@ flowchart LR | `settings` | `@deepseek-ai/dsh-settings-file` | | `credentials` | `@deepseek-ai/dsh-credentials-local` | | `deepseek-llm-api-extensions` | `@deepseek-ai/dsh-deepseek-llm-api-extensions` | +| `session-log-deepseek` | `@deepseek-ai/dsh-session-log-deepseek` | | `plugin-package-inventory-deepseek` | `@deepseek-ai/dsh-plugin-package-inventory-deepseek` | | `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` | | `subprocess` | `@deepseek-ai/dsh-subprocess-local` | diff --git a/examples/headless-agent/cordis.yml b/examples/headless-agent/cordis.yml index e637fa32e3..fa037ab935 100644 --- a/examples/headless-agent/cordis.yml +++ b/examples/headless-agent/cordis.yml @@ -18,6 +18,9 @@ - id: deepseek-llm-api-extensions name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' +- id: session-log-deepseek + name: '@deepseek-ai/dsh-session-log-deepseek' + - id: plugin-package-inventory-deepseek name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek' diff --git a/examples/jsonrpc-agent/cordis.yml b/examples/jsonrpc-agent/cordis.yml index 20f03df0ca..40878c58b2 100644 --- a/examples/jsonrpc-agent/cordis.yml +++ b/examples/jsonrpc-agent/cordis.yml @@ -9,6 +9,9 @@ - id: deepseek-llm-api-extensions name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' +- id: session-log-deepseek + name: '@deepseek-ai/dsh-session-log-deepseek' + - id: plugin-package-inventory-deepseek name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek' diff --git a/examples/jsonrpc-agent/minimal.cordis.yml b/examples/jsonrpc-agent/minimal.cordis.yml index 5615974cf1..fdf3a18e7a 100644 --- a/examples/jsonrpc-agent/minimal.cordis.yml +++ b/examples/jsonrpc-agent/minimal.cordis.yml @@ -11,6 +11,9 @@ - id: deepseek-llm-api-extensions name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' +- id: session-log-deepseek + name: '@deepseek-ai/dsh-session-log-deepseek' + - id: plugin-package-inventory-deepseek name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek' diff --git a/examples/package.json b/examples/package.json index 4c8ca9fbc9..3d0e42710b 100644 --- a/examples/package.json +++ b/examples/package.json @@ -43,6 +43,7 @@ "@deepseek-ai/dsh-llm-deepseek": "workspace:*", "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:*", "@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:*", + "@deepseek-ai/dsh-session-log-deepseek": "workspace:*", "@deepseek-ai/dsh-llm-pi-ai": "workspace:*", "@deepseek-ai/dsh-llm-replay": "workspace:*", "@deepseek-ai/dsh-loader-smoke": "workspace:*", diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index 1563a5defd..41894463af 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -30,6 +30,9 @@ - id: session name: '@deepseek-ai/dsh-session' + - id: session-log-deepseek + name: '@deepseek-ai/dsh-session-log-deepseek' + - id: typert name: '@deepseek-ai/dsh-typert-registry' diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index b3a1e5857e..2d0977a727 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -74,6 +74,7 @@ "@deepseek-ai/dsh-sandbox-policy": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^", + "@deepseek-ai/dsh-session-log-deepseek": "workspace:^", "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^", "@deepseek-ai/dsh-session-projection": "workspace:^", "@deepseek-ai/dsh-session-query-sqlite": "workspace:^", diff --git a/packages/core/session/src/known-event-types.ts b/packages/core/session/src/known-event-types.ts index 098743dd8a..076d9bbb88 100644 --- a/packages/core/session/src/known-event-types.ts +++ b/packages/core/session/src/known-event-types.ts @@ -42,6 +42,7 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet = new Set([ 'request/header', 'sandbox/mode', 'schedule/change', + 'session-log-deepseek/accepted', 'session/end-seed', 'session/title', 'session/title-llm-request', diff --git a/packages/llm/deepseek-llm-api-extensions/README.i18n.yaml b/packages/llm/deepseek-llm-api-extensions/README.i18n.yaml index 59300520e3..85596da12b 100644 --- a/packages/llm/deepseek-llm-api-extensions/README.i18n.yaml +++ b/packages/llm/deepseek-llm-api-extensions/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/deepseek-llm-api-extensions/README.md -README.md: fd25e1e047316acc4b92c0ea2c5cc3fde85cbe8a -README.zh.md: 9f7d8e75bb6ff042d946a918e746f8b1871b2f08 +README.md: 5680b58ee2baff6a6ee2ba0249a7ea145ee7eb78 +README.zh.md: 9e7ae393b0cc72328101c89a245b8a3ace173503 diff --git a/packages/llm/deepseek-llm-api-extensions/README.md b/packages/llm/deepseek-llm-api-extensions/README.md index fd25e1e047..5680b58ee2 100644 --- a/packages/llm/deepseek-llm-api-extensions/README.md +++ b/packages/llm/deepseek-llm-api-extensions/README.md @@ -12,7 +12,7 @@ Provider-specific registry for additive top-level fields on official DeepSeek LL Each provider sees the exact serialized base body, the request `AbortSignal`, plus optional `sessionId` and auxiliary-call `purpose`. It must stop its own work promptly after cancellation and returns `undefined` when its field does not apply to that request. A prepared operation retains the providers it captured even if HMR removes their registrations before HTTP acceptance. -The registry owns addition and lifecycle, not field semantics. `@deepseek-ai/dsh-plugin-package-inventory-deepseek` owns the initial `dsh_plugin_packages` field. The provider-neutral LLM seam and `llm-pi-ai` do not consume this registry. +The registry owns addition and lifecycle, not field semantics. `@deepseek-ai/dsh-session-log-deepseek` owns `dsh_session_log`; `@deepseek-ai/dsh-plugin-package-inventory-deepseek` owns `dsh_plugin_packages`. The provider-neutral LLM seam and `llm-pi-ai` do not consume this registry. ## Model Experience diff --git a/packages/llm/deepseek-llm-api-extensions/README.zh.md b/packages/llm/deepseek-llm-api-extensions/README.zh.md index 9f7d8e75bb..9e7ae393b0 100644 --- a/packages/llm/deepseek-llm-api-extensions/README.zh.md +++ b/packages/llm/deepseek-llm-api-extensions/README.zh.md @@ -12,7 +12,7 @@ 每个提供方都会看到确切的已序列化基础正文、请求 `AbortSignal`,以及可选的 `sessionId` 与辅助调用 `purpose`。提供方必须在取消后迅速停止自身工作;字段不适用于当前请求时返回 `undefined`。即使 HMR(热模块替换)在 HTTP 接受前移除了注册,已准备的操作仍会保留其捕获的提供方。 -注册表拥有字段添加与生命周期,不拥有字段语义。`@deepseek-ai/dsh-plugin-package-inventory-deepseek` 拥有首个 `dsh_plugin_packages` 字段。提供方无关的 LLM seam 与 `llm-pi-ai` 都不消费该注册表。 +注册表拥有字段添加与生命周期,不拥有字段语义。`@deepseek-ai/dsh-session-log-deepseek` 拥有 `dsh_session_log`;`@deepseek-ai/dsh-plugin-package-inventory-deepseek` 拥有 `dsh_plugin_packages`。提供方无关的 LLM seam 与 `llm-pi-ai` 都不消费该注册表。 ## 模型体验 diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml index b5fffc50a6..8076a692e6 100644 --- a/packages/llm/llm-deepseek/README.i18n.yaml +++ b/packages/llm/llm-deepseek/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md -README.md: f50de33dfbf7229968c9335972b47ab5a0bd1c41 -README.zh.md: 3820972a6cc1eb7a1c7b0855c718c6695b6a3c76 +README.md: 7d50a8e99863637a06abf49d26a6eeb419cf1bbd +README.zh.md: c1571149529a2d4e54d67b10f63b60bb2aa1abfd diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index f50de33dfb..7d50a8e998 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -88,7 +88,7 @@ The plugin also declares its route in the configurable-provider directory (`ctx. When `ctx.deepseekLlmApiExtensions` is present, the adapter prepares its registered top-level fields after serializing the exact wire messages and before `fetch`. The same request signal reaches providers, and cancellation stops waiting even when a provider ignores it. Preparation failures and field collisions fail before HTTP with `REQUEST_EXTENSION`. After HTTP 2xx, the adapter awaits the prepared acceptance transaction before consuming the SSE body; an acceptance failure uses the same code, while transport and non-2xx failures do not accept the fields. Fields go to the resolved `baseURL`, including a configured gateway. A composition without the registry sends the base DeepSeek request unchanged. -Shipped profiles and runnable examples mount [`@deepseek-ai/dsh-plugin-package-inventory-deepseek`](../plugin-package-inventory-deepseek/README.md) for the complete active `dsh_plugin_packages` field. Package metadata defaults on and remains model-hidden. `llm-pi-ai` neither imports nor calls this provider-specific registry. +Shipped profiles and runnable examples mount [`@deepseek-ai/dsh-session-log-deepseek`](../../session/session-log-deepseek/README.md) for the incremental `dsh_session_log` field and [`@deepseek-ai/dsh-plugin-package-inventory-deepseek`](../plugin-package-inventory-deepseek/README.md) for the complete active `dsh_plugin_packages` field. Session-log upload defaults off and requires `session-log-deepseek.enabled: true`; package metadata defaults on. Both remain model-hidden. `llm-pi-ai` neither imports nor calls this provider-specific registry. ## App attribution diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md index 3820972a6c..c157114952 100644 --- a/packages/llm/llm-deepseek/README.zh.md +++ b/packages/llm/llm-deepseek/README.zh.md @@ -88,7 +88,7 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器: 存在 `ctx.deepseekLlmApiExtensions` 时,适配器会在序列化确切协议消息后、`fetch` 前准备其中已注册的顶层字段。提供方会收到同一个请求信号;即使某个提供方忽略信号,取消也会停止等待。准备失败与字段冲突会在 HTTP 前以 `REQUEST_EXTENSION` 失败。HTTP 2xx 后,适配器会先等待已准备的接受事务,再消费 SSE 正文;接受失败使用同一 code,而传输失败与非 2xx 失败不会接受字段。字段会发往解析后的 `baseURL`,包括已配置的网关。未组合该注册表的部署会发送未改变的 DeepSeek 基础请求。 -随附 profile 与可运行示例会挂载 [`@deepseek-ai/dsh-plugin-package-inventory-deepseek`](../plugin-package-inventory-deepseek/README.zh.md) 以提供完整存活 `dsh_plugin_packages` 字段。插件包元数据默认开启且对模型不可见。`llm-pi-ai` 既不导入也不调用该提供方特定注册表。 +随附 profile 与可运行示例会挂载 [`@deepseek-ai/dsh-session-log-deepseek`](../../session/session-log-deepseek/README.zh.md) 以提供增量 `dsh_session_log` 字段,并挂载 [`@deepseek-ai/dsh-plugin-package-inventory-deepseek`](../plugin-package-inventory-deepseek/README.zh.md) 以提供完整存活 `dsh_plugin_packages` 字段。会话日志上传默认关闭,需要设置 `session-log-deepseek.enabled: true`;插件包元数据默认开启。两个字段都对模型不可见。`llm-pi-ai` 既不导入也不调用该提供方特定注册表。 ## 应用归因 diff --git a/packages/llm/llm-deepseek/package.json b/packages/llm/llm-deepseek/package.json index 66824f37a9..eba8fbc5a7 100644 --- a/packages/llm/llm-deepseek/package.json +++ b/packages/llm/llm-deepseek/package.json @@ -64,6 +64,7 @@ "@deepseek-ai/dsh-home-paths": "workspace:^", "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-session-log-deepseek": "workspace:^", "@deepseek-ai/dsh-timeout": "workspace:^", "@deepseek-ai/dsh-anonymous-user-id": "workspace:^", "@deepseek-ai/cordis": "workspace:^" diff --git a/packages/llm/llm-deepseek/tests/adapter.e2e.ts b/packages/llm/llm-deepseek/tests/adapter.e2e.ts index 2a29b4b7fb..6e7c3307f7 100644 --- a/packages/llm/llm-deepseek/tests/adapter.e2e.ts +++ b/packages/llm/llm-deepseek/tests/adapter.e2e.ts @@ -19,8 +19,10 @@ import type { StoredImageAttachment, } from '@deepseek-ai/dsh-attachment' import { LocalCredentialProvider } from '@deepseek-ai/dsh-credentials-local' +import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions' import * as PluginPackageInventoryDeepSeek from '@deepseek-ai/dsh-plugin-package-inventory-deepseek' +import * as SessionLogDeepSeek from '@deepseek-ai/dsh-session-log-deepseek' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import type { Config } from '@deepseek-ai/dsh-llm-deepseek' import { assemble, type AssembledResult } from './assemble.ts' @@ -184,23 +186,29 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', () } }) - it('accepts the plugin-package request extension field', async () => { + it('accepts the session-log and plugin-package request extension fields', async () => { const ctx = new Context() contexts.push(ctx) await ctx.plugin(Loader) await ctx.plugin(AgentRegistry) await ctx.plugin(LlmRuntime) + await ctx.plugin(SessionStore) await ctx.plugin(DeepSeekLlmApiExtensionRegistry) + await ctx.plugin(SessionLogDeepSeek, { enabled: true }) await ctx.plugin(PluginPackageInventoryDeepSeek) await ctx.plugin(LlmDeepSeek, { thinking: 'disabled' }) + const session = ctx.sessions.create(SessionId('real-extension-fields')) + session.append('turn/start', { turn: 1 }) const result = await assemble(ctx, { model: FLASH, messages: ask('Reply with exactly the word: pong'), maxTokens: 50, + sessionId: session.id, }) expect(result.finish.kind).toBe('stop') expect(textOf(result).toLowerCase()).toContain('pong') + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0) }) it('serves a real request with the key held only by a credentials-local document', async () => { diff --git a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts index f533d418a2..57c3f34fc5 100644 --- a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts +++ b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts @@ -18,12 +18,14 @@ import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' import LlmRuntime from '@deepseek-ai/dsh-llm' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import { credentialRef } from '@deepseek-ai/dsh-credentials' import LocalCredentialProvider from '@deepseek-ai/dsh-credentials-local' import { settingsNamespace } from '@deepseek-ai/dsh-settings' import FileSettingsProvider from '@deepseek-ai/dsh-settings-file' import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id' import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions' +import * as SessionLogDeepSeek from '@deepseek-ai/dsh-session-log-deepseek' import * as DeepSeekPluginPackageInventory from '@deepseek-ai/dsh-plugin-package-inventory-deepseek' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import { assemble } from './assemble.ts' @@ -45,7 +47,7 @@ afterEach(async () => { }) async function loadComposition( - options: { withDynamic: boolean; baseURL: string; reuseRoot?: string }, + options: { withDynamic: boolean; baseURL: string; reuseRoot?: string; enableSessionLog?: boolean }, ): Promise<{ ctx: Context; settingsPath: string; credentialsPath: string }> { // A reused root is the restart case: the same harness home, its documents // exactly as the previous process left them. @@ -63,10 +65,17 @@ async function loadComposition( await writeFile(configPath, [ '- id: llm', " name: '@deepseek-ai/dsh-llm'", + '- id: session', + " name: '@deepseek-ai/dsh-session'", '- id: agents', " name: '@deepseek-ai/dsh-agent'", '- id: deepseek-llm-api-extensions', " name: '@deepseek-ai/dsh-deepseek-llm-api-extensions'", + '- id: session-log-deepseek', + " name: '@deepseek-ai/dsh-session-log-deepseek'", + ...options.enableSessionLog === true + ? [' config:', ' enabled: true'] + : [], '- id: plugin-package-inventory-deepseek', " name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek'", ...options.withDynamic @@ -97,8 +106,10 @@ async function loadComposition( ctx.loader.builtins.include = Include const modules = new Map([ ['@deepseek-ai/dsh-llm', LlmRuntime], + ['@deepseek-ai/dsh-session', SessionStore], ['@deepseek-ai/dsh-agent', AgentRegistry], ['@deepseek-ai/dsh-deepseek-llm-api-extensions', DeepSeekLlmApiExtensionRegistry], + ['@deepseek-ai/dsh-session-log-deepseek', SessionLogDeepSeek], ['@deepseek-ai/dsh-plugin-package-inventory-deepseek', DeepSeekPluginPackageInventory], ['@deepseek-ai/dsh-settings-file', FileSettingsProvider], ['@deepseek-ai/dsh-credentials-local', LocalCredentialProvider], @@ -131,19 +142,54 @@ async function loadComposition( } describe('llm-deepseek real dynamic composition', () => { - it('sends package inventory by default in the real Loader composition', async () => { + it('keeps session upload off and package inventory on by default in the real Loader composition', async () => { vi.stubEnv('DEEPSEEK_API_KEY', 'entry-key') const server = await mockServer([{ kind: 'sse', events: textEvents }]) const { ctx } = await loadComposition({ withDynamic: false, baseURL: server.url }) + const session = ctx.sessions.create(SessionId('extension-composition')) + session.append('turn/start', { turn: 1 }) - await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] }) + await assemble(ctx, { model: 'deepseek-v4-flash', messages: [], sessionId: session.id }) const request = server.requests[0] as { dsh_plugin_packages: { version: number; packages: unknown[] } } + expect(request).not.toHaveProperty('dsh_session_log') expect(request.dsh_plugin_packages.packages).toEqual(expect.arrayContaining([ { name: '@deepseek-ai/dsh-deepseek-llm-api-extensions', version: '0.1.0-rc.8' }, { name: '@deepseek-ai/dsh-llm-deepseek', version: '0.1.0-rc.8' }, - { name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek', version: '0.1.0-rc.8' }, + { name: '@deepseek-ai/dsh-session-log-deepseek', version: '0.1.0-rc.8' }, ])) expect(request.dsh_plugin_packages.version).toBe(1) + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(-1) + }) + + it('sends the canonical session suffix when the Loader composition explicitly enables upload', async () => { + vi.stubEnv('DEEPSEEK_API_KEY', 'entry-key') + const server = await mockServer([{ kind: 'sse', events: textEvents }]) + const { ctx } = await loadComposition({ + withDynamic: false, + baseURL: server.url, + enableSessionLog: true, + }) + const session = ctx.sessions.create(SessionId('extension-composition-enabled')) + session.append('turn/start', { turn: 1 }) + + await assemble(ctx, { model: 'deepseek-v4-flash', messages: [], sessionId: session.id }) + const request = server.requests[0] as { + dsh_session_log?: { + version: number + session: { id: string } + afterSeq: number + throughSeq: number + events: Array<{ event: { type: string; seq: number } }> + } + } + expect(request.dsh_session_log).toMatchObject({ + version: 1, + session: { id: 'extension-composition-enabled' }, + afterSeq: -1, + throughSeq: 0, + events: [{ event: { type: 'turn/start', seq: 0 } }], + }) + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0) }) it('boots from cordis.yml and routes the next request after external settings and credential edits', async () => { diff --git a/packages/llm/llm-pi-ai/tests/adapter.spec.ts b/packages/llm/llm-pi-ai/tests/adapter.spec.ts index b5413cfb31..29e55e0553 100644 --- a/packages/llm/llm-pi-ai/tests/adapter.spec.ts +++ b/packages/llm/llm-pi-ai/tests/adapter.spec.ts @@ -136,6 +136,7 @@ describe('PiAiAdapter provider routing', () => { thinking: { type: 'enabled' }, reasoning_effort: 'max', }) + expect(server.requests[0]).not.toHaveProperty('dsh_session_log') expect(server.requests[0]).not.toHaveProperty('dsh_plugin_packages') }) diff --git a/packages/session/session-log-deepseek/README.i18n.yaml b/packages/session/session-log-deepseek/README.i18n.yaml new file mode 100644 index 0000000000..dd1e2b2237 --- /dev/null +++ b/packages/session/session-log-deepseek/README.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write packages/session/session-log-deepseek/README.md +README.md: 5c7d077f641accf37b78cdf59ea2f4bca9cacc55 +README.zh.md: 1612441a768ea6e3bdd061fe4da07e4847f22bbe diff --git a/packages/session/session-log-deepseek/README.md b/packages/session/session-log-deepseek/README.md new file mode 100644 index 0000000000..5c7d077f64 --- /dev/null +++ b/packages/session/session-log-deepseek/README.md @@ -0,0 +1,49 @@ +# @deepseek-ai/dsh-session-log-deepseek + +English | [中文](README.zh.md) + +Incremental canonical session-log upload for official DeepSeek LLM API requests. This function plugin injects `ctx.sessions` and `ctx.deepseekLlmApiExtensions`, then owns the `dsh_session_log` request field and the durable `session-log-deepseek/accepted` watermark event. + +## Configuration + +| Key | Default | Meaning | +|---|---:|---| +| `enabled` | `false` | Register the `dsh_session_log` contribution. Set it to `true` to opt into Session-log upload. | + +Shipped profiles mount the plugin so an overlay can enable it, but the default configuration registers no request field and appends no acceptance watermark. + +## Request field + +For a request carrying a live `sessionId`, the plugin folds the greatest accepted watermark for that exact Session identity, snapshots `Session.events`, and sends the contiguous suffix after the watermark. A process-local fold scans each event once and consumes later appends incrementally; restart and HMR rebuild it from the durable log. The version-1 field contains the immutable `SessionHeader`, `afterSeq`, `throughSeq`, and every complete event envelope in that range. Forked sessions ignore inherited parent watermarks because each watermark records the Session id sent on the accepted request. + +Each event is sent raw unless request-relative packing reduces its JSON byte size. The codec traverses the exact serialized DeepSeek `messages`, and a packed string can replace an exact substring with `{ messageIndex, path, utf8Start, utf8End }`. References use half-open UTF-8 offsets into parsed string values; literal fragments retain everything outside the match. The encoder verifies each candidate's local UTF-8 round trip, so a surrogate-splitting or ill-formed UTF-16 match stays raw. The exported decoder rejects missing paths, non-string targets, invalid ranges, and ranges that split a UTF-8 code point. Raw fallback and byte-size comparison make every representation lossless and prevent reference overhead from expanding an event. + +## Acceptance and retry + +The DeepSeek adapter calls the prepared contribution's `accept()` after HTTP 2xx, before it consumes the SSE body. Acceptance appends `session-log-deepseek/accepted` with the uploaded `throughSeq`; the next request uploads that watermark as part of its new suffix. Transport and non-2xx failures append no watermark, so later requests resend the uncertain range. Concurrent accepted requests may append watermarks out of order; folding their maximum prevents cursor regression. + +A crash after server acceptance but before the watermark reaches persistence can replay an accepted range after restart. This is the at-least-once failure direction: uncertainty creates duplicates, never a skipped sequence. The ordinary session checkpoint policy persists the watermark at the next semantic checkpoint; this plugin performs no independent I/O. + +Direct requests without a live Session omit `dsh_session_log`. Normal agent, compaction, and session-title calls carry their live Session id. + +## Model Experience + +### Session-log metadata + +#### What the model sees + +Nothing. `dsh_session_log` is a sibling of the DeepSeek request's model-input fields and is not inserted into `messages`, the system prompt, or tool schemas. + +#### Token effect + +Zero model-input tokens; the field only increases HTTP request bytes. + +#### KV Cache effect + +None; the model-visible request prefix remains unchanged. + +## Known Limitations and Deferred Work + +- **Crash-window duplicates** — a 2xx followed by process loss before the acceptance watermark persists causes conservative replay on resume. +- **No live Session means no field** — direct or stale-session calls have no canonical log to snapshot; explicit absence semantics remain deferred. +- **No independent request-size cap** — complete delivery is fail-closed; provider rejection leaves the cursor unchanged instead of truncating the log. diff --git a/packages/session/session-log-deepseek/README.zh.md b/packages/session/session-log-deepseek/README.zh.md new file mode 100644 index 0000000000..1612441a76 --- /dev/null +++ b/packages/session/session-log-deepseek/README.zh.md @@ -0,0 +1,49 @@ +# @deepseek-ai/dsh-session-log-deepseek + +[English](README.md) | 中文 + +用于 DeepSeek 官方 LLM API 请求的增量权威会话日志上传。该函数插件注入 `ctx.sessions` 与 `ctx.deepseekLlmApiExtensions`,并拥有 `dsh_session_log` 请求字段和持久的 `session-log-deepseek/accepted` 水位事件。 + +## 配置 + +| 配置键 | 默认值 | 含义 | +|---|---:|---| +| `enabled` | `false` | 注册 `dsh_session_log` 贡献。将其设为 `true` 可选择启用会话日志上传。 | + +随附 profile 会挂载该插件,让 overlay 可以启用它;默认配置不会注册请求字段,也不会追加接受水位。 + +## 请求字段 + +对于携带存活 `sessionId` 的请求,插件会折叠该确切会话身份的最大已接受水位,对 `Session.events` 取快照,并发送水位之后的连续后缀。进程内 fold 会让每条事件只被扫描一次并增量消费后续追加;重启与 HMR 会从持久日志重建它。版本 1 字段包含不可变的 `SessionHeader`、`afterSeq`、`throughSeq`,以及该范围内每个完整事件信封。每个水位都会记录已接受请求发送的会话 id,因此 fork 会话会忽略从父会话继承的水位。 + +只有在请求相对打包能够减少 JSON 字节数时,事件才不以原始形式发送。Codec 会遍历确切的已序列化 DeepSeek `messages`,并可在打包字符串中用 `{ messageIndex, path, utf8Start, utf8End }` 替换完全匹配的子字符串。引用使用解析后字符串值的半开 UTF-8 偏移;字面分片保留匹配范围之外的全部内容。编码器会校验每个候选的局部 UTF-8 往返,因此拆分 surrogate 或非良构 UTF-16 的匹配会保持原始形式。导出的解码器会拒绝缺失路径、非字符串目标、无效范围和切断 UTF-8 码点的范围。原始回退与字节数比较让每种表示都保持无损,并阻止引用开销扩张事件。 + +## 接受与重试 + +DeepSeek 适配器会在 HTTP 2xx 后、消费 SSE(Server-Sent Events)正文前调用已准备贡献的 `accept()`。接受操作会追加 `session-log-deepseek/accepted` 及已上传的 `throughSeq`;下一次请求再把该水位作为新后缀的一部分上传。传输失败与非 2xx 失败不会追加水位,因此后续请求会重发不确定范围。并发请求可能乱序追加已接受水位;折叠其中最大值可以防止游标回退。 + +服务端接受后、持久化水位前发生崩溃,可能让恢复后的进程重放已经接受的范围。这是至少一次交付的失败方向:不确定性会制造重复,绝不会跳过序列。普通会话检查点策略会在下一个语义检查点持久化水位;本插件不执行独立 I/O。 + +缺少存活会话的直接请求会省略 `dsh_session_log`。普通 agent(智能体)、压缩(compaction)与会话标题调用都会携带存活会话 id。 + +## 模型体验 + +### 会话日志元数据 + +#### 模型看到的内容 + +无。`dsh_session_log` 是 DeepSeek 请求中模型输入字段的同级字段,不会插入 `messages`、系统提示词或工具 schema。 + +#### Token 影响 + +模型输入 token 为零;该字段只会增加 HTTP 请求字节数。 + +#### KV Cache 影响 + +无;模型可见请求前缀保持不变。 + +## 已知限制与暂缓事项 + +- **崩溃窗口重复**——2xx 后、接受水位持久化前进程丢失,会在恢复时触发保守重放。 +- **缺少存活会话就没有字段**——直接调用或陈旧会话调用没有可供快照的权威日志;显式缺失语义仍暂缓处理。 +- **没有独立请求大小上限**——完整交付会快速失败;提供方拒绝会保持游标不变,而非截断日志。 diff --git a/packages/session/session-log-deepseek/package.json b/packages/session/session-log-deepseek/package.json new file mode 100644 index 0000000000..901bfc6180 --- /dev/null +++ b/packages/session/session-log-deepseek/package.json @@ -0,0 +1,54 @@ +{ + "name": "@deepseek-ai/dsh-session-log-deepseek", + "description": "Incremental lossless session-log request extension for the official DeepSeek LLM API", + "version": "0.1.1-rc.2", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/session/session-log-deepseek" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, + "./types": { + "types": "./lib/types/types.d.ts", + "default": "./lib/types/types.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/invariant.js", + "lib/types/**/*.js", + "lib/types/**/*.d.ts" + ], + "license": "MIT", + "dependencies": { + "@deepseek-ai/schemastery": "workspace:^" + }, + "peerDependencies": { + "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + }, + "devDependencies": { + "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + } +} diff --git a/packages/session/session-log-deepseek/src/codec.ts b/packages/session/session-log-deepseek/src/codec.ts new file mode 100644 index 0000000000..4861bf0524 --- /dev/null +++ b/packages/session/session-log-deepseek/src/codec.ts @@ -0,0 +1,227 @@ +/** Lossless request-relative packing for canonical session events. */ + +import { Buffer } from 'node:buffer' +import type { DeepSeekLlmApiJson } from '@deepseek-ai/dsh-deepseek-llm-api-extensions' +import type { JsonValue, SessionEvent } from '@deepseek-ai/dsh-session' +import type { + DeepSeekMessageStringSlice, + EncodedSessionEvent, + PackedJsonStringPart, + PackedJsonValue, +} from './types.ts' + +interface MessageStringSource { + readonly messageIndex: number + readonly path: readonly (string | number)[] + readonly value: string + readonly bytes: number +} + +interface PackedCandidate { + readonly value: PackedJsonValue + readonly references: boolean +} + +/** Return the serialized UTF-8 size used by the profitability decision. */ +function wireBytes(value: unknown): number { + return Buffer.byteLength(JSON.stringify(value), 'utf8') +} + +/** Enumerate every string leaf of the exact wire messages. */ +function collectSources( + value: DeepSeekLlmApiJson, + messageIndex: number, + path: readonly (string | number)[], + output: MessageStringSource[], +): void { + if (typeof value === 'string') { + if (value.length > 0) output.push({ messageIndex, path, value, bytes: Buffer.byteLength(value, 'utf8') }) + return + } + if (Array.isArray(value)) { + value.forEach((child, index) => { collectSources(child, messageIndex, [...path, index], output) }) + return + } + if (value === null || typeof value !== 'object') return + for (const [key, child] of Object.entries(value)) collectSources(child, messageIndex, [...path, key], output) +} + +/** Build one exact slice from UTF-16 offsets, or reject a lossy UTF-8 conversion. */ +function sliceOf(source: MessageStringSource, start: number, end: number): DeepSeekMessageStringSlice | undefined { + const candidate: DeepSeekMessageStringSlice = { + messageIndex: source.messageIndex, + path: source.path, + utf8Start: Buffer.byteLength(source.value.slice(0, start), 'utf8'), + utf8End: Buffer.byteLength(source.value.slice(0, end), 'utf8'), + } + const bytes = Buffer.from(source.value, 'utf8') + const selected = bytes.subarray(candidate.utf8Start, candidate.utf8End) + const decoded = selected.toString('utf8') + return decoded === source.value.slice(start, end) && Buffer.from(decoded, 'utf8').equals(selected) + ? candidate + : undefined +} + +/** Find a profitable exact whole-string or one-inner-string reference. */ +function packString(value: string, sources: readonly MessageStringSource[]): PackedCandidate { + const literal: PackedJsonValue = { kind: 'literal', value } + if (value.length === 0) return { value: literal, references: false } + + let best: PackedJsonValue | undefined + let bestBytes = wireBytes(literal) + for (const source of sources) { + const start = source.value.indexOf(value) + if (start < 0) continue + const slice = sliceOf(source, start, start + value.length) + if (slice === undefined) continue + const candidate: PackedJsonValue = { + kind: 'string', + parts: [{ kind: 'message-slice', value: slice }], + } + const bytes = wireBytes(candidate) + if (bytes < bestBytes) { + best = candidate + bestBytes = bytes + } + } + if (best !== undefined) return { value: best, references: true } + + for (const source of sources) { + const start = value.indexOf(source.value) + if (start < 0) continue + const slice = sliceOf(source, 0, source.value.length) + if (slice === undefined) continue + const parts: PackedJsonStringPart[] = [] + if (start > 0) parts.push({ kind: 'literal', value: value.slice(0, start) }) + parts.push({ kind: 'message-slice', value: slice }) + const end = start + source.value.length + if (end < value.length) parts.push({ kind: 'literal', value: value.slice(end) }) + const candidate: PackedJsonValue = { kind: 'string', parts } + const bytes = wireBytes(candidate) + if (bytes < bestBytes) { + best = candidate + bestBytes = bytes + } + } + return best === undefined + ? { value: literal, references: false } + : { value: best, references: true } +} + +/** Recursively pack one JSON value, retaining references only when the complete subtree shrinks. */ +function packValue(value: JsonValue, sources: readonly MessageStringSource[]): PackedCandidate { + const literal: PackedJsonValue = { kind: 'literal', value } + if (typeof value === 'string') return packString(value, sources) + if (value === null || typeof value !== 'object') return { value: literal, references: false } + + if (Array.isArray(value)) { + const children = value.map(child => packValue(child, sources)) + if (!children.some(child => child.references)) return { value: literal, references: false } + const candidate: PackedJsonValue = { kind: 'array', items: children.map(child => child.value) } + return { value: candidate, references: true } + } + + const children = Object.entries(value).map(([key, child]) => [key, packValue(child, sources)] as const) + if (!children.some(([, child]) => child.references)) return { value: literal, references: false } + const candidate: PackedJsonValue = { + kind: 'object', + entries: children.map(([key, child]) => [key, child.value]), + } + return { value: candidate, references: true } +} + +/** Resolve one path in a parsed wire message. */ +function valueAt(root: DeepSeekLlmApiJson, path: readonly (string | number)[]): DeepSeekLlmApiJson { + let value = root + for (const segment of path) { + if (typeof segment === 'number') { + if (!Array.isArray(value) || segment < 0 || segment >= value.length) { + throw new Error(`session-log-deepseek: message reference has invalid array segment ${segment}`) + } + value = value[segment] as DeepSeekLlmApiJson + continue + } + if (value === null || typeof value !== 'object' || Array.isArray(value) || !Object.hasOwn(value, segment)) { + throw new Error(`session-log-deepseek: message reference has invalid object segment ${JSON.stringify(segment)}`) + } + value = value[segment] as DeepSeekLlmApiJson + } + return value +} + +/** Decode and validate one exact UTF-8 message slice. */ +function decodeSlice(messages: readonly DeepSeekLlmApiJson[], slice: DeepSeekMessageStringSlice): string { + const message = messages[slice.messageIndex] + if (message === undefined) throw new Error(`session-log-deepseek: message reference index ${slice.messageIndex} is absent`) + const source = valueAt(message, slice.path) + if (typeof source !== 'string') throw new Error('session-log-deepseek: message reference path does not resolve to a string') + const bytes = Buffer.from(source, 'utf8') + if (!Number.isSafeInteger(slice.utf8Start) || !Number.isSafeInteger(slice.utf8End) + || slice.utf8Start < 0 || slice.utf8End <= slice.utf8Start || slice.utf8End > bytes.length) { + throw new Error('session-log-deepseek: message reference byte range is invalid') + } + const selected = bytes.subarray(slice.utf8Start, slice.utf8End) + const decoded = selected.toString('utf8') + if (!Buffer.from(decoded, 'utf8').equals(selected)) { + throw new Error('session-log-deepseek: message reference splits a UTF-8 code point') + } + return decoded +} + +/** + * Decode one packed JSON value against the containing request's messages. + * @param value - tagged literal/reference value to reconstruct. + * @param messages - serialized messages from the same request. + * @returns the exact reconstructed JSON value. + */ +export function unpackJsonValue(value: PackedJsonValue, messages: readonly DeepSeekLlmApiJson[]): JsonValue { + switch (value.kind) { + case 'literal': + return structuredClone(value.value) + case 'string': + return value.parts.map(part => part.kind === 'literal' ? part.value : decodeSlice(messages, part.value)).join('') + case 'array': + return value.items.map(item => unpackJsonValue(item, messages)) + case 'object': + return Object.fromEntries(value.entries.map(([key, child]) => [key, unpackJsonValue(child, messages)])) + default: + throw new Error('session-log-deepseek: unknown packed JSON value') + } +} + +/** + * Encode canonical session events against the exact DeepSeek wire messages. + * @param events - immutable canonical event suffix. + * @param messages - serialized request messages that the receiver also holds. + * @returns one raw-or-referenced representation per event. + */ +export function packSessionEvents( + events: readonly SessionEvent[], + messages: readonly DeepSeekLlmApiJson[], +): EncodedSessionEvent[] { + const sources: MessageStringSource[] = [] + messages.forEach((message, messageIndex) => { collectSources(message, messageIndex, [], sources) }) + sources.sort((left, right) => right.bytes - left.bytes) + return events.map((event) => { + const packed = packValue(event as unknown as JsonValue, sources) + const raw: EncodedSessionEvent = { encoding: 'raw', event } + if (!packed.references) return raw + const referenced: EncodedSessionEvent = { encoding: 'message-references', event: packed.value } + return wireBytes(referenced) < wireBytes(raw) ? referenced : raw + }) +} + +/** + * Reconstruct canonical session events from one request-relative representation. + * @param events - encoded event list from the extension field. + * @param messages - serialized messages from the same request. + * @returns reconstructed event values in order. + */ +export function unpackSessionEvents( + events: readonly EncodedSessionEvent[], + messages: readonly DeepSeekLlmApiJson[], +): SessionEvent[] { + return events.map(encoded => encoded.encoding === 'raw' + ? structuredClone(encoded.event) + : unpackJsonValue(encoded.event, messages) as unknown as SessionEvent) +} diff --git a/packages/session/session-log-deepseek/src/index.ts b/packages/session/session-log-deepseek/src/index.ts new file mode 100644 index 0000000000..65188b535a --- /dev/null +++ b/packages/session/session-log-deepseek/src/index.ts @@ -0,0 +1,109 @@ +/** + * Incremental session-log contribution for official DeepSeek LLM API requests. + * Accepted sequence watermarks live in the canonical log, so restart recovery + * can conservatively resend uncertain tails without maintaining another store. + * @module @deepseek-ai/dsh-session-log-deepseek + */ + +import type { Context } from '@deepseek-ai/cordis' +import z from '@deepseek-ai/schemastery' +import type { DeepSeekLlmApiJson } from '@deepseek-ai/dsh-deepseek-llm-api-extensions' +import { SessionId, type Session, type SessionEvent } from '@deepseek-ai/dsh-session' +import { packSessionEvents } from './codec.ts' +import type { DeepSeekSessionLogExtension } from './types.ts' +import type {} from './types.ts' + +export { packSessionEvents, unpackJsonValue, unpackSessionEvents } from './codec.ts' +export type * from './types.ts' + +/** Cordis plugin name. */ +export const name = 'session-log-deepseek' +/** Services required to resolve sessions and contribute the provider request field. */ +export const inject = ['deepseekLlmApiExtensions', 'sessions'] + +/** Session-log request contribution configuration. */ +export interface Config { + /** Contribute `dsh_session_log` to official DeepSeek requests. Defaults to `false`. */ + enabled?: boolean +} + +/** Validated Session-log request contribution configuration. */ +export const Config: z = z.object({ + enabled: z.boolean().default(false), +}) + +interface AcceptanceFold { + readonly scannedEvents: number + readonly throughSeq: number +} + +const acceptanceFolds = new WeakMap() + +/** + * Highest confirmed sequence for this exact session identity. + * @param session - canonical log whose matching acceptance events are folded. + * @returns greatest accepted sequence, or `-1` before any accepted request. + */ +export function acceptedThrough(session: Session): number { + const previous = acceptanceFolds.get(session) + let throughSeq = previous?.throughSeq ?? -1 + const events = session.events + const start = previous?.scannedEvents ?? 0 + for (let index = start; index < events.length; index++) { + const event = events[index] as SessionEvent + if (event.type !== 'session-log-deepseek/accepted') continue + if (typeof event.data.sessionId !== 'string' || event.data.sessionId.length === 0 + || !Number.isSafeInteger(event.data.throughSeq) || event.data.throughSeq < 0 + || event.data.throughSeq >= event.seq) { + throw new Error(`session-log-deepseek: malformed acceptance watermark at seq ${event.seq}`) + } + if (event.data.sessionId !== session.id) continue + throughSeq = Math.max(throughSeq, event.data.throughSeq) + } + acceptanceFolds.set(session, { scannedEvents: events.length, throughSeq }) + return throughSeq +} + +/** Resolve the serialized message list or fail before transport. */ +function requestMessages(body: Readonly>): readonly DeepSeekLlmApiJson[] { + const messages = body.messages + if (!Array.isArray(messages)) throw new Error('session-log-deepseek: DeepSeek request body has no messages array') + return messages +} + +/** + * Register the incremental `dsh_session_log` request contribution when enabled. + * @param ctx - plugin context carrying Sessions and the DeepSeek request-extension registry. + * @param config - validated opt-in configuration. + */ +export function apply(ctx: Context, config: Config): void { + if (config.enabled !== true) return + ctx.deepseekLlmApiExtensions.register('dsh_session_log', { + prepare: (request) => { + // TODO: Define an explicit wire result for direct or stale-session calls if they become a supported product path. + if (request.sessionId === undefined) return undefined + const session = ctx.sessions.get(SessionId(request.sessionId)) + if (session === undefined) return undefined + + const afterSeq = acceptedThrough(session) + const snapshot = session.events + const throughSeq = snapshot.length - 1 + if (throughSeq < 0) return undefined + const suffix = snapshot.slice(afterSeq + 1) + const value: DeepSeekSessionLogExtension = { + version: 1, + session: session.header, + afterSeq, + throughSeq, + events: packSessionEvents(suffix, requestMessages(request.body)), + } + return { + value, + accept: () => { + session.append('session-log-deepseek/accepted', { sessionId: session.id, throughSeq }) + // TODO: Add an immediate lightweight checkpoint if duplicate replay after a 2xx crash window becomes unacceptable. + }, + } + }, + }) +} diff --git a/packages/session/session-log-deepseek/src/invariant.ts b/packages/session/session-log-deepseek/src/invariant.ts new file mode 100644 index 0000000000..8e0a84db60 --- /dev/null +++ b/packages/session/session-log-deepseek/src/invariant.ts @@ -0,0 +1,58 @@ +/** Package-owned invariants for DeepSeek session-log acceptance watermarks. */ + +import type { Context } from '@deepseek-ai/cordis' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' +import type {} from './types.ts' + +const PACKAGE_NAME = '@deepseek-ai/dsh-session-log-deepseek' + +/** Cordis companion plugin name. */ +export const name = 'session-log-deepseek-invariant' +/** Service required before the companion can reserve package ownership. */ +export const inject = ['invariants'] + +/** Validate one acceptance watermark against its containing event and session. */ +function validateAccepted(session: Session, event: SessionEvent<'session-log-deepseek/accepted'>, fail: InvariantFailure): void { + const { sessionId, throughSeq } = event.data + const inherited = session.header.parentSession !== undefined + && session.header.seedLength !== undefined + && event.seq < session.header.seedLength + if (sessionId !== session.id && !inherited) { + fail('a non-inherited session-log-deepseek/accepted event must name its containing session') + } + if (!Number.isSafeInteger(throughSeq) || throughSeq < 0 || throughSeq >= event.seq) { + fail(`session-log-deepseek/accepted throughSeq must identify an earlier event, got ${throughSeq} at seq ${event.seq}`) + } +} + +/** Validate acceptance watermarks already present in one Session. */ +function validateSession(session: Session, fail: InvariantFailure): void { + for (const event of session.events) { + if (event.type === 'session-log-deepseek/accepted') validateAccepted(session, event, fail) + } +} + +/** Validate one live session-event dispatch. */ +function validateDispatched(args: unknown[], fail: InvariantFailure): void { + const [session, event] = args as [Session, SessionEvent] + if (event.type === 'session-log-deepseek/accepted') validateAccepted(session, event, fail) +} + +/** Install validation for restored, newly created, and newly appended watermarks. */ +const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => { + const validateExisting = (session: Session): void => { validateSession(session, fail) } + ctx.sessions.list().forEach(validateExisting) + ctx.on('session/created', validateExisting, { global: true }) + ctx.on('internal/dispatch', (_mode, eventName, args) => { + if (eventName === 'session/event') validateDispatched(args, fail) + }, { global: true }) +}, { inject: ['sessions'] }) + +/** + * Register this package's invariant companion. + * @param ctx - Cordis context carrying the invariant service. + * @returns the installed registration's disposer after setup succeeds. + */ +export const apply = (ctx: Context): Promise<() => void> => + Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) diff --git a/packages/session/session-log-deepseek/src/types.ts b/packages/session/session-log-deepseek/src/types.ts new file mode 100644 index 0000000000..25d9879f41 --- /dev/null +++ b/packages/session/session-log-deepseek/src/types.ts @@ -0,0 +1,61 @@ +/** Wire types for lossless incremental DeepSeek session-log upload. */ + +import type { JsonValue, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' + +/** Path from one DeepSeek wire message root to a string value. */ +export type DeepSeekMessageStringPath = readonly (string | number)[] + +/** Exact half-open UTF-8 slice of one string in the containing request's messages. */ +export interface DeepSeekMessageStringSlice { + readonly messageIndex: number + readonly path: DeepSeekMessageStringPath + readonly utf8Start: number + readonly utf8End: number +} + +/** One literal or request-relative fragment of a packed JSON string. */ +export type PackedJsonStringPart = + | { readonly kind: 'literal'; readonly value: string } + | { readonly kind: 'message-slice'; readonly value: DeepSeekMessageStringSlice } + +/** Tagged JSON representation whose string leaves may cite the containing request. */ +export type PackedJsonValue = + | { readonly kind: 'literal'; readonly value: JsonValue } + | { readonly kind: 'string'; readonly parts: readonly PackedJsonStringPart[] } + | { readonly kind: 'array'; readonly items: readonly PackedJsonValue[] } + | { readonly kind: 'object'; readonly entries: readonly (readonly [string, PackedJsonValue])[] } + +/** One canonical session event, sent raw unless request-relative references reduce its encoded bytes. */ +export type EncodedSessionEvent = + | { readonly encoding: 'raw'; readonly event: SessionEvent } + | { readonly encoding: 'message-references'; readonly event: PackedJsonValue } + +/** Versioned incremental session-log field carried by an official DeepSeek request. */ +export interface DeepSeekSessionLogExtension { + readonly version: 1 + readonly session: SessionHeader + /** Highest sequence durably recorded as accepted before this request, or `-1`. */ + readonly afterSeq: number + /** Highest sequence represented by {@link events}. */ + readonly throughSeq: number + /** Contiguous canonical events from `afterSeq + 1` through `throughSeq`. */ + readonly events: readonly EncodedSessionEvent[] +} + +declare module '@deepseek-ai/dsh-deepseek-llm-api-extensions/types' { + interface DeepSeekLlmApiExtensionMap { + dsh_session_log: DeepSeekSessionLogExtension + } +} + +declare module '@deepseek-ai/dsh-session/types' { + interface SessionEventMap { + /** Records a confirmed HTTP acceptance watermark for restart-safe suffix selection. */ + 'session-log-deepseek/accepted': { + /** Session identity the accepted request carried; inherited fork markers retain the parent's id. */ + sessionId: import('@deepseek-ai/dsh-session/types').SessionId + /** Last canonical event included in the accepted request. */ + throughSeq: number + } + } +} diff --git a/packages/session/session-log-deepseek/tests/codec.spec.ts b/packages/session/session-log-deepseek/tests/codec.spec.ts new file mode 100644 index 0000000000..90d17ad9d4 --- /dev/null +++ b/packages/session/session-log-deepseek/tests/codec.spec.ts @@ -0,0 +1,139 @@ +import { describe, expect, it } from 'vitest' +import type { DeepSeekLlmApiJson } from '@deepseek-ai/dsh-deepseek-llm-api-extensions' +import type { SessionEvent } from '@deepseek-ai/dsh-session' +import { + packSessionEvents, + unpackJsonValue, + unpackSessionEvents, +} from '../src/codec.ts' +import type { PackedJsonValue } from '../src/types.ts' + +function event(data: unknown, seq = 0): SessionEvent { + return { + type: 'plugin/test', + seq, + time: 1_700_000_000_000 + seq, + data, + } as unknown as SessionEvent +} + +describe('DeepSeek session-log codec', () => { + it('replaces a raw assistant chunk with an exact slice of its assembled wire message', () => { + const delta = 'streamed assistant content '.repeat(30) + const source = { + type: 'assistant/chunk', + seq: 0, + time: 1, + data: { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: delta } }, + } as SessionEvent<'assistant/chunk'> + const messages: DeepSeekLlmApiJson[] = [{ role: 'assistant', content: `prefix:${delta}:suffix` }] + const packed = packSessionEvents([source], messages) + + expect(packed[0]?.encoding).toBe('message-references') + expect(unpackSessionEvents(packed, messages)).toEqual([source]) + }) + + it('references exact whole strings and reconstructs Unicode event values', () => { + const text = `前缀-${'shared text '.repeat(40)}-结尾` + const messages: DeepSeekLlmApiJson[] = [{ role: 'assistant', content: text }] + const source = event({ nested: [{ text }], untouched: 3 }) + const packed = packSessionEvents([source], messages) + + expect(packed[0]?.encoding).toBe('message-references') + expect(JSON.stringify(packed)).toContain('message-slice') + expect(unpackSessionEvents(packed, messages)).toEqual([source]) + }) + + it('keeps surrogate-splitting and ill-formed UTF-16 strings raw', () => { + const high = String.fromCharCode(0xD83D) + const low = String.fromCharCode(0xDE00) + const tail = 'shared-tail-'.repeat(80) + const cases = [ + { message: `😀${tail}`, logged: `${low}${tail}` }, + { message: `${tail}😀`, logged: `${tail}${high}` }, + { message: `${high}${tail}`, logged: `${high}${tail}` }, + ] + + for (const item of cases) { + const source = event({ text: item.logged }) + const messages: DeepSeekLlmApiJson[] = [{ role: 'assistant', content: item.message }] + const packed = packSessionEvents([source], messages) + expect(packed).toEqual([{ encoding: 'raw', event: source }]) + expect(unpackSessionEvents(packed, messages)).toEqual([source]) + } + }) + + it('references one large inner message string and preserves literal prefix and suffix', () => { + const shared = 'payload '.repeat(80) + const messages: DeepSeekLlmApiJson[] = [{ role: 'tool', content: shared }] + const source = event({ output: `before:${shared}:after` }) + const packed = packSessionEvents([source], messages) + + expect(packed[0]?.encoding).toBe('message-references') + expect(unpackSessionEvents(packed, messages)).toEqual([source]) + }) + + it('keeps short or unrelated events raw when references would expand them', () => { + const messages: DeepSeekLlmApiJson[] = [{ role: 'user', content: 'tiny', empty: '', parts: [null, true, 5, 'nested'] }] + const source = event({ text: 'tiny', empty: '', other: ['unrelated', true, null] }) + const packed = packSessionEvents([source], messages) + expect(packed).toEqual([{ encoding: 'raw', event: source }]) + expect(unpackSessionEvents(packed, messages)).toEqual([source]) + }) + + it('falls back to a raw event when referenced children do not reduce the complete envelope', () => { + let found = false + for (let length = 40; length <= 240; length += 1) { + const text = 'x'.repeat(length) + const source = event({ text }) + const packed = packSessionEvents([source], [{ content: text }]) + if (packed[0]?.encoding === 'raw' && length > 80) found = true + } + expect(found).toBe(true) + }) + + it('rejects missing paths, non-string targets, invalid ranges, and split UTF-8 code points', () => { + const messages: DeepSeekLlmApiJson[] = [{ content: '😀abc', nested: [5] }] + const packed = (value: object): PackedJsonValue => ({ + kind: 'string', + parts: [{ kind: 'message-slice', value: value as never }], + }) + expect(() => unpackJsonValue(packed({ messageIndex: 2, path: ['content'], utf8Start: 0, utf8End: 1 }), messages)) + .toThrow(/index 2 is absent/) + expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['missing'], utf8Start: 0, utf8End: 1 }), messages)) + .toThrow(/invalid object segment/) + expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['nested', 0], utf8Start: 0, utf8End: 1 }), messages)) + .toThrow(/does not resolve to a string/) + expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['nested', 2], utf8Start: 0, utf8End: 1 }), messages)) + .toThrow(/invalid array segment/) + expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['content'], utf8Start: -1, utf8End: 1 }), messages)) + .toThrow(/byte range is invalid/) + expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['content'], utf8Start: 0, utf8End: 1 }), messages)) + .toThrow(/splits a UTF-8 code point/) + }) + + it('decodes every packed JSON variant and rejects unknown tags', () => { + const messages: DeepSeekLlmApiJson[] = [{ content: 'abcdef' }] + const value: PackedJsonValue = { + kind: 'object', + entries: [[ + 'items', + { + kind: 'array', + items: [ + { kind: 'literal', value: 1 }, + { + kind: 'string', + parts: [ + { kind: 'literal', value: 'x' }, + { kind: 'message-slice', value: { messageIndex: 0, path: ['content'], utf8Start: 1, utf8End: 4 } }, + ], + }, + ], + }, + ]], + } + expect(unpackJsonValue(value, messages)).toEqual({ items: [1, 'xbcd'] }) + expect(() => unpackJsonValue({ kind: 'future' } as never, messages)).toThrow(/unknown packed JSON value/) + }) +}) diff --git a/packages/session/session-log-deepseek/tests/invariant.spec.ts b/packages/session/session-log-deepseek/tests/invariant.spec.ts new file mode 100644 index 0000000000..4467dede47 --- /dev/null +++ b/packages/session/session-log-deepseek/tests/invariant.spec.ts @@ -0,0 +1,95 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import InvariantRegistry, { InvariantError } from '@deepseek-ai/dsh-invariants' +import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' +import * as SessionLogInvariant from '../src/invariant.ts' +import type {} from '../src/types.ts' + +const contexts: Context[] = [] + +afterEach(async () => { + await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose())) +}) + +async function setup(): Promise { + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(SessionStore) + await ctx.plugin(InvariantRegistry, { enabled: true }) + await ctx.plugin(SessionLogInvariant) + return ctx +} + +describe('DeepSeek session-log acceptance invariant', () => { + it('accepts a watermark naming an earlier event in its containing Session', async () => { + const ctx = await setup() + const session = ctx.sessions.create(SessionId('valid')) + session.append('turn/start', { turn: 1 }) + expect(() => session.append('session-log-deepseek/accepted', { sessionId: session.id, throughSeq: 0 })) + .not.toThrow() + }) + + it('rejects a live watermark for another Session or a non-earlier sequence', async () => { + const ctx = await setup() + const wrongId = ctx.sessions.create(SessionId('wrong-id')) + wrongId.append('turn/start', { turn: 1 }) + expect(() => wrongId.append('session-log-deepseek/accepted', { + sessionId: SessionId('other'), + throughSeq: 0, + })).toThrow(expect.objectContaining>({ + code: 'INVARIANT', + packageName: '@deepseek-ai/dsh-session-log-deepseek', + })) + + const wrongSeq = ctx.sessions.create(SessionId('wrong-seq')) + wrongSeq.append('turn/start', { turn: 1 }) + expect(() => wrongSeq.append('session-log-deepseek/accepted', { + sessionId: wrongSeq.id, + throughSeq: 1, + })).toThrow(expect.objectContaining>({ + code: 'INVARIANT', + packageName: '@deepseek-ai/dsh-session-log-deepseek', + })) + }) + + it('validates existing history when the invariant loads after the Session', async () => { + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(SessionStore) + await ctx.plugin(InvariantRegistry, { enabled: true }) + const id = SessionId('late-invalid') + ctx.sessions.create(id, { seed: [ + { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } }, + { type: 'session-log-deepseek/accepted', seq: 1, time: 2, data: { sessionId: id, throughSeq: 1 } }, + ] }) + + let failure: unknown + try { + await ctx.plugin(SessionLogInvariant) + } catch (error) { + failure = error + } + expect(failure).toMatchObject>({ + code: 'INVARIANT', + packageName: '@deepseek-ai/dsh-session-log-deepseek', + }) + }) + + it('allows an inherited parent watermark inside a fork seed', async () => { + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(SessionStore) + await ctx.plugin(InvariantRegistry, { enabled: true }) + const parentId = SessionId('fork-parent') + const childId = SessionId('fork-child') + ctx.sessions.create(childId, { + seed: [ + { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } }, + { type: 'session-log-deepseek/accepted', seq: 1, time: 2, data: { sessionId: parentId, throughSeq: 0 } }, + ], + meta: { parentSession: parentId, seedLength: 2 }, + }) + + await expect(ctx.plugin(SessionLogInvariant)).resolves.toBeDefined() + }) +}) diff --git a/packages/session/session-log-deepseek/tests/upload.spec.ts b/packages/session/session-log-deepseek/tests/upload.spec.ts new file mode 100644 index 0000000000..510c983ce6 --- /dev/null +++ b/packages/session/session-log-deepseek/tests/upload.spec.ts @@ -0,0 +1,181 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import SessionStore, { Session, SessionId, type CreateSessionOptions, type SessionEvent } from '@deepseek-ai/dsh-session' +import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions' +import * as SessionLogDeepSeek from '../src/index.ts' + +const contexts: Context[] = [] +const SIGNAL = new AbortController().signal + +afterEach(async () => { + await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose())) +}) + +async function harness(id: string, seed?: readonly SessionEvent[], meta?: CreateSessionOptions['meta']): Promise<{ + ctx: Context + session: Session + disposeUpload: () => Promise +}> { + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(SessionStore) + await ctx.plugin(DeepSeekLlmApiExtensionRegistry) + const upload = ctx.plugin(SessionLogDeepSeek, { enabled: true }) + await upload + const options = seed === undefined + ? undefined + : { seed, ...meta === undefined ? {} : { meta } } + const session = ctx.sessions.create(SessionId(id), options) + return { ctx, session, disposeUpload: () => upload.dispose() } +} + +function body(text = 'x'.repeat(300)) { + return { messages: [{ role: 'user', content: text }] } +} + +describe('incremental DeepSeek session-log upload', () => { + it('does not contribute the session log under its default configuration', async () => { + const ctx = new Context() + contexts.push(ctx) + await ctx.plugin(SessionStore) + await ctx.plugin(DeepSeekLlmApiExtensionRegistry) + await ctx.plugin(SessionLogDeepSeek) + const session = ctx.sessions.create(SessionId('default-off')) + session.append('turn/start', { turn: 1 }) + + const prepared = await ctx.deepseekLlmApiExtensions.prepare({ + body: body(), signal: SIGNAL, sessionId: session.id, + }) + expect(prepared.fields).not.toHaveProperty('dsh_session_log') + }) + + it('uploads the full first prefix, records acceptance, then sends only the appended suffix', async () => { + const { ctx, session } = await harness('incremental') + session.append('turn/start', { turn: 1 }) + session.append('step/start', { turn: 1, step: 1 }) + + const first = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) + const firstPayload = first.fields.dsh_session_log + expect(firstPayload).toMatchObject({ afterSeq: -1, throughSeq: 1 }) + expect(firstPayload?.events).toHaveLength(2) + await first.accept() + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(1) + + session.append('step/end', { turn: 1, step: 1 }) + const second = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) + expect(second.fields.dsh_session_log).toMatchObject({ afterSeq: 1, throughSeq: 3 }) + expect(second.fields.dsh_session_log?.events).toHaveLength(2) + expect(second.fields.dsh_session_log?.events[0]).toMatchObject({ + encoding: 'raw', + event: { type: 'session-log-deepseek/accepted', seq: 2 }, + }) + }) + + it('reconstructs a persisted cursor and ignores an inherited parent watermark in a fork', async () => { + const first = await harness('parent') + first.session.append('turn/start', { turn: 1 }) + const prepared = await first.ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: first.session.id }) + await prepared.accept() + const seed = first.session.events + + const resumed = await harness('parent', seed) + expect(SessionLogDeepSeek.acceptedThrough(resumed.session)).toBe(0) + const resumedPayload = await resumed.ctx.deepseekLlmApiExtensions.prepare({ + body: body(), signal: SIGNAL, sessionId: resumed.session.id, + }) + expect(resumedPayload.fields.dsh_session_log?.afterSeq).toBe(0) + + const fork = await harness('child', seed, { parentSession: first.session.id, seedLength: seed.length }) + expect(SessionLogDeepSeek.acceptedThrough(fork.session)).toBe(-1) + const forkPayload = await fork.ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: fork.session.id }) + expect(forkPayload.fields.dsh_session_log).toMatchObject({ afterSeq: -1, throughSeq: fork.session.seq - 1 }) + }) + + it('takes the maximum watermark when concurrent acceptances settle out of order', async () => { + const { ctx, session } = await harness('concurrent') + session.append('turn/start', { turn: 1 }) + const earlier = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) + session.append('step/start', { turn: 1, step: 1 }) + const later = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) + + await later.accept() + await earlier.accept() + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(1) + }) + + it('folds only events appended after the cached acceptance scan', () => { + const id = SessionId('incremental-fold') + const events: SessionEvent[] = [ + { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } }, + { type: 'session-log-deepseek/accepted', seq: 1, time: 2, data: { sessionId: id, throughSeq: 0 } }, + ] + let reads = 0 + const observed = new Proxy(events, { + get(target, property, receiver) { + if (typeof property === 'string' && /^\d+$/.test(property)) reads++ + return Reflect.get(target, property, receiver) as unknown + }, + }) + const session = { id, get events() { return observed } } as unknown as Session + + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0) + expect(reads).toBe(2) + reads = 0 + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0) + expect(reads).toBe(0) + + events.push( + { type: 'step/start', seq: 2, time: 3, data: { turn: 1, step: 1 } }, + { type: 'session-log-deepseek/accepted', seq: 3, time: 4, data: { sessionId: id, throughSeq: 2 } }, + ) + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(2) + expect(reads).toBe(2) + }) + + it('omits the field for direct or stale requests and uploads the prior acceptance marker next', async () => { + const { ctx, session } = await harness('edges') + await expect(ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL })) + .resolves.toMatchObject({ fields: {} }) + await expect(ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: 'missing' })) + .resolves.toMatchObject({ fields: {} }) + await expect(ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })) + .resolves.toMatchObject({ fields: {} }) + session.append('turn/start', { turn: 1 }) + const first = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) + await first.accept() + const current = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) + expect(current.fields.dsh_session_log).toMatchObject({ + afterSeq: 0, + throughSeq: 1, + events: [{ event: { type: 'session-log-deepseek/accepted' } }], + }) + }) + + it('fails preparation when the DeepSeek body has no messages array', async () => { + const { ctx, session } = await harness('bad-body') + session.append('turn/start', { turn: 1 }) + await expect(ctx.deepseekLlmApiExtensions.prepare({ body: {}, signal: SIGNAL, sessionId: session.id })) + .rejects.toThrow(/no messages array/) + }) + + it('fails closed on a malformed persisted acceptance watermark', async () => { + const malformed = [{ + type: 'session-log-deepseek/accepted', + seq: 0, + time: 1, + data: { sessionId: 'malformed', throughSeq: 0 }, + }] as unknown as SessionEvent[] + const session = Session.create(SessionId('malformed'), malformed) + expect(() => SessionLogDeepSeek.acceptedThrough(session)).toThrow(/malformed acceptance watermark/) + }) + + it('withdraws its request field when the contributing plugin reloads', async () => { + const { ctx, session, disposeUpload } = await harness('hmr') + session.append('turn/start', { turn: 1 }) + expect((await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })).fields) + .toHaveProperty('dsh_session_log') + await disposeUpload() + expect((await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id })).fields) + .not.toHaveProperty('dsh_session_log') + }) +}) diff --git a/packages/session/session-log-deepseek/tsconfig.json b/packages/session/session-log-deepseek/tsconfig.json new file mode 100644 index 0000000000..eb85ebeddf --- /dev/null +++ b/packages/session/session-log-deepseek/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/schemastery" + }, + { + "path": "../../core/session" + }, + { + "path": "../../llm/deepseek-llm-api-extensions" + }, + { + "path": "../../runtime-diagnostics/invariants" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ccbc02bc2..de67197c50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -589,6 +589,9 @@ importers: '@deepseek-ai/dsh-session-checkpoint-policy': specifier: workspace:* version: link:../packages/session/session-checkpoint-policy + '@deepseek-ai/dsh-session-log-deepseek': + specifier: workspace:* + version: link:../packages/session/session-log-deepseek '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:* version: link:../packages/session/session-persistence-jsonl @@ -1132,6 +1135,9 @@ importers: '@deepseek-ai/dsh-session-checkpoint-policy': specifier: workspace:^ version: link:../../session/session-checkpoint-policy + '@deepseek-ai/dsh-session-log-deepseek': + specifier: workspace:^ + version: link:../../session/session-log-deepseek '@deepseek-ai/dsh-session-persistence-jsonl': specifier: workspace:^ version: link:../../session/session-persistence-jsonl @@ -5686,6 +5692,9 @@ importers: '@deepseek-ai/dsh-session': specifier: workspace:^ version: link:../../core/session + '@deepseek-ai/dsh-session-log-deepseek': + specifier: workspace:^ + version: link:../../session/session-log-deepseek '@deepseek-ai/dsh-settings': specifier: workspace:^ version: link:../../settings/settings @@ -6529,6 +6538,25 @@ importers: specifier: workspace:^ version: link:../../core/tools + packages/session/session-log-deepseek: + dependencies: + '@deepseek-ai/schemastery': + specifier: link:../../../vendor/schemastery + version: link:../../../vendor/schemastery + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:^ + version: link:../../../vendor/cordis + '@deepseek-ai/dsh-deepseek-llm-api-extensions': + specifier: workspace:^ + version: link:../../llm/deepseek-llm-api-extensions + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../runtime-diagnostics/invariants + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + packages/session/session-persistence: devDependencies: '@deepseek-ai/cordis': @@ -9108,6 +9136,9 @@ importers: '@deepseek-ai/dsh-session-checkpoint-policy': specifier: workspace:^ version: link:../../packages/session/session-checkpoint-policy + '@deepseek-ai/dsh-session-log-deepseek': + specifier: workspace:^ + version: link:../../packages/session/session-log-deepseek '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ version: link:../../packages/session/session-persistence diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index 32ae856c0d..065dd7716e 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -50,6 +50,7 @@ "@deepseek-ai/dsh-llm-deepseek": "workspace:^", "@deepseek-ai/dsh-deepseek-llm-api-extensions": "workspace:^", "@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:^", + "@deepseek-ai/dsh-session-log-deepseek": "workspace:^", "@deepseek-ai/dsh-llm-pi-ai": "workspace:^", "@deepseek-ai/dsh-llm-retry": "workspace:^", "@deepseek-ai/dsh-mcp-client": "workspace:^", diff --git a/python/sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml b/python/sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml index b14746317c..02f8cac145 100644 --- a/python/sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml +++ b/python/sdk-runtime/src/deepseek_harness_runtime/runtime/cordis.yml @@ -20,6 +20,9 @@ - id: deepseek-llm-api-extensions name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' +- id: session-log-deepseek + name: '@deepseek-ai/dsh-session-log-deepseek' + - id: plugin-package-inventory-deepseek name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek' diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 8bdd2aad1c..71530eaf58 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -120,7 +120,7 @@ const SERVICE_ROLES: ServiceRole[] = [ pkg: 'deepseek-llm-api-extensions', title: 'Official DeepSeek request extensions', mode: 'seam', - implementations: ['plugin-package-inventory-deepseek'], + implementations: ['session-log-deepseek', 'plugin-package-inventory-deepseek'], consumers: ['llm-deepseek'], note: 'Plugins prepare independent top-level fields; the official adapter merges them and commits their delivery state after HTTP acceptance.', }, diff --git a/tsconfig.host.json b/tsconfig.host.json index 3587446662..911585f448 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -146,6 +146,7 @@ { "path": "./packages/typert/loader" }, { "path": "./packages/session/session-persistence" }, { "path": "./packages/session/session-checkpoint-policy" }, + { "path": "./packages/session/session-log-deepseek" }, { "path": "./packages/session/session-persistence-jsonl" }, { "path": "./packages/session/session-persistence-sqlite" }, { "path": "./packages/session/session-projection" }, From 1c7af99c809ab6aa1570105b29c8ada900367ce2 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 22 Aug 2026 02:59:50 +0800 Subject: [PATCH 2/6] feat(deepseek): apply session upload review feedback --- ...pseek-llm-api-request-extensions.i18n.yaml | 4 +- ...-21-deepseek-llm-api-request-extensions.md | 4 +- ...-deepseek-llm-api-request-extensions.zh.md | 4 +- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 2 +- docs/config-catalog.zh.md | 2 +- .../jsonrpc-agent/session-upload.cordis.yml | 10 + .../session-upload.snapshot.cordis.yml | 23 + examples/jsonrpc-agent/tests/sdk.snapshot.ts | 3 + .../text-turn/notifications.expected.jsonl | 65 +- .../tests/snapshots/text-turn/session.jsonl | 7 +- .../session-log-deepseek/README.i18n.yaml | 4 +- .../session/session-log-deepseek/README.md | 1 + .../session/session-log-deepseek/README.zh.md | 1 + .../session/session-log-deepseek/src/codec.ts | 4 + .../session/session-log-deepseek/src/index.ts | 1 - scripts/smoke-python-runtime.py | 6 + .../advanced/result.json | 682 +++++++++++------- .../advanced/session.1.jsonl | 3 +- .../advanced/session.2.jsonl | 3 +- .../advanced/session.jsonl | 33 +- 21 files changed, 561 insertions(+), 305 deletions(-) create mode 100644 examples/jsonrpc-agent/session-upload.cordis.yml create mode 100644 examples/jsonrpc-agent/session-upload.snapshot.cordis.yml diff --git a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml index 37e85824d7..d0494dd7e8 100644 --- a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md -2026-08-21-deepseek-llm-api-request-extensions.md: 9353cfc7dcefff3156d1e0be446c6e0e85e94f73 -2026-08-21-deepseek-llm-api-request-extensions.zh.md: 77959c037b4658672639f15e0a1da69bb5a49a1f +2026-08-21-deepseek-llm-api-request-extensions.md: 4ccf913e7944c3905437ceffc60e3b0c19d305ea +2026-08-21-deepseek-llm-api-request-extensions.zh.md: 062d293ec87a6f0b1ef271f186e017be76d141f5 diff --git a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md index 9353cfc7dc..4ccf913e79 100644 --- a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md +++ b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md @@ -50,7 +50,7 @@ The process-lifetime manifest-identity cache remains separate because in-process ## Verification -Registry tests pin duplicate ownership, effect-scoped disposal, detached field values, concurrent and abortable preparation, receiver-preserving acceptance, one acceptance settlement, and failure aggregation. Session tests pin the default-off policy, explicit full-first/suffix-later delivery, incremental watermark folding, persisted restart recovery, fork identity fencing, out-of-order acceptance, exact Unicode reconstruction, invalid references, surrogate-safe raw fallback, and late invariant loading. Package-inventory tests pin default-on and explicit-off policies, host and standing-preset discovery, conflicting Loader resolution bases, manifest resolution, lifecycle filtering, and exact name/version ordering. The direct adapter mock proves pre-HTTP preparation failure, cancellation, non-2xx non-acceptance, 2xx acceptance before a later stream failure, and field collision. Keyless replay pins post-2xx extension acceptance, real Loader composition pins default package metadata plus opt-in Session upload, one real-API request mounts both shipped extensions and proves the official endpoint accepts them, and pi-ai tests retain their unchanged wire requests. +Registry tests pin duplicate ownership, effect-scoped disposal, detached field values, concurrent and abortable preparation, receiver-preserving acceptance, one acceptance settlement, and failure aggregation. Session tests pin the default-off policy, explicit full-first/suffix-later delivery, incremental watermark folding, persisted restart recovery, fork identity fencing, out-of-order acceptance, exact Unicode reconstruction, invalid references, surrogate-safe raw fallback, and late invariant loading. Package-inventory tests pin default-on and explicit-off policies, host and standing-preset discovery, conflicting Loader resolution bases, manifest resolution, lifecycle filtering, and exact name/version ordering. The direct adapter mock proves pre-HTTP preparation failure, cancellation, non-2xx non-acceptance, 2xx acceptance before a later stream failure, and field collision. Keyless replay pins post-2xx extension acceptance, and the TypeScript JSON-RPC plus Python packaged-runtime snapshots project the acceptance event through both SDKs. Real Loader composition pins default package metadata plus opt-in Session upload, one real-API request mounts both shipped extensions and proves the official endpoint accepts them, and pi-ai tests retain their unchanged wire requests. ## Alternatives considered @@ -70,7 +70,7 @@ Registry tests pin duplicate ownership, effect-scoped disposal, detached field v ## Consequences -Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. An explicit Session-log opt-in also carries the complete newly unaccepted Session suffix. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Encoding, manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata. +Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. An explicit Session-log opt-in also carries the complete newly unaccepted Session suffix. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Request-relative packing synchronously compares suffix and message strings before dispatch, so a large first upload or retry backlog can delay the event loop until candidate indexing is implemented. Encoding, manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata. The accepted-watermark event becomes part of the canonical log and is itself delivered on a later request. Crash recovery can duplicate a suffix but does not infer acceptance from assistant output or create a second local cursor store. Direct calls without a live Session omit the session field; host package inventory remains available. diff --git a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md index 77959c037b..062d293ec8 100644 --- a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md @@ -50,7 +50,7 @@ Status: implemented ## 验证 -注册表测试固定重复所有权、effect 作用域 dispose(资源释放)、分离字段值、并发且可取消的准备、保留接收者的接受操作、单次接受结算与失败聚合。会话测试固定默认关闭策略、显式启用后的首次完整/后续后缀交付、增量水位 fold、持久化重启恢复、fork 身份围栏、乱序接受、确切 Unicode 重建、无效引用、surrogate 安全原始回退与 invariant 延迟加载。插件包清单测试固定默认开启与显式关闭策略、宿主与 standing preset 发现、冲突的 Loader 解析基址、manifest 解析、生命周期过滤及确切名称/版本排序。直接适配器 mock 测试证明 HTTP 前准备失败、取消、非 2xx 不接受、2xx 在后续流失败前接受,以及字段冲突。无密钥回放会固定 2xx 后扩展接受,真实 Loader 组合会固定默认包元数据与显式启用的会话上传,一个真实 API 请求会挂载两个随附扩展并证明官方端点接受它们;pi-ai 测试保持其协议请求不变。 +注册表测试固定重复所有权、effect 作用域 dispose(资源释放)、分离字段值、并发且可取消的准备、保留接收者的接受操作、单次接受结算与失败聚合。会话测试固定默认关闭策略、显式启用后的首次完整/后续后缀交付、增量水位 fold、持久化重启恢复、fork 身份围栏、乱序接受、确切 Unicode 重建、无效引用、surrogate 安全原始回退与 invariant 延迟加载。插件包清单测试固定默认开启与显式关闭策略、宿主与 standing preset 发现、冲突的 Loader 解析基址、manifest 解析、生命周期过滤及确切名称/版本排序。直接适配器 mock 测试证明 HTTP 前准备失败、取消、非 2xx 不接受、2xx 在后续流失败前接受,以及字段冲突。无密钥回放会固定 2xx 后扩展接受,TypeScript JSON-RPC 与 Python 打包运行时快照则通过两套 SDK 投影接受事件。真实 Loader 组合会固定默认包元数据与显式启用的会话上传,一个真实 API 请求会挂载两个随附扩展并证明官方端点接受它们;pi-ai 测试保持其协议请求不变。 ## 考虑过的替代方案 @@ -70,7 +70,7 @@ Status: implemented ## 后果 -DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。显式选择启用会话日志后,请求还会携带完整的未接受会话新后缀。这些字段对模型不可见,不增加提示词 token,也不改变 KV Cache,但可能显著增大 HTTP 正文。编码、manifest 解析、字段冲突、接受记录或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。 +DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。显式选择启用会话日志后,请求还会携带完整的未接受会话新后缀。这些字段对模型不可见,不增加提示词 token,也不改变 KV Cache,但可能显著增大 HTTP 正文。请求相对打包会在派发前同步比较后缀字符串与消息字符串,因此在实现候选索引前,较大的首次上传或重试积压可能延迟事件循环。编码、manifest 解析、字段冲突、接受记录或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。 已接受水位事件会成为权威日志的一部分,并在后续请求中自行交付。崩溃恢复可能重复后缀,但不会根据 assistant 输出推断接受,也不会创建第二份本地游标存储。缺少存活会话的直接调用会省略会话字段;宿主包清单仍然可用。 diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index ce47897d2b..e545eedbff 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 77af1ffed349e40f800d0ccd14610b2b2a609a0d -config-catalog.zh.md: d444f337a0a1a9a7419cda8a7eb8774b0666a961 +config-catalog.md: 0159ddf433e3a32bb77864f4b3781d40c1815eb2 +config-catalog.zh.md: 7b0843c664d19a92b4122f92f0f283ab28e0e4a7 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 77af1ffed3..0159ddf433 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1738,7 +1738,7 @@ export interface Config { } ``` -Source: [`packages/session/session-log-deepseek/src/index.ts:25`](../packages/session/session-log-deepseek/src/index.ts) +Source: [`packages/session/session-log-deepseek/src/index.ts:24`](../packages/session/session-log-deepseek/src/index.ts) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index d444f337a0..7b0843c664 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -1740,7 +1740,7 @@ export interface Config { } ``` -来源:[`packages/session/session-log-deepseek/src/index.ts:25`](../packages/session/session-log-deepseek/src/index.ts) +来源:[`packages/session/session-log-deepseek/src/index.ts:24`](../packages/session/session-log-deepseek/src/index.ts) diff --git a/examples/jsonrpc-agent/session-upload.cordis.yml b/examples/jsonrpc-agent/session-upload.cordis.yml new file mode 100644 index 0000000000..9c8594f873 --- /dev/null +++ b/examples/jsonrpc-agent/session-upload.cordis.yml @@ -0,0 +1,10 @@ +# Snapshot recording composition that opts into the provider-specific Session-log field. +- id: base + name: '@deepseek-ai/cordis-plugin-include' + config: + path: ./cordis.yml + patches: + - id: session-log-deepseek + name: '@deepseek-ai/dsh-session-log-deepseek' + config: + enabled: true diff --git a/examples/jsonrpc-agent/session-upload.snapshot.cordis.yml b/examples/jsonrpc-agent/session-upload.snapshot.cordis.yml new file mode 100644 index 0000000000..ea04df1621 --- /dev/null +++ b/examples/jsonrpc-agent/session-upload.snapshot.cordis.yml @@ -0,0 +1,23 @@ +# Keyless counterpart of session-upload.cordis.yml: retain the opt-in while +# replacing only the live adapter with fixture-backed replay. +- id: base + name: '@deepseek-ai/cordis-plugin-include' + config: + path: ./cordis.yml + patches: + - id: session-log-deepseek + name: '@deepseek-ai/dsh-session-log-deepseek' + config: + enabled: true + - id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek' + disabled: true + - insert: + - id: llm-replay + name: '@deepseek-ai/dsh-llm-replay' + config: + providers: + - id: deepseek-official + name: DeepSeek + models: + - id: deepseek-v4-flash diff --git a/examples/jsonrpc-agent/tests/sdk.snapshot.ts b/examples/jsonrpc-agent/tests/sdk.snapshot.ts index 736537af38..28b576a8e4 100644 --- a/examples/jsonrpc-agent/tests/sdk.snapshot.ts +++ b/examples/jsonrpc-agent/tests/sdk.snapshot.ts @@ -37,6 +37,8 @@ const liveConfig = join(testsDir, '..', 'cordis.yml') const replayConfig = join(testsDir, '..', 'cordis.snapshot.yml') const minimalLiveConfig = join(testsDir, '..', 'minimal.cordis.yml') const minimalReplayConfig = join(testsDir, '..', 'minimal.snapshot.cordis.yml') +const sessionUploadLiveConfig = join(testsDir, '..', 'session-upload.cordis.yml') +const sessionUploadReplayConfig = join(testsDir, '..', 'session-upload.snapshot.cordis.yml') const runtimeBin = fileURLToPath(new URL('../../../packages/examples/jsonrpc-demo/src/bin.ts', import.meta.url)) const repoTsconfig = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)) @@ -89,6 +91,7 @@ const SCENARIOS: SdkScenario[] = [ prompt: 'Reply with exactly: SDK snapshot OK', sessionId: 'sdk-snapshot-text', children: 0, + configs: { live: sessionUploadLiveConfig, replay: sessionUploadReplayConfig }, }, { name: 'bash-tool', diff --git a/examples/jsonrpc-agent/tests/snapshots/text-turn/notifications.expected.jsonl b/examples/jsonrpc-agent/tests/snapshots/text-turn/notifications.expected.jsonl index eb60d67a0c..8d85d9bf06 100644 --- a/examples/jsonrpc-agent/tests/snapshots/text-turn/notifications.expected.jsonl +++ b/examples/jsonrpc-agent/tests/snapshots/text-turn/notifications.expected.jsonl @@ -7,36 +7,37 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":5,"time":0,"data":{"title":"Reply with exactly: SDK snapshot","messageSeqs":[4],"source":{"kind":"fallback"}}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":6,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":7,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":18,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"SD"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"K"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" snapshot"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" OK"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":23,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":24,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":25,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" do"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":26,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":27,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":28,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":29,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"SD"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":30,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"K"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":" snapshot"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":" OK"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":33,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly \"SDK snapshot OK\". Let me do that."}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":34,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"SDK snapshot OK"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":35,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1769,"outputTokens":24,"cacheReadTokens":0,"reasoningTokens":19}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":36,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":37,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly \"SDK snapshot OK\". Let me do that."},{"type":"text","text":"SDK snapshot OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":1769,"outputTokens":24,"cacheReadTokens":0,"reasoningTokens":19}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":38,"time":0,"data":{"turn":1,"step":1}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":39,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session-log-deepseek/accepted","seq":8,"time":0,"data":{"sessionId":"{{sessionId}}","throughSeq":7}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" wants"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" to"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" reply"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" with"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" exactly"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":18,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" \""}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"SD"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"K"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" snapshot"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" OK"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":23,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"\"."}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":24,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" Let"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":25,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" me"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":26,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" do"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":27,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" that"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":28,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"."}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":29,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":30,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"SD"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":"K"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":" snapshot"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":33,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":1,"text":" OK"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":34,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly \"SDK snapshot OK\". Let me do that."}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":35,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"SDK snapshot OK"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":36,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1769,"outputTokens":24,"cacheReadTokens":0,"reasoningTokens":19}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":37,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":38,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly \"SDK snapshot OK\". Let me do that."},{"type":"text","text":"SDK snapshot OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":1769,"outputTokens":24,"cacheReadTokens":0,"reasoningTokens":19}},"sourceEventSeqs":[9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":39,"time":0,"data":{"turn":1,"step":1}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":40,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}} {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}} diff --git a/examples/jsonrpc-agent/tests/snapshots/text-turn/session.jsonl b/examples/jsonrpc-agent/tests/snapshots/text-turn/session.jsonl index b36e64b01e..1ab358ca6a 100644 --- a/examples/jsonrpc-agent/tests/snapshots/text-turn/session.jsonl +++ b/examples/jsonrpc-agent/tests/snapshots/text-turn/session.jsonl @@ -7,14 +7,15 @@ {"type":"session/title","data":{"title":"Reply with exactly: SDK snapshot","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} +{"type":"session-log-deepseek/accepted","data":{"sessionId":"sdk-snapshot-text","throughSeq":7}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} -{"type":"reasoning-chunks","data":{"turn":1,"step":1,"index":0,"dt":[27,1,0,0,24,1,0,0,0,26,0,1,25,1,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," \"","SD","K"," snapshot"," OK","\"."," Let"," me"," do"," that","."]}} +{"type":"reasoning-chunks","data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," \"","SD","K"," snapshot"," OK","\"."," Let"," me"," do"," that","."]}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} -{"type":"text-chunks","data":{"turn":1,"step":1,"index":1,"dt":[1,0,0],"texts":["SD","K"," snapshot"," OK"]}} +{"type":"text-chunks","data":{"turn":1,"step":1,"index":1,"dt":[0,0,0],"texts":["SD","K"," snapshot"," OK"]}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with exactly \"SDK snapshot OK\". Let me do that."}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"SDK snapshot OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":1769,"outputTokens":24,"cacheReadTokens":0,"reasoningTokens":19}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly \"SDK snapshot OK\". Let me do that."},{"type":"text","text":"SDK snapshot OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3dd28f2f-9314-41a8-bf15-851be3652c14"},"usage":{"inputTokens":1769,"outputTokens":24,"cacheReadTokens":0,"reasoningTokens":19}},"sourceEventSeqs":[8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to reply with exactly \"SDK snapshot OK\". Let me do that."},{"type":"text","text":"SDK snapshot OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3dd28f2f-9314-41a8-bf15-851be3652c14"},"usage":{"inputTokens":1769,"outputTokens":24,"cacheReadTokens":0,"reasoningTokens":19}},"sourceEventSeqs":[9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/packages/session/session-log-deepseek/README.i18n.yaml b/packages/session/session-log-deepseek/README.i18n.yaml index dd1e2b2237..361eaeb20a 100644 --- a/packages/session/session-log-deepseek/README.i18n.yaml +++ b/packages/session/session-log-deepseek/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-log-deepseek/README.md -README.md: 5c7d077f641accf37b78cdf59ea2f4bca9cacc55 -README.zh.md: 1612441a768ea6e3bdd061fe4da07e4847f22bbe +README.md: 5b2b9d5eb087efd0fecd4040d9000a08ccc906cc +README.zh.md: c477bc7762fa6f4660e743bb1c5d0b80cdeb0c98 diff --git a/packages/session/session-log-deepseek/README.md b/packages/session/session-log-deepseek/README.md index 5c7d077f64..5b2b9d5eb0 100644 --- a/packages/session/session-log-deepseek/README.md +++ b/packages/session/session-log-deepseek/README.md @@ -47,3 +47,4 @@ None; the model-visible request prefix remains unchanged. - **Crash-window duplicates** — a 2xx followed by process loss before the acceptance watermark persists causes conservative replay on resume. - **No live Session means no field** — direct or stale-session calls have no canonical log to snapshot; explicit absence semantics remain deferred. - **No independent request-size cap** — complete delivery is fail-closed; provider rejection leaves the cursor unchanged instead of truncating the log. +- **Synchronous request-time packing** — the encoder compares suffix strings with request-message strings before dispatch. Large first uploads or retry backlogs can delay the event loop until candidate indexing is implemented. diff --git a/packages/session/session-log-deepseek/README.zh.md b/packages/session/session-log-deepseek/README.zh.md index 1612441a76..c477bc7762 100644 --- a/packages/session/session-log-deepseek/README.zh.md +++ b/packages/session/session-log-deepseek/README.zh.md @@ -47,3 +47,4 @@ DeepSeek 适配器会在 HTTP 2xx 后、消费 SSE(Server-Sent Events)正文 - **崩溃窗口重复**——2xx 后、接受水位持久化前进程丢失,会在恢复时触发保守重放。 - **缺少存活会话就没有字段**——直接调用或陈旧会话调用没有可供快照的权威日志;显式缺失语义仍暂缓处理。 - **没有独立请求大小上限**——完整交付会快速失败;提供方拒绝会保持游标不变,而非截断日志。 +- **请求时同步打包**——编码器会在派发前比较后缀字符串与请求消息字符串。在实现候选索引前,较大的首次上传或重试积压可能延迟事件循环。 diff --git a/packages/session/session-log-deepseek/src/codec.ts b/packages/session/session-log-deepseek/src/codec.ts index 4861bf0524..bf7ff8df24 100644 --- a/packages/session/session-log-deepseek/src/codec.ts +++ b/packages/session/session-log-deepseek/src/codec.ts @@ -66,10 +66,12 @@ function sliceOf(source: MessageStringSource, start: number, end: number): DeepS function packString(value: string, sources: readonly MessageStringSource[]): PackedCandidate { const literal: PackedJsonValue = { kind: 'literal', value } if (value.length === 0) return { value: literal, references: false } + const valueBytes = Buffer.byteLength(value, 'utf8') let best: PackedJsonValue | undefined let bestBytes = wireBytes(literal) for (const source of sources) { + if (source.bytes < valueBytes) continue const start = source.value.indexOf(value) if (start < 0) continue const slice = sliceOf(source, start, start + value.length) @@ -87,6 +89,7 @@ function packString(value: string, sources: readonly MessageStringSource[]): Pac if (best !== undefined) return { value: best, references: true } for (const source of sources) { + if (source.bytes > valueBytes) continue const start = value.indexOf(source.value) if (start < 0) continue const slice = sliceOf(source, 0, source.value.length) @@ -199,6 +202,7 @@ export function packSessionEvents( events: readonly SessionEvent[], messages: readonly DeepSeekLlmApiJson[], ): EncodedSessionEvent[] { + // TODO: Index message strings if large opt-in suffixes show material request-preparation latency. const sources: MessageStringSource[] = [] messages.forEach((message, messageIndex) => { collectSources(message, messageIndex, [], sources) }) sources.sort((left, right) => right.bytes - left.bytes) diff --git a/packages/session/session-log-deepseek/src/index.ts b/packages/session/session-log-deepseek/src/index.ts index 65188b535a..99ee268e20 100644 --- a/packages/session/session-log-deepseek/src/index.ts +++ b/packages/session/session-log-deepseek/src/index.ts @@ -11,7 +11,6 @@ import type { DeepSeekLlmApiJson } from '@deepseek-ai/dsh-deepseek-llm-api-exten import { SessionId, type Session, type SessionEvent } from '@deepseek-ai/dsh-session' import { packSessionEvents } from './codec.ts' import type { DeepSeekSessionLogExtension } from './types.ts' -import type {} from './types.ts' export { packSessionEvents, unpackJsonValue, unpackSessionEvents } from './codec.ts' export type * from './types.ts' diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 30de8d1c87..df0d78f811 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -85,6 +85,12 @@ RUNTIME_CONTEXT_PREFIX = "Current runtime context" CUSTOM_CORDIS = """\ - id: sdk-jsonrpc-server name: '@deepseek-ai/dsh-sdk-jsonrpc-server' +- id: deepseek-llm-api-extensions + name: '@deepseek-ai/dsh-deepseek-llm-api-extensions' +- id: session-log-deepseek + name: '@deepseek-ai/dsh-session-log-deepseek' + config: + enabled: true - id: agent-core name: '@deepseek-ai/dsh-agent-spine-demo' config: diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/result.json b/scripts/snapshots/python-sdk-single-exe/advanced/result.json index 2548efb6cb..ff3e3ba3ae 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json +++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json @@ -134,9 +134,18 @@ } }, { - "type": "assistant/chunk", + "type": "session-log-deepseek/accepted", "seq": 8, "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 7 + } + }, + { + "type": "assistant/chunk", + "seq": 9, + "time": 0, "data": { "turn": 1, "step": 1, @@ -149,7 +158,7 @@ }, { "type": "assistant/chunk", - "seq": 9, + "seq": 10, "time": 0, "data": { "turn": 1, @@ -165,7 +174,7 @@ }, { "type": "assistant/chunk", - "seq": 10, + "seq": 11, "time": 0, "data": { "turn": 1, @@ -184,7 +193,7 @@ }, { "type": "assistant/chunk", - "seq": 11, + "seq": 12, "time": 0, "data": { "turn": 1, @@ -200,7 +209,7 @@ }, { "type": "assistant/chunk", - "seq": 12, + "seq": 13, "time": 0, "data": { "turn": 1, @@ -215,7 +224,7 @@ }, { "type": "assistant/message", - "seq": 13, + "seq": 14, "time": 0, "data": { "turn": 1, @@ -243,17 +252,17 @@ } }, "sourceEventSeqs": [ - 8, 9, 10, 11, - 12 + 12, + 13 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 14, + "seq": 15, "time": 0, "data": { "turn": 1, @@ -265,7 +274,7 @@ }, { "type": "tool/result", - "seq": 15, + "seq": 16, "time": 0, "data": { "turn": 1, @@ -297,13 +306,13 @@ } }, "sourceEventSeqs": [ - 14 + 15 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 16, + "seq": 17, "time": 0, "data": { "turn": 1, @@ -312,16 +321,25 @@ }, { "type": "step/start", - "seq": 17, + "seq": 18, "time": 0, "data": { "turn": 1, "step": 2 } }, + { + "type": "session-log-deepseek/accepted", + "seq": 19, + "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 18 + } + }, { "type": "assistant/chunk", - "seq": 18, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -335,7 +353,7 @@ }, { "type": "assistant/chunk", - "seq": 19, + "seq": 21, "time": 0, "data": { "turn": 1, @@ -351,7 +369,7 @@ }, { "type": "assistant/chunk", - "seq": 20, + "seq": 22, "time": 0, "data": { "turn": 1, @@ -370,7 +388,7 @@ }, { "type": "assistant/chunk", - "seq": 21, + "seq": 23, "time": 0, "data": { "turn": 1, @@ -386,7 +404,7 @@ }, { "type": "assistant/chunk", - "seq": 22, + "seq": 24, "time": 0, "data": { "turn": 1, @@ -401,7 +419,7 @@ }, { "type": "assistant/message", - "seq": 23, + "seq": 25, "time": 0, "data": { "turn": 1, @@ -429,17 +447,17 @@ } }, "sourceEventSeqs": [ - 18, - 19, 20, 21, - 22 + 22, + 23, + 24 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 24, + "seq": 26, "time": 0, "data": { "turn": 1, @@ -451,7 +469,7 @@ }, { "type": "tool/result", - "seq": 25, + "seq": 27, "time": 0, "data": { "turn": 1, @@ -484,13 +502,13 @@ } }, "sourceEventSeqs": [ - 24 + 26 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 26, + "seq": 28, "time": 0, "data": { "turn": 1, @@ -499,7 +517,7 @@ }, { "type": "step/start", - "seq": 27, + "seq": 29, "time": 0, "data": { "turn": 1, @@ -508,7 +526,7 @@ }, { "type": "request/header", - "seq": 28, + "seq": 30, "time": 0, "data": { "header": { @@ -543,9 +561,18 @@ "reason": "change" } }, + { + "type": "session-log-deepseek/accepted", + "seq": 31, + "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 30 + } + }, { "type": "assistant/chunk", - "seq": 29, + "seq": 32, "time": 0, "data": { "turn": 1, @@ -559,7 +586,7 @@ }, { "type": "assistant/chunk", - "seq": 30, + "seq": 33, "time": 0, "data": { "turn": 1, @@ -575,7 +602,7 @@ }, { "type": "assistant/chunk", - "seq": 31, + "seq": 34, "time": 0, "data": { "turn": 1, @@ -594,7 +621,7 @@ }, { "type": "assistant/chunk", - "seq": 32, + "seq": 35, "time": 0, "data": { "turn": 1, @@ -610,7 +637,7 @@ }, { "type": "assistant/chunk", - "seq": 33, + "seq": 36, "time": 0, "data": { "turn": 1, @@ -625,7 +652,7 @@ }, { "type": "assistant/message", - "seq": 34, + "seq": 37, "time": 0, "data": { "turn": 1, @@ -653,17 +680,17 @@ } }, "sourceEventSeqs": [ - 29, - 30, - 31, 32, - 33 + 33, + 34, + 35, + 36 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 35, + "seq": 38, "time": 0, "data": { "turn": 1, @@ -675,7 +702,7 @@ }, { "type": "tool/code-dispatch-start", - "seq": 36, + "seq": 39, "time": 0, "data": { "rootCallId": "advanced-code", @@ -689,7 +716,7 @@ }, { "type": "tool/code-dispatch", - "seq": 37, + "seq": 40, "time": 0, "data": { "rootCallId": "advanced-code", @@ -710,7 +737,7 @@ }, { "type": "tool/result", - "seq": 38, + "seq": 41, "time": 0, "data": { "turn": 1, @@ -738,13 +765,13 @@ } }, "sourceEventSeqs": [ - 35 + 38 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 39, + "seq": 42, "time": 0, "data": { "turn": 1, @@ -753,16 +780,25 @@ }, { "type": "step/start", - "seq": 40, + "seq": 43, "time": 0, "data": { "turn": 1, "step": 4 } }, + { + "type": "session-log-deepseek/accepted", + "seq": 44, + "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 43 + } + }, { "type": "assistant/chunk", - "seq": 41, + "seq": 45, "time": 0, "data": { "turn": 1, @@ -776,7 +812,7 @@ }, { "type": "assistant/chunk", - "seq": 42, + "seq": 46, "time": 0, "data": { "turn": 1, @@ -792,7 +828,7 @@ }, { "type": "assistant/chunk", - "seq": 43, + "seq": 47, "time": 0, "data": { "turn": 1, @@ -811,7 +847,7 @@ }, { "type": "assistant/chunk", - "seq": 44, + "seq": 48, "time": 0, "data": { "turn": 1, @@ -827,7 +863,7 @@ }, { "type": "assistant/chunk", - "seq": 45, + "seq": 49, "time": 0, "data": { "turn": 1, @@ -842,7 +878,7 @@ }, { "type": "assistant/message", - "seq": 46, + "seq": 50, "time": 0, "data": { "turn": 1, @@ -870,17 +906,17 @@ } }, "sourceEventSeqs": [ - 41, - 42, - 43, - 44, - 45 + 45, + 46, + 47, + 48, + 49 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 47, + "seq": 51, "time": 0, "data": { "turn": 1, @@ -892,7 +928,7 @@ }, { "type": "tool/result", - "seq": 48, + "seq": 52, "time": 0, "data": { "turn": 1, @@ -920,13 +956,13 @@ } }, "sourceEventSeqs": [ - 47 + 51 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 49, + "seq": 53, "time": 0, "data": { "turn": 1, @@ -935,16 +971,25 @@ }, { "type": "step/start", - "seq": 50, + "seq": 54, "time": 0, "data": { "turn": 1, "step": 5 } }, + { + "type": "session-log-deepseek/accepted", + "seq": 55, + "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 54 + } + }, { "type": "assistant/chunk", - "seq": 51, + "seq": 56, "time": 0, "data": { "turn": 1, @@ -958,7 +1003,7 @@ }, { "type": "assistant/chunk", - "seq": 52, + "seq": 57, "time": 0, "data": { "turn": 1, @@ -974,7 +1019,7 @@ }, { "type": "assistant/chunk", - "seq": 53, + "seq": 58, "time": 0, "data": { "turn": 1, @@ -993,7 +1038,7 @@ }, { "type": "assistant/chunk", - "seq": 54, + "seq": 59, "time": 0, "data": { "turn": 1, @@ -1009,7 +1054,7 @@ }, { "type": "assistant/chunk", - "seq": 55, + "seq": 60, "time": 0, "data": { "turn": 1, @@ -1024,7 +1069,7 @@ }, { "type": "assistant/message", - "seq": 56, + "seq": 61, "time": 0, "data": { "turn": 1, @@ -1052,17 +1097,17 @@ } }, "sourceEventSeqs": [ - 51, - 52, - 53, - 54, - 55 + 56, + 57, + 58, + 59, + 60 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 57, + "seq": 62, "time": 0, "data": { "turn": 1, @@ -1074,7 +1119,7 @@ }, { "type": "tool-workflow/run-start", - "seq": 58, + "seq": 63, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1083,7 +1128,7 @@ }, { "type": "tool-workflow/agent-start", - "seq": 59, + "seq": 64, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1095,7 +1140,7 @@ }, { "type": "tool-workflow/agent-end", - "seq": 60, + "seq": 65, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1105,7 +1150,7 @@ }, { "type": "tool-workflow/run-end", - "seq": 61, + "seq": 66, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -1114,7 +1159,7 @@ }, { "type": "tool/result", - "seq": 62, + "seq": 67, "time": 0, "data": { "turn": 1, @@ -1142,13 +1187,13 @@ } }, "sourceEventSeqs": [ - 57 + 62 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 63, + "seq": 68, "time": 0, "data": { "turn": 1, @@ -1157,16 +1202,25 @@ }, { "type": "step/start", - "seq": 64, + "seq": 69, "time": 0, "data": { "turn": 1, "step": 6 } }, + { + "type": "session-log-deepseek/accepted", + "seq": 70, + "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 69 + } + }, { "type": "assistant/chunk", - "seq": 65, + "seq": 71, "time": 0, "data": { "turn": 1, @@ -1180,7 +1234,7 @@ }, { "type": "assistant/chunk", - "seq": 66, + "seq": 72, "time": 0, "data": { "turn": 1, @@ -1196,7 +1250,7 @@ }, { "type": "assistant/chunk", - "seq": 67, + "seq": 73, "time": 0, "data": { "turn": 1, @@ -1215,7 +1269,7 @@ }, { "type": "assistant/chunk", - "seq": 68, + "seq": 74, "time": 0, "data": { "turn": 1, @@ -1231,7 +1285,7 @@ }, { "type": "assistant/chunk", - "seq": 69, + "seq": 75, "time": 0, "data": { "turn": 1, @@ -1246,7 +1300,7 @@ }, { "type": "assistant/message", - "seq": 70, + "seq": 76, "time": 0, "data": { "turn": 1, @@ -1274,17 +1328,17 @@ } }, "sourceEventSeqs": [ - 65, - 66, - 67, - 68, - 69 + 71, + 72, + 73, + 74, + 75 ], "surfaceOp": "append" }, { "type": "tool/call", - "seq": 71, + "seq": 77, "time": 0, "data": { "turn": 1, @@ -1296,7 +1350,7 @@ }, { "type": "tool/result", - "seq": 72, + "seq": 78, "time": 0, "data": { "turn": 1, @@ -1324,13 +1378,13 @@ } }, "sourceEventSeqs": [ - 71 + 77 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 73, + "seq": 79, "time": 0, "data": { "turn": 1, @@ -1339,7 +1393,7 @@ }, { "type": "step/start", - "seq": 74, + "seq": 80, "time": 0, "data": { "turn": 1, @@ -1348,7 +1402,7 @@ }, { "type": "request/header", - "seq": 75, + "seq": 81, "time": 0, "data": { "header": { @@ -1382,9 +1436,18 @@ "reason": "change" } }, + { + "type": "session-log-deepseek/accepted", + "seq": 82, + "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 81 + } + }, { "type": "assistant/chunk", - "seq": 76, + "seq": 83, "time": 0, "data": { "turn": 1, @@ -1398,7 +1461,7 @@ }, { "type": "assistant/chunk", - "seq": 77, + "seq": 84, "time": 0, "data": { "turn": 1, @@ -1412,7 +1475,7 @@ }, { "type": "assistant/chunk", - "seq": 78, + "seq": 85, "time": 0, "data": { "turn": 1, @@ -1429,7 +1492,7 @@ }, { "type": "assistant/chunk", - "seq": 79, + "seq": 86, "time": 0, "data": { "turn": 1, @@ -1445,7 +1508,7 @@ }, { "type": "assistant/chunk", - "seq": 80, + "seq": 87, "time": 0, "data": { "turn": 1, @@ -1460,7 +1523,7 @@ }, { "type": "assistant/message", - "seq": 81, + "seq": 88, "time": 0, "data": { "turn": 1, @@ -1486,17 +1549,17 @@ } }, "sourceEventSeqs": [ - 76, - 77, - 78, - 79, - 80 + 83, + 84, + 85, + 86, + 87 ], "surfaceOp": "append" }, { "type": "step/end", - "seq": 82, + "seq": 89, "time": 0, "data": { "turn": 1, @@ -1505,7 +1568,7 @@ }, { "type": "turn/end", - "seq": 83, + "seq": 90, "time": 0, "data": { "turn": 1, @@ -1707,9 +1770,24 @@ "payload": { "sessionId": "{{parent}}", "event": { - "type": "assistant/chunk", + "type": "session-log-deepseek/accepted", "seq": 8, "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 7 + } + } + } + }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "assistant/chunk", + "seq": 9, + "time": 0, "data": { "turn": 1, "step": 1, @@ -1728,7 +1806,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 9, + "seq": 10, "time": 0, "data": { "turn": 1, @@ -1750,7 +1828,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 10, + "seq": 11, "time": 0, "data": { "turn": 1, @@ -1775,7 +1853,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 11, + "seq": 12, "time": 0, "data": { "turn": 1, @@ -1797,7 +1875,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 12, + "seq": 13, "time": 0, "data": { "turn": 1, @@ -1818,7 +1896,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 13, + "seq": 14, "time": 0, "data": { "turn": 1, @@ -1846,11 +1924,11 @@ } }, "sourceEventSeqs": [ - 8, 9, 10, 11, - 12 + 12, + 13 ], "surfaceOp": "append" } @@ -1862,7 +1940,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 14, + "seq": 15, "time": 0, "data": { "turn": 1, @@ -1880,7 +1958,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 15, + "seq": 16, "time": 0, "data": { "turn": 1, @@ -1912,7 +1990,7 @@ } }, "sourceEventSeqs": [ - 14 + 15 ], "surfaceOp": "append" } @@ -1924,7 +2002,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 16, + "seq": 17, "time": 0, "data": { "turn": 1, @@ -1939,7 +2017,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 17, + "seq": 18, "time": 0, "data": { "turn": 1, @@ -1948,13 +2026,28 @@ } } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "session-log-deepseek/accepted", + "seq": 19, + "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 18 + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 18, + "seq": 20, "time": 0, "data": { "turn": 1, @@ -1974,7 +2067,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 19, + "seq": 21, "time": 0, "data": { "turn": 1, @@ -1996,7 +2089,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 20, + "seq": 22, "time": 0, "data": { "turn": 1, @@ -2021,7 +2114,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 21, + "seq": 23, "time": 0, "data": { "turn": 1, @@ -2043,7 +2136,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 22, + "seq": 24, "time": 0, "data": { "turn": 1, @@ -2064,7 +2157,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 23, + "seq": 25, "time": 0, "data": { "turn": 1, @@ -2092,11 +2185,11 @@ } }, "sourceEventSeqs": [ - 18, - 19, 20, 21, - 22 + 22, + 23, + 24 ], "surfaceOp": "append" } @@ -2108,7 +2201,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 24, + "seq": 26, "time": 0, "data": { "turn": 1, @@ -2126,7 +2219,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 25, + "seq": 27, "time": 0, "data": { "turn": 1, @@ -2159,7 +2252,7 @@ } }, "sourceEventSeqs": [ - 24 + 26 ], "surfaceOp": "append" } @@ -2171,7 +2264,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 26, + "seq": 28, "time": 0, "data": { "turn": 1, @@ -2186,7 +2279,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 27, + "seq": 29, "time": 0, "data": { "turn": 1, @@ -2201,7 +2294,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/header", - "seq": 28, + "seq": 30, "time": 0, "data": { "header": { @@ -2238,13 +2331,28 @@ } } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "session-log-deepseek/accepted", + "seq": 31, + "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 30 + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 29, + "seq": 32, "time": 0, "data": { "turn": 1, @@ -2264,7 +2372,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 30, + "seq": 33, "time": 0, "data": { "turn": 1, @@ -2286,7 +2394,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 31, + "seq": 34, "time": 0, "data": { "turn": 1, @@ -2311,7 +2419,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 32, + "seq": 35, "time": 0, "data": { "turn": 1, @@ -2333,7 +2441,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 33, + "seq": 36, "time": 0, "data": { "turn": 1, @@ -2354,7 +2462,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 34, + "seq": 37, "time": 0, "data": { "turn": 1, @@ -2382,11 +2490,11 @@ } }, "sourceEventSeqs": [ - 29, - 30, - 31, 32, - 33 + 33, + 34, + 35, + 36 ], "surfaceOp": "append" } @@ -2398,7 +2506,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 35, + "seq": 38, "time": 0, "data": { "turn": 1, @@ -2416,7 +2524,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/code-dispatch-start", - "seq": 36, + "seq": 39, "time": 0, "data": { "rootCallId": "advanced-code", @@ -2436,7 +2544,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/code-dispatch", - "seq": 37, + "seq": 40, "time": 0, "data": { "rootCallId": "advanced-code", @@ -2463,7 +2571,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 38, + "seq": 41, "time": 0, "data": { "turn": 1, @@ -2491,7 +2599,7 @@ } }, "sourceEventSeqs": [ - 35 + 38 ], "surfaceOp": "append" } @@ -2503,7 +2611,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 39, + "seq": 42, "time": 0, "data": { "turn": 1, @@ -2518,7 +2626,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 40, + "seq": 43, "time": 0, "data": { "turn": 1, @@ -2527,13 +2635,28 @@ } } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "session-log-deepseek/accepted", + "seq": 44, + "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 43 + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 41, + "seq": 45, "time": 0, "data": { "turn": 1, @@ -2553,7 +2676,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 42, + "seq": 46, "time": 0, "data": { "turn": 1, @@ -2575,7 +2698,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 43, + "seq": 47, "time": 0, "data": { "turn": 1, @@ -2600,7 +2723,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 44, + "seq": 48, "time": 0, "data": { "turn": 1, @@ -2622,7 +2745,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 45, + "seq": 49, "time": 0, "data": { "turn": 1, @@ -2643,7 +2766,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 46, + "seq": 50, "time": 0, "data": { "turn": 1, @@ -2671,11 +2794,11 @@ } }, "sourceEventSeqs": [ - 41, - 42, - 43, - 44, - 45 + 45, + 46, + 47, + 48, + 49 ], "surfaceOp": "append" } @@ -2687,7 +2810,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 47, + "seq": 51, "time": 0, "data": { "turn": 1, @@ -2948,9 +3071,24 @@ "payload": { "sessionId": "{{child-1}}", "event": { - "type": "assistant/chunk", + "type": "session-log-deepseek/accepted", "seq": 10, "time": 0, + "data": { + "sessionId": "{{child-1}}", + "throughSeq": 9 + } + } + } + }, + { + "method": "session.event", + "payload": { + "sessionId": "{{child-1}}", + "event": { + "type": "assistant/chunk", + "seq": 11, + "time": 0, "data": { "turn": 1, "step": 1, @@ -2969,7 +3107,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 11, + "seq": 12, "time": 0, "data": { "turn": 1, @@ -2989,7 +3127,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 12, + "seq": 13, "time": 0, "data": { "turn": 1, @@ -3012,7 +3150,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 13, + "seq": 14, "time": 0, "data": { "turn": 1, @@ -3034,7 +3172,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/chunk", - "seq": 14, + "seq": 15, "time": 0, "data": { "turn": 1, @@ -3055,7 +3193,7 @@ "sessionId": "{{child-1}}", "event": { "type": "assistant/message", - "seq": 15, + "seq": 16, "time": 0, "data": { "turn": 1, @@ -3081,11 +3219,11 @@ } }, "sourceEventSeqs": [ - 10, 11, 12, 13, - 14 + 14, + 15 ], "surfaceOp": "append" } @@ -3097,7 +3235,7 @@ "sessionId": "{{child-1}}", "event": { "type": "step/end", - "seq": 16, + "seq": 17, "time": 0, "data": { "turn": 1, @@ -3112,7 +3250,7 @@ "sessionId": "{{child-1}}", "event": { "type": "turn/end", - "seq": 17, + "seq": 18, "time": 0, "data": { "turn": 1, @@ -3153,7 +3291,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 48, + "seq": 52, "time": 0, "data": { "turn": 1, @@ -3181,7 +3319,7 @@ } }, "sourceEventSeqs": [ - 47 + 51 ], "surfaceOp": "append" } @@ -3193,7 +3331,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 49, + "seq": 53, "time": 0, "data": { "turn": 1, @@ -3208,7 +3346,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 50, + "seq": 54, "time": 0, "data": { "turn": 1, @@ -3217,13 +3355,28 @@ } } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "session-log-deepseek/accepted", + "seq": 55, + "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 54 + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 51, + "seq": 56, "time": 0, "data": { "turn": 1, @@ -3243,7 +3396,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 52, + "seq": 57, "time": 0, "data": { "turn": 1, @@ -3265,7 +3418,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 53, + "seq": 58, "time": 0, "data": { "turn": 1, @@ -3290,7 +3443,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 54, + "seq": 59, "time": 0, "data": { "turn": 1, @@ -3312,7 +3465,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 55, + "seq": 60, "time": 0, "data": { "turn": 1, @@ -3333,7 +3486,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 56, + "seq": 61, "time": 0, "data": { "turn": 1, @@ -3361,11 +3514,11 @@ } }, "sourceEventSeqs": [ - 51, - 52, - 53, - 54, - 55 + 56, + 57, + 58, + 59, + 60 ], "surfaceOp": "append" } @@ -3377,7 +3530,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 57, + "seq": 62, "time": 0, "data": { "turn": 1, @@ -3395,7 +3548,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/run-start", - "seq": 58, + "seq": 63, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -3653,7 +3806,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/agent-start", - "seq": 59, + "seq": 64, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -3670,9 +3823,24 @@ "payload": { "sessionId": "{{child-2}}", "event": { - "type": "assistant/chunk", + "type": "session-log-deepseek/accepted", "seq": 10, "time": 0, + "data": { + "sessionId": "{{child-2}}", + "throughSeq": 9 + } + } + } + }, + { + "method": "session.event", + "payload": { + "sessionId": "{{child-2}}", + "event": { + "type": "assistant/chunk", + "seq": 11, + "time": 0, "data": { "turn": 1, "step": 1, @@ -3691,7 +3859,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 11, + "seq": 12, "time": 0, "data": { "turn": 1, @@ -3711,7 +3879,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 12, + "seq": 13, "time": 0, "data": { "turn": 1, @@ -3734,7 +3902,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 13, + "seq": 14, "time": 0, "data": { "turn": 1, @@ -3756,7 +3924,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/chunk", - "seq": 14, + "seq": 15, "time": 0, "data": { "turn": 1, @@ -3777,7 +3945,7 @@ "sessionId": "{{child-2}}", "event": { "type": "assistant/message", - "seq": 15, + "seq": 16, "time": 0, "data": { "turn": 1, @@ -3803,11 +3971,11 @@ } }, "sourceEventSeqs": [ - 10, 11, 12, 13, - 14 + 14, + 15 ], "surfaceOp": "append" } @@ -3819,7 +3987,7 @@ "sessionId": "{{child-2}}", "event": { "type": "step/end", - "seq": 16, + "seq": 17, "time": 0, "data": { "turn": 1, @@ -3834,7 +4002,7 @@ "sessionId": "{{child-2}}", "event": { "type": "turn/end", - "seq": 17, + "seq": 18, "time": 0, "data": { "turn": 1, @@ -3875,7 +4043,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/agent-end", - "seq": 60, + "seq": 65, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -3891,7 +4059,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool-workflow/run-end", - "seq": 61, + "seq": 66, "time": 0, "data": { "runId": "{{workflow-run}}", @@ -3906,7 +4074,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 62, + "seq": 67, "time": 0, "data": { "turn": 1, @@ -3934,7 +4102,7 @@ } }, "sourceEventSeqs": [ - 57 + 62 ], "surfaceOp": "append" } @@ -3946,7 +4114,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 63, + "seq": 68, "time": 0, "data": { "turn": 1, @@ -3961,7 +4129,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 64, + "seq": 69, "time": 0, "data": { "turn": 1, @@ -3970,13 +4138,28 @@ } } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "session-log-deepseek/accepted", + "seq": 70, + "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 69 + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 65, + "seq": 71, "time": 0, "data": { "turn": 1, @@ -3996,7 +4179,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 66, + "seq": 72, "time": 0, "data": { "turn": 1, @@ -4018,7 +4201,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 67, + "seq": 73, "time": 0, "data": { "turn": 1, @@ -4043,7 +4226,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 68, + "seq": 74, "time": 0, "data": { "turn": 1, @@ -4065,7 +4248,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 69, + "seq": 75, "time": 0, "data": { "turn": 1, @@ -4086,7 +4269,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 70, + "seq": 76, "time": 0, "data": { "turn": 1, @@ -4114,11 +4297,11 @@ } }, "sourceEventSeqs": [ - 65, - 66, - 67, - 68, - 69 + 71, + 72, + 73, + 74, + 75 ], "surfaceOp": "append" } @@ -4130,7 +4313,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/call", - "seq": 71, + "seq": 77, "time": 0, "data": { "turn": 1, @@ -4148,7 +4331,7 @@ "sessionId": "{{parent}}", "event": { "type": "tool/result", - "seq": 72, + "seq": 78, "time": 0, "data": { "turn": 1, @@ -4176,7 +4359,7 @@ } }, "sourceEventSeqs": [ - 71 + 77 ], "surfaceOp": "append" } @@ -4188,7 +4371,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 73, + "seq": 79, "time": 0, "data": { "turn": 1, @@ -4203,7 +4386,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/start", - "seq": 74, + "seq": 80, "time": 0, "data": { "turn": 1, @@ -4218,7 +4401,7 @@ "sessionId": "{{parent}}", "event": { "type": "request/header", - "seq": 75, + "seq": 81, "time": 0, "data": { "header": { @@ -4254,13 +4437,28 @@ } } }, + { + "method": "session.event", + "payload": { + "sessionId": "{{parent}}", + "event": { + "type": "session-log-deepseek/accepted", + "seq": 82, + "time": 0, + "data": { + "sessionId": "{{parent}}", + "throughSeq": 81 + } + } + } + }, { "method": "session.event", "payload": { "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 76, + "seq": 83, "time": 0, "data": { "turn": 1, @@ -4280,7 +4478,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 77, + "seq": 84, "time": 0, "data": { "turn": 1, @@ -4300,7 +4498,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 78, + "seq": 85, "time": 0, "data": { "turn": 1, @@ -4323,7 +4521,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 79, + "seq": 86, "time": 0, "data": { "turn": 1, @@ -4345,7 +4543,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/chunk", - "seq": 80, + "seq": 87, "time": 0, "data": { "turn": 1, @@ -4366,7 +4564,7 @@ "sessionId": "{{parent}}", "event": { "type": "assistant/message", - "seq": 81, + "seq": 88, "time": 0, "data": { "turn": 1, @@ -4392,11 +4590,11 @@ } }, "sourceEventSeqs": [ - 76, - 77, - 78, - 79, - 80 + 83, + 84, + 85, + 86, + 87 ], "surfaceOp": "append" } @@ -4408,7 +4606,7 @@ "sessionId": "{{parent}}", "event": { "type": "step/end", - "seq": 82, + "seq": 89, "time": 0, "data": { "turn": 1, @@ -4423,7 +4621,7 @@ "sessionId": "{{parent}}", "event": { "type": "turn/end", - "seq": 83, + "seq": 90, "time": 0, "data": { "turn": 1, diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl index a720e5ab22..29496835de 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl @@ -9,11 +9,12 @@ {"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} +{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{child-1}}","throughSeq":9}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl index 0234662433..149798c850 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl @@ -9,11 +9,12 @@ {"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} +{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{child-2}}","throughSeq":9}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[11,12,13,14,15],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl index 908591d00f..c8820f9822 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl @@ -7,79 +7,86 @@ {"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} +{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":7}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-define","name":"cordis_define","argumentsDelta":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[15],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} +{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":18}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-run","name":"cordis_run","argumentsDelta":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[20,21,22,23,24],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}} -{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[24],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-run"},"content":[{"type":"tool-result","toolCallId":"advanced-run","content":[{"type":"text","text":"snap-1/pkg-1 is running (run-1)."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1","pluginRunId":"run-1"}},"sourceEventSeqs":[26],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"change"}} +{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":30}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-code","name":"run_code","argumentsDelta":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[32,33,34,35,36],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}} {"type":"tool/code-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21}}} {"type":"tool/code-dispatch","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"snapshot_double","arguments":{"value":21},"isError":false,"content":[{"type":"text","text":"42"}]}} -{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[38],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} +{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":43}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-direct-child","name":"subagent","argumentsDelta":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[41,42,43,44,45],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[45,46,47,48,49],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":4,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}} -{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[47],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[51],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} +{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":54}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-workflow","name":"workflow","argumentsDelta":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[51,52,53,54,55],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[56,57,58,59,60],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":5,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}} {"type":"tool-workflow/run-start","data":{"runId":"{{workflow-run}}","name":"advanced-exe-snapshot"}} {"type":"tool-workflow/agent-start","data":{"runId":"{{workflow-run}}","seq":1,"label":"workflow-child","phase":"Delegate","childId":"{{child-2}}"}} {"type":"tool-workflow/agent-end","data":{"runId":"{{workflow-run}}","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","data":{"runId":"{{workflow-run}}","stopReason":"completed"}} -{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[57],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[62],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"step/start","data":{"turn":1,"step":6}} +{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":69}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-undefine","name":"cordis_undefine","argumentsDelta":"{\"pluginId\": \"snap-1\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[65,66,67,68,69],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[71,72,73,74,75],"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":6,"callId":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}} -{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[71],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"advanced-undefine"},"content":[{"type":"tool-result","toolCallId":"advanced-undefine","content":[{"type":"text","text":"Removed dynamic Plugin snap-1 and all of its Packages."}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[77],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":6}} {"type":"step/start","data":{"turn":1,"step":7}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"change"}} +{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":81}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_EXECUTABLE_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[76,77,78,79,80],"surfaceOp":"append"} +{"type":"assistant/message","data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model"},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[83,84,85,86,87],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":7}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} From 8ac8245d39fb24b91658c019b93dea50b415413f Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:07:06 +0800 Subject: [PATCH 3/6] docs(deepseek): specify session log wire format --- ...deepseek-llm-api-wire-extensions.i18n.yaml | 4 +- docs/deepseek-llm-api-wire-extensions.md | 189 ++++++++++++++++- docs/deepseek-llm-api-wire-extensions.zh.md | 193 +++++++++++++++++- 3 files changed, 376 insertions(+), 10 deletions(-) diff --git a/docs/deepseek-llm-api-wire-extensions.i18n.yaml b/docs/deepseek-llm-api-wire-extensions.i18n.yaml index b417423d76..9ee7ed4d61 100644 --- a/docs/deepseek-llm-api-wire-extensions.i18n.yaml +++ b/docs/deepseek-llm-api-wire-extensions.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/deepseek-llm-api-wire-extensions.md -deepseek-llm-api-wire-extensions.md: 7992048f3f8e23bf7a039facae0a3f36cb3c42a7 -deepseek-llm-api-wire-extensions.zh.md: 330d0e2d67d920fdca386c10ac4d4d25d19e49d3 +deepseek-llm-api-wire-extensions.md: 39cb6ddd5096c0a94552a6ad08c2cd9cddb6f92e +deepseek-llm-api-wire-extensions.zh.md: a9cf637bed746f66af6ad1cf6da3915090494c54 diff --git a/docs/deepseek-llm-api-wire-extensions.md b/docs/deepseek-llm-api-wire-extensions.md index 7992048f3f..39cb6ddd50 100644 --- a/docs/deepseek-llm-api-wire-extensions.md +++ b/docs/deepseek-llm-api-wire-extensions.md @@ -11,7 +11,9 @@ The adapter sends the additions to its resolved `baseURL`, including a configure | Location | Naming | Examples | |---|---|---| | HTTP field names | Lowercase kebab-case; HTTP matching remains case-insensitive | `user-agent`, `x-deepseek-harness-session-id` | -| DeepSeek request-body extension fields | Snake case with the reserved `dsh_` prefix | `dsh_plugin_packages` | +| DeepSeek request-body extension fields | Snake case with the reserved `dsh_` prefix | `dsh_plugin_packages`, `dsh_session_log` | +| DSH-owned nested JSON members | Camel case | `afterSeq`, `messageIndex`, `utf8Start` | +| Tagged values | Kebab-case strings; durable events use `domain/action` | `message-references`, `session-log-deepseek/accepted` | Each body extension owns its `version` independently. A version applies only to the object that contains it; no compatibility or ordering relationship exists between versions of different fields. JSON member order is not part of the protocol. @@ -69,8 +71,189 @@ Disabled, pending, failed, unloading, disposed, and structural Loader entries ar An enabled inventory with no qualifying entries sends `packages: []`; disabling the contributor omits the entire `dsh_plugin_packages` field. Package identities are provider metadata and never enter model input. +## `dsh_session_log` + +[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.md) contributes one contiguous suffix of the canonical Session log. The field is disabled by default. When enabled, it applies to a request with a live Session and at least one event; a direct request, a stale Session id, or an empty log omits the field. + +```json +{ + "dsh_session_log": { + "version": 1, + "session": { + "version": 0, + "id": "session-id", + "createdAt": 1780000000000 + }, + "afterSeq": -1, + "throughSeq": 0, + "events": [ + { + "encoding": "raw", + "event": { + "type": "turn/start", + "seq": 0, + "time": 1780000000001, + "data": { + "turn": 1 + } + } + } + ] + } +} +``` + +| Member | Type | Meaning | +|---|---|---| +| `version` | `1` | Schema version for `dsh_session_log` | +| `session` | object | Immutable canonical `SessionHeader` | +| `afterSeq` | integer | Greatest sequence recorded as accepted before this request, or `-1` | +| `throughSeq` | non-negative integer | Greatest sequence represented by this request | +| `events` | array | Contiguous events from `afterSeq + 1` through `throughSeq` | + +The first upload uses `afterSeq: -1` and carries the complete current log. Each later upload starts after the greatest accepted watermark for the same Session id. The sender snapshots the event array once per request; appends after that snapshot belong to a later request. + +### Session header + +The `session` member is the exact `Session.header`, not a complete runtime Session. The outer `dsh_session_log.version` selects this extension schema, while `session.version` selects the canonical on-disk Session format; the two version values evolve independently. + +| Member | Presence | Meaning | +|---|---|---| +| `version` | required | Canonical Session format version; currently `0` | +| `id` | required | Exact Session id | +| `createdAt` | required | Non-negative safe-integer Unix epoch milliseconds | +| `cwd` | optional | Absolute working directory recorded at Session creation | +| `parentSession` | optional | Parent Session id for a fork | +| `seedLength` | optional | Number of leading events inherited through the seed | +| `origin` | optional | Literal `subagent` for a subagent child | +| `delegationDepth` | optional | Non-negative persisted subagent delegation depth | +| `agentPreset` | optional | Agent preset id used to compose this Session | + +### Event envelope and encodings + +Each `events` item is an `EncodedSessionEvent` selected by its `encoding` value. + +| `encoding` | `event` value | +|---|---| +| `raw` | Complete canonical `SessionEvent` | +| `message-references` | `PackedJsonValue` that reconstructs the complete canonical `SessionEvent` against this request's `messages` | + +A canonical event always carries `type`, `seq`, `time`, and `data`. It may carry `ignorable: true`; surface events may additionally carry `sourceEventSeqs` and `surfaceOp`. A raw encoding copies every present member without projection or redaction. + +A `PackedJsonValue` uses one of four `kind` values: + +| `kind` | Members | Decoded value | +|---|---|---| +| `literal` | `value` | The contained JSON value, which may itself be composite | +| `string` | `parts` | Concatenation of decoded `PackedJsonStringPart` values | +| `array` | `items` | Array of recursively decoded values | +| `object` | `entries` | Object built from ordered `[key, value]` pairs | + +A packed string contains these part variants: + +| `kind` | `value` | +|---|---| +| `literal` | Literal string fragment | +| `message-slice` | Exact request-relative `DeepSeekMessageStringSlice` | + +```json +{ + "encoding": "message-references", + "event": { + "kind": "object", + "entries": [ + [ + "type", + { + "kind": "literal", + "value": "plugin/test" + } + ], + [ + "seq", + { + "kind": "literal", + "value": 0 + } + ], + [ + "time", + { + "kind": "literal", + "value": 1780000000001 + } + ], + [ + "data", + { + "kind": "object", + "entries": [ + [ + "text", + { + "kind": "string", + "parts": [ + { + "kind": "literal", + "value": "prefix:" + }, + { + "kind": "message-slice", + "value": { + "messageIndex": 0, + "path": [ + "content" + ], + "utf8Start": 0, + "utf8End": 12 + } + } + ] + } + ] + ] + } + ] + ] + } +} +``` + +| Slice member | Meaning | +|---|---| +| `messageIndex` | Zero-based index into the exact DeepSeek `messages` array in the containing request | +| `path` | Array of string object keys and numeric array indexes from that message root to a string value | +| `utf8Start` | Inclusive UTF-8 byte offset in the resolved string value | +| `utf8End` | Exclusive UTF-8 byte offset in the resolved string value | + +Offsets address the UTF-8 bytes of the parsed string value, not its JSON-escaped source text. A decoder rejects a missing path, a non-string target, an invalid range, or a range that splits a UTF-8 code point. Reconstruction therefore requires the exact `messages` array from the same request. + +The encoder uses a reference only for an exact substring and only when the complete referenced event occupies fewer serialized UTF-8 bytes than its raw form. Literal fragments retain unmatched text. A candidate that cannot round-trip exactly stays raw; the protocol performs no fuzzy matching, truncation, or lossy omission. + +### Acceptance watermark and at-least-once delivery + +After the endpoint returns HTTP 2xx, the contribution appends this canonical event to the same Session: + +```json +{ + "type": "session-log-deepseek/accepted", + "seq": 8, + "time": 1780000000002, + "data": { + "sessionId": "session-id", + "throughSeq": 7 + } +} +``` + +`accepted` means that the configured endpoint returned HTTP 2xx for the containing LLM request. It does not assert SSE completion or remote persistence. The event's `throughSeq` must identify an earlier event, and its `sessionId` identifies the Session whose suffix was sent. + +The sender folds the greatest matching `throughSeq`, so concurrent accepted requests cannot move the cursor backward. A resumed process rebuilds the cursor from the durable log. A fork ignores inherited watermarks that name its parent, and therefore sends its own complete inherited prefix before advancing under the child id. The watermark event itself belongs to the next unsent suffix. + +Transport and non-2xx failures append no watermark. A crash after endpoint acceptance but before local persistence may resend an already accepted range; uncertainty produces duplicates, never a sequence gap. There is no independent upload store, size cap, or truncation path. + ## Exposure and receiver requirements -The request headers expose the Harness application version, one anonymous Harness-home identity, and an optional Session identity. `dsh_plugin_packages` exposes active npm package names and versions. A gateway selected through `baseURL` receives the same values as the official endpoint. +The request headers expose the Harness application version, one anonymous Harness-home identity, and an optional Session identity. `dsh_plugin_packages` exposes active npm package names and versions. When enabled, `dsh_session_log` may expose the Session working directory, system-prompt snapshots, user and assistant content, raw assistant chunks, tool arguments and results, compaction summaries, feedback, and plugin-owned events. Adapter API keys are not Session events and therefore do not enter the field. A gateway selected through `baseURL` receives the same values as the official endpoint. -Receivers address extension fields by name, dispatch each field by its own `version`, preserve distinct package versions, and ignore JSON member ordering. The base request remains usable without either the registry or a particular contribution; field absence means that contribution did not apply to that request. +Receivers address extension fields by name, dispatch each field by its own `version`, preserve distinct package versions, and ignore JSON member ordering. A session-log receiver validates the contiguous sequence range and reconstructs every referenced event against the containing request before interpreting event types. An unrecognized canonical event without `ignorable: true` prevents lossless reconstruction. The base request remains usable without either the registry or a particular contribution; field absence means that contribution did not apply to that request. diff --git a/docs/deepseek-llm-api-wire-extensions.zh.md b/docs/deepseek-llm-api-wire-extensions.zh.md index 330d0e2d67..a9cf637bed 100644 --- a/docs/deepseek-llm-api-wire-extensions.zh.md +++ b/docs/deepseek-llm-api-wire-extensions.zh.md @@ -2,7 +2,7 @@ [English](deepseek-llm-api-wire-extensions.md) | 中文 -本参考文档定义 [`@deepseek-ai/dsh-llm-deepseek`](../packages/llm/llm-deepseek/README.zh.md) 在 `deepseek-official` 聊天补全请求中发送的全部 DeepSeek Harness 特有 HTTP 标头和附加 JSON 字段。本文不重复定义 DeepSeek 上游 API 持有的字段。提供方无关的 LLM 接口与 `llm-pi-ai` 均不实现这些扩展。 +本参考文档定义 [`@deepseek-ai/dsh-llm-deepseek`](../packages/llm/llm-deepseek/README.zh.md) 在 `deepseek-official` 聊天补全请求中发送的全部 DeepSeek Harness 特有 HTTP 标头和附加 JSON 字段。本文不重复定义 DeepSeek 上游 API 持有的字段。提供方无关的 LLM(大语言模型)接口与 `llm-pi-ai` 均不实现这些扩展。 适配器将这些扩展发送至已解析的 `baseURL`,包括已配置的网关。扩展位于 `messages`、系统提示词和工具 schema 之外,因此不会增加模型输入 token,也不会改变模型可见前缀。 @@ -11,7 +11,9 @@ | 位置 | 命名方式 | 示例 | |---|---|---| | HTTP 字段名 | 小写 kebab-case;HTTP 匹配仍不区分大小写 | `user-agent`, `x-deepseek-harness-session-id` | -| DeepSeek 请求正文扩展字段 | 使用保留 `dsh_` 前缀的 snake case | `dsh_plugin_packages` | +| DeepSeek 请求正文扩展字段 | 使用保留 `dsh_` 前缀的 snake case | `dsh_plugin_packages`, `dsh_session_log` | +| DSH 持有的嵌套 JSON 成员 | Camel case | `afterSeq`, `messageIndex`, `utf8Start` | +| 带标签的值 | 使用 kebab-case 字符串;持久事件采用 `domain/action` | `message-references`, `session-log-deepseek/accepted` | 每个正文扩展独立持有自身的 `version`。版本仅适用于包含该字段的对象;不同字段的版本之间不存在兼容或排序关系。JSON 成员顺序不属于协议。 @@ -58,7 +60,7 @@ |---|---|---| | `version` | `1` | `dsh_plugin_packages` 的 schema 版本 | | `packages` | 数组 | 本次请求的完整存活集合 | -| `packages[].name` | 字符串 | 来自所属 manifest 的确切非空 npm 包名 | +| `packages[].name` | 字符串 | 来自所属 manifest(元数据清单)的确切非空 npm 包名 | | `packages[].version` | 字符串 | 来自同一 manifest 的确切非空包版本 | 每个请求都会重新读取宿主树中的存活非分组 Loader 配置项;请求会话存在 standing agent-preset 树时,也会读取该树。相对与绝对模块使用距离自身最近的所属 manifest;裸包配置项使用激活自身的 Loader 解析基准。具名 manifest 未提供非空版本时,请求准备会失败。 @@ -69,8 +71,189 @@ 清单已启用但没有符合条件的配置项时,系统发送 `packages: []`;禁用贡献插件时,系统省略整个 `dsh_plugin_packages` 字段。包身份属于提供方元数据,绝不进入模型输入。 +## `dsh_session_log` + +[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.zh.md) 贡献权威会话日志的一段连续后缀。该字段默认禁用。启用后,它适用于携带存活会话且至少存在一个事件的请求;直接请求、陈旧会话 id 或空日志会省略该字段。 + +```json +{ + "dsh_session_log": { + "version": 1, + "session": { + "version": 0, + "id": "session-id", + "createdAt": 1780000000000 + }, + "afterSeq": -1, + "throughSeq": 0, + "events": [ + { + "encoding": "raw", + "event": { + "type": "turn/start", + "seq": 0, + "time": 1780000000001, + "data": { + "turn": 1 + } + } + } + ] + } +} +``` + +| 成员 | 类型 | 含义 | +|---|---|---| +| `version` | `1` | `dsh_session_log` 的 schema 版本 | +| `session` | 对象 | 不可变的权威 `SessionHeader` | +| `afterSeq` | 整数 | 本次请求前记录为已接受的最大序号,或 `-1` | +| `throughSeq` | 非负整数 | 本次请求所表示的最大序号 | +| `events` | 数组 | 从 `afterSeq + 1` 到 `throughSeq` 的连续事件 | + +首次上传使用 `afterSeq: -1`,并携带当前的完整日志。此后每次上传都从同一会话 id 的最大已接受水位(watermark)之后开始。发送方为每次请求仅快照一次事件数组;快照后的追加内容属于后续请求。 + +### 会话头 + +`session` 成员是确切的 `Session.header`,不是完整的运行时会话。外层 `dsh_session_log.version` 选择本扩展 schema,`session.version` 则选择权威磁盘会话格式;两个版本值相互独立演进。 + +| 成员 | 出现条件 | 含义 | +|---|---|---| +| `version` | 必需 | 权威会话格式版本;当前为 `0` | +| `id` | 必需 | 确切的会话 id | +| `createdAt` | 必需 | 非负安全整数 Unix epoch 毫秒数 | +| `cwd` | 可选 | 创建会话时记录的绝对工作目录 | +| `parentSession` | 可选 | fork 的父会话 id | +| `seedLength` | 可选 | 通过 seed 继承的前导事件数量 | +| `origin` | 可选 | subagent 子项使用的字面值 `subagent` | +| `delegationDepth` | 可选 | 持久化的非负 subagent 委派深度 | +| `agentPreset` | 可选 | 用于组合该会话的 agent preset id | + +### 事件信封与编码 + +每个 `events` 元素都是通过 `encoding` 值选择的 `EncodedSessionEvent`。 + +| `encoding` | `event` 值 | +|---|---| +| `raw` | 完整的权威 `SessionEvent` | +| `message-references` | 根据本次请求 `messages` 重建完整权威 `SessionEvent` 的 `PackedJsonValue` | + +权威事件始终携带 `type`、`seq`、`time` 和 `data`。它可以携带 `ignorable: true`;展示事件还可以携带 `sourceEventSeqs` 和 `surfaceOp`。raw 编码会复制全部已有成员,不做投影或脱敏。 + +`PackedJsonValue` 使用以下四种 `kind` 值之一: + +| `kind` | 成员 | 解码值 | +|---|---|---| +| `literal` | `value` | 所含 JSON 值;该值自身也可以是复合值 | +| `string` | `parts` | 已解码 `PackedJsonStringPart` 值的拼接结果 | +| `array` | `items` | 递归解码值组成的数组 | +| `object` | `entries` | 根据有序 `[key, value]` 对构建的对象 | + +已打包字符串包含以下两种片段: + +| `kind` | `value` | +|---|---| +| `literal` | 字面字符串片段 | +| `message-slice` | 确切的请求相对 `DeepSeekMessageStringSlice` | + +```json +{ + "encoding": "message-references", + "event": { + "kind": "object", + "entries": [ + [ + "type", + { + "kind": "literal", + "value": "plugin/test" + } + ], + [ + "seq", + { + "kind": "literal", + "value": 0 + } + ], + [ + "time", + { + "kind": "literal", + "value": 1780000000001 + } + ], + [ + "data", + { + "kind": "object", + "entries": [ + [ + "text", + { + "kind": "string", + "parts": [ + { + "kind": "literal", + "value": "prefix:" + }, + { + "kind": "message-slice", + "value": { + "messageIndex": 0, + "path": [ + "content" + ], + "utf8Start": 0, + "utf8End": 12 + } + } + ] + } + ] + ] + } + ] + ] + } +} +``` + +| slice 成员 | 含义 | +|---|---| +| `messageIndex` | 包含该扩展的请求中,确切 DeepSeek `messages` 数组的零基序号 | +| `path` | 从该消息根到某个字符串值的路径数组;对象键使用字符串,数组索引使用数字 | +| `utf8Start` | 已解析字符串值中的包含性 UTF-8 字节偏移 | +| `utf8End` | 已解析字符串值中的排他性 UTF-8 字节偏移 | + +偏移量指向已解析字符串值的 UTF-8 字节,而不是经过 JSON 转义的源文本。路径缺失、目标不是字符串、范围无效或范围切开 UTF-8 码点时,解码器会拒绝。因此,重建必须使用同一次请求的确切 `messages` 数组。 + +编码器只会引用确切子字符串,并且仅在整个引用事件占用的序列化 UTF-8 字节少于 raw 形式时才会使用引用。字面片段保留未匹配文本。无法精确重建为原值的候选项会保留 raw 形式;协议不执行模糊匹配、截断或有损省略。 + +### 接受水位与至少一次交付 + +端点返回 HTTP 2xx 后,该贡献会向同一会话追加以下权威事件: + +```json +{ + "type": "session-log-deepseek/accepted", + "seq": 8, + "time": 1780000000002, + "data": { + "sessionId": "session-id", + "throughSeq": 7 + } +} +``` + +`accepted` 表示已配置端点为包含该字段的 LLM 请求返回 HTTP 2xx。它不表示 SSE 已完整结束,也不表示远端已经持久化。该事件的 `throughSeq` 必须标识一项更早的事件,`sessionId` 则标识已发送后缀所属的会话。 + +发送方会折叠最大的匹配 `throughSeq`,因此并发已接受请求无法使游标倒退。恢复后的进程会从持久日志重建游标。fork 会忽略命名其父会话的继承水位,因此先发送自身完整的继承前缀,再以子会话 id 推进。水位事件自身属于下一段未发送后缀。 + +传输失败和非 2xx 响应不会追加水位。端点接受后、本地持久化前发生崩溃时,系统可能重新发送已接受范围;不确定性只会产生重复,绝不会产生序号缺口。系统没有独立上传存储、大小上限或截断路径。 + ## 暴露内容与接收方要求 -请求标头会暴露 Harness 应用版本、一个匿名 Harness-home 身份和可选的会话身份。`dsh_plugin_packages` 会暴露存活 npm 包的名称与版本。通过 `baseURL` 选择的网关会收到与官方端点相同的值。 +请求标头会暴露 Harness 应用版本、一个匿名 Harness-home 身份和可选的会话身份。`dsh_plugin_packages` 会暴露存活 npm 包的名称与版本。启用后,`dsh_session_log` 可能暴露会话工作目录、系统提示词快照、用户与 assistant 内容、原始 assistant 分片、工具参数与结果、压缩摘要、反馈和插件持有的事件。适配器 API key 不是会话事件,因此不会进入该字段。通过 `baseURL` 选择的网关会收到与官方端点相同的值。 -接收方按名称定位扩展字段,按各字段自己的 `version` 分派,保留不同的包版本,并忽略 JSON 成员顺序。即使缺少注册表或某项贡献,基础请求仍然可用;字段缺失表示该项贡献不适用于本次请求。 +接收方按名称定位扩展字段,按各字段自己的 `version` 分派,保留不同的包版本,并忽略 JSON 成员顺序。会话日志接收方必须校验连续序号范围,并在解释事件类型前,根据包含该扩展的请求重建每个引用事件。遇到不带 `ignorable: true` 的未知权威事件时,接收方无法进行无损重建。即使缺少注册表或某项贡献,基础请求仍然可用;字段缺失表示该项贡献不适用于本次请求。 From e0a8050aea6062115187b59cc7ac74969be198bf Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 22 Aug 2026 22:52:01 +0800 Subject: [PATCH 4/6] docs(deepseek): merge todo event graph updates --- docs/event-producer-consumer.i18n.yaml | 2 +- docs/event-producer-consumer.zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index ab3357cfc4..13327b1742 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/event-producer-consumer.md event-producer-consumer.md: ee1ab4fe3eba4b6585e980a096a4b50467a7d287 -event-producer-consumer.zh.md: f9873bff957e6d141221ff6f5af000a585d25f69 +event-producer-consumer.zh.md: 363e2de4d8e81494d10cb73dcfdb6d161d195b61 diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index f9873bff95..363e2de4d8 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -73,7 +73,7 @@ | 事件字符串 | 派发方 | 监听方 | | --- | --- | --- | -| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) | +| `internal/dispatch` | - | `agent-team`, [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`terminal-bash`](../packages/terminal/terminal-bash), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) | | `internal/plugin` | - | `loader`, [`lsp-stdio`](../packages/lsp/lsp-stdio), `modules`, `webserver` | | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` | | `internal/status` | - | [`agent`](../packages/core/agent) | From 3c0da7bef7dd3151d2e9e2e22191e2988da406a0 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 22 Aug 2026 23:18:13 +0800 Subject: [PATCH 5/6] refactor(session): name delivery acceptance event --- ...pseek-llm-api-request-extensions.i18n.yaml | 4 +-- ...-21-deepseek-llm-api-request-extensions.md | 4 +-- ...-deepseek-llm-api-request-extensions.zh.md | 4 +-- ...deepseek-llm-api-wire-extensions.i18n.yaml | 4 +-- docs/deepseek-llm-api-wire-extensions.md | 6 ++-- docs/deepseek-llm-api-wire-extensions.zh.md | 6 ++-- docs/persistence-catalog.i18n.yaml | 4 +-- docs/persistence-catalog.md | 10 +++--- docs/persistence-catalog.zh.md | 10 +++--- .../text-turn/notifications.expected.jsonl | 2 +- .../tests/snapshots/text-turn/session.jsonl | 2 +- .../core/session/src/known-event-types.ts | 2 +- .../session-log-deepseek/README.i18n.yaml | 4 +-- .../session/session-log-deepseek/README.md | 4 +-- .../session/session-log-deepseek/README.zh.md | 4 +-- .../session/session-log-deepseek/src/index.ts | 4 +-- .../session-log-deepseek/src/invariant.ts | 10 +++--- .../session/session-log-deepseek/src/types.ts | 6 ++-- .../tests/invariant.spec.ts | 10 +++--- .../session-log-deepseek/tests/upload.spec.ts | 10 +++--- .../advanced/result.json | 32 +++++++++---------- .../advanced/session.1.jsonl | 2 +- .../advanced/session.2.jsonl | 2 +- .../advanced/session.jsonl | 14 ++++---- 24 files changed, 80 insertions(+), 80 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml index d0494dd7e8..d6de9c84a1 100644 --- a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md -2026-08-21-deepseek-llm-api-request-extensions.md: 4ccf913e7944c3905437ceffc60e3b0c19d305ea -2026-08-21-deepseek-llm-api-request-extensions.zh.md: 062d293ec87a6f0b1ef271f186e017be76d141f5 +2026-08-21-deepseek-llm-api-request-extensions.md: 8e4d1e375ecf4169ac8f7f93a3a341e4d4252191 +2026-08-21-deepseek-llm-api-request-extensions.zh.md: 05d68cdce547ff8bde059fd2d0fe1a1b44af67f0 diff --git a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md index 4ccf913e79..8e4d1e375e 100644 --- a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md +++ b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md @@ -20,7 +20,7 @@ The provider-neutral `llm` package and `llm-pi-ai` contain no extension type, se ## Incremental session-log field -`@deepseek-ai/dsh-session-log-deepseek` owns `dsh_session_log` as an explicit opt-in. When enabled, each request carrying a live Session id sends the contiguous canonical event suffix after the greatest durable `session-log-deepseek/accepted` watermark for that same Session identity. The field includes the immutable Session header and complete event envelopes. A 2xx appends a new watermark for the transmitted `throughSeq`; that event enters the following request's suffix. Forked logs retain parent watermark ids, so a child starts from sequence zero under its own identity. Concurrent acceptances may arrive out of order, and the maximum watermark remains authoritative. A process-local fold scans each Session event once and incrementally consumes later appends; a new Session object or HMR generation rebuilds the fold from durable history. +`@deepseek-ai/dsh-session-log-deepseek` owns `dsh_session_log` as an explicit opt-in. When enabled, each request carrying a live Session id sends the contiguous canonical event suffix after the greatest durable `session-log-deepseek/delivery-accepted` watermark for that same Session identity. The field includes the immutable Session header and complete event envelopes. A 2xx appends a new watermark for the transmitted `throughSeq`; that event enters the following request's suffix. Forked logs retain parent watermark ids, so a child starts from sequence zero under its own identity. Concurrent acceptances may arrive out of order, and the maximum watermark remains authoritative. A process-local fold scans each Session event once and incrementally consumes later appends; a new Session object or HMR generation rebuilds the fold from durable history. The failure direction is at least once. A transport or provider rejection records no watermark. A crash after remote acceptance but before the watermark persists causes replay after resume, never a skipped sequence. Existing session checkpoints persist the event; the upload plugin owns no second store. @@ -72,6 +72,6 @@ Registry tests pin duplicate ownership, effect-scoped disposal, detached field v Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. An explicit Session-log opt-in also carries the complete newly unaccepted Session suffix. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Request-relative packing synchronously compares suffix and message strings before dispatch, so a large first upload or retry backlog can delay the event loop until candidate indexing is implemented. Encoding, manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata. -The accepted-watermark event becomes part of the canonical log and is itself delivered on a later request. Crash recovery can duplicate a suffix but does not infer acceptance from assistant output or create a second local cursor store. Direct calls without a live Session omit the session field; host package inventory remains available. +The `delivery-accepted` event becomes part of the canonical log and is itself delivered on a later request. Crash recovery can duplicate a suffix but does not infer acceptance from assistant output or create a second local cursor store. Direct calls without a live Session omit the session field; host package inventory remains available. The [DeepSeek request-identity decision](../feature/2026-08-11-deepseek-request-user-id-header.md) continues to own user/session headers, which remain outside the body. The [session-telemetry decision](../feature/2026-07-23-session-telemetry-otel-revival.md) remains current until a separate change removes that seam and backend; this request path does not alter OTel capture or sharing modes. diff --git a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md index 062d293ec8..05d68cdce5 100644 --- a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md @@ -20,7 +20,7 @@ Status: implemented ## 增量会话日志字段 -`@deepseek-ai/dsh-session-log-deepseek` 以显式选择启用的方式拥有 `dsh_session_log`。启用后,每个携带存活会话 id 的请求都会发送该确切会话身份最大持久 `session-log-deepseek/accepted` 水位之后的连续权威事件后缀。该字段包含不可变会话 header 与完整事件信封。2xx 会为已发送的 `throughSeq` 追加新水位;该事件会进入下一次请求的后缀。Fork 日志会保留父级水位 id,因此子会话会在自己的身份下从序列零开始。并发接受可能乱序到达,最大水位仍保持权威。进程内 fold 会让每条会话事件只被扫描一次,并增量消费后续追加;新的会话对象或 HMR generation 会从持久历史重建该 fold。 +`@deepseek-ai/dsh-session-log-deepseek` 以显式选择启用的方式拥有 `dsh_session_log`。启用后,每个携带存活会话 id 的请求都会发送该确切会话身份最大持久 `session-log-deepseek/delivery-accepted` 水位之后的连续权威事件后缀。该字段包含不可变会话 header 与完整事件信封。2xx 会为已发送的 `throughSeq` 追加新水位;该事件会进入下一次请求的后缀。Fork 日志会保留父级水位 id,因此子会话会在自己的身份下从序列零开始。并发接受可能乱序到达,最大水位仍保持权威。进程内 fold 会让每条会话事件只被扫描一次,并增量消费后续追加;新的会话对象或 HMR generation 会从持久历史重建该 fold。 失败方向为至少一次。传输失败或提供方拒绝不会记录水位。远端接受后、水位持久化前发生崩溃,会在恢复后触发重放,绝不会跳过序列。现有会话检查点会持久化该事件;上传插件不拥有第二份存储。 @@ -72,6 +72,6 @@ Status: implemented DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。显式选择启用会话日志后,请求还会携带完整的未接受会话新后缀。这些字段对模型不可见,不增加提示词 token,也不改变 KV Cache,但可能显著增大 HTTP 正文。请求相对打包会在派发前同步比较后缀字符串与消息字符串,因此在实现候选索引前,较大的首次上传或重试积压可能延迟事件循环。编码、manifest 解析、字段冲突、接受记录或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。 -已接受水位事件会成为权威日志的一部分,并在后续请求中自行交付。崩溃恢复可能重复后缀,但不会根据 assistant 输出推断接受,也不会创建第二份本地游标存储。缺少存活会话的直接调用会省略会话字段;宿主包清单仍然可用。 +`delivery-accepted` 事件会成为权威日志的一部分,并在后续请求中自行交付。崩溃恢复可能重复后缀,但不会根据 assistant 输出推断接受,也不会创建第二份本地游标存储。缺少存活会话的直接调用会省略会话字段;宿主包清单仍然可用。 [DeepSeek 请求身份决策](../feature/2026-08-11-deepseek-request-user-id-header.zh.md)继续拥有 user/session header,且这些 header 仍位于正文之外。[会话遥测决策](../feature/2026-07-23-session-telemetry-otel-revival.zh.md)在另一项变更删除该 seam 与后端之前仍保持当前有效;本请求路径不改变 OTel 捕获或共享模式。 diff --git a/docs/deepseek-llm-api-wire-extensions.i18n.yaml b/docs/deepseek-llm-api-wire-extensions.i18n.yaml index 9ee7ed4d61..b8af9b2f92 100644 --- a/docs/deepseek-llm-api-wire-extensions.i18n.yaml +++ b/docs/deepseek-llm-api-wire-extensions.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/deepseek-llm-api-wire-extensions.md -deepseek-llm-api-wire-extensions.md: 39cb6ddd5096c0a94552a6ad08c2cd9cddb6f92e -deepseek-llm-api-wire-extensions.zh.md: a9cf637bed746f66af6ad1cf6da3915090494c54 +deepseek-llm-api-wire-extensions.md: 8a3f089c77a0879ef4056f3c16583935f2219248 +deepseek-llm-api-wire-extensions.zh.md: 1cf5ea1ce1cd410b23b011480a8c623827753464 diff --git a/docs/deepseek-llm-api-wire-extensions.md b/docs/deepseek-llm-api-wire-extensions.md index 39cb6ddd50..8a3f089c77 100644 --- a/docs/deepseek-llm-api-wire-extensions.md +++ b/docs/deepseek-llm-api-wire-extensions.md @@ -13,7 +13,7 @@ The adapter sends the additions to its resolved `baseURL`, including a configure | HTTP field names | Lowercase kebab-case; HTTP matching remains case-insensitive | `user-agent`, `x-deepseek-harness-session-id` | | DeepSeek request-body extension fields | Snake case with the reserved `dsh_` prefix | `dsh_plugin_packages`, `dsh_session_log` | | DSH-owned nested JSON members | Camel case | `afterSeq`, `messageIndex`, `utf8Start` | -| Tagged values | Kebab-case strings; durable events use `domain/action` | `message-references`, `session-log-deepseek/accepted` | +| Tagged values | Kebab-case strings; durable events use `domain/action` | `message-references`, `session-log-deepseek/delivery-accepted` | Each body extension owns its `version` independently. A version applies only to the object that contains it; no compatibility or ordering relationship exists between versions of different fields. JSON member order is not part of the protocol. @@ -236,7 +236,7 @@ After the endpoint returns HTTP 2xx, the contribution appends this canonical eve ```json { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 8, "time": 1780000000002, "data": { @@ -246,7 +246,7 @@ After the endpoint returns HTTP 2xx, the contribution appends this canonical eve } ``` -`accepted` means that the configured endpoint returned HTTP 2xx for the containing LLM request. It does not assert SSE completion or remote persistence. The event's `throughSeq` must identify an earlier event, and its `sessionId` identifies the Session whose suffix was sent. +`delivery-accepted` means that the configured endpoint returned HTTP 2xx for the containing LLM request. It does not assert SSE completion or remote persistence. The event's `throughSeq` must identify an earlier event, and its `sessionId` identifies the Session whose suffix was sent. The sender folds the greatest matching `throughSeq`, so concurrent accepted requests cannot move the cursor backward. A resumed process rebuilds the cursor from the durable log. A fork ignores inherited watermarks that name its parent, and therefore sends its own complete inherited prefix before advancing under the child id. The watermark event itself belongs to the next unsent suffix. diff --git a/docs/deepseek-llm-api-wire-extensions.zh.md b/docs/deepseek-llm-api-wire-extensions.zh.md index a9cf637bed..1cf5ea1ce1 100644 --- a/docs/deepseek-llm-api-wire-extensions.zh.md +++ b/docs/deepseek-llm-api-wire-extensions.zh.md @@ -13,7 +13,7 @@ | HTTP 字段名 | 小写 kebab-case;HTTP 匹配仍不区分大小写 | `user-agent`, `x-deepseek-harness-session-id` | | DeepSeek 请求正文扩展字段 | 使用保留 `dsh_` 前缀的 snake case | `dsh_plugin_packages`, `dsh_session_log` | | DSH 持有的嵌套 JSON 成员 | Camel case | `afterSeq`, `messageIndex`, `utf8Start` | -| 带标签的值 | 使用 kebab-case 字符串;持久事件采用 `domain/action` | `message-references`, `session-log-deepseek/accepted` | +| 带标签的值 | 使用 kebab-case 字符串;持久事件采用 `domain/action` | `message-references`, `session-log-deepseek/delivery-accepted` | 每个正文扩展独立持有自身的 `version`。版本仅适用于包含该字段的对象;不同字段的版本之间不存在兼容或排序关系。JSON 成员顺序不属于协议。 @@ -236,7 +236,7 @@ ```json { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 8, "time": 1780000000002, "data": { @@ -246,7 +246,7 @@ } ``` -`accepted` 表示已配置端点为包含该字段的 LLM 请求返回 HTTP 2xx。它不表示 SSE 已完整结束,也不表示远端已经持久化。该事件的 `throughSeq` 必须标识一项更早的事件,`sessionId` 则标识已发送后缀所属的会话。 +`delivery-accepted` 表示已配置端点为包含该字段的 LLM 请求返回 HTTP 2xx。它不表示 SSE 已完整结束,也不表示远端已经持久化。该事件的 `throughSeq` 必须标识一项更早的事件,`sessionId` 则标识已发送后缀所属的会话。 发送方会折叠最大的匹配 `throughSeq`,因此并发已接受请求无法使游标倒退。恢复后的进程会从持久日志重建游标。fork 会忽略命名其父会话的继承水位,因此先发送自身完整的继承前缀,再以子会话 id 推进。水位事件自身属于下一段未发送后缀。 diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 234cd56f74..b8be9cdbc5 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-catalog.md -persistence-catalog.md: 3969ec00068a0b40eb7362a1805313ad35c84e5e -persistence-catalog.zh.md: f0e58af4181c60e1d675f7f5b394accfee0d7cf0 +persistence-catalog.md: ad65bc6bd3971d204f9bce3c7ee822920ebe6a85 +persistence-catalog.zh.md: 3af5d7c46e7969b29a66b04adddc41d9d108553d diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 3969ec0006..ad65bc6bd3 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -669,14 +669,14 @@ Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/sessi ### `session-log-deepseek/*` - + -#### `session-log-deepseek/accepted` — log-only +#### `session-log-deepseek/delivery-accepted` — log-only ```ts persistence-catalog -/** Records a confirmed HTTP acceptance watermark for restart-safe suffix selection. */ -'session-log-deepseek/accepted': { - /** Session identity the accepted request carried; inherited fork markers retain the parent's id. */ +/** Records that the configured endpoint accepted one delivery through `throughSeq`. */ +'session-log-deepseek/delivery-accepted': { + /** Session identity the accepted delivery carried; inherited fork markers retain the parent's id. */ sessionId: import('@deepseek-ai/dsh-session/types').SessionId /** Last canonical event included in the accepted request. */ throughSeq: number diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index f0e58af418..3af5d7c46e 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -671,14 +671,14 @@ export type SessionEvent = { ### `session-log-deepseek/*` - + -#### `session-log-deepseek/accepted` — log-only +#### `session-log-deepseek/delivery-accepted` — log-only ```ts persistence-catalog -/** Records a confirmed HTTP acceptance watermark for restart-safe suffix selection. */ -'session-log-deepseek/accepted': { - /** Session identity the accepted request carried; inherited fork markers retain the parent's id. */ +/** Records that the configured endpoint accepted one delivery through `throughSeq`. */ +'session-log-deepseek/delivery-accepted': { + /** Session identity the accepted delivery carried; inherited fork markers retain the parent's id. */ sessionId: import('@deepseek-ai/dsh-session/types').SessionId /** Last canonical event included in the accepted request. */ throughSeq: number diff --git a/examples/jsonrpc-agent/tests/snapshots/text-turn/notifications.expected.jsonl b/examples/jsonrpc-agent/tests/snapshots/text-turn/notifications.expected.jsonl index 8d85d9bf06..9ddc977448 100644 --- a/examples/jsonrpc-agent/tests/snapshots/text-turn/notifications.expected.jsonl +++ b/examples/jsonrpc-agent/tests/snapshots/text-turn/notifications.expected.jsonl @@ -7,7 +7,7 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":5,"time":0,"data":{"title":"Reply with exactly: SDK snapshot","messageSeqs":[4],"source":{"kind":"fallback"}}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":6,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":7,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session-log-deepseek/accepted","seq":8,"time":0,"data":{"sessionId":"{{sessionId}}","throughSeq":7}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session-log-deepseek/delivery-accepted","seq":8,"time":0,"data":{"sessionId":"{{sessionId}}","throughSeq":7}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":"The"}}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"reasoning-delta","index":0,"text":" user"}}}}} diff --git a/examples/jsonrpc-agent/tests/snapshots/text-turn/session.jsonl b/examples/jsonrpc-agent/tests/snapshots/text-turn/session.jsonl index 1ab358ca6a..3438bae4c5 100644 --- a/examples/jsonrpc-agent/tests/snapshots/text-turn/session.jsonl +++ b/examples/jsonrpc-agent/tests/snapshots/text-turn/session.jsonl @@ -7,7 +7,7 @@ {"type":"session/title","data":{"title":"Reply with exactly: SDK snapshot","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"session-log-deepseek/accepted","data":{"sessionId":"sdk-snapshot-text","throughSeq":7}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"sdk-snapshot-text","throughSeq":7}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}} {"type":"reasoning-chunks","data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," reply"," with"," exactly"," \"","SD","K"," snapshot"," OK","\"."," Let"," me"," do"," that","."]}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}} diff --git a/packages/core/session/src/known-event-types.ts b/packages/core/session/src/known-event-types.ts index 076d9bbb88..b4e7117541 100644 --- a/packages/core/session/src/known-event-types.ts +++ b/packages/core/session/src/known-event-types.ts @@ -42,7 +42,7 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet = new Set([ 'request/header', 'sandbox/mode', 'schedule/change', - 'session-log-deepseek/accepted', + 'session-log-deepseek/delivery-accepted', 'session/end-seed', 'session/title', 'session/title-llm-request', diff --git a/packages/session/session-log-deepseek/README.i18n.yaml b/packages/session/session-log-deepseek/README.i18n.yaml index 361eaeb20a..362db6ab85 100644 --- a/packages/session/session-log-deepseek/README.i18n.yaml +++ b/packages/session/session-log-deepseek/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-log-deepseek/README.md -README.md: 5b2b9d5eb087efd0fecd4040d9000a08ccc906cc -README.zh.md: c477bc7762fa6f4660e743bb1c5d0b80cdeb0c98 +README.md: 39ff31b8c33f2f8e0a12cb6ed0814d97f982e3c9 +README.zh.md: 0121644cc0b7ebbe2e6b80ac6c31ae21e088fe95 diff --git a/packages/session/session-log-deepseek/README.md b/packages/session/session-log-deepseek/README.md index 5b2b9d5eb0..39ff31b8c3 100644 --- a/packages/session/session-log-deepseek/README.md +++ b/packages/session/session-log-deepseek/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Incremental canonical session-log upload for official DeepSeek LLM API requests. This function plugin injects `ctx.sessions` and `ctx.deepseekLlmApiExtensions`, then owns the `dsh_session_log` request field and the durable `session-log-deepseek/accepted` watermark event. +Incremental canonical session-log upload for official DeepSeek LLM API requests. This function plugin injects `ctx.sessions` and `ctx.deepseekLlmApiExtensions`, then owns the `dsh_session_log` request field and the durable `session-log-deepseek/delivery-accepted` event from which it derives the acceptance watermark. ## Configuration @@ -20,7 +20,7 @@ Each event is sent raw unless request-relative packing reduces its JSON byte siz ## Acceptance and retry -The DeepSeek adapter calls the prepared contribution's `accept()` after HTTP 2xx, before it consumes the SSE body. Acceptance appends `session-log-deepseek/accepted` with the uploaded `throughSeq`; the next request uploads that watermark as part of its new suffix. Transport and non-2xx failures append no watermark, so later requests resend the uncertain range. Concurrent accepted requests may append watermarks out of order; folding their maximum prevents cursor regression. +The DeepSeek adapter calls the prepared contribution's `accept()` after HTTP 2xx, before it consumes the SSE body. Acceptance appends `session-log-deepseek/delivery-accepted` with the uploaded `throughSeq`; the next request uploads that event as part of its new suffix. Transport and non-2xx failures append no acceptance record, so later requests resend the uncertain range. Concurrent deliveries may be accepted out of order; folding the maximum matching `throughSeq` prevents cursor regression. A crash after server acceptance but before the watermark reaches persistence can replay an accepted range after restart. This is the at-least-once failure direction: uncertainty creates duplicates, never a skipped sequence. The ordinary session checkpoint policy persists the watermark at the next semantic checkpoint; this plugin performs no independent I/O. diff --git a/packages/session/session-log-deepseek/README.zh.md b/packages/session/session-log-deepseek/README.zh.md index c477bc7762..0121644cc0 100644 --- a/packages/session/session-log-deepseek/README.zh.md +++ b/packages/session/session-log-deepseek/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -用于 DeepSeek 官方 LLM API 请求的增量权威会话日志上传。该函数插件注入 `ctx.sessions` 与 `ctx.deepseekLlmApiExtensions`,并拥有 `dsh_session_log` 请求字段和持久的 `session-log-deepseek/accepted` 水位事件。 +用于 DeepSeek 官方 LLM API 请求的增量权威会话日志上传。该函数插件注入 `ctx.sessions` 与 `ctx.deepseekLlmApiExtensions`,并拥有 `dsh_session_log` 请求字段以及用于派生接受水位的持久 `session-log-deepseek/delivery-accepted` 事件。 ## 配置 @@ -20,7 +20,7 @@ ## 接受与重试 -DeepSeek 适配器会在 HTTP 2xx 后、消费 SSE(Server-Sent Events)正文前调用已准备贡献的 `accept()`。接受操作会追加 `session-log-deepseek/accepted` 及已上传的 `throughSeq`;下一次请求再把该水位作为新后缀的一部分上传。传输失败与非 2xx 失败不会追加水位,因此后续请求会重发不确定范围。并发请求可能乱序追加已接受水位;折叠其中最大值可以防止游标回退。 +DeepSeek 适配器会在 HTTP 2xx 后、消费 SSE(Server-Sent Events)正文前调用已准备贡献的 `accept()`。接受操作会追加 `session-log-deepseek/delivery-accepted` 及已上传的 `throughSeq`;下一次请求再把该事件作为新后缀的一部分上传。传输失败与非 2xx 失败不会追加接受记录,因此后续请求会重发不确定范围。并发交付可能乱序得到接受;折叠匹配记录中最大的 `throughSeq` 可以防止游标回退。 服务端接受后、持久化水位前发生崩溃,可能让恢复后的进程重放已经接受的范围。这是至少一次交付的失败方向:不确定性会制造重复,绝不会跳过序列。普通会话检查点策略会在下一个语义检查点持久化水位;本插件不执行独立 I/O。 diff --git a/packages/session/session-log-deepseek/src/index.ts b/packages/session/session-log-deepseek/src/index.ts index 99ee268e20..2d957c839d 100644 --- a/packages/session/session-log-deepseek/src/index.ts +++ b/packages/session/session-log-deepseek/src/index.ts @@ -50,7 +50,7 @@ export function acceptedThrough(session: Session): number { const start = previous?.scannedEvents ?? 0 for (let index = start; index < events.length; index++) { const event = events[index] as SessionEvent - if (event.type !== 'session-log-deepseek/accepted') continue + if (event.type !== 'session-log-deepseek/delivery-accepted') continue if (typeof event.data.sessionId !== 'string' || event.data.sessionId.length === 0 || !Number.isSafeInteger(event.data.throughSeq) || event.data.throughSeq < 0 || event.data.throughSeq >= event.seq) { @@ -99,7 +99,7 @@ export function apply(ctx: Context, config: Config): void { return { value, accept: () => { - session.append('session-log-deepseek/accepted', { sessionId: session.id, throughSeq }) + session.append('session-log-deepseek/delivery-accepted', { sessionId: session.id, throughSeq }) // TODO: Add an immediate lightweight checkpoint if duplicate replay after a 2xx crash window becomes unacceptable. }, } diff --git a/packages/session/session-log-deepseek/src/invariant.ts b/packages/session/session-log-deepseek/src/invariant.ts index 8e0a84db60..e739126ab6 100644 --- a/packages/session/session-log-deepseek/src/invariant.ts +++ b/packages/session/session-log-deepseek/src/invariant.ts @@ -13,30 +13,30 @@ export const name = 'session-log-deepseek-invariant' export const inject = ['invariants'] /** Validate one acceptance watermark against its containing event and session. */ -function validateAccepted(session: Session, event: SessionEvent<'session-log-deepseek/accepted'>, fail: InvariantFailure): void { +function validateDeliveryAccepted(session: Session, event: SessionEvent<'session-log-deepseek/delivery-accepted'>, fail: InvariantFailure): void { const { sessionId, throughSeq } = event.data const inherited = session.header.parentSession !== undefined && session.header.seedLength !== undefined && event.seq < session.header.seedLength if (sessionId !== session.id && !inherited) { - fail('a non-inherited session-log-deepseek/accepted event must name its containing session') + fail('a non-inherited session-log-deepseek/delivery-accepted event must name its containing session') } if (!Number.isSafeInteger(throughSeq) || throughSeq < 0 || throughSeq >= event.seq) { - fail(`session-log-deepseek/accepted throughSeq must identify an earlier event, got ${throughSeq} at seq ${event.seq}`) + fail(`session-log-deepseek/delivery-accepted throughSeq must identify an earlier event, got ${throughSeq} at seq ${event.seq}`) } } /** Validate acceptance watermarks already present in one Session. */ function validateSession(session: Session, fail: InvariantFailure): void { for (const event of session.events) { - if (event.type === 'session-log-deepseek/accepted') validateAccepted(session, event, fail) + if (event.type === 'session-log-deepseek/delivery-accepted') validateDeliveryAccepted(session, event, fail) } } /** Validate one live session-event dispatch. */ function validateDispatched(args: unknown[], fail: InvariantFailure): void { const [session, event] = args as [Session, SessionEvent] - if (event.type === 'session-log-deepseek/accepted') validateAccepted(session, event, fail) + if (event.type === 'session-log-deepseek/delivery-accepted') validateDeliveryAccepted(session, event, fail) } /** Install validation for restored, newly created, and newly appended watermarks. */ diff --git a/packages/session/session-log-deepseek/src/types.ts b/packages/session/session-log-deepseek/src/types.ts index 25d9879f41..2680130c88 100644 --- a/packages/session/session-log-deepseek/src/types.ts +++ b/packages/session/session-log-deepseek/src/types.ts @@ -50,9 +50,9 @@ declare module '@deepseek-ai/dsh-deepseek-llm-api-extensions/types' { declare module '@deepseek-ai/dsh-session/types' { interface SessionEventMap { - /** Records a confirmed HTTP acceptance watermark for restart-safe suffix selection. */ - 'session-log-deepseek/accepted': { - /** Session identity the accepted request carried; inherited fork markers retain the parent's id. */ + /** Records that the configured endpoint accepted one delivery through `throughSeq`. */ + 'session-log-deepseek/delivery-accepted': { + /** Session identity the accepted delivery carried; inherited fork markers retain the parent's id. */ sessionId: import('@deepseek-ai/dsh-session/types').SessionId /** Last canonical event included in the accepted request. */ throughSeq: number diff --git a/packages/session/session-log-deepseek/tests/invariant.spec.ts b/packages/session/session-log-deepseek/tests/invariant.spec.ts index 4467dede47..f39c4aec45 100644 --- a/packages/session/session-log-deepseek/tests/invariant.spec.ts +++ b/packages/session/session-log-deepseek/tests/invariant.spec.ts @@ -25,7 +25,7 @@ describe('DeepSeek session-log acceptance invariant', () => { const ctx = await setup() const session = ctx.sessions.create(SessionId('valid')) session.append('turn/start', { turn: 1 }) - expect(() => session.append('session-log-deepseek/accepted', { sessionId: session.id, throughSeq: 0 })) + expect(() => session.append('session-log-deepseek/delivery-accepted', { sessionId: session.id, throughSeq: 0 })) .not.toThrow() }) @@ -33,7 +33,7 @@ describe('DeepSeek session-log acceptance invariant', () => { const ctx = await setup() const wrongId = ctx.sessions.create(SessionId('wrong-id')) wrongId.append('turn/start', { turn: 1 }) - expect(() => wrongId.append('session-log-deepseek/accepted', { + expect(() => wrongId.append('session-log-deepseek/delivery-accepted', { sessionId: SessionId('other'), throughSeq: 0, })).toThrow(expect.objectContaining>({ @@ -43,7 +43,7 @@ describe('DeepSeek session-log acceptance invariant', () => { const wrongSeq = ctx.sessions.create(SessionId('wrong-seq')) wrongSeq.append('turn/start', { turn: 1 }) - expect(() => wrongSeq.append('session-log-deepseek/accepted', { + expect(() => wrongSeq.append('session-log-deepseek/delivery-accepted', { sessionId: wrongSeq.id, throughSeq: 1, })).toThrow(expect.objectContaining>({ @@ -60,7 +60,7 @@ describe('DeepSeek session-log acceptance invariant', () => { const id = SessionId('late-invalid') ctx.sessions.create(id, { seed: [ { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } }, - { type: 'session-log-deepseek/accepted', seq: 1, time: 2, data: { sessionId: id, throughSeq: 1 } }, + { type: 'session-log-deepseek/delivery-accepted', seq: 1, time: 2, data: { sessionId: id, throughSeq: 1 } }, ] }) let failure: unknown @@ -85,7 +85,7 @@ describe('DeepSeek session-log acceptance invariant', () => { ctx.sessions.create(childId, { seed: [ { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } }, - { type: 'session-log-deepseek/accepted', seq: 1, time: 2, data: { sessionId: parentId, throughSeq: 0 } }, + { type: 'session-log-deepseek/delivery-accepted', seq: 1, time: 2, data: { sessionId: parentId, throughSeq: 0 } }, ], meta: { parentSession: parentId, seedLength: 2 }, }) diff --git a/packages/session/session-log-deepseek/tests/upload.spec.ts b/packages/session/session-log-deepseek/tests/upload.spec.ts index 510c983ce6..611a1a50da 100644 --- a/packages/session/session-log-deepseek/tests/upload.spec.ts +++ b/packages/session/session-log-deepseek/tests/upload.spec.ts @@ -67,7 +67,7 @@ describe('incremental DeepSeek session-log upload', () => { expect(second.fields.dsh_session_log?.events).toHaveLength(2) expect(second.fields.dsh_session_log?.events[0]).toMatchObject({ encoding: 'raw', - event: { type: 'session-log-deepseek/accepted', seq: 2 }, + event: { type: 'session-log-deepseek/delivery-accepted', seq: 2 }, }) }) @@ -107,7 +107,7 @@ describe('incremental DeepSeek session-log upload', () => { const id = SessionId('incremental-fold') const events: SessionEvent[] = [ { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } }, - { type: 'session-log-deepseek/accepted', seq: 1, time: 2, data: { sessionId: id, throughSeq: 0 } }, + { type: 'session-log-deepseek/delivery-accepted', seq: 1, time: 2, data: { sessionId: id, throughSeq: 0 } }, ] let reads = 0 const observed = new Proxy(events, { @@ -126,7 +126,7 @@ describe('incremental DeepSeek session-log upload', () => { events.push( { type: 'step/start', seq: 2, time: 3, data: { turn: 1, step: 1 } }, - { type: 'session-log-deepseek/accepted', seq: 3, time: 4, data: { sessionId: id, throughSeq: 2 } }, + { type: 'session-log-deepseek/delivery-accepted', seq: 3, time: 4, data: { sessionId: id, throughSeq: 2 } }, ) expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(2) expect(reads).toBe(2) @@ -147,7 +147,7 @@ describe('incremental DeepSeek session-log upload', () => { expect(current.fields.dsh_session_log).toMatchObject({ afterSeq: 0, throughSeq: 1, - events: [{ event: { type: 'session-log-deepseek/accepted' } }], + events: [{ event: { type: 'session-log-deepseek/delivery-accepted' } }], }) }) @@ -160,7 +160,7 @@ describe('incremental DeepSeek session-log upload', () => { it('fails closed on a malformed persisted acceptance watermark', async () => { const malformed = [{ - type: 'session-log-deepseek/accepted', + type: 'session-log-deepseek/delivery-accepted', seq: 0, time: 1, data: { sessionId: 'malformed', throughSeq: 0 }, diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/result.json b/scripts/snapshots/python-sdk-single-exe/advanced/result.json index ff3e3ba3ae..0889fd6b9f 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json +++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json @@ -134,7 +134,7 @@ } }, { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 8, "time": 0, "data": { @@ -329,7 +329,7 @@ } }, { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 19, "time": 0, "data": { @@ -562,7 +562,7 @@ } }, { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 31, "time": 0, "data": { @@ -788,7 +788,7 @@ } }, { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 44, "time": 0, "data": { @@ -979,7 +979,7 @@ } }, { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 55, "time": 0, "data": { @@ -1210,7 +1210,7 @@ } }, { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 70, "time": 0, "data": { @@ -1437,7 +1437,7 @@ } }, { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 82, "time": 0, "data": { @@ -1770,7 +1770,7 @@ "payload": { "sessionId": "{{parent}}", "event": { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 8, "time": 0, "data": { @@ -2031,7 +2031,7 @@ "payload": { "sessionId": "{{parent}}", "event": { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 19, "time": 0, "data": { @@ -2336,7 +2336,7 @@ "payload": { "sessionId": "{{parent}}", "event": { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 31, "time": 0, "data": { @@ -2640,7 +2640,7 @@ "payload": { "sessionId": "{{parent}}", "event": { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 44, "time": 0, "data": { @@ -3071,7 +3071,7 @@ "payload": { "sessionId": "{{child-1}}", "event": { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 10, "time": 0, "data": { @@ -3360,7 +3360,7 @@ "payload": { "sessionId": "{{parent}}", "event": { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 55, "time": 0, "data": { @@ -3823,7 +3823,7 @@ "payload": { "sessionId": "{{child-2}}", "event": { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 10, "time": 0, "data": { @@ -4143,7 +4143,7 @@ "payload": { "sessionId": "{{parent}}", "event": { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 70, "time": 0, "data": { @@ -4442,7 +4442,7 @@ "payload": { "sessionId": "{{parent}}", "event": { - "type": "session-log-deepseek/accepted", + "type": "session-log-deepseek/delivery-accepted", "seq": 82, "time": 0, "data": { diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl index 29496835de..6e2f62bfe1 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.jsonl @@ -9,7 +9,7 @@ {"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} -{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{child-1}}","throughSeq":9}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-1}}","throughSeq":9}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl index 149798c850..e11cec5548 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.jsonl @@ -9,7 +9,7 @@ {"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} -{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{child-2}}","throughSeq":9}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-2}}","throughSeq":9}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl index c8820f9822..2e65037b37 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.jsonl @@ -7,7 +7,7 @@ {"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[4],"source":{"kind":"fallback"}}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} -{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":7}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":7}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-define","name":"cordis_define","argumentsDelta":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-define","name":"cordis_define","arguments":"{\"plugin\": {\"kind\": \"new\", \"idPrefix\": \"snap\"}, \"name\": \"Snapshot Double\", \"purpose\": \"Expose a deterministic doubling tool for executable snapshot verification.\", \"code\": {\"host\": \"return (ctx) => {\\n harness.registerTool(ctx, harness.defineTool({\\n name: 'snapshot_double',\\n description: 'Double a number for executable snapshot verification.',\\n parameters: { value: { type: 'number', required: true } },\\n output: {\\n schema: { type: 'number' },\\n render(_args, value) {\\n return [{ type: 'text', text: String(value) }]\\n }\\n },\\n async execute(args) {\\n return args.value * 2\\n }\\n }))\\n}\\n\"}}"}}}} @@ -18,7 +18,7 @@ {"type":"tool/result","data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-define"},"content":[{"type":"tool-result","toolCallId":"advanced-define","content":[{"type":"text","text":"Defined snap-1/pkg-1 (Snapshot Double); it is not running yet. Use cordis_run to activate this Package."}],"isError":false}],"role":"user","id":"{{messageId}}"},"meta":{"pluginId":"snap-1","packageId":"pkg-1"}},"sourceEventSeqs":[15],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":18}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":18}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-run","name":"cordis_run","argumentsDelta":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-run","name":"cordis_run","arguments":"{\"pluginId\": \"snap-1\", \"packageId\": \"pkg-1\", \"mode\": \"run\"}"}}}} @@ -30,7 +30,7 @@ {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","snapshot_double","subagent","workflow"]},"reason":"change"}} -{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":30}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":30}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-code","name":"run_code","argumentsDelta":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\", \"description\": \"Run the temporary Plugin tool\"}"}}}} @@ -43,7 +43,7 @@ {"type":"tool/result","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"42"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[38],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} {"type":"step/start","data":{"turn":1,"step":4}} -{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":43}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":43}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-direct-child","name":"subagent","argumentsDelta":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\": \"Check direct child\", \"prompt\": \"Reply with exactly DIRECT_CHILD_OK and nothing else.\"}"}}}} @@ -54,7 +54,7 @@ {"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[51],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":4}} {"type":"step/start","data":{"turn":1,"step":5}} -{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":54}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":54}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-workflow","name":"workflow","argumentsDelta":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\": \"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\", \"meta\": {\"name\": \"advanced-exe-snapshot\", \"description\": \"exercise one packaged workflow child\"}}"}}}} @@ -69,7 +69,7 @@ {"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-exe-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"{{messageId}}"}},"sourceEventSeqs":[62],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":5}} {"type":"step/start","data":{"turn":1,"step":6}} -{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":69}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":69}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-undefine","name":"cordis_undefine","argumentsDelta":"{\"pluginId\": \"snap-1\"}"}}} {"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-undefine","name":"cordis_undefine","arguments":"{\"pluginId\": \"snap-1\"}"}}}} @@ -81,7 +81,7 @@ {"type":"step/end","data":{"turn":1,"step":6}} {"type":"step/start","data":{"turn":1,"step":7}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":["cordis_define","cordis_inspect_list","cordis_inspect_query","cordis_inspect_self","cordis_run","cordis_stop","cordis_undefine","job_kill","job_list","job_output","run_code","subagent","workflow"]},"reason":"change"}} -{"type":"session-log-deepseek/accepted","data":{"sessionId":"{{parent}}","throughSeq":81}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","throughSeq":81}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_EXECUTABLE_OK"}}} {"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}}} From 9c7e142f7940847eaf2713fab58ab7c870dc9f32 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 23 Aug 2026 00:22:41 +0800 Subject: [PATCH 6/6] refactor(session): send canonical events directly --- ...pseek-llm-api-request-extensions.i18n.yaml | 4 +- ...-21-deepseek-llm-api-request-extensions.md | 23 +- ...-deepseek-llm-api-request-extensions.zh.md | 23 +- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 2 +- docs/config-catalog.zh.md | 2 +- ...deepseek-llm-api-wire-extensions.i18n.yaml | 4 +- docs/deepseek-llm-api-wire-extensions.md | 120 +-------- docs/deepseek-llm-api-wire-extensions.zh.md | 120 +-------- docs/persistence-catalog.i18n.yaml | 4 +- docs/persistence-catalog.md | 2 +- docs/persistence-catalog.zh.md | 2 +- .../tests/loader-composition.spec.ts | 4 +- .../session-log-deepseek/README.i18n.yaml | 4 +- .../session/session-log-deepseek/README.md | 5 +- .../session/session-log-deepseek/README.zh.md | 5 +- .../session/session-log-deepseek/src/codec.ts | 231 ------------------ .../session/session-log-deepseek/src/index.ts | 13 +- .../session/session-log-deepseek/src/types.ts | 34 +-- .../session-log-deepseek/tests/codec.spec.ts | 139 ----------- .../session-log-deepseek/tests/upload.spec.ts | 14 +- 21 files changed, 88 insertions(+), 671 deletions(-) delete mode 100644 packages/session/session-log-deepseek/src/codec.ts delete mode 100644 packages/session/session-log-deepseek/tests/codec.spec.ts diff --git a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml index d6de9c84a1..d7990f37fe 100644 --- a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md -2026-08-21-deepseek-llm-api-request-extensions.md: 8e4d1e375ecf4169ac8f7f93a3a341e4d4252191 -2026-08-21-deepseek-llm-api-request-extensions.zh.md: 05d68cdce547ff8bde059fd2d0fe1a1b44af67f0 +2026-08-21-deepseek-llm-api-request-extensions.md: 018b93115f5376affd86a4da3c76f0f367ba9ed0 +2026-08-21-deepseek-llm-api-request-extensions.zh.md: 4bc0f0c992445c5897069b68efa47fdba46dfdb4 diff --git a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md index 8e4d1e375e..018b93115f 100644 --- a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md +++ b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.md @@ -24,7 +24,7 @@ The provider-neutral `llm` package and `llm-pi-ai` contain no extension type, se The failure direction is at least once. A transport or provider rejection records no watermark. A crash after remote acceptance but before the watermark persists causes replay after resume, never a skipped sequence. Existing session checkpoints persist the event; the upload plugin owns no second store. -Event strings are raw unless exact request-relative references reduce encoded bytes. A reference identifies one serialized DeepSeek message, a path to one parsed string value, and a half-open UTF-8 byte range. The encoder verifies that each candidate range decodes to the exact matched UTF-16 substring; surrogate-splitting and ill-formed UTF-16 candidates stay raw. The decoder reconstructs every literal and cited byte exactly and rejects invalid paths, ranges, or code-point splits. Fuzzy similarity, normalization, and lossy omission are absent. +The `events` array contains complete canonical `SessionEvent` objects directly. The sender copies every present event member without projection or redaction; the field is self-contained and requires no reconstruction against `messages`. ## Plugin package field @@ -50,7 +50,7 @@ The process-lifetime manifest-identity cache remains separate because in-process ## Verification -Registry tests pin duplicate ownership, effect-scoped disposal, detached field values, concurrent and abortable preparation, receiver-preserving acceptance, one acceptance settlement, and failure aggregation. Session tests pin the default-off policy, explicit full-first/suffix-later delivery, incremental watermark folding, persisted restart recovery, fork identity fencing, out-of-order acceptance, exact Unicode reconstruction, invalid references, surrogate-safe raw fallback, and late invariant loading. Package-inventory tests pin default-on and explicit-off policies, host and standing-preset discovery, conflicting Loader resolution bases, manifest resolution, lifecycle filtering, and exact name/version ordering. The direct adapter mock proves pre-HTTP preparation failure, cancellation, non-2xx non-acceptance, 2xx acceptance before a later stream failure, and field collision. Keyless replay pins post-2xx extension acceptance, and the TypeScript JSON-RPC plus Python packaged-runtime snapshots project the acceptance event through both SDKs. Real Loader composition pins default package metadata plus opt-in Session upload, one real-API request mounts both shipped extensions and proves the official endpoint accepts them, and pi-ai tests retain their unchanged wire requests. +Registry tests pin duplicate ownership, effect-scoped disposal, detached field values, concurrent and abortable preparation, receiver-preserving acceptance, one acceptance settlement, and failure aggregation. Session tests pin the default-off policy, explicit full-first/suffix-later delivery, direct complete event envelopes independent of base-body messages, incremental watermark folding, persisted restart recovery, fork identity fencing, out-of-order acceptance, and late invariant loading. Package-inventory tests pin default-on and explicit-off policies, host and standing-preset discovery, conflicting Loader resolution bases, manifest resolution, lifecycle filtering, and exact name/version ordering. The direct adapter mock proves pre-HTTP preparation failure, cancellation, non-2xx non-acceptance, 2xx acceptance before a later stream failure, and field collision. Keyless replay pins post-2xx extension acceptance, and the TypeScript JSON-RPC plus Python packaged-runtime snapshots project the acceptance event through both SDKs. Real Loader composition pins default package metadata plus opt-in Session upload, one real-API request mounts both shipped extensions and proves the official endpoint accepts them, and pi-ai tests retain their unchanged wire requests. ## Alternatives considered @@ -58,7 +58,22 @@ Registry tests pin duplicate ownership, effect-scoped disposal, detached field v **Hard-wire the two producers into `llm-deepseek`.** Rejected because the adapter would import Session, Loader, preset, package-manifest, and cursor logic. The registry keeps transport responsible only for field merge and HTTP acceptance. -**Use fuzzy message similarity or omit overlapping event data.** Rejected because the receiver could not reconstruct the canonical log. Exact byte references with raw fallback preserve every value. +### Why not request-relative message references? + +A recursive tagged representation could replace exact event-string ranges with paths and UTF-8 byte offsets into the containing request's `messages`. Measurement used Node v24.16.0 on macOS arm64 and the three largest available local Zstandard Session artifacts, whose compressed artifact sizes were 2,437,052, 572,602, and 118,811 bytes. Late-enable replay used each final completed request boundary; steady replay covered 411 completed boundaries. The byte counts cover complete minified DeepSeek requests. + +| Replay | Raw JSON | Referenced JSON | Saving | Synchronous encoder time | +|---|---:|---:|---:|---:| +| Late enable | 29,668,725 B | 27,645,825 B | 6.82% | 500.1 s total | +| Steady state | 389,295,815 B | 387,180,848 B | 0.54% | 285.0 s total | + +The three late-enable calls took 470.5, 29.4, and 0.158 seconds. Only 701 of 115,071 events (0.61%) selected references. A hypothetical level-6 whole-request gzip comparison reduced raw request bytes by 89.38% for late enable and 73.42% for steady state; message references added 21.68% and 0.59% respectively after gzip. + +The receiver would also need to traverse the tagged tree, resolve paths into the exact request messages, validate UTF-8 ranges, and reconstruct every referenced event. Even treating that receiver cost as zero, the steady-state byte saving, synchronous sender cost, and dependence on another request field do not justify a versioned wire format. + +### Why not omit assistant chunks or overlapping event data? + +About 98% of the measured real-session events were `assistant/chunk`. Omitting chunks after reference encoding reduced the complete identity JSON by another 84.79% for late enable and 6.49% for steady state, but it prevents lossless canonical-log reconstruction and leaves `assistant/message.sourceEventSeqs` pointing to absent events. Fuzzy or normalized substitutions have the same reconstruction defect. **Keep the upload cursor only in memory.** Rejected because a normal process restart would resend the entire Session. A canonical acceptance event makes restart recovery best-effort durable without another storage backend; the remaining crash window produces allowed duplicates. @@ -70,7 +85,7 @@ Registry tests pin duplicate ownership, effect-scoped disposal, detached field v ## Consequences -Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. An explicit Session-log opt-in also carries the complete newly unaccepted Session suffix. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Request-relative packing synchronously compares suffix and message strings before dispatch, so a large first upload or retry backlog can delay the event loop until candidate indexing is implemented. Encoding, manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata. +Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. An explicit Session-log opt-in also carries the complete newly unaccepted Session suffix. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata. The `delivery-accepted` event becomes part of the canonical log and is itself delivered on a later request. Crash recovery can duplicate a suffix but does not infer acceptance from assistant output or create a second local cursor store. Direct calls without a live Session omit the session field; host package inventory remains available. diff --git a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md index 05d68cdce5..4bc0f0c992 100644 --- a/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-21-deepseek-llm-api-request-extensions.zh.md @@ -24,7 +24,7 @@ Status: implemented 失败方向为至少一次。传输失败或提供方拒绝不会记录水位。远端接受后、水位持久化前发生崩溃,会在恢复后触发重放,绝不会跳过序列。现有会话检查点会持久化该事件;上传插件不拥有第二份存储。 -只有在确切的请求相对引用能够减少编码字节时,事件字符串才不使用原始形式。一个引用会标识一条已序列化 DeepSeek 消息、通向一个解析后字符串值的路径,以及半开 UTF-8 字节范围。编码器会验证每个候选范围都能解码为完全相同的已匹配 UTF-16 子串;拆分 surrogate 或非良构 UTF-16 的候选会保持原始形式。解码器会精确重建每个字面值与引用字节,并拒绝无效路径、范围或码点切分。系统不使用模糊相似度、规范化或有损省略。 +`events` 数组会直接包含完整的权威 `SessionEvent` 对象。发送方会复制事件的每个已有成员,不执行投影或脱敏;该字段自包含,无需根据 `messages` 重建内容。 ## 插件包字段 @@ -50,7 +50,7 @@ Status: implemented ## 验证 -注册表测试固定重复所有权、effect 作用域 dispose(资源释放)、分离字段值、并发且可取消的准备、保留接收者的接受操作、单次接受结算与失败聚合。会话测试固定默认关闭策略、显式启用后的首次完整/后续后缀交付、增量水位 fold、持久化重启恢复、fork 身份围栏、乱序接受、确切 Unicode 重建、无效引用、surrogate 安全原始回退与 invariant 延迟加载。插件包清单测试固定默认开启与显式关闭策略、宿主与 standing preset 发现、冲突的 Loader 解析基址、manifest 解析、生命周期过滤及确切名称/版本排序。直接适配器 mock 测试证明 HTTP 前准备失败、取消、非 2xx 不接受、2xx 在后续流失败前接受,以及字段冲突。无密钥回放会固定 2xx 后扩展接受,TypeScript JSON-RPC 与 Python 打包运行时快照则通过两套 SDK 投影接受事件。真实 Loader 组合会固定默认包元数据与显式启用的会话上传,一个真实 API 请求会挂载两个随附扩展并证明官方端点接受它们;pi-ai 测试保持其协议请求不变。 +注册表测试固定重复所有权、effect 作用域 dispose(资源释放)、分离字段值、并发且可取消的准备、保留接收者的接受操作、单次接受结算与失败聚合。会话测试固定默认关闭策略、显式启用后的首次完整/后续后缀交付、与基础正文消息无关的直接完整事件信封、增量水位 fold、持久化重启恢复、fork 身份围栏、乱序接受与 invariant 延迟加载。插件包清单测试固定默认开启与显式关闭策略、宿主与 standing preset 发现、冲突的 Loader 解析基址、manifest 解析、生命周期过滤及确切名称/版本排序。直接适配器 mock 测试证明 HTTP 前准备失败、取消、非 2xx 不接受、2xx 在后续流失败前接受,以及字段冲突。无密钥回放会固定 2xx 后扩展接受,TypeScript JSON-RPC 与 Python 打包运行时快照则通过两套 SDK 投影接受事件。真实 Loader 组合会固定默认包元数据与显式启用的会话上传,一个真实 API 请求会挂载两个随附扩展并证明官方端点接受它们;pi-ai 测试保持其协议请求不变。 ## 考虑过的替代方案 @@ -58,7 +58,22 @@ Status: implemented **把两个提供方硬编码进 `llm-deepseek`。** 已否决,因为适配器将导入会话、Loader、preset、包 manifest 与游标逻辑。注册表让传输只负责字段合并与 HTTP 接受。 -**使用模糊消息相似度,或省略重叠事件数据。** 已否决,因为接收方无法重建权威日志。带原始回退的确切字节引用会保留每个值。 +### 为什么不使用请求相对消息引用? + +一种递归的带标签表示可以用所属请求 `messages` 中的路径与 UTF-8 字节偏移,替换事件字符串的确切范围。测量使用 Node v24.16.0、macOS arm64 与可用的三份最大本地 Zstandard 会话产物;其压缩产物大小分别为 2,437,052、572,602 与 118,811 字节。延迟启用回放使用各会话最后一个已完成请求边界;稳态回放覆盖 411 个已完成边界。字节数覆盖完整且最小化的 DeepSeek 请求。 + +| 回放方式 | 原始 JSON | 引用 JSON | 节省比例 | 同步编码器耗时 | +|---|---:|---:|---:|---:| +| 延迟启用 | 29,668,725 B | 27,645,825 B | 6.82% | 合计 500.1 s | +| 稳态 | 389,295,815 B | 387,180,848 B | 0.54% | 合计 285.0 s | + +三次延迟启用调用分别耗时 470.5、29.4 与 0.158 秒。115,071 个事件中只有 701 个(0.61%)选择引用。一项假设采用 level-6 整请求 gzip 的对照,使原始请求字节在延迟启用场景减少 89.38%,在稳态场景减少 73.42%;加入消息引用后,gzip 结果分别额外减少 21.68% 与 0.59%。 + +接收方还需要遍历带标签树、解析通向确切请求消息的路径、校验 UTF-8 范围,并重建每个引用事件。即使把接收方成本视为零,稳态字节节省、发送方同步成本以及对另一请求字段的依赖,也不足以支撑带版本的协议格式。 + +### 为什么不省略 assistant 分片或重叠事件数据? + +实测真实会话事件中约 98% 为 `assistant/chunk`。在引用编码后省略分片,会让完整未压缩 JSON 在延迟启用场景进一步减少 84.79%,在稳态场景进一步减少 6.49%,但这会阻止权威日志的无损重建,并让 `assistant/message.sourceEventSeqs` 指向缺失事件。模糊替换或规范化替换也存在同一重建缺陷。 **只在内存中保留上传游标。** 已否决,因为普通进程重启会重发完整会话。权威接受事件让重启恢复获得尽力而为的持久性,无需另一存储后端;剩余崩溃窗口只会产生允许的重复。 @@ -70,7 +85,7 @@ Status: implemented ## 后果 -DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。显式选择启用会话日志后,请求还会携带完整的未接受会话新后缀。这些字段对模型不可见,不增加提示词 token,也不改变 KV Cache,但可能显著增大 HTTP 正文。请求相对打包会在派发前同步比较后缀字符串与消息字符串,因此在实现候选索引前,较大的首次上传或重试积压可能延迟事件循环。编码、manifest 解析、字段冲突、接受记录或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。 +DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。显式选择启用会话日志后,请求还会携带完整的未接受会话新后缀。这些字段对模型不可见,不增加提示词 token,也不改变 KV Cache,但可能显著增大 HTTP 正文。Manifest 解析、字段冲突、接受记录或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。 `delivery-accepted` 事件会成为权威日志的一部分,并在后续请求中自行交付。崩溃恢复可能重复后缀,但不会根据 assistant 输出推断接受,也不会创建第二份本地游标存储。缺少存活会话的直接调用会省略会话字段;宿主包清单仍然可用。 diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index e545eedbff..3fa94f6530 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 0159ddf433e3a32bb77864f4b3781d40c1815eb2 -config-catalog.zh.md: 7b0843c664d19a92b4122f92f0f283ab28e0e4a7 +config-catalog.md: 0e7244f84fe275c3f4f574c29a646cdbd9ea2ec8 +config-catalog.zh.md: b17c641400f8e73d526a42eeb521583f9d9b1b97 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 0159ddf433..0e7244f84f 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1738,7 +1738,7 @@ export interface Config { } ``` -Source: [`packages/session/session-log-deepseek/src/index.ts:24`](../packages/session/session-log-deepseek/src/index.ts) +Source: [`packages/session/session-log-deepseek/src/index.ts:22`](../packages/session/session-log-deepseek/src/index.ts) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 7b0843c664..b17c641400 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -1740,7 +1740,7 @@ export interface Config { } ``` -来源:[`packages/session/session-log-deepseek/src/index.ts:24`](../packages/session/session-log-deepseek/src/index.ts) +来源:[`packages/session/session-log-deepseek/src/index.ts:22`](../packages/session/session-log-deepseek/src/index.ts) diff --git a/docs/deepseek-llm-api-wire-extensions.i18n.yaml b/docs/deepseek-llm-api-wire-extensions.i18n.yaml index b8af9b2f92..96013a27ad 100644 --- a/docs/deepseek-llm-api-wire-extensions.i18n.yaml +++ b/docs/deepseek-llm-api-wire-extensions.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/deepseek-llm-api-wire-extensions.md -deepseek-llm-api-wire-extensions.md: 8a3f089c77a0879ef4056f3c16583935f2219248 -deepseek-llm-api-wire-extensions.zh.md: 1cf5ea1ce1cd410b23b011480a8c623827753464 +deepseek-llm-api-wire-extensions.md: fd42609693ac6fbf91dd82b6e73b2d1d06e65a54 +deepseek-llm-api-wire-extensions.zh.md: 61af718841c8778e8943a1a1c16621a9a6618add diff --git a/docs/deepseek-llm-api-wire-extensions.md b/docs/deepseek-llm-api-wire-extensions.md index 8a3f089c77..fd42609693 100644 --- a/docs/deepseek-llm-api-wire-extensions.md +++ b/docs/deepseek-llm-api-wire-extensions.md @@ -12,8 +12,8 @@ The adapter sends the additions to its resolved `baseURL`, including a configure |---|---|---| | HTTP field names | Lowercase kebab-case; HTTP matching remains case-insensitive | `user-agent`, `x-deepseek-harness-session-id` | | DeepSeek request-body extension fields | Snake case with the reserved `dsh_` prefix | `dsh_plugin_packages`, `dsh_session_log` | -| DSH-owned nested JSON members | Camel case | `afterSeq`, `messageIndex`, `utf8Start` | -| Tagged values | Kebab-case strings; durable events use `domain/action` | `message-references`, `session-log-deepseek/delivery-accepted` | +| DSH-owned nested JSON members | Camel case | `afterSeq`, `throughSeq`, `sessionId` | +| Tagged values | Kebab-case strings; durable events use `domain/action` | `session-log-deepseek/delivery-accepted` | Each body extension owns its `version` independently. A version applies only to the object that contains it; no compatibility or ordering relationship exists between versions of different fields. JSON member order is not part of the protocol. @@ -88,14 +88,11 @@ An enabled inventory with no qualifying entries sends `packages: []`; disabling "throughSeq": 0, "events": [ { - "encoding": "raw", - "event": { - "type": "turn/start", - "seq": 0, - "time": 1780000000001, - "data": { - "turn": 1 - } + "type": "turn/start", + "seq": 0, + "time": 1780000000001, + "data": { + "turn": 1 } } ] @@ -129,106 +126,9 @@ The `session` member is the exact `Session.header`, not a complete runtime Sessi | `delegationDepth` | optional | Non-negative persisted subagent delegation depth | | `agentPreset` | optional | Agent preset id used to compose this Session | -### Event envelope and encodings +### Canonical event envelopes -Each `events` item is an `EncodedSessionEvent` selected by its `encoding` value. - -| `encoding` | `event` value | -|---|---| -| `raw` | Complete canonical `SessionEvent` | -| `message-references` | `PackedJsonValue` that reconstructs the complete canonical `SessionEvent` against this request's `messages` | - -A canonical event always carries `type`, `seq`, `time`, and `data`. It may carry `ignorable: true`; surface events may additionally carry `sourceEventSeqs` and `surfaceOp`. A raw encoding copies every present member without projection or redaction. - -A `PackedJsonValue` uses one of four `kind` values: - -| `kind` | Members | Decoded value | -|---|---|---| -| `literal` | `value` | The contained JSON value, which may itself be composite | -| `string` | `parts` | Concatenation of decoded `PackedJsonStringPart` values | -| `array` | `items` | Array of recursively decoded values | -| `object` | `entries` | Object built from ordered `[key, value]` pairs | - -A packed string contains these part variants: - -| `kind` | `value` | -|---|---| -| `literal` | Literal string fragment | -| `message-slice` | Exact request-relative `DeepSeekMessageStringSlice` | - -```json -{ - "encoding": "message-references", - "event": { - "kind": "object", - "entries": [ - [ - "type", - { - "kind": "literal", - "value": "plugin/test" - } - ], - [ - "seq", - { - "kind": "literal", - "value": 0 - } - ], - [ - "time", - { - "kind": "literal", - "value": 1780000000001 - } - ], - [ - "data", - { - "kind": "object", - "entries": [ - [ - "text", - { - "kind": "string", - "parts": [ - { - "kind": "literal", - "value": "prefix:" - }, - { - "kind": "message-slice", - "value": { - "messageIndex": 0, - "path": [ - "content" - ], - "utf8Start": 0, - "utf8End": 12 - } - } - ] - } - ] - ] - } - ] - ] - } -} -``` - -| Slice member | Meaning | -|---|---| -| `messageIndex` | Zero-based index into the exact DeepSeek `messages` array in the containing request | -| `path` | Array of string object keys and numeric array indexes from that message root to a string value | -| `utf8Start` | Inclusive UTF-8 byte offset in the resolved string value | -| `utf8End` | Exclusive UTF-8 byte offset in the resolved string value | - -Offsets address the UTF-8 bytes of the parsed string value, not its JSON-escaped source text. A decoder rejects a missing path, a non-string target, an invalid range, or a range that splits a UTF-8 code point. Reconstruction therefore requires the exact `messages` array from the same request. - -The encoder uses a reference only for an exact substring and only when the complete referenced event occupies fewer serialized UTF-8 bytes than its raw form. Literal fragments retain unmatched text. A candidate that cannot round-trip exactly stays raw; the protocol performs no fuzzy matching, truncation, or lossy omission. +Each `events` item is a complete canonical `SessionEvent`, independent of every other request field. An event always carries `type`, `seq`, `time`, and `data`; it may carry `ignorable: true`, and surface events may additionally carry `sourceEventSeqs` and `surfaceOp`. The sender copies every present member without projection, redaction, or reconstruction. ### Acceptance watermark and at-least-once delivery @@ -256,4 +156,4 @@ Transport and non-2xx failures append no watermark. A crash after endpoint accep The request headers expose the Harness application version, one anonymous Harness-home identity, and an optional Session identity. `dsh_plugin_packages` exposes active npm package names and versions. When enabled, `dsh_session_log` may expose the Session working directory, system-prompt snapshots, user and assistant content, raw assistant chunks, tool arguments and results, compaction summaries, feedback, and plugin-owned events. Adapter API keys are not Session events and therefore do not enter the field. A gateway selected through `baseURL` receives the same values as the official endpoint. -Receivers address extension fields by name, dispatch each field by its own `version`, preserve distinct package versions, and ignore JSON member ordering. A session-log receiver validates the contiguous sequence range and reconstructs every referenced event against the containing request before interpreting event types. An unrecognized canonical event without `ignorable: true` prevents lossless reconstruction. The base request remains usable without either the registry or a particular contribution; field absence means that contribution did not apply to that request. +Receivers address extension fields by name, dispatch each field by its own `version`, preserve distinct package versions, and ignore JSON member ordering. A session-log receiver validates the contiguous sequence range before interpreting event types. An unrecognized canonical event without `ignorable: true` prevents lossless reconstruction. The base request remains usable without either the registry or a particular contribution; field absence means that contribution did not apply to that request. diff --git a/docs/deepseek-llm-api-wire-extensions.zh.md b/docs/deepseek-llm-api-wire-extensions.zh.md index 1cf5ea1ce1..61af718841 100644 --- a/docs/deepseek-llm-api-wire-extensions.zh.md +++ b/docs/deepseek-llm-api-wire-extensions.zh.md @@ -12,8 +12,8 @@ |---|---|---| | HTTP 字段名 | 小写 kebab-case;HTTP 匹配仍不区分大小写 | `user-agent`, `x-deepseek-harness-session-id` | | DeepSeek 请求正文扩展字段 | 使用保留 `dsh_` 前缀的 snake case | `dsh_plugin_packages`, `dsh_session_log` | -| DSH 持有的嵌套 JSON 成员 | Camel case | `afterSeq`, `messageIndex`, `utf8Start` | -| 带标签的值 | 使用 kebab-case 字符串;持久事件采用 `domain/action` | `message-references`, `session-log-deepseek/delivery-accepted` | +| DSH 持有的嵌套 JSON 成员 | Camel case | `afterSeq`, `throughSeq`, `sessionId` | +| 带标签的值 | 使用 kebab-case 字符串;持久事件采用 `domain/action` | `session-log-deepseek/delivery-accepted` | 每个正文扩展独立持有自身的 `version`。版本仅适用于包含该字段的对象;不同字段的版本之间不存在兼容或排序关系。JSON 成员顺序不属于协议。 @@ -88,14 +88,11 @@ "throughSeq": 0, "events": [ { - "encoding": "raw", - "event": { - "type": "turn/start", - "seq": 0, - "time": 1780000000001, - "data": { - "turn": 1 - } + "type": "turn/start", + "seq": 0, + "time": 1780000000001, + "data": { + "turn": 1 } } ] @@ -129,106 +126,9 @@ | `delegationDepth` | 可选 | 持久化的非负 subagent 委派深度 | | `agentPreset` | 可选 | 用于组合该会话的 agent preset id | -### 事件信封与编码 +### 权威事件信封 -每个 `events` 元素都是通过 `encoding` 值选择的 `EncodedSessionEvent`。 - -| `encoding` | `event` 值 | -|---|---| -| `raw` | 完整的权威 `SessionEvent` | -| `message-references` | 根据本次请求 `messages` 重建完整权威 `SessionEvent` 的 `PackedJsonValue` | - -权威事件始终携带 `type`、`seq`、`time` 和 `data`。它可以携带 `ignorable: true`;展示事件还可以携带 `sourceEventSeqs` 和 `surfaceOp`。raw 编码会复制全部已有成员,不做投影或脱敏。 - -`PackedJsonValue` 使用以下四种 `kind` 值之一: - -| `kind` | 成员 | 解码值 | -|---|---|---| -| `literal` | `value` | 所含 JSON 值;该值自身也可以是复合值 | -| `string` | `parts` | 已解码 `PackedJsonStringPart` 值的拼接结果 | -| `array` | `items` | 递归解码值组成的数组 | -| `object` | `entries` | 根据有序 `[key, value]` 对构建的对象 | - -已打包字符串包含以下两种片段: - -| `kind` | `value` | -|---|---| -| `literal` | 字面字符串片段 | -| `message-slice` | 确切的请求相对 `DeepSeekMessageStringSlice` | - -```json -{ - "encoding": "message-references", - "event": { - "kind": "object", - "entries": [ - [ - "type", - { - "kind": "literal", - "value": "plugin/test" - } - ], - [ - "seq", - { - "kind": "literal", - "value": 0 - } - ], - [ - "time", - { - "kind": "literal", - "value": 1780000000001 - } - ], - [ - "data", - { - "kind": "object", - "entries": [ - [ - "text", - { - "kind": "string", - "parts": [ - { - "kind": "literal", - "value": "prefix:" - }, - { - "kind": "message-slice", - "value": { - "messageIndex": 0, - "path": [ - "content" - ], - "utf8Start": 0, - "utf8End": 12 - } - } - ] - } - ] - ] - } - ] - ] - } -} -``` - -| slice 成员 | 含义 | -|---|---| -| `messageIndex` | 包含该扩展的请求中,确切 DeepSeek `messages` 数组的零基序号 | -| `path` | 从该消息根到某个字符串值的路径数组;对象键使用字符串,数组索引使用数字 | -| `utf8Start` | 已解析字符串值中的包含性 UTF-8 字节偏移 | -| `utf8End` | 已解析字符串值中的排他性 UTF-8 字节偏移 | - -偏移量指向已解析字符串值的 UTF-8 字节,而不是经过 JSON 转义的源文本。路径缺失、目标不是字符串、范围无效或范围切开 UTF-8 码点时,解码器会拒绝。因此,重建必须使用同一次请求的确切 `messages` 数组。 - -编码器只会引用确切子字符串,并且仅在整个引用事件占用的序列化 UTF-8 字节少于 raw 形式时才会使用引用。字面片段保留未匹配文本。无法精确重建为原值的候选项会保留 raw 形式;协议不执行模糊匹配、截断或有损省略。 +每个 `events` 元素都是完整的权威 `SessionEvent`,不依赖任何其他请求字段。事件始终携带 `type`、`seq`、`time` 与 `data`;它可以携带 `ignorable: true`,展示事件还可携带 `sourceEventSeqs` 与 `surfaceOp`。发送方会复制每个已有成员,不执行投影、脱敏或重建。 ### 接受水位与至少一次交付 @@ -256,4 +156,4 @@ 请求标头会暴露 Harness 应用版本、一个匿名 Harness-home 身份和可选的会话身份。`dsh_plugin_packages` 会暴露存活 npm 包的名称与版本。启用后,`dsh_session_log` 可能暴露会话工作目录、系统提示词快照、用户与 assistant 内容、原始 assistant 分片、工具参数与结果、压缩摘要、反馈和插件持有的事件。适配器 API key 不是会话事件,因此不会进入该字段。通过 `baseURL` 选择的网关会收到与官方端点相同的值。 -接收方按名称定位扩展字段,按各字段自己的 `version` 分派,保留不同的包版本,并忽略 JSON 成员顺序。会话日志接收方必须校验连续序号范围,并在解释事件类型前,根据包含该扩展的请求重建每个引用事件。遇到不带 `ignorable: true` 的未知权威事件时,接收方无法进行无损重建。即使缺少注册表或某项贡献,基础请求仍然可用;字段缺失表示该项贡献不适用于本次请求。 +接收方按名称定位扩展字段,按各字段自己的 `version` 分派,保留不同的包版本,并忽略 JSON 成员顺序。会话日志接收方必须先校验连续序号范围,再解释事件类型。遇到不带 `ignorable: true` 的未知权威事件时,接收方无法进行无损重建。即使缺少注册表或某项贡献,基础请求仍然可用;字段缺失表示该项贡献不适用于本次请求。 diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index b8be9cdbc5..ea2e211bea 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-catalog.md -persistence-catalog.md: ad65bc6bd3971d204f9bce3c7ee822920ebe6a85 -persistence-catalog.zh.md: 3af5d7c46e7969b29a66b04adddc41d9d108553d +persistence-catalog.md: 8b1206ba18a6f49a0eb809a1a3a12828fd94f827 +persistence-catalog.zh.md: 9a085ece1fd2df331f34e311c48bfe0c8ca92c91 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index ad65bc6bd3..8b1206ba18 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -683,7 +683,7 @@ Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/sessi } ``` -Source: [`packages/session/session-log-deepseek/src/types.ts:54`](../packages/session/session-log-deepseek/src/types.ts) +Source: [`packages/session/session-log-deepseek/src/types.ts:26`](../packages/session/session-log-deepseek/src/types.ts) ### `step/*` diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 3af5d7c46e..9a085ece1f 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -685,7 +685,7 @@ export type SessionEvent = { } ``` -来源:[`packages/session/session-log-deepseek/src/types.ts:54`](../packages/session/session-log-deepseek/src/types.ts) +来源:[`packages/session/session-log-deepseek/src/types.ts:26`](../packages/session/session-log-deepseek/src/types.ts) ### `step/*` diff --git a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts index 57c3f34fc5..6237221aae 100644 --- a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts +++ b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts @@ -179,7 +179,7 @@ describe('llm-deepseek real dynamic composition', () => { session: { id: string } afterSeq: number throughSeq: number - events: Array<{ event: { type: string; seq: number } }> + events: Array<{ type: string; seq: number }> } } expect(request.dsh_session_log).toMatchObject({ @@ -187,7 +187,7 @@ describe('llm-deepseek real dynamic composition', () => { session: { id: 'extension-composition-enabled' }, afterSeq: -1, throughSeq: 0, - events: [{ event: { type: 'turn/start', seq: 0 } }], + events: [{ type: 'turn/start', seq: 0 }], }) expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0) }) diff --git a/packages/session/session-log-deepseek/README.i18n.yaml b/packages/session/session-log-deepseek/README.i18n.yaml index 362db6ab85..d9650b10d8 100644 --- a/packages/session/session-log-deepseek/README.i18n.yaml +++ b/packages/session/session-log-deepseek/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-log-deepseek/README.md -README.md: 39ff31b8c33f2f8e0a12cb6ed0814d97f982e3c9 -README.zh.md: 0121644cc0b7ebbe2e6b80ac6c31ae21e088fe95 +README.md: 24cff61f77dab76caf086375931edd5ee45e3f04 +README.zh.md: 0411fcd298acae86aee93da7efe812ab6d589c29 diff --git a/packages/session/session-log-deepseek/README.md b/packages/session/session-log-deepseek/README.md index 39ff31b8c3..24cff61f77 100644 --- a/packages/session/session-log-deepseek/README.md +++ b/packages/session/session-log-deepseek/README.md @@ -14,9 +14,7 @@ Shipped profiles mount the plugin so an overlay can enable it, but the default c ## Request field -For a request carrying a live `sessionId`, the plugin folds the greatest accepted watermark for that exact Session identity, snapshots `Session.events`, and sends the contiguous suffix after the watermark. A process-local fold scans each event once and consumes later appends incrementally; restart and HMR rebuild it from the durable log. The version-1 field contains the immutable `SessionHeader`, `afterSeq`, `throughSeq`, and every complete event envelope in that range. Forked sessions ignore inherited parent watermarks because each watermark records the Session id sent on the accepted request. - -Each event is sent raw unless request-relative packing reduces its JSON byte size. The codec traverses the exact serialized DeepSeek `messages`, and a packed string can replace an exact substring with `{ messageIndex, path, utf8Start, utf8End }`. References use half-open UTF-8 offsets into parsed string values; literal fragments retain everything outside the match. The encoder verifies each candidate's local UTF-8 round trip, so a surrogate-splitting or ill-formed UTF-16 match stays raw. The exported decoder rejects missing paths, non-string targets, invalid ranges, and ranges that split a UTF-8 code point. Raw fallback and byte-size comparison make every representation lossless and prevent reference overhead from expanding an event. +For a request carrying a live `sessionId`, the plugin folds the greatest accepted watermark for that exact Session identity, snapshots `Session.events`, and sends the contiguous suffix after the watermark. A process-local fold scans each event once and consumes later appends incrementally; restart and HMR rebuild it from the durable log. The version-1 field contains the immutable `SessionHeader`, `afterSeq`, `throughSeq`, and every complete canonical `SessionEvent` in that range as a direct array element. Forked sessions ignore inherited parent watermarks because each watermark records the Session id sent on the accepted request. ## Acceptance and retry @@ -47,4 +45,3 @@ None; the model-visible request prefix remains unchanged. - **Crash-window duplicates** — a 2xx followed by process loss before the acceptance watermark persists causes conservative replay on resume. - **No live Session means no field** — direct or stale-session calls have no canonical log to snapshot; explicit absence semantics remain deferred. - **No independent request-size cap** — complete delivery is fail-closed; provider rejection leaves the cursor unchanged instead of truncating the log. -- **Synchronous request-time packing** — the encoder compares suffix strings with request-message strings before dispatch. Large first uploads or retry backlogs can delay the event loop until candidate indexing is implemented. diff --git a/packages/session/session-log-deepseek/README.zh.md b/packages/session/session-log-deepseek/README.zh.md index 0121644cc0..0411fcd298 100644 --- a/packages/session/session-log-deepseek/README.zh.md +++ b/packages/session/session-log-deepseek/README.zh.md @@ -14,9 +14,7 @@ ## 请求字段 -对于携带存活 `sessionId` 的请求,插件会折叠该确切会话身份的最大已接受水位,对 `Session.events` 取快照,并发送水位之后的连续后缀。进程内 fold 会让每条事件只被扫描一次并增量消费后续追加;重启与 HMR 会从持久日志重建它。版本 1 字段包含不可变的 `SessionHeader`、`afterSeq`、`throughSeq`,以及该范围内每个完整事件信封。每个水位都会记录已接受请求发送的会话 id,因此 fork 会话会忽略从父会话继承的水位。 - -只有在请求相对打包能够减少 JSON 字节数时,事件才不以原始形式发送。Codec 会遍历确切的已序列化 DeepSeek `messages`,并可在打包字符串中用 `{ messageIndex, path, utf8Start, utf8End }` 替换完全匹配的子字符串。引用使用解析后字符串值的半开 UTF-8 偏移;字面分片保留匹配范围之外的全部内容。编码器会校验每个候选的局部 UTF-8 往返,因此拆分 surrogate 或非良构 UTF-16 的匹配会保持原始形式。导出的解码器会拒绝缺失路径、非字符串目标、无效范围和切断 UTF-8 码点的范围。原始回退与字节数比较让每种表示都保持无损,并阻止引用开销扩张事件。 +对于携带存活 `sessionId` 的请求,插件会折叠该确切会话身份的最大已接受水位,对 `Session.events` 取快照,并发送水位之后的连续后缀。进程内 fold 会让每条事件只被扫描一次并增量消费后续追加;重启与 HMR 会从持久日志重建它。版本 1 字段包含不可变的 `SessionHeader`、`afterSeq`、`throughSeq`,以及作为数组直接元素的该范围内每个完整权威 `SessionEvent`。每个水位都会记录已接受请求发送的会话 id,因此 fork 会话会忽略从父会话继承的水位。 ## 接受与重试 @@ -47,4 +45,3 @@ DeepSeek 适配器会在 HTTP 2xx 后、消费 SSE(Server-Sent Events)正文 - **崩溃窗口重复**——2xx 后、接受水位持久化前进程丢失,会在恢复时触发保守重放。 - **缺少存活会话就没有字段**——直接调用或陈旧会话调用没有可供快照的权威日志;显式缺失语义仍暂缓处理。 - **没有独立请求大小上限**——完整交付会快速失败;提供方拒绝会保持游标不变,而非截断日志。 -- **请求时同步打包**——编码器会在派发前比较后缀字符串与请求消息字符串。在实现候选索引前,较大的首次上传或重试积压可能延迟事件循环。 diff --git a/packages/session/session-log-deepseek/src/codec.ts b/packages/session/session-log-deepseek/src/codec.ts deleted file mode 100644 index bf7ff8df24..0000000000 --- a/packages/session/session-log-deepseek/src/codec.ts +++ /dev/null @@ -1,231 +0,0 @@ -/** Lossless request-relative packing for canonical session events. */ - -import { Buffer } from 'node:buffer' -import type { DeepSeekLlmApiJson } from '@deepseek-ai/dsh-deepseek-llm-api-extensions' -import type { JsonValue, SessionEvent } from '@deepseek-ai/dsh-session' -import type { - DeepSeekMessageStringSlice, - EncodedSessionEvent, - PackedJsonStringPart, - PackedJsonValue, -} from './types.ts' - -interface MessageStringSource { - readonly messageIndex: number - readonly path: readonly (string | number)[] - readonly value: string - readonly bytes: number -} - -interface PackedCandidate { - readonly value: PackedJsonValue - readonly references: boolean -} - -/** Return the serialized UTF-8 size used by the profitability decision. */ -function wireBytes(value: unknown): number { - return Buffer.byteLength(JSON.stringify(value), 'utf8') -} - -/** Enumerate every string leaf of the exact wire messages. */ -function collectSources( - value: DeepSeekLlmApiJson, - messageIndex: number, - path: readonly (string | number)[], - output: MessageStringSource[], -): void { - if (typeof value === 'string') { - if (value.length > 0) output.push({ messageIndex, path, value, bytes: Buffer.byteLength(value, 'utf8') }) - return - } - if (Array.isArray(value)) { - value.forEach((child, index) => { collectSources(child, messageIndex, [...path, index], output) }) - return - } - if (value === null || typeof value !== 'object') return - for (const [key, child] of Object.entries(value)) collectSources(child, messageIndex, [...path, key], output) -} - -/** Build one exact slice from UTF-16 offsets, or reject a lossy UTF-8 conversion. */ -function sliceOf(source: MessageStringSource, start: number, end: number): DeepSeekMessageStringSlice | undefined { - const candidate: DeepSeekMessageStringSlice = { - messageIndex: source.messageIndex, - path: source.path, - utf8Start: Buffer.byteLength(source.value.slice(0, start), 'utf8'), - utf8End: Buffer.byteLength(source.value.slice(0, end), 'utf8'), - } - const bytes = Buffer.from(source.value, 'utf8') - const selected = bytes.subarray(candidate.utf8Start, candidate.utf8End) - const decoded = selected.toString('utf8') - return decoded === source.value.slice(start, end) && Buffer.from(decoded, 'utf8').equals(selected) - ? candidate - : undefined -} - -/** Find a profitable exact whole-string or one-inner-string reference. */ -function packString(value: string, sources: readonly MessageStringSource[]): PackedCandidate { - const literal: PackedJsonValue = { kind: 'literal', value } - if (value.length === 0) return { value: literal, references: false } - const valueBytes = Buffer.byteLength(value, 'utf8') - - let best: PackedJsonValue | undefined - let bestBytes = wireBytes(literal) - for (const source of sources) { - if (source.bytes < valueBytes) continue - const start = source.value.indexOf(value) - if (start < 0) continue - const slice = sliceOf(source, start, start + value.length) - if (slice === undefined) continue - const candidate: PackedJsonValue = { - kind: 'string', - parts: [{ kind: 'message-slice', value: slice }], - } - const bytes = wireBytes(candidate) - if (bytes < bestBytes) { - best = candidate - bestBytes = bytes - } - } - if (best !== undefined) return { value: best, references: true } - - for (const source of sources) { - if (source.bytes > valueBytes) continue - const start = value.indexOf(source.value) - if (start < 0) continue - const slice = sliceOf(source, 0, source.value.length) - if (slice === undefined) continue - const parts: PackedJsonStringPart[] = [] - if (start > 0) parts.push({ kind: 'literal', value: value.slice(0, start) }) - parts.push({ kind: 'message-slice', value: slice }) - const end = start + source.value.length - if (end < value.length) parts.push({ kind: 'literal', value: value.slice(end) }) - const candidate: PackedJsonValue = { kind: 'string', parts } - const bytes = wireBytes(candidate) - if (bytes < bestBytes) { - best = candidate - bestBytes = bytes - } - } - return best === undefined - ? { value: literal, references: false } - : { value: best, references: true } -} - -/** Recursively pack one JSON value, retaining references only when the complete subtree shrinks. */ -function packValue(value: JsonValue, sources: readonly MessageStringSource[]): PackedCandidate { - const literal: PackedJsonValue = { kind: 'literal', value } - if (typeof value === 'string') return packString(value, sources) - if (value === null || typeof value !== 'object') return { value: literal, references: false } - - if (Array.isArray(value)) { - const children = value.map(child => packValue(child, sources)) - if (!children.some(child => child.references)) return { value: literal, references: false } - const candidate: PackedJsonValue = { kind: 'array', items: children.map(child => child.value) } - return { value: candidate, references: true } - } - - const children = Object.entries(value).map(([key, child]) => [key, packValue(child, sources)] as const) - if (!children.some(([, child]) => child.references)) return { value: literal, references: false } - const candidate: PackedJsonValue = { - kind: 'object', - entries: children.map(([key, child]) => [key, child.value]), - } - return { value: candidate, references: true } -} - -/** Resolve one path in a parsed wire message. */ -function valueAt(root: DeepSeekLlmApiJson, path: readonly (string | number)[]): DeepSeekLlmApiJson { - let value = root - for (const segment of path) { - if (typeof segment === 'number') { - if (!Array.isArray(value) || segment < 0 || segment >= value.length) { - throw new Error(`session-log-deepseek: message reference has invalid array segment ${segment}`) - } - value = value[segment] as DeepSeekLlmApiJson - continue - } - if (value === null || typeof value !== 'object' || Array.isArray(value) || !Object.hasOwn(value, segment)) { - throw new Error(`session-log-deepseek: message reference has invalid object segment ${JSON.stringify(segment)}`) - } - value = value[segment] as DeepSeekLlmApiJson - } - return value -} - -/** Decode and validate one exact UTF-8 message slice. */ -function decodeSlice(messages: readonly DeepSeekLlmApiJson[], slice: DeepSeekMessageStringSlice): string { - const message = messages[slice.messageIndex] - if (message === undefined) throw new Error(`session-log-deepseek: message reference index ${slice.messageIndex} is absent`) - const source = valueAt(message, slice.path) - if (typeof source !== 'string') throw new Error('session-log-deepseek: message reference path does not resolve to a string') - const bytes = Buffer.from(source, 'utf8') - if (!Number.isSafeInteger(slice.utf8Start) || !Number.isSafeInteger(slice.utf8End) - || slice.utf8Start < 0 || slice.utf8End <= slice.utf8Start || slice.utf8End > bytes.length) { - throw new Error('session-log-deepseek: message reference byte range is invalid') - } - const selected = bytes.subarray(slice.utf8Start, slice.utf8End) - const decoded = selected.toString('utf8') - if (!Buffer.from(decoded, 'utf8').equals(selected)) { - throw new Error('session-log-deepseek: message reference splits a UTF-8 code point') - } - return decoded -} - -/** - * Decode one packed JSON value against the containing request's messages. - * @param value - tagged literal/reference value to reconstruct. - * @param messages - serialized messages from the same request. - * @returns the exact reconstructed JSON value. - */ -export function unpackJsonValue(value: PackedJsonValue, messages: readonly DeepSeekLlmApiJson[]): JsonValue { - switch (value.kind) { - case 'literal': - return structuredClone(value.value) - case 'string': - return value.parts.map(part => part.kind === 'literal' ? part.value : decodeSlice(messages, part.value)).join('') - case 'array': - return value.items.map(item => unpackJsonValue(item, messages)) - case 'object': - return Object.fromEntries(value.entries.map(([key, child]) => [key, unpackJsonValue(child, messages)])) - default: - throw new Error('session-log-deepseek: unknown packed JSON value') - } -} - -/** - * Encode canonical session events against the exact DeepSeek wire messages. - * @param events - immutable canonical event suffix. - * @param messages - serialized request messages that the receiver also holds. - * @returns one raw-or-referenced representation per event. - */ -export function packSessionEvents( - events: readonly SessionEvent[], - messages: readonly DeepSeekLlmApiJson[], -): EncodedSessionEvent[] { - // TODO: Index message strings if large opt-in suffixes show material request-preparation latency. - const sources: MessageStringSource[] = [] - messages.forEach((message, messageIndex) => { collectSources(message, messageIndex, [], sources) }) - sources.sort((left, right) => right.bytes - left.bytes) - return events.map((event) => { - const packed = packValue(event as unknown as JsonValue, sources) - const raw: EncodedSessionEvent = { encoding: 'raw', event } - if (!packed.references) return raw - const referenced: EncodedSessionEvent = { encoding: 'message-references', event: packed.value } - return wireBytes(referenced) < wireBytes(raw) ? referenced : raw - }) -} - -/** - * Reconstruct canonical session events from one request-relative representation. - * @param events - encoded event list from the extension field. - * @param messages - serialized messages from the same request. - * @returns reconstructed event values in order. - */ -export function unpackSessionEvents( - events: readonly EncodedSessionEvent[], - messages: readonly DeepSeekLlmApiJson[], -): SessionEvent[] { - return events.map(encoded => encoded.encoding === 'raw' - ? structuredClone(encoded.event) - : unpackJsonValue(encoded.event, messages) as unknown as SessionEvent) -} diff --git a/packages/session/session-log-deepseek/src/index.ts b/packages/session/session-log-deepseek/src/index.ts index 2d957c839d..b66c21706c 100644 --- a/packages/session/session-log-deepseek/src/index.ts +++ b/packages/session/session-log-deepseek/src/index.ts @@ -7,12 +7,10 @@ import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' -import type { DeepSeekLlmApiJson } from '@deepseek-ai/dsh-deepseek-llm-api-extensions' +import type {} from '@deepseek-ai/dsh-deepseek-llm-api-extensions' import { SessionId, type Session, type SessionEvent } from '@deepseek-ai/dsh-session' -import { packSessionEvents } from './codec.ts' import type { DeepSeekSessionLogExtension } from './types.ts' -export { packSessionEvents, unpackJsonValue, unpackSessionEvents } from './codec.ts' export type * from './types.ts' /** Cordis plugin name. */ @@ -63,13 +61,6 @@ export function acceptedThrough(session: Session): number { return throughSeq } -/** Resolve the serialized message list or fail before transport. */ -function requestMessages(body: Readonly>): readonly DeepSeekLlmApiJson[] { - const messages = body.messages - if (!Array.isArray(messages)) throw new Error('session-log-deepseek: DeepSeek request body has no messages array') - return messages -} - /** * Register the incremental `dsh_session_log` request contribution when enabled. * @param ctx - plugin context carrying Sessions and the DeepSeek request-extension registry. @@ -94,7 +85,7 @@ export function apply(ctx: Context, config: Config): void { session: session.header, afterSeq, throughSeq, - events: packSessionEvents(suffix, requestMessages(request.body)), + events: suffix, } return { value, diff --git a/packages/session/session-log-deepseek/src/types.ts b/packages/session/session-log-deepseek/src/types.ts index 2680130c88..d3281bf327 100644 --- a/packages/session/session-log-deepseek/src/types.ts +++ b/packages/session/session-log-deepseek/src/types.ts @@ -1,34 +1,6 @@ /** Wire types for lossless incremental DeepSeek session-log upload. */ -import type { JsonValue, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' - -/** Path from one DeepSeek wire message root to a string value. */ -export type DeepSeekMessageStringPath = readonly (string | number)[] - -/** Exact half-open UTF-8 slice of one string in the containing request's messages. */ -export interface DeepSeekMessageStringSlice { - readonly messageIndex: number - readonly path: DeepSeekMessageStringPath - readonly utf8Start: number - readonly utf8End: number -} - -/** One literal or request-relative fragment of a packed JSON string. */ -export type PackedJsonStringPart = - | { readonly kind: 'literal'; readonly value: string } - | { readonly kind: 'message-slice'; readonly value: DeepSeekMessageStringSlice } - -/** Tagged JSON representation whose string leaves may cite the containing request. */ -export type PackedJsonValue = - | { readonly kind: 'literal'; readonly value: JsonValue } - | { readonly kind: 'string'; readonly parts: readonly PackedJsonStringPart[] } - | { readonly kind: 'array'; readonly items: readonly PackedJsonValue[] } - | { readonly kind: 'object'; readonly entries: readonly (readonly [string, PackedJsonValue])[] } - -/** One canonical session event, sent raw unless request-relative references reduce its encoded bytes. */ -export type EncodedSessionEvent = - | { readonly encoding: 'raw'; readonly event: SessionEvent } - | { readonly encoding: 'message-references'; readonly event: PackedJsonValue } +import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' /** Versioned incremental session-log field carried by an official DeepSeek request. */ export interface DeepSeekSessionLogExtension { @@ -38,8 +10,8 @@ export interface DeepSeekSessionLogExtension { readonly afterSeq: number /** Highest sequence represented by {@link events}. */ readonly throughSeq: number - /** Contiguous canonical events from `afterSeq + 1` through `throughSeq`. */ - readonly events: readonly EncodedSessionEvent[] + /** Complete canonical event envelopes for every sequence from `afterSeq + 1` through `throughSeq`. */ + readonly events: readonly SessionEvent[] } declare module '@deepseek-ai/dsh-deepseek-llm-api-extensions/types' { diff --git a/packages/session/session-log-deepseek/tests/codec.spec.ts b/packages/session/session-log-deepseek/tests/codec.spec.ts deleted file mode 100644 index 90d17ad9d4..0000000000 --- a/packages/session/session-log-deepseek/tests/codec.spec.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { describe, expect, it } from 'vitest' -import type { DeepSeekLlmApiJson } from '@deepseek-ai/dsh-deepseek-llm-api-extensions' -import type { SessionEvent } from '@deepseek-ai/dsh-session' -import { - packSessionEvents, - unpackJsonValue, - unpackSessionEvents, -} from '../src/codec.ts' -import type { PackedJsonValue } from '../src/types.ts' - -function event(data: unknown, seq = 0): SessionEvent { - return { - type: 'plugin/test', - seq, - time: 1_700_000_000_000 + seq, - data, - } as unknown as SessionEvent -} - -describe('DeepSeek session-log codec', () => { - it('replaces a raw assistant chunk with an exact slice of its assembled wire message', () => { - const delta = 'streamed assistant content '.repeat(30) - const source = { - type: 'assistant/chunk', - seq: 0, - time: 1, - data: { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: delta } }, - } as SessionEvent<'assistant/chunk'> - const messages: DeepSeekLlmApiJson[] = [{ role: 'assistant', content: `prefix:${delta}:suffix` }] - const packed = packSessionEvents([source], messages) - - expect(packed[0]?.encoding).toBe('message-references') - expect(unpackSessionEvents(packed, messages)).toEqual([source]) - }) - - it('references exact whole strings and reconstructs Unicode event values', () => { - const text = `前缀-${'shared text '.repeat(40)}-结尾` - const messages: DeepSeekLlmApiJson[] = [{ role: 'assistant', content: text }] - const source = event({ nested: [{ text }], untouched: 3 }) - const packed = packSessionEvents([source], messages) - - expect(packed[0]?.encoding).toBe('message-references') - expect(JSON.stringify(packed)).toContain('message-slice') - expect(unpackSessionEvents(packed, messages)).toEqual([source]) - }) - - it('keeps surrogate-splitting and ill-formed UTF-16 strings raw', () => { - const high = String.fromCharCode(0xD83D) - const low = String.fromCharCode(0xDE00) - const tail = 'shared-tail-'.repeat(80) - const cases = [ - { message: `😀${tail}`, logged: `${low}${tail}` }, - { message: `${tail}😀`, logged: `${tail}${high}` }, - { message: `${high}${tail}`, logged: `${high}${tail}` }, - ] - - for (const item of cases) { - const source = event({ text: item.logged }) - const messages: DeepSeekLlmApiJson[] = [{ role: 'assistant', content: item.message }] - const packed = packSessionEvents([source], messages) - expect(packed).toEqual([{ encoding: 'raw', event: source }]) - expect(unpackSessionEvents(packed, messages)).toEqual([source]) - } - }) - - it('references one large inner message string and preserves literal prefix and suffix', () => { - const shared = 'payload '.repeat(80) - const messages: DeepSeekLlmApiJson[] = [{ role: 'tool', content: shared }] - const source = event({ output: `before:${shared}:after` }) - const packed = packSessionEvents([source], messages) - - expect(packed[0]?.encoding).toBe('message-references') - expect(unpackSessionEvents(packed, messages)).toEqual([source]) - }) - - it('keeps short or unrelated events raw when references would expand them', () => { - const messages: DeepSeekLlmApiJson[] = [{ role: 'user', content: 'tiny', empty: '', parts: [null, true, 5, 'nested'] }] - const source = event({ text: 'tiny', empty: '', other: ['unrelated', true, null] }) - const packed = packSessionEvents([source], messages) - expect(packed).toEqual([{ encoding: 'raw', event: source }]) - expect(unpackSessionEvents(packed, messages)).toEqual([source]) - }) - - it('falls back to a raw event when referenced children do not reduce the complete envelope', () => { - let found = false - for (let length = 40; length <= 240; length += 1) { - const text = 'x'.repeat(length) - const source = event({ text }) - const packed = packSessionEvents([source], [{ content: text }]) - if (packed[0]?.encoding === 'raw' && length > 80) found = true - } - expect(found).toBe(true) - }) - - it('rejects missing paths, non-string targets, invalid ranges, and split UTF-8 code points', () => { - const messages: DeepSeekLlmApiJson[] = [{ content: '😀abc', nested: [5] }] - const packed = (value: object): PackedJsonValue => ({ - kind: 'string', - parts: [{ kind: 'message-slice', value: value as never }], - }) - expect(() => unpackJsonValue(packed({ messageIndex: 2, path: ['content'], utf8Start: 0, utf8End: 1 }), messages)) - .toThrow(/index 2 is absent/) - expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['missing'], utf8Start: 0, utf8End: 1 }), messages)) - .toThrow(/invalid object segment/) - expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['nested', 0], utf8Start: 0, utf8End: 1 }), messages)) - .toThrow(/does not resolve to a string/) - expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['nested', 2], utf8Start: 0, utf8End: 1 }), messages)) - .toThrow(/invalid array segment/) - expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['content'], utf8Start: -1, utf8End: 1 }), messages)) - .toThrow(/byte range is invalid/) - expect(() => unpackJsonValue(packed({ messageIndex: 0, path: ['content'], utf8Start: 0, utf8End: 1 }), messages)) - .toThrow(/splits a UTF-8 code point/) - }) - - it('decodes every packed JSON variant and rejects unknown tags', () => { - const messages: DeepSeekLlmApiJson[] = [{ content: 'abcdef' }] - const value: PackedJsonValue = { - kind: 'object', - entries: [[ - 'items', - { - kind: 'array', - items: [ - { kind: 'literal', value: 1 }, - { - kind: 'string', - parts: [ - { kind: 'literal', value: 'x' }, - { kind: 'message-slice', value: { messageIndex: 0, path: ['content'], utf8Start: 1, utf8End: 4 } }, - ], - }, - ], - }, - ]], - } - expect(unpackJsonValue(value, messages)).toEqual({ items: [1, 'xbcd'] }) - expect(() => unpackJsonValue({ kind: 'future' } as never, messages)).toThrow(/unknown packed JSON value/) - }) -}) diff --git a/packages/session/session-log-deepseek/tests/upload.spec.ts b/packages/session/session-log-deepseek/tests/upload.spec.ts index 611a1a50da..d473cc7de9 100644 --- a/packages/session/session-log-deepseek/tests/upload.spec.ts +++ b/packages/session/session-log-deepseek/tests/upload.spec.ts @@ -66,8 +66,8 @@ describe('incremental DeepSeek session-log upload', () => { expect(second.fields.dsh_session_log).toMatchObject({ afterSeq: 1, throughSeq: 3 }) expect(second.fields.dsh_session_log?.events).toHaveLength(2) expect(second.fields.dsh_session_log?.events[0]).toMatchObject({ - encoding: 'raw', - event: { type: 'session-log-deepseek/delivery-accepted', seq: 2 }, + type: 'session-log-deepseek/delivery-accepted', + seq: 2, }) }) @@ -147,15 +147,15 @@ describe('incremental DeepSeek session-log upload', () => { expect(current.fields.dsh_session_log).toMatchObject({ afterSeq: 0, throughSeq: 1, - events: [{ event: { type: 'session-log-deepseek/delivery-accepted' } }], + events: [{ type: 'session-log-deepseek/delivery-accepted' }], }) }) - it('fails preparation when the DeepSeek body has no messages array', async () => { - const { ctx, session } = await harness('bad-body') + it('contributes complete events without reading request messages', async () => { + const { ctx, session } = await harness('direct-events') session.append('turn/start', { turn: 1 }) - await expect(ctx.deepseekLlmApiExtensions.prepare({ body: {}, signal: SIGNAL, sessionId: session.id })) - .rejects.toThrow(/no messages array/) + const prepared = await ctx.deepseekLlmApiExtensions.prepare({ body: {}, signal: SIGNAL, sessionId: session.id }) + expect(prepared.fields.dsh_session_log?.events).toEqual(session.events) }) it('fails closed on a malformed persisted acceptance watermark', async () => {