refactor(session): register request upload exclusions

This commit is contained in:
Yichen Jiang
2026-10-26 13:55:47 +08:00
parent 6fcb9b6c78
commit a36ddf4c40
60 changed files with 689 additions and 164 deletions
@@ -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
@@ -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.
@@ -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` 豁免。
+2 -2
View File
@@ -6,5 +6,5 @@
en: 41d772192075e133
zh: 5ef3da69b4de1d8a
/capability-seams-and-core-services:
en: 506d61dfa8c71186
zh: 46f05c4ef2f5c322
en: 697165390e1bddde
zh: eaa2dd9b9d988d69
+4 -1
View File
@@ -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<br/>Session-log upload policy"]
pkg_deepseek_llm_api_extensions["deepseek-llm-api-extensions"]
svc_deepseekLlmApiExtensions["ctx.deepseekLlmApiExtensions<br/>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<br/>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. |
+4 -1
View File
@@ -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<br/>Session-log upload policy"]
pkg_deepseek_llm_api_extensions["deepseek-llm-api-extensions"]
svc_deepseekLlmApiExtensions["ctx.deepseekLlmApiExtensions<br/>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<br/>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) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 |
+2 -2
View File
@@ -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. */
+2 -2
View File
@@ -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. */
@@ -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
+1 -1
View File
@@ -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
{
+1 -1
View File
@@ -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
{
+8 -8
View File
@@ -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
+4 -4
View File
@@ -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 |
|---|---|---|
+4 -4
View File
@@ -880,7 +880,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
类型:[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<T extends SessionEventType = SessionEventType> = {
}
```
来源:[`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)
| 属性 | 存在性 | 类型 |
|---|---|---|
+2 -2
View File
@@ -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":[]},
+2 -2
View File
@@ -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
+2 -1
View File
@@ -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) |
+2 -1
View File
@@ -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) |
+16 -3
View File
@@ -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<keyof LlmRequestPurposeMap, string>
```
```ts type-equiv
/**
* A single model request, fully assembled.
* @typeParam Purpose - declared purpose keys for callers; forwarding adapters may accept `string`.
*/
interface GenerateOptions<Purpose extends string = LlmRequestPurpose> {
/** 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
}
```
+16 -3
View File
@@ -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<keyof LlmRequestPurposeMap, string>
```
```ts type-equiv
/**
* A single model request, fully assembled.
* @typeParam Purpose - declared purpose keys for callers; forwarding adapters may accept `string`.
*/
interface GenerateOptions<Purpose extends string = LlmRequestPurpose> {
/** 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
}
```
+3
View File
@@ -68,3 +68,6 @@
/sessions/remote-catalog-and-workspace-opening:
en: dd27307e0d7c893c
zh: b77d2a749b7b641c
/sessions/session-log-upload-exclusions:
en: cfe3323aade972be
zh: 8423b490c47c5013
+43
View File
@@ -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
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
@@ -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)
<a id="ctxsessionlogdeepseek--sessionlogdeepseekservice"></a>
### `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<void>
```
Source: [`packages/session/session-log-deepseek/src/index.ts`](../../packages/session/session-log-deepseek/src/index.ts)
<a id="ctxsessions--sessionstore"></a>
### `ctx.sessions` — `SessionStore`
+43
View File
@@ -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
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
@@ -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)
<a id="ctxsessionlogdeepseek--sessionlogdeepseekservice"></a>
### `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<void>
```
Source: [`packages/session/session-log-deepseek/src/index.ts`](../../packages/session/session-log-deepseek/src/index.ts)
<a id="ctxsessions--sessionstore"></a>
### `ctx.sessions` — `SessionStore`
@@ -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
@@ -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.
@@ -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. 使用新包的声明编译插件,确认请求接受已声明用途,并拒绝未声明的键。
@@ -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
@@ -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.
</details>
-----
@@ -148,6 +148,8 @@ kind: "package-reference"
| [`src/config.ts`](src/config.ts) | 加载时验证与路由模型策略解析 |
| [`src/types.ts`](src/types.ts) | `BasicCompactionConfig` 与已解析策略词汇 |
公开类型入口在 `LlmRequestPurposeMap` 中声明 `compaction`。直接构造压缩请求的调用方应导入本包或其公开类型,使 `GenerateOptions.purpose` 包含该键。
</details>
-----
@@ -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. */
@@ -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
+1 -1
View File
@@ -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
@@ -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 日志。
### 工具拒绝
@@ -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,
@@ -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<void>',
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<Record<string, DeepSeekLlmApiJson>>;\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<Record<string, DeepSeekLlmApiJson>>;\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<Purpose extends string = LlmRequestPurpose> {\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<keyof LlmRequestPurposeMap, string>;',
},
{
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}',
@@ -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
@@ -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.
<a id="model-experience"></a>
@@ -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` 都不消费该注册表。
<a id="model-experience"></a>
@@ -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
}
+15 -11
View File
@@ -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<C extends Connection = Connection> extends LlmAdapt
this.dependencies.admit?.(connection, model)
return { model: modelInfo(connection, provider, model), stream: options => this.generate(options, connection) }
}
stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
stream(options: GenerateOptions<string>): AsyncIterable<StreamChunk> {
const connection = this.dependencies.options()
this.dependencies.admit?.(connection, options.model)
return this.generate(options, connection)
@@ -62,7 +62,7 @@ export class DeepSeekAdapter<C extends Connection = Connection> extends LlmAdapt
return this.dependencies.connect?.(signal) ?? Promise.resolve(this.dependencies.options())
}
private async * generate(options: GenerateOptions, connection: C): AsyncGenerator<StreamChunk> {
private async * generate(options: GenerateOptions<string>, connection: C): AsyncGenerator<StreamChunk> {
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<C extends Connection = Connection> extends LlmAdapt
}
private async * request(
options: GenerateOptions, connection: C, signal: AbortSignal, activity: () => void,
options: GenerateOptions<string>, connection: C, signal: AbortSignal, activity: () => void,
): AsyncGenerator<StreamChunk> {
signal.throwIfAborted()
const { messages, versions } = await prepareImages(
@@ -119,15 +119,19 @@ export class DeepSeekAdapter<C extends Connection = Connection> 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<Record<string, DeepSeekLlmApiJson>>, {
const requestIdentity: Omit<DeepSeekLlmApiExtensionRequest, 'body'> = {
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<Record<string, DeepSeekLlmApiJson>>, 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<C extends Connection = Connection> 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) {
+1 -1
View File
@@ -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<string>, connection: Connection, history: readonly RequestMessage[],
images: ReadonlyMap<ImageAttachmentRef['attachmentId'], RequestImageAttachment>, access: ImageAttachmentAccessResolver,
onReplayDegrade?: (reason: string) => void,
fileIds?: ReadonlyMap<ImageAttachmentRef['attachmentId'], DeepSeekFileId>,
@@ -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<string, unknown>([
['@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 }])
+2 -2
View File
@@ -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
+2
View File
@@ -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.
+2
View File
@@ -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`,以及捕获的适配器代次所提供的模型元数据。调用方可组合普通函数来选择具体控制项;后执行的写入替换先前的值。准备操作保留捕获的路由,为省略的控制项应用默认值,校验结果,再返回用于记录与分发的冻结配置。回调失败或取消会在分发前拒绝。准备操作要求注册适配器。
+12 -3
View File
@@ -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<keyof LlmRequestPurposeMap, string>
/**
* A single model request, fully assembled.
* @typeParam Purpose - declared purpose keys for callers; forwarding adapters may accept `string`.
*/
export interface GenerateOptions<Purpose extends string = LlmRequestPurpose> {
/** 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
}
@@ -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
@@ -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.
<a id="upload-exclusions"></a>
## 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.
<a id="acceptance-and-retry"></a>
## 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.
<a id="model-experience"></a>
## Model Experience
@@ -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` 都无法容纳它。
<a id="upload-exclusions"></a>
## 上传排除条件
插件可通过 `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 日志前执行;其他请求扩展仍可参与。已经准备好的在途贡献保留其载荷与接受回调。
<a id="acceptance-and-retry"></a>
## 接受与重试
@@ -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,并应用相同的上传资格判断。
<a id="model-experience"></a>
## 模型体验
@@ -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<SessionId>(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<void> {
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<SessionId>(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<void> {
await ctx.plugin(SessionLogDeepSeekService, config)
}
@@ -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
@@ -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 () => {
@@ -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
+1 -1
View File
@@ -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.
</details>
@@ -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`;导入其公开入口即可加载该声明。
</details>
@@ -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'
@@ -856,7 +856,7 @@ class ReplayAdapter extends LlmAdapter {
constructor(
providers: readonly ReplayProviderConfig[],
private readonly replay: (options: GenerateOptions) => AsyncIterable<StreamChunk>,
private readonly replay: (options: GenerateOptions<string>) => AsyncIterable<StreamChunk>,
) {
super()
this.providers = new Map(providers.map(provider => [provider.id, provider]))
@@ -940,7 +940,7 @@ class ReplayAdapter extends LlmAdapter {
})
}
override stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
override stream(options: GenerateOptions<string>): AsyncIterable<StreamChunk> {
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<StreamChunk> => {
const replay = (options: GenerateOptions<string>): AsyncIterable<StreamChunk> => {
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<string>, _next) => replay(options))
return {
dispose,
assertConsumed(): void {
+3
View File
@@ -122,6 +122,7 @@ export const SERVICE_PAGE: Record<string, string> = {
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<Record<string, string>> = {
AgentMessageSource: 'subagent.md',
SubagentCatalogEntry: 'subagent.md',
SubagentCatalogState: 'subagent.md',
SessionLogUploadRequest: 'session.md',
SessionLogUploadExclusion: 'session.md',
SessionProjectionsRequest: 'session.md',
SessionProjectionsValue: 'session.md',
SubagentDescendantListEntry: 'subagent.md',
+8
View File
@@ -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',
+20
View File
@@ -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",