Ver código fonte

fix(session-reference): retain fallback for adapterless routes

Tianyi Cui 1 mês atrás
pai
commit
72ce6964ac

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-09-05-session-reference-model-budget.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-05-session-reference-model-budget.md
-2026-09-05-session-reference-model-budget.md: 7d8ba2f7db83caa5c5b4768e9a31b8a36b10145a
-2026-09-05-session-reference-model-budget.zh.md: 89fc00f51486056f40056d9f7607ed2ddfd41ca6
+2026-09-05-session-reference-model-budget.md: 0654f2b89187727668fddb97ab6e982bacd82dc1
+2026-09-05-session-reference-model-budget.zh.md: 84f4fee76c7fb54f3ec37f0ae3417cc56d772af7

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-09-05-session-reference-model-budget.md

@@ -12,7 +12,7 @@ A fixed 64 KiB reference budget discards useful source context on large-context
 
 [Session-reference](../../../../packages/context/session-reference/README.md) observes the completed `system-prompt/assemble` waterfall with a local prepend listener and stores its provider/model pair in a WeakMap keyed by Agent. Preparation resolves that route through the optional LLM service; direct preparation before any assembly uses agent options. Diagnostics without an Agent do not update the map.
 
-Each source receives `max(65536, floor(contextWindow × 4 × referenceContextFraction))` bytes, with a default fraction of `0.2`. Four bytes per token is a sizing heuristic. Explicit `maxReferenceBytes` bypasses model lookup and remains exact. Missing route, service, or capacity retains the floor; lookup failures and cancellation propagate.
+Each source receives `max(65536, floor(contextWindow × 4 × referenceContextFraction))` bytes, with a default fraction of `0.2`. Four bytes per token is a sizing heuristic. Explicit `maxReferenceBytes` bypasses model lookup and remains exact. Missing route, service, adapter, or capacity retains the floor; other lookup failures and cancellation propagate. An absent adapter does not prevent stream middleware from serving the route.
 
 ## Alternatives considered
 

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-09-05-session-reference-model-budget.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 [Session-reference](../../../../packages/context/session-reference/README.zh.md) 通过本地 prepend 监听器观察已完成的 `system-prompt/assemble` 瀑布,并把 provider/model 对存入以 Agent 为键的 WeakMap。准备阶段通过可选 LLM 服务解析该路由;首次组装前直接准备则使用 agent options。不带 Agent 的诊断不会更新映射。
 
-每个来源获得 `max(65536, floor(contextWindow × 4 × referenceContextFraction))` 字节,默认比例为 `0.2`。每个 token 四字节是容量估算。显式 `maxReferenceBytes` 跳过模型查询并保持精确值。缺少路由、服务或容量时保留下限;查询失败和取消会传播。
+每个来源获得 `max(65536, floor(contextWindow × 4 × referenceContextFraction))` 字节,默认比例为 `0.2`。每个 token 四字节是容量估算。显式 `maxReferenceBytes` 跳过模型查询并保持精确值。缺少路由、服务、适配器或容量时保留下限;其他查询失败和取消会传播。缺少适配器不妨碍流中间件处理该路由。
 
 ## Alternatives considered
 

+ 2 - 2
docs/module-graph.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/module-graph.md
-module-graph.md: acf01f4cf7c8bb4c7356332832c92623265904f0
-module-graph.zh.md: 78dfc42cfffb1c1515197a9132fcca70d56b15e6
+module-graph.md: a3d288d2d1ca5fa9e7329e4e4fa4b0da935f4aca
+module-graph.zh.md: b04cbbf206bb71fcabe1ac73d6fadf4787bffc03

+ 2 - 1
docs/module-graph.md

@@ -999,6 +999,7 @@ flowchart TD
   pkg_session_reference --> pkg_session_projection_cache
   pkg_session_reference --> pkg_session_query
   pkg_session_reference --> pkg_session_title
+  pkg_session_reference --> pkg_system_prompt
   pkg_session_reference --> pkg_typert_protocol
   pkg_webhook_github --> pkg_credentials
   pkg_webhook_github --> pkg_host_webserver
@@ -1388,7 +1389,7 @@ flowchart TD
 | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
-| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`typert-protocol`](../packages/typert/protocol) |
+| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
 | [`webhook-github`](../packages/webhook/webhook-github) | `webhook` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`session`](../packages/core/session), [`webhook`](../packages/webhook/webhook) |
 | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |

+ 2 - 1
docs/module-graph.zh.md

@@ -1001,6 +1001,7 @@ flowchart TD
   pkg_session_reference --> pkg_session_projection_cache
   pkg_session_reference --> pkg_session_query
   pkg_session_reference --> pkg_session_title
+  pkg_session_reference --> pkg_system_prompt
   pkg_session_reference --> pkg_typert_protocol
   pkg_webhook_github --> pkg_credentials
   pkg_webhook_github --> pkg_host_webserver
@@ -1390,7 +1391,7 @@ flowchart TD
 | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
-| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`typert-protocol`](../packages/typert/protocol) |
+| [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) |
 | [`webhook-github`](../packages/webhook/webhook-github) | `webhook` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`session`](../packages/core/session), [`webhook`](../packages/webhook/webhook) |
 | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | `subagent` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |

+ 2 - 2
docs/subsystems/session-reference.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/subsystems/session-reference.md
-session-reference.md: 17135637105e97087490791c47204ba3427767ed
-session-reference.zh.md: 6db8055455704f4cb88a3d8cb6b7b56ffdfbc17e
+session-reference.md: af7fcb0578e14a0a8e3084d61eb3a57275e834c8
+session-reference.zh.md: b6f1a435b35c5a1a3e4129e2abcec06ad6fd63fb

+ 1 - 1
docs/subsystems/session-reference.md

@@ -205,7 +205,7 @@ async listCandidates( agent: Agent, query: string = '', limit: number = this.con
 /**
  * Snapshot all references for one accepted direct message and return one aggregated durable context.
  * Automatic budgets use the last assembled route, or agent options before any assembly.
- * Missing model capacity uses 64 KiB; metadata lookup failures and cancellation reject preparation.
+ * Missing model capacity or adapter uses 64 KiB; other metadata lookup failures and cancellation reject preparation.
  * @param agent - target agent; references to it are rejected.
  * @param content - already host-normalized readable message content.
  * @param references - structured source sessions in mention order.

+ 1 - 1
docs/subsystems/session-reference.zh.md

@@ -205,7 +205,7 @@ async listCandidates( agent: Agent, query: string = '', limit: number = this.con
 /**
  * Snapshot all references for one accepted direct message and return one aggregated durable context.
  * Automatic budgets use the last assembled route, or agent options before any assembly.
- * Missing model capacity uses 64 KiB; metadata lookup failures and cancellation reject preparation.
+ * Missing model capacity or adapter uses 64 KiB; other metadata lookup failures and cancellation reject preparation.
  * @param agent - target agent; references to it are rejected.
  * @param content - already host-normalized readable message content.
  * @param references - structured source sessions in mention order.

+ 2 - 2
packages/context/session-reference/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/context/session-reference/README.md
-README.md: bd6b9f779f3fa4542ab958496aca82462d803c28
-README.zh.md: 2c75d8f2ee0523432e0ed5aa7732d968e27268f7
+README.md: 5d00582b83ca8f71b06d6b3c39bd1cbe6e279c7e
+README.zh.md: 9d0dc230eb7ec91415ee02d877825962c020f9e3

+ 1 - 1
packages/context/session-reference/README.md

@@ -48,7 +48,7 @@ A message that cites other sessions is followed immediately by a `## Referenced
 | `maxReferenceBytes` | automatic | Explicit maximum serialized JSON bytes per source; overrides the automatic budget exactly |
 | `referenceContextFraction` | `0.2` | Context-window fraction per source, from `0` to `1` |
 
-The automatic budget is `max(65536, floor(contextWindow × 4 × referenceContextFraction))` bytes per source. Model context capacity is measured in tokens; four bytes per token is a sizing heuristic, not an exact token conversion. A missing route, LLM service, or capacity uses 64 KiB; model metadata lookup errors and cancellation fail preparation.
+The automatic budget is `max(65536, floor(contextWindow × 4 × referenceContextFraction))` bytes per source. Model context capacity is measured in tokens; four bytes per token is a sizing heuristic, not an exact token conversion. A missing route, LLM service, adapter, or capacity uses 64 KiB; other model metadata lookup errors and cancellation fail preparation.
 
 The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-session-reference) is the exhaustive source for every accepted field and its JSDoc.
 

+ 1 - 1
packages/context/session-reference/README.zh.md

@@ -48,7 +48,7 @@ kind: "package-reference"
 | `maxReferenceBytes` | 自动 | 每个来源的最大序列化 JSON 字节数;显式设置时精确覆盖自动预算 |
 | `referenceContextFraction` | `0.2` | 每个来源的上下文窗口比例,范围为 `0` 到 `1` |
 
-自动预算为每个来源 `max(65536, floor(contextWindow × 4 × referenceContextFraction))` 字节。模型上下文容量以 token 计量;每个 token 四字节是容量估算,不是精确的 token 换算。缺少路由、LLM 服务或容量时使用 64 KiB;模型元数据查询错误与取消会使准备失败。
+自动预算为每个来源 `max(65536, floor(contextWindow × 4 × referenceContextFraction))` 字节。模型上下文容量以 token 计量;每个 token 四字节是容量估算,不是精确的 token 换算。缺少路由、LLM 服务、适配器或容量时使用 64 KiB;其他模型元数据查询错误与取消会使准备失败。
 
 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-reference)是每个受支持字段及其 JSDoc 的穷尽式真源。
 

+ 1 - 0
packages/context/session-reference/package.json

@@ -60,6 +60,7 @@
     "@deepseek-ai/dsh-session-projection-cache": "workspace:^",
     "@deepseek-ai/dsh-session-query": "workspace:^",
     "@deepseek-ai/dsh-session-title": "workspace:^",
+    "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-typert-protocol": "workspace:^"
   },
   "peerDependenciesMeta": {

+ 13 - 4
packages/context/session-reference/src/index.ts

@@ -9,8 +9,8 @@ import { Context } from '@deepseek-ai/cordis'
 import z from '@deepseek-ai/schemastery'
 import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
 import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
-import { createUserMessage, freezeMessage } from '@deepseek-ai/dsh-llm'
-import type { ContentBlock, UserMessage } from '@deepseek-ai/dsh-llm'
+import { createUserMessage, freezeMessage, LlmError } from '@deepseek-ai/dsh-llm'
+import type { ContentBlock, LlmResolvedModelInfo, UserMessage } from '@deepseek-ai/dsh-llm'
 import { SessionLogOffset } from '@deepseek-ai/dsh-session'
 import type { SessionId } from '@deepseek-ai/dsh-session'
 // Type-only: the `title` projection key plus the live registry and durable
@@ -18,6 +18,7 @@ import type { SessionId } from '@deepseek-ai/dsh-session'
 import type { ProjectionSnapshot } from '@deepseek-ai/dsh-session-projection'
 import type {} from '@deepseek-ai/dsh-session-projection-cache'
 import type {} from '@deepseek-ai/dsh-session-title'
+import type {} from '@deepseek-ai/dsh-system-prompt'
 import type { SessionRecord, SessionSurfaceSnapshot } from '@deepseek-ai/dsh-session-query'
 import {
   DEFAULT_CANDIDATE_LIMIT,
@@ -122,6 +123,7 @@ export class SessionReferenceResolver extends TypertRemoteService {
         'SESSION_REFERENCE_INVALID_CONFIG',
       )
     }
+    // Prepend observes model-selection overrides after downstream assembly completes.
     ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
       const assembly = await next()
       if (context.agent !== undefined) {
@@ -284,7 +286,7 @@ export class SessionReferenceResolver extends TypertRemoteService {
   /**
    * Snapshot all references for one accepted direct message and return one aggregated durable context.
    * Automatic budgets use the last assembled route, or agent options before any assembly.
-   * Missing model capacity uses 64 KiB; metadata lookup failures and cancellation reject preparation.
+   * Missing model capacity or adapter uses 64 KiB; other metadata lookup failures and cancellation reject preparation.
    * @param agent - target agent; references to it are rejected.
    * @param content - already host-normalized readable message content.
    * @param references - structured source sessions in mention order.
@@ -350,7 +352,14 @@ export class SessionReferenceResolver extends TypertRemoteService {
     const { provider, model } = this.assembledRoutes.get(agent) ?? agent.options
     const llm = this.ctx.get('llm')
     if (provider === undefined || model === undefined || llm === undefined) return DEFAULT_MAX_REFERENCE_BYTES
-    const info = await settleWithCancellation(llm.resolveModelInfo(provider, model, signal), signal)
+    let info: LlmResolvedModelInfo
+    try {
+      info = await settleWithCancellation(llm.resolveModelInfo(provider, model, signal), signal)
+    } catch (error: unknown) {
+      // Stream middleware can serve routes without a registered adapter.
+      if (!(error instanceof LlmError) || error.code !== 'NO_ADAPTER') throw error
+      return DEFAULT_MAX_REFERENCE_BYTES
+    }
     if (info.context === undefined) return DEFAULT_MAX_REFERENCE_BYTES
     // Context capacity is in tokens; four bytes/token is a sizing heuristic, not token counting.
     return Math.max(DEFAULT_MAX_REFERENCE_BYTES, Math.floor(info.context.contextWindow * 4 * this.config.referenceContextFraction))

+ 23 - 1
packages/context/session-reference/tests/session-reference.spec.ts

@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import { agentEvents, installModelSelection, type Agent, type ModelSelectionRef } from '@deepseek-ai/dsh-agent'
 import { CompactionId, compactCheckpointSource } from '@deepseek-ai/dsh-compaction'
-import LlmRuntime, { createUserMessage, ToolCallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
+import LlmRuntime, { createUserMessage, ToolCallId , createMessage, createToolResultMessage, LlmError } from '@deepseek-ai/dsh-llm'
 import SessionStore, { Session, SessionId, SessionSeq } from '@deepseek-ai/dsh-session'
 import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import SessionQueryEngine from '@deepseek-ai/dsh-session-query'
@@ -366,6 +366,28 @@ describe('model-relative reference budgets', () => {
     expect(bytes(await ctx.sessionReferenceResolver.prepare(other, [], [{ sessionId: SessionId('source') }]))).toBe(65_536)
   })
 
+  it('uses the floor when the real LLM runtime has no adapter for the route', async () => {
+    const { ctx, resolve, prepare } = await setup()
+    resolve.mockRestore()
+    await expect(ctx.llm.resolveModelInfo('seed', 'seed')).rejects.toMatchObject({ code: 'NO_ADAPTER' })
+    expect(bytes(await prepare())).toBe(65_536)
+  })
+
+  it('does not swallow other LLM errors or cancellation coincident with an absent adapter', async () => {
+    const { ctx, resolve, prepare } = await setup()
+    const read = vi.spyOn(ctx.sessionQuery, 'readSurface')
+    const failure = new LlmError('invalid model context', 'INVALID_MODEL_CONTEXT')
+    resolve.mockRejectedValueOnce(failure)
+    await expect(prepare()).rejects.toBe(failure)
+    const controller = new AbortController()
+    resolve.mockImplementationOnce(async () => {
+      controller.abort('cancel missing route')
+      throw new LlmError('no adapter', 'NO_ADAPTER')
+    })
+    await expect(prepare(controller.signal)).rejects.toThrow(expectCode('SESSION_REFERENCE_CANCELLED'))
+    expect(read).not.toHaveBeenCalled()
+  })
+
   it('propagates lookup errors and cancels an unresolved lookup without reading sources', async () => {
     const { ctx, resolve, prepare } = await setup()
     const read = vi.spyOn(ctx.sessionQuery, 'readSurface')

+ 3 - 0
packages/context/session-reference/tsconfig.json

@@ -29,6 +29,9 @@
     {
       "path": "../../core/agent"
     },
+    {
+      "path": "../../core/system-prompt"
+    },
     {
       "path": "../../compaction/compaction"
     },

+ 1 - 1
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -1827,7 +1827,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
       },
       {
         signature: 'async prepare( agent: Agent, content: ContentBlock[], references: SessionReferenceInput[], signal?: AbortSignal, ): Promise<PreparedReferencedMessage>',
-        description: 'Snapshot all references for one accepted direct message and return one aggregated durable context. Automatic budgets use the last assembled route, or agent options before any assembly. Missing model capacity uses 64 KiB; metadata lookup failures and cancellation reject preparation.',
+        description: 'Snapshot all references for one accepted direct message and return one aggregated durable context. Automatic budgets use the last assembled route, or agent options before any assembly. Missing model capacity or adapter uses 64 KiB; other metadata lookup failures and cancellation reject preparation.',
         parameters: [{ name: 'agent', description: 'target agent; references to it are rejected.' }, { name: 'content', description: 'already host-normalized readable message content.' }, { name: 'references', description: 'structured source sessions in mention order.' }, { name: 'signal', description: 'optional cancellation boundary for the active turn.' }],
         returns: 'detached content and optional referenced-session context.',
       },