From a36ddf4c409a2cb08ca907b190b0c0642d8bc420 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 26 Oct 2026 13:55:47 +0800 Subject: [PATCH] refactor(session): register request upload exclusions --- .../feature/2026-08-28-auto-review.i18n.yaml | 4 +- .../feature/2026-08-28-auto-review.md | 2 +- .../feature/2026-08-28-auto-review.zh.md | 2 +- docs/capability-seams.i18n.yaml | 4 +- docs/capability-seams.md | 5 +- docs/capability-seams.zh.md | 5 +- docs/config-catalog.md | 4 +- docs/config-catalog.zh.md | 4 +- ...deepseek-llm-api-wire-extensions.i18n.yaml | 4 +- docs/deepseek-llm-api-wire-extensions.md | 2 +- docs/deepseek-llm-api-wire-extensions.zh.md | 2 +- docs/persistence-catalog.i18n.yaml | 16 +- docs/persistence-catalog.md | 8 +- docs/persistence-catalog.zh.md | 8 +- docs/persistence-schema.json | 4 +- docs/subsystems/core.i18n.yaml | 4 +- docs/subsystems/core.md | 3 +- docs/subsystems/core.zh.md | 3 +- docs/subsystems/llm-streaming.md | 19 +- docs/subsystems/llm-streaming.zh.md | 19 +- docs/subsystems/session.i18n.yaml | 3 + docs/subsystems/session.md | 43 +++++ docs/subsystems/session.zh.md | 43 +++++ .../guide.i18n.yaml | 16 ++ .../llm-request-purpose-declarations/guide.md | 29 +++ .../guide.zh.md | 29 +++ .../compaction-basic/README.i18n.yaml | 4 +- .../compaction/compaction-basic/README.md | 2 + .../compaction/compaction-basic/README.zh.md | 2 + .../compaction/compaction-basic/src/types.ts | 6 + .../experimental/auto-review/README.i18n.yaml | 4 +- packages/experimental/auto-review/README.md | 2 +- .../experimental/auto-review/README.zh.md | 2 +- .../experimental/auto-review/src/index.ts | 8 +- .../extensions/tool-cordis/src/api-catalog.ts | 33 +++- .../README.i18n.yaml | 4 +- .../llm/deepseek-llm-api-extensions/README.md | 2 + .../deepseek-llm-api-extensions/README.zh.md | 2 + .../deepseek-llm-api-extensions/src/types.ts | 2 +- packages/llm/llm-deepseek/src/adapter.ts | 26 +-- packages/llm/llm-deepseek/src/serialize.ts | 2 +- .../tests/loader-composition.spec.ts | 38 +++- packages/llm/llm/README.i18n.yaml | 4 +- packages/llm/llm/README.md | 2 + packages/llm/llm/README.zh.md | 2 + packages/llm/llm/src/types.ts | 15 +- .../session-log-deepseek/README.i18n.yaml | 15 +- .../session/session-log-deepseek/README.md | 21 ++- .../session/session-log-deepseek/README.zh.md | 21 ++- .../session/session-log-deepseek/src/index.ts | 174 +++++++++++------- .../session/session-log-deepseek/src/types.ts | 17 +- .../session-log-deepseek/tests/upload.spec.ts | 105 ++++++++++- .../session-title-llm/README.i18n.yaml | 4 +- packages/session/session-title-llm/README.md | 2 +- .../session/session-title-llm/README.zh.md | 2 +- .../session/session-title-llm/src/index.ts | 6 + packages/test-support/llm-replay/src/index.ts | 8 +- scripts/gen-cordis-catalog.ts | 3 + scripts/gen-doc-graphs.ts | 8 + scripts/type-equiv.manifest.json | 20 ++ 60 files changed, 689 insertions(+), 164 deletions(-) create mode 100644 docs/upgrade-guide/v0.2.1-rc.3/llm-request-purpose-declarations/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.1-rc.3/llm-request-purpose-declarations/guide.md create mode 100644 docs/upgrade-guide/v0.2.1-rc.3/llm-request-purpose-declarations/guide.zh.md diff --git a/.agents/notes/implemented/feature/2026-08-28-auto-review.i18n.yaml b/.agents/notes/implemented/feature/2026-08-28-auto-review.i18n.yaml index 89a0ff19cb..96e5042c3e 100644 --- a/.agents/notes/implemented/feature/2026-08-28-auto-review.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-28-auto-review.i18n.yaml @@ -15,8 +15,8 @@ en: db0a2deb9193a00d zh: dab5a65b88957464 /agent-note-experimental-auto-review-before-each-tool-call/decision/one-complete-reviewer-request: - en: b2bbe402aeb65640 - zh: 9dafa4525e2a2899 + en: 45ac7b4e759b29ad + zh: 5374cc89e587f076 /agent-note-experimental-auto-review-before-each-tool-call/decision/result-and-cancellation: en: 647fe68c52b907e0 zh: 267a739bad44ad65 diff --git a/.agents/notes/implemented/feature/2026-08-28-auto-review.md b/.agents/notes/implemented/feature/2026-08-28-auto-review.md index 62270108a4..547dd3a2cf 100644 --- a/.agents/notes/implemented/feature/2026-08-28-auto-review.md +++ b/.agents/notes/implemented/feature/2026-08-28-auto-review.md @@ -41,7 +41,7 @@ The integration uses only the latest `request/header.config` provider/model and Each item is its own message and keeps the same text in later reviews: the pending call becomes a historical call, and the integration remembers, for each live Session, the schema each reviewed call showed so that it reappears at the same position. A review's messages therefore begin with all messages of the previous review. Providers that cache only at message ends, such as OpenAI implicit breakpoints on GPT-5.6 and later, or at a final marker with lookback, such as Anthropic `cache_control`, reuse the previous review; one growing user message let them reuse only the policy. The schema memory is process-local, so the first review after a restart omits earlier schemas and misses the cache once. -The request also carries the reviewed Session's `sessionId` and `purpose: 'auto-review'`. Routes that require a per-conversation identity or key prompt caching on one, such as the `x-opencode-session` header OpenCode requires and OpenAI `prompt_cache_key`, derive it from that id; without it OpenCode rejects every review, and Auto fails every call. The [DeepSeek Session-log upload](../../../../packages/session/session-log-deepseek/README.md#request-field) skips this purpose, so a review neither uploads the log nor appends an acceptance record to the Session. +The request also carries the reviewed Session's `sessionId` and `purpose: 'auto-review'`. Routes that require a per-conversation identity or key prompt caching on one, such as the `x-opencode-session` header OpenCode requires and OpenAI `prompt_cache_key`, derive it from that id; without it OpenCode rejects every review, and Auto fails every call. The [DeepSeek Session-log upload](../../../../packages/session/session-log-deepseek/README.md#request-field) applies the same eligibility and registered exclusions to reviews as to other requests. The uploaded data is the canonical Session log; review prompts and decisions remain outside that log. The reviewer builds the history and the pending call from the Session's complete action history: an authorization is the earlier call the `REVIEW_POLICY` source rules make eligible, and a duplicate or conflicting identity has to be visible anywhere in the log rather than inside a recent window, so neither the Session projections nor a bounded read serves the decision. The read is the deprecated synchronous `snapshotEvents()` under a line-scoped `typescript/no-deprecated` waiver. diff --git a/.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md b/.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md index 68a2c9e762..bc833d3fc3 100644 --- a/.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md +++ b/.agents/notes/implemented/feature/2026-08-28-auto-review.zh.md @@ -41,7 +41,7 @@ Integration 只使用最新 `request/header.config` 的 provider/model 与 shi 每个条目各占一条消息,并在后续评审中保持相同文本:待审调用会变成历史调用,integration 为每个存活 Session 记住每次已审调用展示的 schema,使其在同一位置再次出现。因此一次评审的消息以上一次评审的全部消息开头。只在消息结尾缓存的 provider(如 GPT-5.6 及以后的 OpenAI 隐式断点),或在末尾标记处向前回溯的 provider(如 Anthropic `cache_control`),都能复用上一次评审;单条不断增长的 user 消息只能让它们复用策略。Schema 记忆只在进程内,因此重启后的第一次评审会缺少先前的 schema,并有一次缓存未命中。 -请求还携带被评审 Session 的 `sessionId` 与 `purpose: 'auto-review'`。需要按对话标识路由或以其为 prompt 缓存键的路由(例如 OpenCode 要求的 `x-opencode-session` header 与 OpenAI 的 `prompt_cache_key`)由该 id 派生;缺少它时 OpenCode 会拒绝每次评审,Auto 会让每个调用失败。[DeepSeek session-log 上传](../../../../packages/session/session-log-deepseek/README.zh.md#request-field)会跳过该用途,因此评审既不上传日志,也不向 Session 追加接受记录。 +请求还携带被评审 Session 的 `sessionId` 与 `purpose: 'auto-review'`。需要按对话标识路由或以其为 prompt 缓存键的路由(例如 OpenCode 要求的 `x-opencode-session` header 与 OpenAI 的 `prompt_cache_key`)由该 id 派生;缺少它时 OpenCode 会拒绝每次评审,Auto 会让每个调用失败。[DeepSeek session-log 上传](../../../../packages/session/session-log-deepseek/README.zh.md#request-field)对评审和其他请求应用相同的上传资格与注册排除条件。上传的是权威 Session 日志;评审提示词和决策仍不进入该日志。 reviewer 从 Session 的完整动作历史构建历史与待审调用:授权是符合 `REVIEW_POLICY` 来源规则的那个更早调用,而重复或冲突的身份必须在整份日志的任何位置都可见,而不是只在近期窗口内可见,因此 Session 投影与有界读取都无法支撑该判定。这次读取使用已废弃的同步 `snapshotEvents()`,并带有行级 `typescript/no-deprecated` 豁免。 diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml index fba6972189..f49e52193f 100644 --- a/docs/capability-seams.i18n.yaml +++ b/docs/capability-seams.i18n.yaml @@ -6,5 +6,5 @@ en: 41d772192075e133 zh: 5ef3da69b4de1d8a /capability-seams-and-core-services: - en: 506d61dfa8c71186 - zh: 46f05c4ef2f5c322 + en: 697165390e1bddde + zh: eaa2dd9b9d988d69 diff --git a/docs/capability-seams.md b/docs/capability-seams.md index d28f3f3aee..2b514ccb5f 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -77,9 +77,10 @@ flowchart LR pkg_llm_replay["llm-replay"] pkg_agent_loop["agent-loop"] pkg_compaction_basic["compaction-basic"] + pkg_session_log_deepseek["session-log-deepseek"] + svc_sessionLogDeepSeek["ctx.sessionLogDeepSeek
Session-log upload policy"] 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"] @@ -408,6 +409,7 @@ flowchart LR pkg_schedule --> svc_schedule pkg_session --> svc_sessions pkg_session_log_deepseek --> svc_deepseekLlmApiExtensions + pkg_session_log_deepseek --> svc_sessionLogDeepSeek pkg_session_persistence --> svc_sessionPersistence pkg_session_persistence_jsonl --> svc_sessionPersistence pkg_session_projection --> svc_sessionProjections @@ -652,6 +654,7 @@ flowchart LR | `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | [`api-session-controller`](../packages/api/session-controller), [`tool-fs`](../packages/fs/tool-fs), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-deepseek`](../packages/llm/llm-deepseek) | - | The host commits accepted images before session events; provider adapters resolve authorized durable references into provider-native content. | | `ctx.fileUploads` | `core` | [`client-file-upload`](../packages/client/file-upload) | - | [`api-session-controller`](../packages/api/session-controller) | - | Owns streaming intake, durable storage, and staged receipt lifetime; the Session controller binds receipts to accepted submissions. | | `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.sessionLogDeepSeek` | `core` | [`session-log-deepseek`](../packages/session/session-log-deepseek) | - | - | - | Plugins register caller-owned exclusions before the canonical Session-log request contribution is prepared; unmatched requests use normal upload eligibility. | | `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. | diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index 8a2963f3cc..9d363743fc 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -79,9 +79,10 @@ flowchart LR pkg_llm_replay["llm-replay"] pkg_agent_loop["agent-loop"] pkg_compaction_basic["compaction-basic"] + pkg_session_log_deepseek["session-log-deepseek"] + svc_sessionLogDeepSeek["ctx.sessionLogDeepSeek
Session-log upload policy"] 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"] @@ -410,6 +411,7 @@ flowchart LR pkg_schedule --> svc_schedule pkg_session --> svc_sessions pkg_session_log_deepseek --> svc_deepseekLlmApiExtensions + pkg_session_log_deepseek --> svc_sessionLogDeepSeek pkg_session_persistence --> svc_sessionPersistence pkg_session_persistence_jsonl --> svc_sessionPersistence pkg_session_projection --> svc_sessionProjections @@ -654,6 +656,7 @@ flowchart LR | `ctx.attachments` | `seam` | [`attachment`](../packages/attachment/attachment) | [`attachment-local`](../packages/attachment/attachment-local) | [`api-session-controller`](../packages/api/session-controller), [`tool-fs`](../packages/fs/tool-fs), [`llm-pi-ai`](../packages/llm/llm-pi-ai), [`llm-deepseek`](../packages/llm/llm-deepseek) | - | 宿主会在会话事件之前提交已接受的图片;提供方适配器将已授权的持久引用解析为提供方原生内容。 | | `ctx.fileUploads` | `core` | [`client-file-upload`](../packages/client/file-upload) | - | [`api-session-controller`](../packages/api/session-controller) | - | 负责流式接收、持久存储和暂存回执生命周期;Session Controller 将回执绑定到已接受的提交。 | | `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.sessionLogDeepSeek` | `core` | [`session-log-deepseek`](../packages/session/session-log-deepseek) | - | - | - | 插件在准备权威 Session 日志请求贡献前注册归调用方所有的排除条件;没有条件匹配的请求应用普通上传资格判断。 | | `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) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 86cb2f94cb..ab6ae20df7 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -712,7 +712,7 @@ export type ThemePreference = typeof THEME_PREFERENCES[number] ## `@deepseek-ai/dsh-compaction-basic` - `inject`: `llm` · `tokenMeter` · `sessions` -- `source`: [`packages/compaction/compaction-basic/src/types.ts:40`](../packages/compaction/compaction-basic/src/types.ts) +- `source`: [`packages/compaction/compaction-basic/src/types.ts:46`](../packages/compaction/compaction-basic/src/types.ts) ```ts config-catalog /** Basic compaction configuration with an optional exact-target policy table. */ @@ -2876,7 +2876,7 @@ export interface JsonRpcConfig { - `inject`: `deepseekLlmApiExtensions` · `sessions` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/session/session-log-deepseek/src/index.ts:39`](../packages/session/session-log-deepseek/src/index.ts) +- `source`: [`packages/session/session-log-deepseek/src/index.ts:47`](../packages/session/session-log-deepseek/src/index.ts) ```ts config-catalog /** Session-log request contribution configuration. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 762ef0cf9d..3c6614d854 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -714,7 +714,7 @@ export type ThemePreference = typeof THEME_PREFERENCES[number] ## `@deepseek-ai/dsh-compaction-basic` - `inject`: `llm` · `tokenMeter` · `sessions` -- `source`: [`packages/compaction/compaction-basic/src/types.ts:40`](../packages/compaction/compaction-basic/src/types.ts) +- `source`: [`packages/compaction/compaction-basic/src/types.ts:46`](../packages/compaction/compaction-basic/src/types.ts) ```ts config-catalog /** Basic compaction configuration with an optional exact-target policy table. */ @@ -2878,7 +2878,7 @@ export interface JsonRpcConfig { - `inject`: `deepseekLlmApiExtensions` · `sessions` - `refs`: `Volatile` (`@deepseek-ai/cordis`) -- `source`: [`packages/session/session-log-deepseek/src/index.ts:39`](../packages/session/session-log-deepseek/src/index.ts) +- `source`: [`packages/session/session-log-deepseek/src/index.ts:47`](../packages/session/session-log-deepseek/src/index.ts) ```ts config-catalog /** Session-log request contribution configuration. */ diff --git a/docs/deepseek-llm-api-wire-extensions.i18n.yaml b/docs/deepseek-llm-api-wire-extensions.i18n.yaml index c3d5434645..c5248fa3f7 100644 --- a/docs/deepseek-llm-api-wire-extensions.i18n.yaml +++ b/docs/deepseek-llm-api-wire-extensions.i18n.yaml @@ -18,8 +18,8 @@ en: f89059050613f3e8 zh: da5cf2a4eeb81b75 /official-deepseek-llm-api-wire-extensions/dsh-session-log: - en: e361038fa6e0ae7a - zh: ef4aab10edc29b57 + en: 0d033f72951e2920 + zh: 767efca0dc39ac03 /official-deepseek-llm-api-wire-extensions/dsh-session-log/wire-session-header: en: ccd8fcd773df21c4 zh: 7ea020068ad093ae diff --git a/docs/deepseek-llm-api-wire-extensions.md b/docs/deepseek-llm-api-wire-extensions.md index 2d77d66cd7..e9a270d98e 100644 --- a/docs/deepseek-llm-api-wire-extensions.md +++ b/docs/deepseek-llm-api-wire-extensions.md @@ -73,7 +73,7 @@ An enabled inventory with no qualifying entries sends `packages: []`; disabling ## `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 enabled by default. It applies to a request with a live Session and at least one event; a direct request, a stale Session id, an Auto review request (purpose `auto-review`), or an empty log omits the field, as does a request whose first pending event alone exceeds `maxBytes` or cannot be serialized; a composition disables it with `enabled: false`. The examples below use logical Session format 2 only to illustrate the wire fields; they do not identify the [current writer format](session-format-status.md). +[`@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 enabled by default. It applies to a request with a live Session and at least one event; a direct request, a stale Session id, an empty log, or a request matched by a registered upload exclusion omits the field, as does a request whose first pending event alone exceeds `maxBytes` or cannot be serialized; a composition disables it with `enabled: false`. The examples below use logical Session format 2 only to illustrate the wire fields; they do not identify the [current writer format](session-format-status.md). ```json { diff --git a/docs/deepseek-llm-api-wire-extensions.zh.md b/docs/deepseek-llm-api-wire-extensions.zh.md index bb36d71f3a..d498e8f951 100644 --- a/docs/deepseek-llm-api-wire-extensions.zh.md +++ b/docs/deepseek-llm-api-wire-extensions.zh.md @@ -73,7 +73,7 @@ ## `dsh_session_log` -[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.zh.md) 贡献权威会话日志的一段连续后缀。该字段默认启用。它适用于携带存活会话且至少存在一个事件的请求;直接请求、陈旧会话 id、Auto 审查请求(用途为 `auto-review`)或空日志会省略该字段,第一条待发事件本身超过 `maxBytes` 或无法序列化的请求也会省略该字段;组合可用 `enabled: false` 禁用它。下方示例使用逻辑 Session 格式 2 仅为说明协议字段,并不标识[当前写入格式](session-format-status.zh.md)。 +[`@deepseek-ai/dsh-session-log-deepseek`](../packages/session/session-log-deepseek/README.zh.md) 贡献权威会话日志的一段连续后缀。该字段默认启用。它适用于携带存活会话且至少存在一个事件的请求;直接请求、陈旧会话 id、空日志或命中已注册上传排除条件的请求会省略该字段,第一条待发事件本身超过 `maxBytes` 或无法序列化的请求也会省略该字段;组合可用 `enabled: false` 禁用它。下方示例使用逻辑 Session 格式 2 仅为说明协议字段,并不标识[当前写入格式](session-format-status.zh.md)。 ```json { diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 0fecbb8961..4edde042d4 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -186,14 +186,14 @@ en: 6c6a27578dff5828 zh: 9a5add2bc0df2d0a /session-persistence-event-catalog/events/session/session-title-llm-request-log-only: - en: 50d9e29d3d2bfe52 - zh: 3ff1d959cf7776b9 + en: 59b02baa2be32ca0 + zh: 3637c99652ca89cf /session-persistence-event-catalog/events/session-log-deepseek: en: 7b827672c80aecdd zh: 7b827672c80aecdd /session-persistence-event-catalog/events/session-log-deepseek/session-log-deepseek-delivery-accepted-log-only: - en: 4e9ce9182ec05808 - zh: 7f643a6756ec8f76 + en: a3e2059fcc540b64 + zh: cc9933fd677560f0 /session-persistence-event-catalog/events/step: en: bd0dcbbc352b771a zh: bd0dcbbc352b771a @@ -1209,8 +1209,8 @@ en: c0104327b481ae54 zh: b4624d112bee37f1 /session-persistence-event-catalog/resolved-persistence-types/sessiontitlellmrequesteventdata: - en: 2e40681d575da503 - zh: 57cbccb16548e2da + en: fc25ed80eac06c9c + zh: 8097f450de593040 /session-persistence-event-catalog/resolved-persistence-types/sessiontitlemodelidentity: en: 9531468264499498 zh: 9da23c0ad2776563 @@ -1920,8 +1920,8 @@ en: 04b34b8ffe7e7764 zh: a792d5371516fd9d /session-persistence-event-catalog/resolved-persistence-types/sessionformatversion-sessionid-throughseq: - en: 8aaa02b06896492d - zh: 4dd0e1ee40d8f22d + en: 8063597183ef767f + zh: b064698c54089f07 /session-persistence-event-catalog/resolved-persistence-types/shadowedrange-shadowedseqs-shadowedtokencount: en: 7a2ab7b62244f43b zh: 1084e3f8876feebc diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 74a7956486..cd01070f45 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -878,7 +878,7 @@ Source: [`packages/session/session-title/src/index.ts:77`](../packages/session/s Types: [SessionTitleLlmRequestEventData](subsystems/session-title.md) -Source: [`packages/session/session-title-llm/src/index.ts:57`](../packages/session/session-title-llm/src/index.ts) +Source: [`packages/session/session-title-llm/src/index.ts:63`](../packages/session/session-title-llm/src/index.ts) ### `session-log-deepseek/*` @@ -898,7 +898,7 @@ Source: [`packages/session/session-title-llm/src/index.ts:57`](../packages/sessi } ``` -Source: [`packages/session/session-log-deepseek/src/types.ts:81`](../packages/session/session-log-deepseek/src/types.ts) +Source: [`packages/session/session-log-deepseek/src/types.ts:96`](../packages/session/session-log-deepseek/src/types.ts) ### `step/*` @@ -4717,7 +4717,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/ SHA-256: `a3e299483348f27a4787a36bb3f29cd86024c65336abc10bb3d687468302c4cf` -Sources: [`packages/session/session-title-llm/src/index.ts:37`](../packages/session/session-title-llm/src/index.ts) +Sources: [`packages/session/session-title-llm/src/index.ts:43`](../packages/session/session-title-llm/src/index.ts) | Property | Presence | Type | |---|---|---| @@ -7930,7 +7930,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:64`](../packages/web SHA-256: `8bb8c3751a6ce64b61aba1ec7ae798c00a5803c4d868a0430873216c6569ed52` -Sources: [`packages/session/session-log-deepseek/src/types.ts:81`](../packages/session/session-log-deepseek/src/types.ts) +Sources: [`packages/session/session-log-deepseek/src/types.ts:96`](../packages/session/session-log-deepseek/src/types.ts) | Property | Presence | Type | |---|---|---| diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 7b4f09039e..5c7181b217 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -880,7 +880,7 @@ export type SessionEvent = { 类型:[SessionTitleLlmRequestEventData](subsystems/session-title.zh.md) -来源:[`packages/session/session-title-llm/src/index.ts:57`](../packages/session/session-title-llm/src/index.ts) +来源:[`packages/session/session-title-llm/src/index.ts:63`](../packages/session/session-title-llm/src/index.ts) ### `session-log-deepseek/*` @@ -900,7 +900,7 @@ export type SessionEvent = { } ``` -来源:[`packages/session/session-log-deepseek/src/types.ts:81`](../packages/session/session-log-deepseek/src/types.ts) +来源:[`packages/session/session-log-deepseek/src/types.ts:96`](../packages/session/session-log-deepseek/src/types.ts) ### `step/*` @@ -4719,7 +4719,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` SHA-256: `a3e299483348f27a4787a36bb3f29cd86024c65336abc10bb3d687468302c4cf` -来源:[`packages/session/session-title-llm/src/index.ts:37`](../packages/session/session-title-llm/src/index.ts) +来源:[`packages/session/session-title-llm/src/index.ts:43`](../packages/session/session-title-llm/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -7932,7 +7932,7 @@ SHA-256: `9a2a9029f8d7ede05336980d8342737557f487b913bfd28853d0ab5214600ab5` SHA-256: `8bb8c3751a6ce64b61aba1ec7ae798c00a5803c4d868a0430873216c6569ed52` -来源:[`packages/session/session-log-deepseek/src/types.ts:81`](../packages/session/session-log-deepseek/src/types.ts) +来源:[`packages/session/session-log-deepseek/src/types.ts:96`](../packages/session/session-log-deepseek/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index 247fb463f6..dca6d953ea 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -407,7 +407,7 @@ {"digest":"8a9c990773f798eac10ca8878e6bcc531ee080bc589cac956730e462cc6a09ad","names":[],"sources":["packages/interaction/permission-presets/src/index.ts:53"]}, {"digest":"8ad45ad053b4d89f8865c17cc9ff3b8748e86045f3f0fb816269a34af2619141","names":[],"sources":["packages/experimental/hook-protocol/src/types.ts:31"]}, {"digest":"8af841fc9b265ade3441caf9fe40dd24db24226befba48150e6d0eeccc1389c3","names":[],"sources":[]}, - {"digest":"8bb8c3751a6ce64b61aba1ec7ae798c00a5803c4d868a0430873216c6569ed52","names":[],"sources":["packages/session/session-log-deepseek/src/types.ts:81"]}, + {"digest":"8bb8c3751a6ce64b61aba1ec7ae798c00a5803c4d868a0430873216c6569ed52","names":[],"sources":["packages/session/session-log-deepseek/src/types.ts:96"]}, {"digest":"8c1c4f72755a78ea163523a46565316e36e9b644382eb48e6635c71a6f412dd2","names":[],"sources":[]}, {"digest":"8c627d7a45fa406cea879a3922105e27ed4c3c7966e264afaf11ea7a330f2dd1","names":[],"sources":[]}, {"digest":"8cdd3fdd295cee53035de16a1b23411716f75e9f994fe9510cac5d7fef605c76","names":[],"sources":[]}, @@ -464,7 +464,7 @@ {"digest":"a2be0f0cb59f0d4a06557d45847d1938aced238baa77a6da416e590b8b958553","names":[],"sources":[]}, {"digest":"a3a259a1aa5401af786ce08c4a8419681d3000d73467991b5fc563389bc4ebd2","names":[],"sources":[]}, {"digest":"a3c6f1ea38e9a61d7a9706057ba65aabe02dfe6d9dd90d4736aa62325d874531","names":[],"sources":[]}, - {"digest":"a3e299483348f27a4787a36bb3f29cd86024c65336abc10bb3d687468302c4cf","names":["packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData"],"sources":["packages/session/session-title-llm/src/index.ts:37"]}, + {"digest":"a3e299483348f27a4787a36bb3f29cd86024c65336abc10bb3d687468302c4cf","names":["packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData"],"sources":["packages/session/session-title-llm/src/index.ts:43"]}, {"digest":"a3edd1efd1fef21b72c55e213c8a8b24196b091abde2b89db52be0b2aaae3cfa","names":[],"sources":[]}, {"digest":"a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c","names":[],"sources":["packages/experimental/agent-team/src/types.ts:232"]}, {"digest":"a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc","names":[],"sources":[]}, diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml index 02e7a5d4f6..a2b168f3fd 100644 --- a/docs/subsystems/core.i18n.yaml +++ b/docs/subsystems/core.i18n.yaml @@ -30,8 +30,8 @@ en: 057db6d8bf356c0f zh: 1441b39e5142b0bf /core/repo-wide-type-patterns/the-map-derived-union-pattern: - en: 84c039e929edb897 - zh: 84241bb993f23ef2 + en: 87a19311e2a97270 + zh: 8df92c72793e8275 /core/repo-wide-type-patterns/branded-ids: en: 10f8ac0f97e7c54a zh: 75cd2311aabfe464 diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index 86df19aa94..48ff2ce633 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -450,12 +450,13 @@ declare module '@deepseek-ai/dsh-llm' { } ``` -Five canonical maps use this pattern; a plugin author extends these: +Six canonical maps use this pattern; a plugin author extends these: | Map | Package | Derives | Catalog | |---|---|---|---| | `ContentBlockMap` | dsh-llm | `ContentBlock` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) | | `MessageSourceMap` | dsh-llm | `MessageSource` | [llm-streaming.md](llm-streaming.md#content-blocks-and-messages) | +| `LlmRequestPurposeMap` | dsh-llm | `LlmRequestPurpose` | [llm-streaming.md](llm-streaming.md#the-model-request-and-result) | | `FinishReasonMap` | dsh-llm | `FinishReason` | [llm-streaming.md](llm-streaming.md#the-model-request-and-result) | | `TurnEndReasonMap` | dsh-session | `TurnEndReason` | [session.md](session.md) | | `SessionEventMap` | dsh-session | `SessionEvent` | [session.md](session.md) | diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index db4bb206f5..19acf51b51 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -460,12 +460,13 @@ declare module '@deepseek-ai/dsh-llm' { } ``` -五个规范 map 使用此模式;插件作者扩展它们: +六个规范 map 使用此模式;插件作者扩展它们: | Map | 包 | 派生 | 目录 | |---|---|---|---| | `ContentBlockMap` | dsh-llm | `ContentBlock` | [llm-streaming.md](llm-streaming.zh.md#content-blocks-and-messages) | | `MessageSourceMap` | dsh-llm | `MessageSource` | [llm-streaming.md](llm-streaming.zh.md#content-blocks-and-messages) | +| `LlmRequestPurposeMap` | dsh-llm | `LlmRequestPurpose` | [llm-streaming.zh.md](llm-streaming.zh.md#the-model-request-and-result) | | `FinishReasonMap` | dsh-llm | `FinishReason` | [llm-streaming.md](llm-streaming.zh.md#the-model-request-and-result) | | `TurnEndReasonMap` | dsh-session | `TurnEndReason` | [session.md](session.zh.md) | | `SessionEventMap` | dsh-session | `SessionEvent` | [session.md](session.zh.md) | diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md index 2d3e833544..bae8e156f0 100644 --- a/docs/subsystems/llm-streaming.md +++ b/docs/subsystems/llm-streaming.md @@ -617,8 +617,21 @@ type RequestMessage = Message | RequestUserInput ``` ```ts type-equiv -/** A single model request, fully assembled. */ -interface GenerateOptions { +/** Auxiliary request purposes declared by their producer packages. */ +interface LlmRequestPurposeMap {} +``` + +```ts type-equiv +/** Declared auxiliary request purpose; ordinary conversation requests leave it absent. */ +type LlmRequestPurpose = Extract +``` + +```ts type-equiv +/** + * A single model request, fully assembled. + * @typeParam Purpose - declared purpose keys for callers; forwarding adapters may accept `string`. + */ +interface GenerateOptions { /** Registered provider route selecting the adapter instance. */ provider: string model: string @@ -659,7 +672,7 @@ interface GenerateOptions { * map the purpose to model-hidden transport metadata. Ordinary conversation * requests leave it unset. */ - purpose?: 'compaction' | 'session-title' | 'auto-review' + purpose?: Purpose } ``` diff --git a/docs/subsystems/llm-streaming.zh.md b/docs/subsystems/llm-streaming.zh.md index 29810ca0e3..9fc9d24dd9 100644 --- a/docs/subsystems/llm-streaming.zh.md +++ b/docs/subsystems/llm-streaming.zh.md @@ -623,8 +623,21 @@ type RequestMessage = Message | RequestUserInput ``` ```ts type-equiv -/** A single model request, fully assembled. */ -interface GenerateOptions { +/** Auxiliary request purposes declared by their producer packages. */ +interface LlmRequestPurposeMap {} +``` + +```ts type-equiv +/** Declared auxiliary request purpose; ordinary conversation requests leave it absent. */ +type LlmRequestPurpose = Extract +``` + +```ts type-equiv +/** + * A single model request, fully assembled. + * @typeParam Purpose - declared purpose keys for callers; forwarding adapters may accept `string`. + */ +interface GenerateOptions { /** Registered provider route selecting the adapter instance. */ provider: string model: string @@ -665,7 +678,7 @@ interface GenerateOptions { * map the purpose to model-hidden transport metadata. Ordinary conversation * requests leave it unset. */ - purpose?: 'compaction' | 'session-title' | 'auto-review' + purpose?: Purpose } ``` diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml index 53d89173b4..b4cd4bba34 100644 --- a/docs/subsystems/session.i18n.yaml +++ b/docs/subsystems/session.i18n.yaml @@ -68,3 +68,6 @@ /sessions/remote-catalog-and-workspace-opening: en: dd27307e0d7c893c zh: b77d2a749b7b641c +/sessions/session-log-upload-exclusions: + en: cfe3323aade972be + zh: 8423b490c47c5013 diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md index 4e1ef23338..37da779df6 100644 --- a/docs/subsystems/session.md +++ b/docs/subsystems/session.md @@ -817,6 +817,29 @@ The backends that consume this contract are on [persistence.md](persistence.md). The optional `application` selects a registered file handler without changing the system default; `workspacePathApplications({ path })` returns current handlers, names, icons, and default selection after Host path verification. +## Session-log upload exclusions + +`ctx.sessionLogDeepSeek.registerExclusion()` registers a caller-owned predicate over `SessionLogUploadRequest`. Any match omits the log contribution and its acceptance callback before reading Session events. No match preserves normal upload eligibility. The [package README](../../packages/session/session-log-deepseek/README.md#upload-exclusions) defines failure and disposal behavior. + +```ts type-equiv +/** Request identity inspected before preparing a Session-log contribution. */ +interface SessionLogUploadRequest { + /** Live Session whose canonical log would be contributed. */ + readonly sessionId: SessionId + /** Auxiliary request classification; absent for ordinary conversation requests. */ + readonly purpose?: string +} +``` + +```ts type-equiv +/** + * Synchronous, side-effect-free upload exclusion owned by its registering plugin. + * @param request - request identity; the predicate must not mutate it. + * @returns `true` to omit the log field and acceptance callback, or `false` to keep checking eligibility. + */ +type SessionLogUploadExclusion = (request: SessionLogUploadRequest) => boolean +``` + @@ -995,6 +1018,26 @@ Types: [SessionId](core.md) · [SessionSearchRequest](session-query.md) Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/session-controller/src/index.ts) + + +### `ctx.sessionLogDeepSeek` — `SessionLogDeepSeekService` + +Incremental Session-log contributor with caller-owned upload exclusions. + +```ts cordis-catalog +/** + * Exclude matching requests before reading or preparing their log contribution. + * Any matching rule suppresses both the field and its acceptance callback. With + * no matching rules, normal upload eligibility applies. A throwing rule fails + * this contribution; the request-extension registry omits it and reports the error. + * @param exclude - synchronous, side-effect-free predicate; `true` skips this upload. + * @returns effect disposer that removes this registration when called or when the caller unloads. + */ +registerExclusion(exclude: SessionLogUploadExclusion): () => Promise +``` + +Source: [`packages/session/session-log-deepseek/src/index.ts`](../../packages/session/session-log-deepseek/src/index.ts) + ### `ctx.sessions` — `SessionStore` diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md index 57e732d08c..8d1b6299c0 100644 --- a/docs/subsystems/session.zh.md +++ b/docs/subsystems/session.zh.md @@ -821,6 +821,29 @@ interface PluginRecord { 可选的 `application` 指定文件关联应用,不修改系统默认值;`workspacePathApplications({ path })` 在校验 Host 路径后返回当前关联应用、名称、图标和默认项。 +## Session 日志上传排除条件 + +`ctx.sessionLogDeepSeek.registerExclusion()` 注册归调用方所有的判断函数,输入为 `SessionLogUploadRequest`。任意条件匹配时,在读取 Session 事件前省略日志贡献与接受回调。没有条件匹配时保留普通上传资格判断。[包 README](../../packages/session/session-log-deepseek/README.zh.md#upload-exclusions) 定义失败与释放行为。 + +```ts type-equiv +/** Request identity inspected before preparing a Session-log contribution. */ +interface SessionLogUploadRequest { + /** Live Session whose canonical log would be contributed. */ + readonly sessionId: SessionId + /** Auxiliary request classification; absent for ordinary conversation requests. */ + readonly purpose?: string +} +``` + +```ts type-equiv +/** + * Synchronous, side-effect-free upload exclusion owned by its registering plugin. + * @param request - request identity; the predicate must not mutate it. + * @returns `true` to omit the log field and acceptance callback, or `false` to keep checking eligibility. + */ +type SessionLogUploadExclusion = (request: SessionLogUploadRequest) => boolean +``` + @@ -999,6 +1022,26 @@ Types: [SessionId](core.zh.md) · [SessionSearchRequest](session-query.zh.md) Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/session-controller/src/index.ts) + + +### `ctx.sessionLogDeepSeek` — `SessionLogDeepSeekService` + +Incremental Session-log contributor with caller-owned upload exclusions. + +```ts cordis-catalog +/** + * Exclude matching requests before reading or preparing their log contribution. + * Any matching rule suppresses both the field and its acceptance callback. With + * no matching rules, normal upload eligibility applies. A throwing rule fails + * this contribution; the request-extension registry omits it and reports the error. + * @param exclude - synchronous, side-effect-free predicate; `true` skips this upload. + * @returns effect disposer that removes this registration when called or when the caller unloads. + */ +registerExclusion(exclude: SessionLogUploadExclusion): () => Promise +``` + +Source: [`packages/session/session-log-deepseek/src/index.ts`](../../packages/session/session-log-deepseek/src/index.ts) + ### `ctx.sessions` — `SessionStore` diff --git a/docs/upgrade-guide/v0.2.1-rc.3/llm-request-purpose-declarations/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/llm-request-purpose-declarations/guide.i18n.yaml new file mode 100644 index 0000000000..8aa99dc276 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/llm-request-purpose-declarations/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.3/llm-request-purpose-declarations/guide.md +/: + en: 0a5cfa851101f3e9 + zh: 1c8e0e437718d4e2 +/declare-auxiliary-llm-request-purposes: + en: 5e5a221cd1ef18d9 + zh: 11b74947b2ce177f +/declare-auxiliary-llm-request-purposes/change: + en: 0682a43eb52792fd + zh: 954b186cc66fdb08 +/declare-auxiliary-llm-request-purposes/migration: + en: fa658147afcf705d + zh: 1493fd1ef0a0306a diff --git a/docs/upgrade-guide/v0.2.1-rc.3/llm-request-purpose-declarations/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/llm-request-purpose-declarations/guide.md new file mode 100644 index 0000000000..dcdcdcbe76 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/llm-request-purpose-declarations/guide.md @@ -0,0 +1,29 @@ +--- +kind: upgrade-guide +description: "Auxiliary LLM request purposes require producer-owned type declarations instead of a shared fixed union." +--- + +# Declare auxiliary LLM request purposes + +English | [中文](guide.zh.md) + +## Change + +`GenerateOptions.purpose` derives from the declaration-mergeable `LlmRequestPurposeMap` exported by `@deepseek-ai/dsh-llm/types`. The shared package no longer declares `compaction` or `session-title`. Plugins that construct auxiliary requests must load the producer's declarations or declare their own purpose. Existing runtime purpose strings keep their meaning. + +## Migration + +1. Import `@deepseek-ai/dsh-compaction-basic/types` for direct compaction requests, or `@deepseek-ai/dsh-session-title-llm` for title requests, using a type-only import when only the declaration is needed. +2. For a plugin-owned purpose, augment `LlmRequestPurposeMap` from the plugin's public type entry. Map the purpose key to `true`: + + ```ts + import type {} from '@deepseek-ai/dsh-llm/types' + + declare module '@deepseek-ai/dsh-llm/types' { + interface LlmRequestPurposeMap { + 'my-plugin-purpose': true + } + } + ``` + +3. Compile the plugin against the new package declarations. Confirm that its request accepts the declared purpose and rejects an undeclared key. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/llm-request-purpose-declarations/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/llm-request-purpose-declarations/guide.zh.md new file mode 100644 index 0000000000..c5283d59a3 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/llm-request-purpose-declarations/guide.zh.md @@ -0,0 +1,29 @@ +--- +kind: upgrade-guide +description: "辅助 LLM 请求用途改为生产方拥有的类型声明,不再使用共享固定联合。" +--- + +# 声明辅助 LLM 请求用途 + +[English](guide.md) | 中文 + +## 变更 + +`GenerateOptions.purpose` 由 `@deepseek-ai/dsh-llm/types` 导出的可声明合并的 `LlmRequestPurposeMap` 派生。共享包不再声明 `compaction` 或 `session-title`。构造辅助请求的插件必须加载生产方的声明,或声明自己的用途。现有运行时用途字符串保持原有含义。 + +## 迁移 + +1. 直接构造压缩请求时导入 `@deepseek-ai/dsh-compaction-basic/types`,构造标题请求时导入 `@deepseek-ai/dsh-session-title-llm`;只需要声明时使用仅类型导入。 +2. 对插件拥有的用途,在插件公开类型入口中扩展 `LlmRequestPurposeMap`,将用途键映射到 `true`: + + ```ts + import type {} from '@deepseek-ai/dsh-llm/types' + + declare module '@deepseek-ai/dsh-llm/types' { + interface LlmRequestPurposeMap { + 'my-plugin-purpose': true + } + } + ``` + +3. 使用新包的声明编译插件,确认请求接受已声明用途,并拒绝未声明的键。 diff --git a/packages/compaction/compaction-basic/README.i18n.yaml b/packages/compaction/compaction-basic/README.i18n.yaml index 9e67ab2df6..ef64d92dbb 100644 --- a/packages/compaction/compaction-basic/README.i18n.yaml +++ b/packages/compaction/compaction-basic/README.i18n.yaml @@ -57,8 +57,8 @@ en: 313e019b1a9f8aa8 zh: 3c75d11b02adb693 /deepseek-ai-dsh-compaction-basic/understand-the-implementation/source-map: - en: 3f1d7a55b790e623 - zh: 39e878c0785a885c + en: 5b4dea4aa91f3b95 + zh: 4eb4beca922da884 /deepseek-ai-dsh-compaction-basic/further-exploration: en: 84b5b81eb9b5965d zh: 7000af0fa502fcc1 diff --git a/packages/compaction/compaction-basic/README.md b/packages/compaction/compaction-basic/README.md index 3956c8a57d..ecc7de8488 100644 --- a/packages/compaction/compaction-basic/README.md +++ b/packages/compaction/compaction-basic/README.md @@ -148,6 +148,8 @@ The transaction validates the surface span and the durable lock, appends `compac | [`src/config.ts`](src/config.ts) | Load-time validation and routed-model policy resolution | | [`src/types.ts`](src/types.ts) | `BasicCompactionConfig` and resolved policy vocabulary | +The public type entry declares `compaction` in `LlmRequestPurposeMap`. Direct callers constructing compaction requests import this package or its public types so `GenerateOptions.purpose` includes that key. + ----- diff --git a/packages/compaction/compaction-basic/README.zh.md b/packages/compaction/compaction-basic/README.zh.md index 283a4d19eb..5955f21d65 100644 --- a/packages/compaction/compaction-basic/README.zh.md +++ b/packages/compaction/compaction-basic/README.zh.md @@ -148,6 +148,8 @@ kind: "package-reference" | [`src/config.ts`](src/config.ts) | 加载时验证与路由模型策略解析 | | [`src/types.ts`](src/types.ts) | `BasicCompactionConfig` 与已解析策略词汇 | +公开类型入口在 `LlmRequestPurposeMap` 中声明 `compaction`。直接构造压缩请求的调用方应导入本包或其公开类型,使 `GenerateOptions.purpose` 包含该键。 + ----- diff --git a/packages/compaction/compaction-basic/src/types.ts b/packages/compaction/compaction-basic/src/types.ts index e8160dba2c..23e195aab7 100644 --- a/packages/compaction/compaction-basic/src/types.ts +++ b/packages/compaction/compaction-basic/src/types.ts @@ -4,6 +4,12 @@ * @module @deepseek-ai/dsh-compaction-basic/types */ +declare module '@deepseek-ai/dsh-llm/types' { + interface LlmRequestPurposeMap { + 'compaction': true + } +} + import type { LlmCallConfig } from '@deepseek-ai/dsh-llm' /** Policy fields shared by the default policy and exact model overrides. */ diff --git a/packages/experimental/auto-review/README.i18n.yaml b/packages/experimental/auto-review/README.i18n.yaml index 41e0627b8b..b4345e70f0 100644 --- a/packages/experimental/auto-review/README.i18n.yaml +++ b/packages/experimental/auto-review/README.i18n.yaml @@ -45,8 +45,8 @@ en: 9c3e575c061d53fe zh: 74db1c5a77b2d2a7 /deepseek-ai-dsh-experimental-auto-review/model-experience/per-call-reviewer/kv-cache-effect: - en: b1985ccf9ace0fa0 - zh: 5af95319d2c888bc + en: ec9e16640f1ea1bc + zh: 8052d486d61807c6 /deepseek-ai-dsh-experimental-auto-review/model-experience/tool-denial: en: f9b4d7b686f14fc5 zh: a2fac9a8eaf5e2a3 diff --git a/packages/experimental/auto-review/README.md b/packages/experimental/auto-review/README.md index 8a43daf012..63f136177d 100644 --- a/packages/experimental/auto-review/README.md +++ b/packages/experimental/auto-review/README.md @@ -110,7 +110,7 @@ One additional model request per supported call, without caching, retries, trunc #### KV Cache effect -Each review's messages begin with all messages of the previous review, including its pending call, so routes that cache by prompt prefix, at message ends, or at a final marker with lookback reuse the policy and earlier history. Compaction, a cwd change, an earlier item leaving the surface, and the first review after a restart end that reuse once. Auto adds no dedicated runtime context or mode-switch prompt to the main agent. Each review request carries the reviewed Session's `sessionId` and `purpose: 'auto-review'` as model-hidden transport metadata. Routes that key prompt caching or request routing by Session, such as OpenAI `prompt_cache_key` and the `x-opencode-session` header that OpenCode requires, send reviews under that Session's key; [the DeepSeek Session-log upload](../../session/session-log-deepseek/README.md#request-field) skips review requests. +Each review's messages begin with all messages of the previous review, including its pending call, so routes that cache by prompt prefix, at message ends, or at a final marker with lookback reuse the policy and earlier history. Compaction, a cwd change, an earlier item leaving the surface, and the first review after a restart end that reuse once. Auto adds no dedicated runtime context or mode-switch prompt to the main agent. Each review request carries the reviewed Session's `sessionId` and `purpose: 'auto-review'` as model-hidden transport metadata. Routes that key prompt caching or request routing by Session, such as OpenAI `prompt_cache_key` and the `x-opencode-session` header that OpenCode requires, send reviews under that Session's key; [the DeepSeek Session-log upload](../../session/session-log-deepseek/README.md#request-field) contributes the canonical Session log when enabled and no registered exclusion matches. ### Tool denial diff --git a/packages/experimental/auto-review/README.zh.md b/packages/experimental/auto-review/README.zh.md index cc06a9adda..39aa14ce15 100644 --- a/packages/experimental/auto-review/README.zh.md +++ b/packages/experimental/auto-review/README.zh.md @@ -110,7 +110,7 @@ Reviewer 使用最新 `request/header.config` 的 provider 与模型,并沿用 #### KV Cache 影响 -每次评审的消息都以上一次评审的全部消息(包括其待审调用)开头,因此按 prompt 前缀、按消息结尾或在末尾标记处回溯缓存的路由都能复用策略与先前历史。Compaction、cwd 变化、较早条目离开 surface,以及重启后的第一次评审会让复用中断一次。Auto 不向主 agent 增加专门 runtime context 或模式切换提示词。每个评审请求都携带被评审 Session 的 `sessionId` 和 `purpose: 'auto-review'`,作为模型不可见的传输元数据。按 Session 划分 prompt 缓存或请求路由的路由(例如 OpenAI 的 `prompt_cache_key`,以及 OpenCode 要求的 `x-opencode-session` header)会以该 Session 的键发送评审;[DeepSeek session-log 上传](../../session/session-log-deepseek/README.zh.md#request-field)会跳过评审请求。 +每次评审的消息都以上一次评审的全部消息(包括其待审调用)开头,因此按 prompt 前缀、按消息结尾或在末尾标记处回溯缓存的路由都能复用策略与先前历史。Compaction、cwd 变化、较早条目离开 surface,以及重启后的第一次评审会让复用中断一次。Auto 不向主 agent 增加专门 runtime context 或模式切换提示词。每个评审请求都携带被评审 Session 的 `sessionId` 和 `purpose: 'auto-review'`,作为模型不可见的传输元数据。按 Session 划分 prompt 缓存或请求路由的路由(例如 OpenAI 的 `prompt_cache_key`,以及 OpenCode 要求的 `x-opencode-session` header)会以该 Session 的键发送评审;[DeepSeek session-log 上传](../../session/session-log-deepseek/README.zh.md#request-field)在启用且没有已注册排除条件匹配时贡献权威 Session 日志。 ### 工具拒绝 diff --git a/packages/experimental/auto-review/src/index.ts b/packages/experimental/auto-review/src/index.ts index 27a09eff5a..29df7771f0 100644 --- a/packages/experimental/auto-review/src/index.ts +++ b/packages/experimental/auto-review/src/index.ts @@ -8,6 +8,12 @@ * @module @deepseek-ai/dsh-experimental-auto-review */ +declare module '@deepseek-ai/dsh-llm/types' { + interface LlmRequestPurposeMap { + 'auto-review': true + } +} + import type {} from '@deepseek-ai/dsh-tool-executor-ptc/types' import type {} from '@deepseek-ai/dsh-working-directory' import { FiberState, type Context } from '@deepseek-ai/cordis' @@ -672,7 +678,7 @@ async function classifyRisk( const snapshot = snapshotAutoReview(agent, exec, cwd, shown) shown.set(snapshot.pending.key, snapshot.pending.schema) // This review prompt is sent only through ctx.llm.stream and never enters a Session log. - // The Session id is the provider routing key; the purpose excludes the call from Session-log upload. + // The Session id also supplies provider routing and request-extension identity. const options: GenerateOptions = deepFreeze({ provider: snapshot.provider, model: snapshot.model, diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 9508448afb..97fc2b8624 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -2412,6 +2412,19 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, ], }, + { + key: 'sessionLogDeepSeek', + summary: 'Incremental Session-log contributor with caller-owned upload exclusions.', + description: 'Incremental Session-log contributor with caller-owned upload exclusions.', + methods: [ + { + signature: 'registerExclusion(exclude: SessionLogUploadExclusion): () => Promise', + description: 'Exclude matching requests before reading or preparing their log contribution. Any matching rule suppresses both the field and its acceptance callback. With no matching rules, normal upload eligibility applies. A throwing rule fails this contribution; the request-extension registry omits it and reports the error.', + parameters: [{ name: 'exclude', description: 'synchronous, side-effect-free predicate; `true` skips this upload.' }], + returns: 'effect disposer that removes this registration when called or when the caller unloads.', + }, + ], + }, { key: 'sessionPersistence', summary: 'Durable append-only session storage addressed through per-session handles.', @@ -5626,7 +5639,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'DeepSeekLlmApiExtensionRequest', - declaration: 'export interface DeepSeekLlmApiExtensionRequest {\n readonly body: Readonly>;\n readonly sessionId?: string;\n readonly purpose?: \'compaction\' | \'session-title\' | \'auto-review\';\n readonly signal: AbortSignal;\n}', + declaration: 'export interface DeepSeekLlmApiExtensionRequest {\n readonly body: Readonly>;\n readonly sessionId?: string;\n readonly purpose?: string;\n readonly signal: AbortSignal;\n}', }, { name: 'DeepSeekLlmApiJson', @@ -5870,7 +5883,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'GenerateOptions', - declaration: 'export interface GenerateOptions {\n provider: string;\n model: string;\n reasoningEffort?: ReasoningEffortId;\n messages: RequestMessage[];\n system?: string;\n tools?: ToolSchema[];\n toolHistory?: ToolHistory;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n signal?: AbortSignal;\n sessionId?: Branded<\'SessionId\'>;\n purpose?: \'compaction\' | \'session-title\' | \'auto-review\';\n}', + declaration: 'export interface GenerateOptions {\n provider: string;\n model: string;\n reasoningEffort?: ReasoningEffortId;\n messages: RequestMessage[];\n system?: string;\n tools?: ToolSchema[];\n toolHistory?: ToolHistory;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n signal?: AbortSignal;\n sessionId?: Branded<\'SessionId\'>;\n purpose?: Purpose;\n}', }, { name: 'GenericCallView', @@ -6276,6 +6289,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'LlmReasoningEffortInfo', declaration: 'export interface LlmReasoningEffortInfo {\n id: ReasoningEffortId;\n name: string;\n description?: string;\n}', }, + { + name: 'LlmRequestPurpose', + declaration: 'export type LlmRequestPurpose = Extract;', + }, + { + name: 'LlmRequestPurposeMap', + declaration: 'export interface LlmRequestPurposeMap {\n}', + }, { name: 'LlmResolvedModelInfo', declaration: 'export interface LlmResolvedModelInfo extends LlmModelInfo {\n context?: LlmModelContext;\n defaultMaxTokens?: number;\n reasoning?: LlmModelReasoningInfo;\n systemPromptUpdate?: SystemPromptUpdate;\n toolUpdate?: ToolUpdate;\n}', @@ -7632,6 +7653,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'SessionLogSnapshot', declaration: 'export interface SessionLogSnapshot {\n session: SessionHeader;\n inheritedEventCount: SessionLogOffset;\n events: SessionEvent[];\n}', }, + { + name: 'SessionLogUploadExclusion', + declaration: 'export type SessionLogUploadExclusion = (request: SessionLogUploadRequest) => boolean;', + }, + { + name: 'SessionLogUploadRequest', + declaration: 'export interface SessionLogUploadRequest {\n readonly sessionId: SessionId;\n readonly purpose?: string;\n}', + }, { name: 'SessionMessage', declaration: 'export interface SessionMessage {\n role: \'user\' | \'assistant\';\n text: string;\n toolUses: ToolUseSummary[];\n}', diff --git a/packages/llm/deepseek-llm-api-extensions/README.i18n.yaml b/packages/llm/deepseek-llm-api-extensions/README.i18n.yaml index a9f16eead6..34a43376e2 100644 --- a/packages/llm/deepseek-llm-api-extensions/README.i18n.yaml +++ b/packages/llm/deepseek-llm-api-extensions/README.i18n.yaml @@ -15,8 +15,8 @@ en: 6b7f62c318c9aad6 zh: 5e22d58884e60c87 /deepseek-ai-dsh-deepseek-llm-api-extensions/service: - en: d9782207ffab7c50 - zh: 22c2a10a5f0de61f + en: 67cf2183647de143 + zh: 924aad182c107ed8 /deepseek-ai-dsh-deepseek-llm-api-extensions/model-experience: en: 88a785350ced5c14 zh: 755349be023a3f5c diff --git a/packages/llm/deepseek-llm-api-extensions/README.md b/packages/llm/deepseek-llm-api-extensions/README.md index 410954fcb2..108af40353 100644 --- a/packages/llm/deepseek-llm-api-extensions/README.md +++ b/packages/llm/deepseek-llm-api-extensions/README.md @@ -29,6 +29,8 @@ Provider-specific registry for additive top-level fields on official DeepSeek LL Each provider sees the exact serialized Messages 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. +Request `purpose` is the producer-defined string forwarded by the adapter, or absent for ordinary conversation requests. The registry forwards unfamiliar purposes without choosing field-specific policy. + 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. diff --git a/packages/llm/deepseek-llm-api-extensions/README.zh.md b/packages/llm/deepseek-llm-api-extensions/README.zh.md index 50efa0891e..958187632c 100644 --- a/packages/llm/deepseek-llm-api-extensions/README.zh.md +++ b/packages/llm/deepseek-llm-api-extensions/README.zh.md @@ -29,6 +29,8 @@ kind: "package-reference" 每个提供方都会看到确切的已序列化 Messages 请求体、请求 `AbortSignal`,以及可选的 `sessionId` 与辅助调用 `purpose`。提供方必须在取消后迅速停止自身工作;字段不适用于当前请求时返回 `undefined`。即使 HMR(热模块替换)在 HTTP 接受前移除了注册,已准备的操作仍会保留其捕获的提供方。 +请求的 `purpose` 是适配器转发的生产方自定义字符串,普通对话请求则省略它。注册器转发不熟悉的用途,不替各个字段决定策略。 + 注册表拥有字段添加与生命周期,不拥有字段语义。`@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/deepseek-llm-api-extensions/src/types.ts b/packages/llm/deepseek-llm-api-extensions/src/types.ts index 6b0a528041..5ae15265af 100644 --- a/packages/llm/deepseek-llm-api-extensions/src/types.ts +++ b/packages/llm/deepseek-llm-api-extensions/src/types.ts @@ -22,7 +22,7 @@ export interface DeepSeekLlmApiExtensionRequest { /** Session identity carried by the model request, when present. */ readonly sessionId?: string /** Auxiliary request classification, when present. */ - readonly purpose?: 'compaction' | 'session-title' | 'auto-review' + readonly purpose?: string /** Cancellation for request preparation; providers must stop promptly after abort. */ readonly signal: AbortSignal } diff --git a/packages/llm/llm-deepseek/src/adapter.ts b/packages/llm/llm-deepseek/src/adapter.ts index 53b288f1f2..df35b6ce3b 100644 --- a/packages/llm/llm-deepseek/src/adapter.ts +++ b/packages/llm/llm-deepseek/src/adapter.ts @@ -2,7 +2,7 @@ import { attributionHeaders, LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, ImageAttachmentAccessResolver, PreparedAdapterCall, StreamChunk } from '@deepseek-ai/dsh-llm' -import type { DeepSeekLlmApiJson } from '@deepseek-ai/dsh-deepseek-llm-api-extensions' +import type { DeepSeekLlmApiExtensionRequest, DeepSeekLlmApiJson } from '@deepseek-ai/dsh-deepseek-llm-api-extensions' import { idleWatchdog, timeoutOf } from '@deepseek-ai/dsh-timeout' import { modelInfo } from './model-info.ts' import type { DeepSeekAdapterOptions, DeepSeekConnectionOptions as Connection } from './types.ts' @@ -52,7 +52,7 @@ export class DeepSeekAdapter extends LlmAdapt this.dependencies.admit?.(connection, model) return { model: modelInfo(connection, provider, model), stream: options => this.generate(options, connection) } } - stream(options: GenerateOptions): AsyncIterable { + stream(options: GenerateOptions): AsyncIterable { const connection = this.dependencies.options() this.dependencies.admit?.(connection, options.model) return this.generate(options, connection) @@ -62,7 +62,7 @@ export class DeepSeekAdapter extends LlmAdapt return this.dependencies.connect?.(signal) ?? Promise.resolve(this.dependencies.options()) } - private async * generate(options: GenerateOptions, connection: C): AsyncGenerator { + private async * generate(options: GenerateOptions, connection: C): AsyncGenerator { const consumer = new AbortController() const signal = options.signal === undefined ? consumer.signal : AbortSignal.any([consumer.signal, options.signal]) using watchdog = idleWatchdog(signal, connection.streamIdleTimeoutMs, 'MESSAGES_IDLE') @@ -89,7 +89,7 @@ export class DeepSeekAdapter extends LlmAdapt } private async * request( - options: GenerateOptions, connection: C, signal: AbortSignal, activity: () => void, + options: GenerateOptions, connection: C, signal: AbortSignal, activity: () => void, ): AsyncGenerator { signal.throwIfAborted() const { messages, versions } = await prepareImages( @@ -119,15 +119,19 @@ export class DeepSeekAdapter extends LlmAdapt const body = serialize(options, connection, history, versions, this.imageAccess, (reason) => { this.dependencies.onReplayDegrade?.({ provider: options.provider, model: options.model, reason }) }, fileIds) - const extensions = await prepareRequestExtensions(body as Readonly>, { + const requestIdentity: Omit = { signal, ...options.sessionId === undefined ? {} : { sessionId: String(options.sessionId) }, ...options.purpose === undefined ? {} : { purpose: options.purpose }, - }, this.dependencies.prepareExtensions, (fields, error) => { - this.dependencies.onExtensionsOmitted?.({ provider: options.provider, model: options.model, fields, error }) - }, (error) => { - this.dependencies.onExtensionsUnaccepted?.({ provider: options.provider, model: options.model, error }) - }) + } + const extensions = await prepareRequestExtensions( + body as Readonly>, requestIdentity, + this.dependencies.prepareExtensions, (fields, error) => { + this.dependencies.onExtensionsOmitted?.({ provider: options.provider, model: options.model, fields, error }) + }, (error) => { + this.dependencies.onExtensionsUnaccepted?.({ provider: options.provider, model: options.model, error }) + }, + ) signal.throwIfAborted() const betas = [ ...fileIds !== undefined && fileIds.size > 0 ? [MESSAGES_FILES_BETA] : [], @@ -145,7 +149,7 @@ export class DeepSeekAdapter extends LlmAdapt ...betas.length === 0 ? {} : { 'anthropic-beta': betas.join(',') }, 'x-deepseek-harness-user-id': this.dependencies.resolveUserId(), ...options.sessionId === undefined ? {} : { 'x-deepseek-harness-session-id': String(options.sessionId) }, - ...options.purpose === 'compaction' ? { 'x-deepseek-harness-compact': '1' } : {}, + ...requestIdentity.purpose === 'compaction' ? { 'x-deepseek-harness-compact': '1' } : {}, }, }) if (!response.ok) { diff --git a/packages/llm/llm-deepseek/src/serialize.ts b/packages/llm/llm-deepseek/src/serialize.ts index 095211e8fc..f08412c0e9 100644 --- a/packages/llm/llm-deepseek/src/serialize.ts +++ b/packages/llm/llm-deepseek/src/serialize.ts @@ -55,7 +55,7 @@ function assistant(message: Message, model: string, onReplayDegrade?: (reason: s * @returns the Messages API JSON body. */ export function serialize( - options: GenerateOptions, connection: Connection, history: readonly RequestMessage[], + options: GenerateOptions, connection: Connection, history: readonly RequestMessage[], images: ReadonlyMap, access: ImageAttachmentAccessResolver, onReplayDegrade?: (reason: string) => void, fileIds?: ReadonlyMap, diff --git a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts index 01d54e98a5..8570f35a6d 100644 --- a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts +++ b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts @@ -24,6 +24,20 @@ import { assemble } from './assemble.ts' import { closeMockServers, mockServer, textEvents } from './mock-server.ts' import { sourceModuleLoader } from './helpers.ts' +declare module '@deepseek-ai/dsh-llm/types' { + interface LlmRequestPurposeMap { + 'test-upload-exclusion': true + } +} + +const uploadExclusion = { + name: 'test-upload-exclusion', + inject: ['sessionLogDeepSeek'], + apply(ctx: Context) { + ctx.sessionLogDeepSeek.registerExclusion(request => request.purpose === 'test-upload-exclusion') + }, +} + const NS = 'llm-deepseek' const KEY_REF = credentialRef('DEEPSEEK_API_KEY') @@ -41,7 +55,7 @@ afterEach(async () => { }) async function loadComposition( - options: { withDynamic: boolean; baseURL: string; reuseRoot?: string; enableSessionLog?: boolean }, + options: { withDynamic: boolean; baseURL: string; reuseRoot?: string; enableSessionLog?: boolean; exclusionFirst?: 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. @@ -67,11 +81,13 @@ async function loadComposition( "- name: '@deepseek-ai/dsh-session-projection'", '- id: deepseek-llm-api-extensions', " name: '@deepseek-ai/dsh-deepseek-llm-api-extensions'", + ...options.exclusionFirst === true ? ["- name: '@test/upload-exclusion'"] : [], '- id: session-log-deepseek', " name: '@deepseek-ai/dsh-session-log-deepseek'", ...options.enableSessionLog !== undefined ? [' config:', ` enabled: ${String(options.enableSessionLog)}`] : [], + ...options.exclusionFirst === false ? ["- name: '@test/upload-exclusion'"] : [], '- id: plugin-package-inventory-deepseek', " name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek'", ...options.withDynamic @@ -96,6 +112,7 @@ async function loadComposition( await ctx.plugin(Loader) ctx.loader.builtins.include = Include const modules = new Map([ + ['@test/upload-exclusion', uploadExclusion], ['@deepseek-ai/dsh-llm', LlmRuntime], ['@deepseek-ai/dsh-session', SessionStore], ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry], @@ -231,6 +248,25 @@ describe('llm-deepseek real dynamic composition', () => { expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0) }) + it.each([false, true])('applies a registered upload exclusion through HTTP with consumer first=%s', async (exclusionFirst) => { + vi.stubEnv('DEEPSEEK_API_KEY', 'entry-key') + const server = await mockServer([{ kind: 'sse', events: textEvents }, { kind: 'sse', events: textEvents }]) + const { ctx } = await loadComposition({ withDynamic: false, baseURL: server.url, exclusionFirst }) + const session = ctx.sessions.create(SessionId('excluded-composition')) + session.append('turn/start', { turn: 1 }) + + await assemble(ctx, { model: 'deepseek-v4-flash', messages: [], sessionId: session.id, purpose: 'test-upload-exclusion' }) + expect(server.headers[0]?.['x-deepseek-harness-session-id']).toBe(session.id) + expect(server.requests[0]).not.toHaveProperty('dsh_session_log') + expect(server.requests[0]).toHaveProperty('dsh_plugin_packages') + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(-1) + expect(session.snapshotEvents()).toHaveLength(1) + + await assemble(ctx, { model: 'deepseek-v4-flash', messages: [], sessionId: session.id }) + expect(server.requests[1]).toHaveProperty('dsh_session_log') + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0) + }) + it('boots from cordis.yml and routes the next request after external settings and credential edits', async () => { vi.stubEnv('DEEPSEEK_API_KEY', '') const serverA = await mockServer([{ kind: 'sse', events: textEvents }]) diff --git a/packages/llm/llm/README.i18n.yaml b/packages/llm/llm/README.i18n.yaml index 215b8cb41a..d6a1d37f1e 100644 --- a/packages/llm/llm/README.i18n.yaml +++ b/packages/llm/llm/README.i18n.yaml @@ -21,8 +21,8 @@ en: c3ecb5a1421f27db zh: 4bbd3c0e513845e1 /deepseek-ai-dsh-llm/use-this-package/minimal-composition: - en: 9d481d19dc52e404 - zh: aa09cf4462e93ba6 + en: cf221d60fb42a278 + zh: 0ea17503495d386b /deepseek-ai-dsh-llm/use-this-package/what-you-can-do: en: dc61eb86585f6e0d zh: 9c0d6b240dfd2cda diff --git a/packages/llm/llm/README.md b/packages/llm/llm/README.md index 00bab76ce0..c43680b34d 100644 --- a/packages/llm/llm/README.md +++ b/packages/llm/llm/README.md @@ -58,6 +58,8 @@ for await (const chunk of ctx.llm.stream({ After a successful mount, `ctx.llm.listProviders()` reports the registered routes in registration order. +Auxiliary request producers declare their `purpose` in `LlmRequestPurposeMap` through module augmentation of `@deepseek-ai/dsh-llm/types`; each key maps to `true`. `GenerateOptions.purpose` accepts the declared keys, while ordinary conversation requests omit it. Import the producer package or its public types before constructing that auxiliary request. Adapters carry the purpose as model-hidden transport metadata and handle unfamiliar purposes without rejecting the request. + `GenerateOptions.messages` accepts durable `Message` values and request-only `RequestUserInput` values. Request-only inputs carry user-role content with no `id` or `source`; Session writes and Agent delivery still require durable messages. Callers keep auxiliary inputs unchanged until the stream settles. A caller that records its exact request, such as session-title generation, must use durable messages. `prepareCall(config, signal, configure?)` accepts concrete `LlmCallConfig` values. The optional synchronous, pure `ConfigureCall` function receives detached, deeply frozen `LlmCallControls` and model metadata from the captured adapter generation. Callers can compose ordinary functions to choose concrete controls; later writes replace earlier ones. Preparation retains the captured route, applies defaults to omitted controls, validates the result, and returns a frozen configuration for recording and dispatch. Callback failures or cancellation reject before dispatch. Preparation requires a registered adapter. diff --git a/packages/llm/llm/README.zh.md b/packages/llm/llm/README.zh.md index e38f24ea5b..beccb99a7e 100644 --- a/packages/llm/llm/README.zh.md +++ b/packages/llm/llm/README.zh.md @@ -58,6 +58,8 @@ for await (const chunk of ctx.llm.stream({ 挂载成功后,`ctx.llm.listProviders()` 会按注册顺序报告已注册路由。 +辅助请求的生产方通过扩展 `@deepseek-ai/dsh-llm/types` 中的 `LlmRequestPurposeMap` 声明自己的 `purpose`,每个键映射到 `true`。`GenerateOptions.purpose` 接受已声明的键,普通对话请求则省略它。构造辅助请求前,应导入生产方包或其公开类型。适配器将用途作为模型不可见的传输元数据传递,并在遇到不熟悉的用途时继续处理请求。 + `GenerateOptions.messages` 接受持久 `Message` 值和仅供请求使用的 `RequestUserInput` 值。仅供请求使用的输入包含 user-role 内容,不含 `id` 或 `source`;Session 写入和 Agent 投递仍然要求持久消息。调用方必须在流结束前保持辅助输入不变。会记录完整请求的调用方(例如会话标题生成)必须使用持久消息。 `prepareCall(config, signal, configure?)` 接受具体的 `LlmCallConfig` 值。可选的同步纯函数 `ConfigureCall` 接收已分离且深度冻结的 `LlmCallControls`,以及捕获的适配器代次所提供的模型元数据。调用方可组合普通函数来选择具体控制项;后执行的写入替换先前的值。准备操作保留捕获的路由,为省略的控制项应用默认值,校验结果,再返回用于记录与分发的冻结配置。回调失败或取消会在分发前拒绝。准备操作要求注册适配器。 diff --git a/packages/llm/llm/src/types.ts b/packages/llm/llm/src/types.ts index 1acf3b9af7..ecd6e23df3 100644 --- a/packages/llm/llm/src/types.ts +++ b/packages/llm/llm/src/types.ts @@ -525,8 +525,17 @@ export interface ToolHistory { }[] } -/** A single model request, fully assembled. */ -export interface GenerateOptions { +/** Auxiliary request purposes declared by their producer packages. */ +export interface LlmRequestPurposeMap {} + +/** Declared auxiliary request purpose; ordinary conversation requests leave it absent. */ +export type LlmRequestPurpose = Extract + +/** + * A single model request, fully assembled. + * @typeParam Purpose - declared purpose keys for callers; forwarding adapters may accept `string`. + */ +export interface GenerateOptions { /** Registered provider route selecting the adapter instance. */ provider: string model: string @@ -567,5 +576,5 @@ export interface GenerateOptions { * map the purpose to model-hidden transport metadata. Ordinary conversation * requests leave it unset. */ - purpose?: 'compaction' | 'session-title' | 'auto-review' + purpose?: Purpose } diff --git a/packages/session/session-log-deepseek/README.i18n.yaml b/packages/session/session-log-deepseek/README.i18n.yaml index dafdf7f1c2..3fec12ee52 100644 --- a/packages/session/session-log-deepseek/README.i18n.yaml +++ b/packages/session/session-log-deepseek/README.i18n.yaml @@ -12,17 +12,20 @@ en: 19c182dbd7d8e064 zh: 51c043d6ea52db0d /deepseek-ai-dsh-session-log-deepseek/table-of-contents: - en: e2aa29e41bc101c4 - zh: 2ef629f2a444bc13 + en: eb945093933ee19b + zh: 7a40fe2e98a491c5 /deepseek-ai-dsh-session-log-deepseek/configuration: en: 1b899e63d8039643 zh: d489461bbb618b5d /deepseek-ai-dsh-session-log-deepseek/request-field: - en: 65795dc44af4b15e - zh: 849c7d57f16f66af + en: f11095717a9aec6f + zh: fb872499eea955be +/deepseek-ai-dsh-session-log-deepseek/upload-exclusions: + en: 9d303e05a05a8737 + zh: 8b12d1e7a2d04060 /deepseek-ai-dsh-session-log-deepseek/acceptance-and-retry: - en: d99c6b3c7a039079 - zh: 7bb4c0c94100c985 + en: 6997969916bb1b42 + zh: 929fd0485278ff8b /deepseek-ai-dsh-session-log-deepseek/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 diff --git a/packages/session/session-log-deepseek/README.md b/packages/session/session-log-deepseek/README.md index 48258d49ca..f686732ba9 100644 --- a/packages/session/session-log-deepseek/README.md +++ b/packages/session/session-log-deepseek/README.md @@ -15,6 +15,7 @@ Incremental canonical session-log upload for official DeepSeek LLM API requests. - [Configuration](#configuration) - [Request field](#request-field) +- [Upload exclusions](#upload-exclusions) - [Acceptance and retry](#acceptance-and-retry) - [Model Experience](#model-experience) - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) @@ -41,6 +42,24 @@ For a request carrying a live `sessionId`, the plugin folds the greatest accepte `maxBytes` bounds the complete serialized field in UTF-8 bytes, including the header and numeric envelope fields. A backlog above the limit drains across consecutive accepted requests, each continuing after the previous `throughSeq`. When the first pending event alone exceeds the limit, the request omits `dsh_session_log`, the plugin logs one warning per blocking event, and the watermark stays before that event until `maxBytes` admits it. An event too large for the runtime to serialize at all is handled the same way, and no `maxBytes` value admits it. + +## Upload exclusions + +Plugins can register synchronous, side-effect-free predicates with `ctx.sessionLogDeepSeek.registerExclusion()`. A predicate receives the live `sessionId` and optional request `purpose`; `true` skips this request's log field and acceptance callback. Any matching predicate excludes the upload. With no matching predicates, normal eligibility applies. A throwing predicate omits this log contribution, and the request-extension registry reports its first preparation failure; the model request continues with other prepared fields. + +```ts +import type { Context } from '@deepseek-ai/cordis' +import type {} from '@deepseek-ai/dsh-session-log-deepseek' + +export function apply(ctx: Context): void { + ctx.inject(['sessionLogDeepSeek'], (child) => { + child.sessionLogDeepSeek.registerExclusion(request => request.purpose === 'private-operation') + }) +} +``` + +Registration belongs to the calling plugin and returns an effect disposer. Disposing that registration or unloading its caller removes only that rule. Exclusions run before reading the Session log; other request extensions remain eligible. Prepared in-flight contributions retain their payload and acceptance callback. + ## Acceptance and retry @@ -48,7 +67,7 @@ The DeepSeek adapter calls the prepared contribution's `accept()` after HTTP 2xx 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. Auto review calls carry the reviewed Session's id with purpose `auto-review`; they omit `dsh_session_log` and record no acceptance. +Direct requests without a live Session omit `dsh_session_log`. Normal agent, compaction, session-title, and Auto review calls carry their live Session id and use the same upload eligibility. ## Model Experience diff --git a/packages/session/session-log-deepseek/README.zh.md b/packages/session/session-log-deepseek/README.zh.md index d612440fea..a87e6b5dfd 100644 --- a/packages/session/session-log-deepseek/README.zh.md +++ b/packages/session/session-log-deepseek/README.zh.md @@ -15,6 +15,7 @@ kind: "package-reference" - [配置](#configuration) - [请求字段](#request-field) +- [上传排除条件](#upload-exclusions) - [接受与重试](#acceptance-and-retry) - [模型体验](#model-experience) - [已知限制与暂缓事项](#known-limitations-and-deferred-work) @@ -41,6 +42,24 @@ Web **设置 → 通用 → 在使用官方模型 API 时上传 Session Log** `maxBytes` 按 UTF-8 字节限制完整的序列化字段,包括 header 与数值型 envelope 字段。超过上限的积压会分多次已接受的请求补传,每次都从上一次的 `throughSeq` 之后继续。第一条待发事件本身就超过上限时,请求会省略 `dsh_session_log`,插件对每个阻塞事件只记录一次告警,水位停在该事件之前,直到 `maxBytes` 能容纳它。运行时根本无法序列化的超大事件也按同样方式处理,且任何 `maxBytes` 都无法容纳它。 + +## 上传排除条件 + +插件可通过 `ctx.sessionLogDeepSeek.registerExclusion()` 注册同步、无副作用的判断函数。函数接收存活的 `sessionId` 与可选请求用途 `purpose`;返回 `true` 时,省略本次请求的日志字段与接受回调。任意条件匹配都会排除上传。没有条件匹配时,应用普通上传资格判断。条件抛错时省略本次日志贡献,请求扩展注册器报告首次准备失败;模型请求继续携带其他已准备字段。 + +```ts +import type { Context } from '@deepseek-ai/cordis' +import type {} from '@deepseek-ai/dsh-session-log-deepseek' + +export function apply(ctx: Context): void { + ctx.inject(['sessionLogDeepSeek'], (child) => { + child.sessionLogDeepSeek.registerExclusion(request => request.purpose === 'private-operation') + }) +} +``` + +注册归调用方插件所有,并返回 effect 释放函数。释放该注册或卸载调用方时,只移除对应规则。排除条件在读取 Session 日志前执行;其他请求扩展仍可参与。已经准备好的在途贡献保留其载荷与接受回调。 + ## 接受与重试 @@ -48,7 +67,7 @@ DeepSeek 适配器会在 HTTP 2xx 后、消费 SSE(Server-Sent Events)正文 服务端接受后、持久化水位前发生崩溃,可能让恢复后的进程重放已经接受的范围。这是至少一次交付的失败方向:不确定性会制造重复,绝不会跳过序列。普通会话检查点策略会在下一个语义检查点持久化水位;本插件不执行独立 I/O。 -缺少存活会话的直接请求会省略 `dsh_session_log`。普通 agent(智能体)、压缩(compaction)与会话标题调用都会携带存活会话 id。Auto 审查调用携带被审查会话的 id 与用途 `auto-review`;它们会省略 `dsh_session_log`,也不记录接受。 +缺少存活会话的直接请求会省略 `dsh_session_log`。普通 agent(智能体)、压缩(compaction)、会话标题与 Auto 审查调用都会携带存活会话 id,并应用相同的上传资格判断。 ## 模型体验 diff --git a/packages/session/session-log-deepseek/src/index.ts b/packages/session/session-log-deepseek/src/index.ts index f831507d6e..20d5dcf138 100644 --- a/packages/session/session-log-deepseek/src/index.ts +++ b/packages/session/session-log-deepseek/src/index.ts @@ -6,7 +6,7 @@ */ import { Buffer } from 'node:buffer' -import type { Context, Volatile } from '@deepseek-ai/cordis' +import { Service, type Context, type Volatile } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { brandString } from '@deepseek-ai/dsh-brand' import type {} from '@deepseek-ai/dsh-deepseek-llm-api-extensions' @@ -26,10 +26,18 @@ import type { DeepSeekSessionLogWireEvent, DeepSeekSessionLogWireHeader, DeepSeekSessionLogWireSurfaceOp, + SessionLogUploadExclusion, + SessionLogUploadRequest, } from './types.ts' export type * from './types.ts' +declare module '@deepseek-ai/cordis' { + interface Context { + sessionLogDeepSeek: SessionLogDeepSeekService + } +} + /** Cordis plugin name. */ export const name = 'session-log-deepseek' /** Services required to resolve sessions and contribute the provider request field. */ @@ -176,74 +184,108 @@ export function acceptedThrough(session: Session): SessionSeqCursor { return throughSeq } +/** Incremental Session-log contributor with caller-owned upload exclusions. */ +export class SessionLogDeepSeekService extends Service { + static inject = inject + + private readonly exclusions = new Set<{ readonly exclude: SessionLogUploadExclusion }>() + + constructor(ctx: Context, config: Config) { + super(ctx, 'sessionLogDeepSeek') + const { maxBytes } = config + ctx.deepseekLlmApiExtensions.register('dsh_session_log', { + prepare: (request) => { + if (!config.enabled.get()) return undefined + // 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(brandString(request.sessionId)) + if (session === undefined) return undefined + const upload: SessionLogUploadRequest = { + sessionId: session.id, + ...request.purpose === undefined ? {} : { purpose: request.purpose }, + } + for (const { exclude } of this.exclusions) { + if (exclude(upload)) return undefined + } + + const afterSeq = acceptedThrough(session) + // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. + const pending = session.snapshotEvents(SessionLogOffset(afterSeq + 1)) + const envelope = { + version: 1, + sessionFormatVersion: session.header.version, + session: wireHeader(session), + afterSeq: Number(afterSeq), + } as const + // Field bytes without events or throughSeq digits; each candidate prefix adds its own. + let bytes = jsonBytes({ ...envelope, throughSeq: 0, events: [] }) - 1 + const events: DeepSeekSessionLogWireEvent[] = [] + // Field bytes with the last examined event; when none fits, the first event's own field size. + let candidateBytes = 0 + for (const event of pending) { + const wire = wireEvent(event) + const next = bytes + (events.length === 0 ? 0 : 1) + jsonBytes(wire) + candidateBytes = next + String(event.seq).length + if (candidateBytes > maxBytes) break + bytes = next + events.push(wire) + } + const last = events.length === 0 ? undefined : pending[events.length - 1] + if (last === undefined) { + const first = pending[0] + if (first !== undefined && blockedWarnings.get(session) !== first.seq) { + blockedWarnings.set(session, first.seq) + const seq = String(first.seq) + ctx.logger.warn(Number.isFinite(candidateBytes) + ? `session-log-deepseek: event ${seq} of session "${session.id}" needs a ${String(candidateBytes)}-byte` + + ` dsh_session_log field, above maxBytes ${String(maxBytes)}; this session's upload stays at event ${seq}` + + ' until maxBytes admits it' + : `session-log-deepseek: event ${seq} of session "${session.id}" is too large to serialize into a dsh_session_log field;` + + ` this session's upload stays at event ${seq}`) + } + return undefined + } + const throughSeq = last.seq + const value: DeepSeekSessionLogExtension = { ...envelope, throughSeq: Number(throughSeq), events } + return { + value, + accept: () => { + session.append('session-log-deepseek/delivery-accepted', { + sessionId: session.id, + sessionFormatVersion: session.header.version, + throughSeq, + }) + // TODO: Add an immediate lightweight checkpoint if duplicate replay after a 2xx crash window becomes unacceptable. + }, + } + }, + }) + } + + /** + * Exclude matching requests before reading or preparing their log contribution. + * Any matching rule suppresses both the field and its acceptance callback. With + * no matching rules, normal upload eligibility applies. A throwing rule fails + * this contribution; the request-extension registry omits it and reports the error. + * @param exclude - synchronous, side-effect-free predicate; `true` skips this upload. + * @returns effect disposer that removes this registration when called or when the caller unloads. + */ + registerExclusion(exclude: SessionLogUploadExclusion): () => Promise { + const entry = { exclude } + const exclusions = this.exclusions + return this.ctx.effect(() => { + exclusions.add(entry) + return () => { exclusions.delete(entry) } + }, 'sessionLogDeepSeek.registerExclusion()') + } +} + /** * Register the incremental request contribution; enablement is read for each request. * @param ctx - plugin context carrying Sessions and the DeepSeek request-extension registry. * @param config - validated configuration. + * @returns fulfillment after the upload service is mounted. */ -export function apply(ctx: Context, config: Config): void { - // Schemastery validates and fills the defaults before `apply` runs. - const { maxBytes } = config - ctx.deepseekLlmApiExtensions.register('dsh_session_log', { - prepare: (request) => { - if (!config.enabled.get()) return undefined - // An Auto review call is not part of its Session's conversation, so it neither carries nor acknowledges the log. - if (request.purpose === 'auto-review') return undefined - // 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(brandString(request.sessionId)) - if (session === undefined) return undefined - - const afterSeq = acceptedThrough(session) - // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. - const pending = session.snapshotEvents(SessionLogOffset(afterSeq + 1)) - const envelope = { - version: 1, - sessionFormatVersion: session.header.version, - session: wireHeader(session), - afterSeq: Number(afterSeq), - } as const - // Field bytes without events or throughSeq digits; each candidate prefix adds its own. - let bytes = jsonBytes({ ...envelope, throughSeq: 0, events: [] }) - 1 - const events: DeepSeekSessionLogWireEvent[] = [] - // Field bytes with the last examined event; when none fits, the first event's own field size. - let candidateBytes = 0 - for (const event of pending) { - const wire = wireEvent(event) - const next = bytes + (events.length === 0 ? 0 : 1) + jsonBytes(wire) - candidateBytes = next + String(event.seq).length - if (candidateBytes > maxBytes) break - bytes = next - events.push(wire) - } - const last = events.length === 0 ? undefined : pending[events.length - 1] - if (last === undefined) { - const first = pending[0] - if (first !== undefined && blockedWarnings.get(session) !== first.seq) { - blockedWarnings.set(session, first.seq) - const seq = String(first.seq) - ctx.logger.warn(Number.isFinite(candidateBytes) - ? `session-log-deepseek: event ${seq} of session "${session.id}" needs a ${String(candidateBytes)}-byte` - + ` dsh_session_log field, above maxBytes ${String(maxBytes)}; this session's upload stays at event ${seq}` - + ' until maxBytes admits it' - : `session-log-deepseek: event ${seq} of session "${session.id}" is too large to serialize into a dsh_session_log field;` - + ` this session's upload stays at event ${seq}`) - } - return undefined - } - const throughSeq = last.seq - const value: DeepSeekSessionLogExtension = { ...envelope, throughSeq: Number(throughSeq), events } - return { - value, - accept: () => { - session.append('session-log-deepseek/delivery-accepted', { - sessionId: session.id, - sessionFormatVersion: session.header.version, - throughSeq, - }) - // TODO: Add an immediate lightweight checkpoint if duplicate replay after a 2xx crash window becomes unacceptable. - }, - } - }, - }) +export async function apply(ctx: Context, config: Config): Promise { + await ctx.plugin(SessionLogDeepSeekService, config) } diff --git a/packages/session/session-log-deepseek/src/types.ts b/packages/session/session-log-deepseek/src/types.ts index 4201c51f99..13561cfe0d 100644 --- a/packages/session/session-log-deepseek/src/types.ts +++ b/packages/session/session-log-deepseek/src/types.ts @@ -1,8 +1,23 @@ /** Wire types for lossless incremental DeepSeek session-log upload. */ -import type { SessionEvent, SurfaceEventType } from '@deepseek-ai/dsh-session' +import type { SessionEvent, SessionId, SurfaceEventType } from '@deepseek-ai/dsh-session' import type { JsonValue } from '@deepseek-ai/dsh-util-values' +/** Request identity inspected before preparing a Session-log contribution. */ +export interface SessionLogUploadRequest { + /** Live Session whose canonical log would be contributed. */ + readonly sessionId: SessionId + /** Auxiliary request classification; absent for ordinary conversation requests. */ + readonly purpose?: string +} + +/** + * Synchronous, side-effect-free upload exclusion owned by its registering plugin. + * @param request - request identity; the predicate must not mutate it. + * @returns `true` to omit the log field and acceptance callback, or `false` to keep checking eligibility. + */ +export type SessionLogUploadExclusion = (request: SessionLogUploadRequest) => boolean + /** Session header fields serialized as raw JSON primitives on the external request wire. */ export interface DeepSeekSessionLogWireHeader { readonly version: number diff --git a/packages/session/session-log-deepseek/tests/upload.spec.ts b/packages/session/session-log-deepseek/tests/upload.spec.ts index 4ad150f421..b52b038fd9 100644 --- a/packages/session/session-log-deepseek/tests/upload.spec.ts +++ b/packages/session/session-log-deepseek/tests/upload.spec.ts @@ -404,19 +404,110 @@ describe('incremental DeepSeek session-log upload', () => { }) }) - it('neither uploads nor acknowledges the log for an Auto review call', async () => { - const { ctx, session } = await harness('auto-review') + it.each([undefined, 'compaction', 'session-title', 'auto-review'])( + 'uploads and acknowledges eligible requests with purpose %s by default', async (purpose) => { + const { ctx, session } = await harness('default-upload') + session.append('turn/start', { turn: 1 }) + const prepared = await ctx.deepseekLlmApiExtensions.prepare({ + body: body(), signal: SIGNAL, sessionId: session.id, ...purpose === undefined ? {} : { purpose }, + }) + expect(prepared.fields.dsh_session_log).toMatchObject({ afterSeq: -1, throughSeq: 0 }) + await prepared.accept() + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0) + expect(session.snapshotEvents().at(-1)?.type).toBe('session-log-deepseek/delivery-accepted') + }, + ) + + it.each([false, true])('omits a matching upload independently of registration order (%s)', async (reverse) => { + const { ctx, session } = await harness('excluded-upload') session.append('turn/start', { turn: 1 }) - const review = await ctx.deepseekLlmApiExtensions.prepare({ - body: body(), signal: SIGNAL, sessionId: session.id, purpose: 'auto-review', + const rules = [ + () => false, + (request: import('../src/types.ts').SessionLogUploadRequest) => request.purpose === 'private-operation', + ] + const consumer = ctx.inject(['sessionLogDeepSeek'], (child) => { + for (const rule of reverse ? rules.toReversed() : rules) child.sessionLogDeepSeek.registerExclusion(rule) }) - expect(review.fields).not.toHaveProperty('dsh_session_log') - await review.accept() + await consumer + const excluded = await ctx.deepseekLlmApiExtensions.prepare({ + body: body(), signal: SIGNAL, sessionId: session.id, purpose: 'private-operation', + }) + expect(excluded.fields).not.toHaveProperty('dsh_session_log') + await excluded.accept() expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(-1) expect(session.snapshotEvents()).toHaveLength(1) + const ordinary = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) + expect(ordinary.fields.dsh_session_log).toMatchObject({ afterSeq: -1, throughSeq: 0 }) + await consumer.dispose() + const restored = await ctx.deepseekLlmApiExtensions.prepare({ + body: body(), signal: SIGNAL, sessionId: session.id, purpose: 'private-operation', + }) + expect(restored.fields.dsh_session_log).toMatchObject({ afterSeq: -1, throughSeq: 0 }) + await restored.accept() + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0) + }) + + it('owns duplicate predicates as separate disposable registrations', async () => { + const { ctx, session } = await harness('duplicate-exclusion') + session.append('turn/start', { turn: 1 }) + const rule = () => true + const first = ctx.sessionLogDeepSeek.registerExclusion(rule) + const second = ctx.sessionLogDeepSeek.registerExclusion(rule) + await first() + const retained = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) + expect(retained.fields).not.toHaveProperty('dsh_session_log') + await second() + const restored = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) + expect(restored.fields.dsh_session_log).toMatchObject({ afterSeq: -1, throughSeq: 0 }) + }) + + it('reports a throwing exclusion and omits its log contribution without acknowledgement', async () => { + const { ctx, session } = await harness('failed-exclusion') + session.append('turn/start', { turn: 1 }) + const failure = new Error('upload exclusion failed') + const warnings: unknown[][] = [] + ctx.logger.exporter({ levels: { default: LoggerLevel.WARN }, export: (message) => { if (message.type === 'warn') warnings.push(message.args) } }) + const dispose = ctx.sessionLogDeepSeek.registerExclusion(() => { throw failure }) + const prepared = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) + expect(prepared.fields).not.toHaveProperty('dsh_session_log') + await prepared.accept() + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(-1) + expect(session.snapshotEvents()).toHaveLength(1) + expect(warnings).toEqual([[expect.stringContaining('omitting field "dsh_session_log"'), failure]]) + await dispose() + const restored = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) + expect(restored.fields.dsh_session_log).toMatchObject({ afterSeq: -1, throughSeq: 0 }) + }) + + it('rebinds caller exclusions after the upload service is replaced', async () => { + const { ctx, session, disposeUpload } = await harness('replacement-exclusion') + session.append('turn/start', { turn: 1 }) + const consumer = ctx.inject(['sessionLogDeepSeek'], (child) => { + child.sessionLogDeepSeek.registerExclusion(request => request.sessionId === session.id) + }) + await consumer + const prepare = () => ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) + expect((await prepare()).fields).not.toHaveProperty('dsh_session_log') + await disposeUpload() + expect(ctx.get('sessionLogDeepSeek')).toBeUndefined() + await ctx.plugin(SessionLogDeepSeek, { enabled: true }) + await consumer + expect((await prepare()).fields).not.toHaveProperty('dsh_session_log') + await consumer.dispose() + expect((await prepare()).fields).toHaveProperty('dsh_session_log') + }) + + it('keeps prepared contributions valid when an exclusion is registered later', async () => { + const { ctx, session } = await harness('prepared-exclusion') + session.append('turn/start', { turn: 1 }) + const prepared = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) + ctx.sessionLogDeepSeek.registerExclusion(() => true) + expect(prepared.fields).toHaveProperty('dsh_session_log') + await prepared.accept() + expect(SessionLogDeepSeek.acceptedThrough(session)).toBe(0) const next = await ctx.deepseekLlmApiExtensions.prepare({ body: body(), signal: SIGNAL, sessionId: session.id }) - expect(next.fields.dsh_session_log).toMatchObject({ afterSeq: -1, throughSeq: 0 }) + expect(next.fields).not.toHaveProperty('dsh_session_log') }) it('contributes complete events without reading request messages', async () => { diff --git a/packages/session/session-title-llm/README.i18n.yaml b/packages/session/session-title-llm/README.i18n.yaml index 0a02981f01..3c179c438b 100644 --- a/packages/session/session-title-llm/README.i18n.yaml +++ b/packages/session/session-title-llm/README.i18n.yaml @@ -36,8 +36,8 @@ en: ee1ac7a50e65d430 zh: 2c97dd5cc71e8f22 /deepseek-ai-dsh-session-title-llm/understand-the-implementation/request-flow: - en: b3f23a8c2c8b38e3 - zh: de612ea07796fe9d + en: 6b9d2ce3eaf13d3d + zh: 1a5fa6d1165e6b23 /deepseek-ai-dsh-session-title-llm/further-exploration: en: d9844c0fc3e16c5c zh: 1e0c0b4b5d566972 diff --git a/packages/session/session-title-llm/README.md b/packages/session/session-title-llm/README.md index 8ea85bbe1c..22a31a8147 100644 --- a/packages/session/session-title-llm/README.md +++ b/packages/session/session-title-llm/README.md @@ -72,7 +72,7 @@ One small shared execution policy so operational behavior cannot drift: executio ### Request flow -The caller builds the prepared input and frames it; the function checks the complete input against `maxInputBytes`. The provider's reasoning selector chooses at most one effort during `ctx.llm.prepareCall()`. The function records the exact input, output cap, and resolved effort in `session/title-llm-request`, then dispatches through the same captured adapter generation under the shared deadline. `purpose: 'session-title'` supplies attribution only. The request has no agent-loop identity and does not enter conversation history. Generation failures preserve the request record. +The caller builds the prepared input and frames it; the function checks the complete input against `maxInputBytes`. The provider's reasoning selector chooses at most one effort during `ctx.llm.prepareCall()`. The function records the exact input, output cap, and resolved effort in `session/title-llm-request`, then dispatches through the same captured adapter generation under the shared deadline. `purpose: 'session-title'` supplies attribution only. The request has no agent-loop identity and does not enter conversation history. Generation failures preserve the request record. The package declares `session-title` in `LlmRequestPurposeMap`; importing its public entry loads that declaration. diff --git a/packages/session/session-title-llm/README.zh.md b/packages/session/session-title-llm/README.zh.md index 5b2b714a92..8ff187665a 100644 --- a/packages/session/session-title-llm/README.zh.md +++ b/packages/session/session-title-llm/README.zh.md @@ -72,7 +72,7 @@ kind: "package-library" ### 请求流程 -调用方构建准备好的输入并进行封装;函数依据 `maxInputBytes` 检查完整输入。提供方的推理选择器在 `ctx.llm.prepareCall()` 期间最多选择一个强度。函数把确切输入、输出上限与解析后的强度记录到 `session/title-llm-request`,然后在共享截止时间内通过同一捕获的适配器代次分发。`purpose: 'session-title'` 仅提供归属标注。请求不带 agent loop 身份,也不进入对话历史。生成失败仍保留请求记录。 +调用方构建准备好的输入并进行封装;函数依据 `maxInputBytes` 检查完整输入。提供方的推理选择器在 `ctx.llm.prepareCall()` 期间最多选择一个强度。函数把确切输入、输出上限与解析后的强度记录到 `session/title-llm-request`,然后在共享截止时间内通过同一捕获的适配器代次分发。`purpose: 'session-title'` 仅提供归属标注。请求不带 agent loop 身份,也不进入对话历史。生成失败仍保留请求记录。 本包在 `LlmRequestPurposeMap` 中声明 `session-title`;导入其公开入口即可加载该声明。 diff --git a/packages/session/session-title-llm/src/index.ts b/packages/session/session-title-llm/src/index.ts index e89962d7c7..0e09938fe3 100644 --- a/packages/session/session-title-llm/src/index.ts +++ b/packages/session/session-title-llm/src/index.ts @@ -6,6 +6,12 @@ * @module @deepseek-ai/dsh-session-title-llm */ +declare module '@deepseek-ai/dsh-llm/types' { + interface LlmRequestPurposeMap { + 'session-title': true + } +} + import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { createUserMessage, BlockAssembler } from '@deepseek-ai/dsh-llm' diff --git a/packages/test-support/llm-replay/src/index.ts b/packages/test-support/llm-replay/src/index.ts index 656ec3bcb6..45cf13f0d3 100644 --- a/packages/test-support/llm-replay/src/index.ts +++ b/packages/test-support/llm-replay/src/index.ts @@ -856,7 +856,7 @@ class ReplayAdapter extends LlmAdapter { constructor( providers: readonly ReplayProviderConfig[], - private readonly replay: (options: GenerateOptions) => AsyncIterable, + private readonly replay: (options: GenerateOptions) => AsyncIterable, ) { super() this.providers = new Map(providers.map(provider => [provider.id, provider])) @@ -940,7 +940,7 @@ class ReplayAdapter extends LlmAdapter { }) } - override stream(options: GenerateOptions): AsyncIterable { + override stream(options: GenerateOptions): AsyncIterable { return this.replay(options) } } @@ -1041,7 +1041,7 @@ export function installLlmReplay(ctx: Context, config: ReplayConfig): ReplayHand const liveSessionIds: (string | undefined)[] = Array.from({ length: scripts.length }) let nextScript = 0 const ANON = '\0anon\0' // the key for a call that carries no sessionId - const replay = (options: GenerateOptions): AsyncIterable => { + const replay = (options: GenerateOptions): AsyncIterable => { const key = options.sessionId ?? ANON let state = bound.get(key) let unrecorded = false @@ -1101,7 +1101,7 @@ export function installLlmReplay(ctx: Context, config: ReplayConfig): ReplayHand const providers = config.providers ?? [] const dispose = providers.length > 0 ? ctx.llm.registerAdapter(providers.map(provider => provider.id), new ReplayAdapter(providers, replay)) - : ctx.on('llm/stream', (options: GenerateOptions, _next) => replay(options)) + : ctx.on('llm/stream', (options: GenerateOptions, _next) => replay(options)) return { dispose, assertConsumed(): void { diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 9a0ecfcc1b..f92638111a 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -122,6 +122,7 @@ export const SERVICE_PAGE: Record = { sessionController: 'session.md', sessionSkillCatalog: 'skills.md', sessions: 'session.md', + sessionLogDeepSeek: 'session.md', settings: 'settings.md', sessionTitle: 'session-title.md', skills: 'skills.md', @@ -653,6 +654,8 @@ export const LINK_MAP: Readonly> = { AgentMessageSource: 'subagent.md', SubagentCatalogEntry: 'subagent.md', SubagentCatalogState: 'subagent.md', + SessionLogUploadRequest: 'session.md', + SessionLogUploadExclusion: 'session.md', SessionProjectionsRequest: 'session.md', SessionProjectionsValue: 'session.md', SubagentDescendantListEntry: 'subagent.md', diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 2208639652..e0f63d16a0 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -275,6 +275,14 @@ const SERVICE_ROLES: ServiceRole[] = [ consumers: ['agent-loop', 'compaction-basic'], note: 'Adapters register provider implementations; the loop and compaction call the provider-neutral stream service.', }, + { + key: 'sessionLogDeepSeek', + pkg: 'session-log-deepseek', + title: 'Session-log upload policy', + mode: 'core', + implementations: [], + note: 'Plugins register caller-owned exclusions before the canonical Session-log request contribution is prepared; unmatched requests use normal upload eligibility.', + }, { key: 'deepseekLlmApiExtensions', pkg: 'deepseek-llm-api-extensions', diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index d36f52d1f2..25c0e5885b 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -1,6 +1,16 @@ { "comment": "Maps each primary ` ```ts type-equiv ` or ` ```ts public-api ` block (by doc + declared symbol + projection) to the source declaration and original JSDoc it must match. Paired `.zh.md` blocks are byte-identical derivatives checked through their unsuffixed sibling and have no duplicate entry. Omit projection for the complete declaration; use public-api with a ` ```ts public-api ` block for a body-stripped public class declaration. verify-type-equiv.ts enforces a 1:1 correspondence between primary blocks and entries. Add an entry when you add a primary source-equivalence block; remove it when you remove the block.", "entries": [ + { + "doc": "docs/subsystems/session.md", + "symbol": "SessionLogUploadRequest", + "source": "packages/session/session-log-deepseek/src/types.ts" + }, + { + "doc": "docs/subsystems/session.md", + "symbol": "SessionLogUploadExclusion", + "source": "packages/session/session-log-deepseek/src/types.ts" + }, { "doc": "docs/subsystems/compaction.md", "symbol": "ImageOffloadTarget", @@ -111,6 +121,16 @@ "symbol": "RequestMessage", "source": "packages/llm/llm/src/types.ts" }, + { + "doc": "docs/subsystems/llm-streaming.md", + "symbol": "LlmRequestPurposeMap", + "source": "packages/llm/llm/src/types.ts" + }, + { + "doc": "docs/subsystems/llm-streaming.md", + "symbol": "LlmRequestPurpose", + "source": "packages/llm/llm/src/types.ts" + }, { "doc": "docs/subsystems/llm-streaming.md", "symbol": "GenerateOptions",