mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge pull request #1006 from deepseek-harness/worktree/deepseek-max-tokens-defaults
feat(llm-deepseek): configure max tokens and 1M context defaults
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-adapter-owned-max-token-defaults.md
|
||||
2026-07-30-adapter-owned-max-token-defaults.md: a522848fd4482f84859e587505b6a5e6f5c72d60
|
||||
2026-07-30-adapter-owned-max-token-defaults.zh.md: 8db6a06199fc1c4e73c86492d12dc86edafe8c7e
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Adapter-owned max-token defaults
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-30-adapter-owned-max-token-defaults.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
An LLM adapter could serialize an explicit `GenerateOptions.maxTokens`, but its Cordis configuration could not establish a reconstructable conversation default. Applying a fallback only inside provider serialization would make the wire request differ from the durable `request/header`; putting every provider's default in Agent Loop would instead transfer deployment and model policy into the provider-neutral driver.
|
||||
|
||||
## Decision
|
||||
|
||||
`LlmResolvedModelInfo.defaultMaxTokens` carries an optional adapter-configured per-request output cap for one exact provider/model route. `LlmService` validates it as a positive safe integer and materializes it into `LlmCallConfig.maxTokens` only when the caller omitted a value. A prepared call identifies materialized `maxTokens` and `reasoningEffort` fields as adapter defaults; explicit request or Agent options remain unmarked and therefore win without clamping.
|
||||
|
||||
The agent loop continues to prepare calls before logging `request/header`, so the effective config and its adapter-default provenance become durable request facts before dispatch. Before the next `agent/request` waterfall, the loop removes marked fields from the proposal; exact-model resolution then materializes the current route's defaults again. A provider/model switch therefore cannot mistake a previous adapter's default for an explicit override, while explicit conversation values persist. Direct `LlmService.stream()` calls resolve the same default at the final adapter boundary. The field is a request default rather than a hard model output limit; adapters that preserve provider-owned defaults omit it.
|
||||
|
||||
The native DeepSeek adapter exposes `maxTokens` in Cordis config with a 256,000-token default and maps the effective value to `max_tokens`. Its default context capacity is 1,000,000 tokens: both built-in V4 entries publish that exact capacity, while configured entries without capacity and unlisted pass-through ids inherit the same adapter-wide fallback.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Apply the default only in DeepSeek serialization.** Rejected because the provider wire would contain a model-visible value absent from the durable request header.
|
||||
|
||||
**Set `AgentOptions.maxTokens` in every shipped application.** Rejected because applications would duplicate adapter deployment policy, direct LLM calls would behave differently, and selecting another provider would retain a DeepSeek-specific cap.
|
||||
|
||||
**Represent 256,000 as a hard per-model maximum.** Rejected because the configured value is the desired request budget, not evidence that every configured endpoint rejects larger outputs. Explicit callers remain authoritative.
|
||||
|
||||
**Leave the provider default in control.** Rejected for the native DeepSeek deployment because the product requires a stable 256,000-token conversation budget across compatible endpoints.
|
||||
|
||||
## Consequences
|
||||
|
||||
DeepSeek conversations send `max_tokens: 256000` by default, and the same value plus its adapter provenance appear in the session request header. Deployments can change the adapter default through `llm-deepseek.config.maxTokens`; per-agent and per-request values override it. Changing the route rematerializes the new exact adapter's default instead of carrying DeepSeek's derived value forward. Other adapters retain their existing behavior until they intentionally publish `defaultMaxTokens`.
|
||||
|
||||
The 256,000-token output budget reserves a large part of the one-million-token context on endpoints that pre-allocate requested output. Deployments whose gateway or model supports a smaller budget must lower `maxTokens`; the explicit configuration is preferable to an undocumented provider fallback.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: 适配器持有的最大 token 默认值
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-30-adapter-owned-max-token-defaults.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
LLM(大语言模型)适配器可以序列化显式的 `GenerateOptions.maxTokens`,但无法通过 Cordis 配置建立可重建的对话默认值。仅在提供方序列化中应用回退,会导致协议请求与持久 `request/header` 不一致;若将各提供方默认值都放进 agent loop(智能体循环),则会把部署与模型策略转移到提供方无关的驱动器中。
|
||||
|
||||
## Decision
|
||||
|
||||
`LlmResolvedModelInfo.defaultMaxTokens` 携带一条确切提供方/模型路由的可选单次请求输出上限,该值由适配器配置。`LlmService` 将其校验为正安全整数,并且仅在调用方省略值时才填入 `LlmCallConfig.maxTokens`。准备后的调用会将已填入的 `maxTokens` 和 `reasoningEffort` 字段标记为适配器默认值;显式请求值或 Agent 选项不带该标记,因此优先且不会被自动调整。
|
||||
|
||||
agent loop 仍在记录 `request/header` 前准备调用,因此生效配置及其适配器默认值来源会在分派前成为持久请求事实。下一次 `agent/request` waterfall(瀑布式事件)前,agent loop 会从提议中移除带标记字段,随后精确模型解析会再次填入当前路由的默认值。因此,切换提供方/模型不会把前一个适配器的默认值误当成显式覆盖,而显式对话值则会保留。直接调用 `LlmService.stream()` 时,也会在最终适配器边界解析同一默认值。该字段是请求默认值,而非模型输出硬上限;保留提供方持有默认值的适配器会省略它。
|
||||
|
||||
原生 DeepSeek 适配器在 Cordis 配置中公开 `maxTokens`,默认值为 256,000 token,并将生效值映射为 `max_tokens`。其默认上下文容量为 1,000,000 token:两个内置 V4 配置项均公布这一精确容量;不含容量的已配置项和未列出的原样传递 id 则继承同一个适配器级回退值。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**仅在 DeepSeek 序列化中应用默认值。** 不予采纳,因为提供方协议会包含持久请求 header 中缺失的模型可见值。
|
||||
|
||||
**在每个已发布应用中设置 `AgentOptions.maxTokens`。** 不予采纳,因为应用会重复适配器部署策略,直接 LLM 调用的行为将不同,而且选择另一个提供方后仍会保留 DeepSeek 专用上限。
|
||||
|
||||
**将 256,000 表示为每模型硬上限。** 不予采纳,因为配置值是所需请求预算,无法证明每个已配置端点都会拒绝更大的输出。显式调用方仍具有最终决定权。
|
||||
|
||||
**由提供方默认值控制。** 对原生 DeepSeek 部署不予采纳,因为产品要求各兼容端点都采用稳定的 256,000 token 对话预算。
|
||||
|
||||
## Consequences
|
||||
|
||||
DeepSeek 对话默认发送 `max_tokens: 256000`,会话请求 header 中也会出现相同的值及其适配器来源。部署可以通过 `llm-deepseek.config.maxTokens` 更改适配器默认值;每个 agent 和每次请求的值都会覆盖它。更改路由会重新填入新的精确适配器默认值,而不是继续沿用 DeepSeek 派生出的值。其他适配器会保留现有行为,直至主动公布 `defaultMaxTokens`。
|
||||
|
||||
对于预分配请求输出的端点,256,000 token 的输出预算会占用 1,000,000 token 上下文中的很大部分。如果部署使用的 gateway 或模型仅支持较小预算,则必须调低 `maxTokens`;显式配置优于未记录的提供方回退值。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-28-sdk-max-output-tokens.md
|
||||
2026-07-28-sdk-max-output-tokens.md: 5db48f21892d56addea7b72f73319f9dbfd1e71f
|
||||
2026-07-28-sdk-max-output-tokens.zh.md: 38b172718716d100726188163ff22be9ea0a7325
|
||||
2026-07-28-sdk-max-output-tokens.md: 3ba3e226d64b7d3d192d67bd88af463d2b0d9dc5
|
||||
2026-07-28-sdk-max-output-tokens.zh.md: aec566011d2d7a311b4de509c47ebba383c3b0d7
|
||||
|
||||
@@ -12,7 +12,7 @@ The Python and TypeScript SDKs could select a provider and model but could not b
|
||||
|
||||
The high-level SDKs expose one optional process-wide output cap: Python names it `max_tokens`, TypeScript names it `maxTokens`, and the shared `initialize` wire payload carries `maxTokens`. The JSON-RPC server rejects values that are not positive safe integers and stores the accepted cap with its provider/model route.
|
||||
|
||||
Each SDK-created root Agent receives the cap through `AgentOptions.maxTokens`. Agent Loop places that value in the initial `LlmCallConfig`, logs it in the request header, and reconstructs every dispatched conversation request from that durable header. Omitting the option leaves `maxTokens` absent so the selected provider retains its default.
|
||||
Each SDK-created root Agent receives the cap through `AgentOptions.maxTokens`. Agent Loop places that value in the initial `LlmCallConfig`; final call preparation preserves the explicit value or materializes an exact-model adapter default, logs the effective cap in the request header, and reconstructs every dispatched conversation request from that durable header. Omitting the SDK option therefore allows the selected adapter or provider route default to apply.
|
||||
|
||||
In-process subagents inherit the parent's provider, model, and output cap. An explicit `SubagentStartRequest.agentOptions.maxTokens`, including one configured by `dsh-tool-subagent`, overrides the inherited value for that child and its descendants. Out-of-process providers own the configuration of their separate runtime; `subagent-dsh-sdk` therefore exposes its own optional `maxTokens` and forwards it through that child runtime's SDK handshake.
|
||||
|
||||
@@ -20,7 +20,7 @@ Compaction, session-title generation, web search, and other auxiliary calls keep
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Set an adapter environment variable.** This would be DeepSeek-adapter-specific, invisible in the session request header, ineffective for intercepted or alternate adapters, and easy to confuse with a provider default. The cap belongs in provider-neutral request configuration.
|
||||
**Set only an adapter environment variable.** A serializer-private fallback would be DeepSeek-adapter-specific, invisible in the session request header, ineffective for intercepted or alternate adapters, and easy to confuse with a provider default. Adapter-owned defaults may instead be exposed as exact-model metadata and materialized into provider-neutral request configuration before logging.
|
||||
|
||||
**Add `maxTokens` to every `session/prompt`.** Per-turn mutation would enlarge the wire and introduce request-config transitions that callers do not need for the current evaluation use case. A runtime initialization option gives every session in one SDK process the same reproducible budget.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Python 与 TypeScript SDK 可以选择提供方和模型,却无法限制对话
|
||||
|
||||
高层 SDK 公开一个可选的进程级输出上限:Python 命名为 `max_tokens`,TypeScript 命名为 `maxTokens`,共享的 `initialize` 线载荷使用 `maxTokens`。JSON-RPC 服务端拒绝非正安全整数,并将通过校验的上限与提供方/模型路由一同保存。
|
||||
|
||||
每个由 SDK 创建的根 Agent 都通过 `AgentOptions.maxTokens` 获得该上限。Agent Loop 将它放入初始 `LlmCallConfig`、记录到请求 header,并从该持久化 header 重建每次分派的对话请求。省略该选项时,`maxTokens` 保持缺失,由所选提供方保留默认值。
|
||||
每个由 SDK 创建的根 Agent 都通过 `AgentOptions.maxTokens` 获得该上限。Agent Loop 将它放入初始 `LlmCallConfig`;最终调用准备会保留显式值,或填入确切模型的适配器默认值,再将生效上限记录到请求 header,并从该持久化 header 重建每次分派的对话请求。因此,省略 SDK 选项时会应用所选适配器或提供方路由的默认值。
|
||||
|
||||
进程内 subagent 继承父级的提供方、模型和输出上限。显式的 `SubagentStartRequest.agentOptions.maxTokens`(包括通过 `dsh-tool-subagent` 配置的值)会覆盖该子级及其后代的继承值。进程外提供方自行持有其独立运行时的配置;因此 `subagent-dsh-sdk` 公开独立的可选 `maxTokens`,并通过该子运行时自己的 SDK 握手传入。
|
||||
|
||||
@@ -20,7 +20,7 @@ Python 与 TypeScript SDK 可以选择提供方和模型,却无法限制对话
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**设置适配器环境变量。** 这种方式仅适用于 DeepSeek 适配器,不会出现在会话请求 header 中,对被拦截请求或其他适配器无效,也容易与提供方默认值混淆。该上限属于提供方无关的请求配置。
|
||||
**仅设置适配器环境变量。** 序列化器私有回退仅适用于 DeepSeek 适配器,不会出现在会话请求 header 中,对被拦截请求或其他适配器无效,也容易与提供方默认值混淆。适配器持有的默认值可以改为通过确切模型元数据公开,并在记录前填入提供方无关的请求配置。
|
||||
|
||||
**在每个 `session/prompt` 上增加 `maxTokens`。** 按轮次修改会扩大线协议,并引入当前评测用例不需要的请求配置转换。运行时初始化选项可让一个 SDK 进程中的每个会话拥有相同、可重现的预算。
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
Verify your work by running the code or tests. Keep answers brief and
|
||||
factual.
|
||||
|
||||
# Shipped default: full thinking at max effort on every request (wire-only
|
||||
# defaults; they never enter the request header).
|
||||
# Shipped default: full thinking at max effort on every request. Exact-model
|
||||
# resolution materializes request defaults before the request header is logged.
|
||||
- id: llm-deepseek
|
||||
config:
|
||||
apiKey: !!js process.env.DEEPSEEK_API_KEY
|
||||
|
||||
@@ -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/architecture.md
|
||||
architecture.md: bfea67b9f83958e16b58e63b99e326349f6eff15
|
||||
architecture.zh.md: c2fd6cdd84ad2f6435faebffa0c4c1a6da0ade96
|
||||
architecture.md: c6e14fac6436b2401509aaf8bb20ccaf29aeeafc
|
||||
architecture.zh.md: 2e85f25eb3f40f58c8ffbfa7691bb793638c8b37
|
||||
|
||||
@@ -96,7 +96,7 @@ forever:
|
||||
assemble system prompt and tool schemas
|
||||
snapshot the derived messages (the reconstruction boundary)
|
||||
'step/start'
|
||||
agent/request (config only) -> prepare reasoning/default under turn signal -> log request/header -> llm/stream (frozen, registration-bound)
|
||||
agent/request (config only) -> prepare adapter defaults/provenance under turn signal -> log request/header -> llm/stream (frozen, registration-bound)
|
||||
'assistant/chunk'
|
||||
'assistant/message'
|
||||
schedule tool calls by ctx.tools.executionMode:
|
||||
@@ -145,7 +145,7 @@ Each agent owns scoped `agent.ctx`; shared storage overlays its tool, prompt, an
|
||||
|
||||
The session log is authoritative. `deriveMessages()` projects model history; raw `assistant/chunk` events preserve replay and UI fidelity. Fork, resume, transcript rendering, telemetry, and persistence derive from this stream.
|
||||
|
||||
**Model-visible ⟺ logged**: messages at `step/start` plus the folded `request/header` reconstruct every request; package-owned `dsh-agent-loop/invariant` can assert this through `ctx.invariants` ([reconstructability](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)).
|
||||
**Model-visible ⟺ logged**: messages at `step/start` plus the folded `request/header` reconstruct every request; the header also marks adapter-materialized defaults so the next proposal can discard them and resolve the selected route without losing explicit conversation settings. Package-owned `dsh-agent-loop/invariant` can assert reconstructability through `ctx.invariants` ([reconstructability](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)).
|
||||
|
||||
Durability is a plugin concern. Backends eagerly drain synchronous `session/event` notifications. `session/flush` barriers precede each request and top-level tool dispatch, then follow `turn/end` before another queued turn or idle observation. `SessionPersistence` stores `SessionEvent` directly and metadata in `SessionHeader`; JSONL defaults to checksummed Zstandard, while SQLite shares the contract ([decision](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md)).
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ forever:
|
||||
assemble system prompt and tool schemas
|
||||
snapshot the derived messages (the reconstruction boundary)
|
||||
'step/start'
|
||||
agent/request (config only) -> prepare reasoning/default under turn signal -> log request/header -> llm/stream (frozen, registration-bound)
|
||||
agent/request (config only) -> prepare adapter defaults/provenance under turn signal -> log request/header -> llm/stream (frozen, registration-bound)
|
||||
'assistant/chunk'
|
||||
'assistant/message'
|
||||
schedule tool calls by ctx.tools.executionMode:
|
||||
@@ -145,7 +145,7 @@ idle inject:
|
||||
|
||||
会话日志是权威依据。`deriveMessages()` 投影出模型历史;原始 `assistant/chunk` 事件保证回放和 UI 保真。fork、恢复、transcript(文本记录)渲染、遥测和持久化均派生自该事件流。
|
||||
|
||||
**模型可见 ⟺ 已记录**:`step/start` 时的消息与折叠后的 `request/header` 可以重建每个请求;该包的 `dsh-agent-loop/invariant` 可通过 `ctx.invariants` 断言这一点([可重建性](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md))。
|
||||
**模型可见 ⟺ 已记录**:`step/start` 时的消息与折叠后的 `request/header` 可以重建每个请求;该 header 还会标记适配器填入的默认值,使下一次提议可以丢弃这些值并解析所选路由,同时不丢失显式对话设置。该包的 `dsh-agent-loop/invariant` 可通过 `ctx.invariants` 断言可重建性([可重建性](../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md))。
|
||||
|
||||
持久性由插件负责。后端会尽快排空同步的 `session/event` 通知。`session/flush` 屏障位于每次请求与顶层工具分发之前,并在 `turn/end` 之后、处理另一个已排队轮次或观察到空闲状态之前执行。`SessionPersistence` 直接存储 `SessionEvent`,并将元数据存入 `SessionHeader`;JSONL 默认采用带校验和的 Zstandard,SQLite 遵循同一契约([决策](../.agents/notes/implemented/bug-fix/2026-07-21-semantic-session-checkpoints.md))。
|
||||
|
||||
|
||||
@@ -638,7 +638,9 @@ export interface Config {
|
||||
thinking?: 'enabled' | 'disabled'
|
||||
/** Default thinking effort (default `high`); `off` disables thinking per request. */
|
||||
reasoningEffort?: 'off' | 'high' | 'max'
|
||||
/** Positive context capacity used when the selected model has no exact value. */
|
||||
/** Default per-request output cap (default 256,000); explicit request values win. */
|
||||
maxTokens?: number
|
||||
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
|
||||
defaultContextWindow?: number
|
||||
/** Advisory models shown by discovery consumers; defaults to V4 Flash and V4 Pro. */
|
||||
models?: DeepSeekCatalogModel[]
|
||||
@@ -663,7 +665,7 @@ export interface DeepSeekCatalogModel {
|
||||
|
||||
Depends on: [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
|
||||
|
||||
Source: [`packages/llm/llm-deepseek/src/index.ts:50`](../packages/llm/llm-deepseek/src/index.ts)
|
||||
Source: [`packages/llm/llm-deepseek/src/index.ts:60`](../packages/llm/llm-deepseek/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-llm-pi-ai`
|
||||
|
||||
|
||||
@@ -844,7 +844,7 @@ async resolveModelInfo( provider: string, model: string, signal?: AbortSignal, )
|
||||
|
||||
/**
|
||||
* Validate a conversation call config against its exact model capability and
|
||||
* materialize an adapter-configured default. Unsupported explicit efforts
|
||||
* materialize adapter-configured defaults. Unsupported explicit efforts
|
||||
* reject before provider I/O; no clamping or aliasing is performed. This
|
||||
* standalone query does not bind a later dispatch; use {@link prepareCall}
|
||||
* when logging and streaming must share one adapter registration.
|
||||
@@ -882,7 +882,7 @@ stream(options: GenerateOptions): AsyncIterable<StreamChunk>
|
||||
|
||||
Types: [AdapterRegistrationHandle](../core-data-structures/core.md) · [GenerateOptions](../core-data-structures/core.md) · [LlmAdapter](../core-data-structures/llm-streaming.md) · [LlmCallConfig](../core-data-structures/core.md) · [LlmConfigurableProvider](../core-data-structures/core.md) · [LlmModelInfo](../core-data-structures/core.md) · [LlmProviderInfo](../core-data-structures/core.md) · [LlmResolvedModelInfo](../core-data-structures/core.md) · [PreparedLlmCall](../core-data-structures/llm-streaming.md) · [ResolvedRetryPolicy](../core-data-structures/llm-streaming.md) · [StreamChunk](../core-data-structures/llm-streaming.md)
|
||||
|
||||
Source: [`packages/llm/llm/src/index.ts:227`](../../packages/llm/llm/src/index.ts)
|
||||
Source: [`packages/llm/llm/src/index.ts:229`](../../packages/llm/llm/src/index.ts)
|
||||
|
||||
## `ctx.permission` — `PermissionService`
|
||||
|
||||
@@ -1652,7 +1652,7 @@ fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId):
|
||||
|
||||
Types: [CreateSessionOptions](../core-data-structures/persistence.md) · [Session](../core-data-structures/session.md) · [SessionId](../core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/session/src/index.ts:714`](../../packages/core/session/src/index.ts)
|
||||
Source: [`packages/core/session/src/index.ts:739`](../../packages/core/session/src/index.ts)
|
||||
|
||||
## `ctx.sessionTitle` — `SessionTitleService`
|
||||
|
||||
|
||||
@@ -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/core-data-structures/core.md
|
||||
core.md: 7176a6949211566c5f162bc3289189d047c6fc85
|
||||
core.zh.md: de06529d602fc0871688d0b8e038c8d1c3ac2ef7
|
||||
core.md: 5ed6a47c5488005d41fdac9349e4c9d1c550d13d
|
||||
core.zh.md: 1b16b7ec994c6fccd6fedf1508dec6b1b057edf3
|
||||
|
||||
@@ -259,7 +259,7 @@ interface LlmModelInfo {
|
||||
}
|
||||
```
|
||||
|
||||
Correctness-sensitive metadata is resolved separately from the advisory catalog and is owned by the adapter serving the exact route. Context capacity and reasoning choices share one exact-model result so consumers do not repeat authoritative model resolution.
|
||||
Correctness-sensitive metadata is resolved separately from the advisory catalog and is owned by the adapter serving the exact route. Context capacity, adapter call defaults, and reasoning choices share one exact-model result so consumers do not repeat authoritative model resolution.
|
||||
|
||||
```ts type-equiv
|
||||
/** Provider-owned context capacity for one exact provider/model route. */
|
||||
@@ -306,6 +306,8 @@ interface LlmModelReasoningInfo {
|
||||
interface LlmResolvedModelInfo extends LlmModelInfo {
|
||||
/** Provider-owned context capacity when known. */
|
||||
context?: LlmModelContext
|
||||
/** Adapter-configured per-request output cap materialized when callers omit one. */
|
||||
defaultMaxTokens?: number
|
||||
/** Adapter-owned selectable reasoning levels when exposed. */
|
||||
reasoning?: LlmModelReasoningInfo
|
||||
}
|
||||
@@ -392,9 +394,9 @@ The model-facing `ToolSchema` is the wire shape; the registered `ToolDefinition`
|
||||
|
||||
### The request envelope: `LlmCallConfig` and the logged header
|
||||
|
||||
The loop builds each request from logged state. `EpochHeader` records call config, rendered prompt, and authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset) through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
|
||||
The loop builds each request from logged state. `EpochHeader` records call config, adapter-default provenance, rendered prompt, and authoritative returned tool order (configured by `toolOrder`, or lexicographic when unset) through full `request/header` snapshots. Together with derived history, this makes the request reconstructable from the session log. See [session.md](session.md#the-request-header-event-requestheader) and the [reconstructability Agent Note](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
|
||||
|
||||
`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, reasoning effort, or sampling. After the waterfall, the loop prepares the exact model capability under the turn signal, rejects unsupported explicit effort ids without clamping, materializes an adapter-configured default, and logs the effective value. The prepared call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests.
|
||||
`agent/request` receives a frozen call-config seed and may return a replacement to switch provider, model, reasoning effort, or sampling. Before the waterfall, the loop removes values marked as adapter defaults so exact-model preparation materializes the selected route's current values; unmarked explicit settings remain in the proposal. After the waterfall, preparation rejects unsupported explicit effort ids without clamping and logs the effective config plus provenance under the turn signal. The prepared call keeps one adapter registration through dispatch. Requests reaching `llm/stream` are deep-frozen, so mutation throws, and carry a process-local loop identity so observers do not confuse separately logged frozen auxiliary calls with conversation requests.
|
||||
|
||||
On the wire, a loop-built request reads the `system` slot (the rendered prompt assembly) followed by the derived history — the boundary snapshot, whose tail is the newest `user/message` on a turn's first step and the previous step's tool results on later steps. The dev invariant recomputes exactly this equation against every loop-built request.
|
||||
|
||||
@@ -417,6 +419,17 @@ interface LlmCallConfig {
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Effective config fields supplied by exact-model adapter resolution rather
|
||||
* than by the caller's request proposal.
|
||||
*/
|
||||
interface LlmCallConfigAdapterDefaults {
|
||||
reasoningEffort?: true
|
||||
maxTokens?: true
|
||||
}
|
||||
```
|
||||
|
||||
## Sessions
|
||||
|
||||
A `Session` is an **append-only log** of typed `SessionEvent`s — the single source of truth. The LLM message history is *derived* from the log (`deriveMessages()`), not stored separately. The event vocabulary derives from `SessionEventMap`:
|
||||
@@ -658,7 +671,7 @@ interface Agent {
|
||||
}
|
||||
```
|
||||
|
||||
`AgentStatus` is `'idle' | 'running'`, and `SessionId` is branded. Disposal removes the agent from the registry and emits `agent/disposed`; it is not a terminal status value. `running` describes the driver-wide drain interval and may span consecutive queued turns; it does not prove a turn is still open. `acceptsNextStep` is the narrower routing predicate for callers that must choose between steering the current admission/turn and submitting a fresh admitted prompt. `AgentOptions` is merge-extensible: core declares `provider?`, `model?`, and `maxTokens?` (dispatch requires provider and model after `agent/request`). When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission leaves the provider default in control. Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default.
|
||||
`AgentStatus` is `'idle' | 'running'`, and `SessionId` is branded. Disposal removes the agent from the registry and emits `agent/disposed`; it is not a terminal status value. `running` describes the driver-wide drain interval and may span consecutive queued turns; it does not prove a turn is still open. `acceptsNextStep` is the narrower routing predicate for callers that must choose between steering the current admission/turn and submitting a fresh admitted prompt. `AgentOptions` is merge-extensible: core declares `provider?`, `model?`, and `maxTokens?` (dispatch requires provider and model after `agent/request`). When present, `maxTokens` must be a positive safe integer and caps every conversation-model request; omission allows the exact-model adapter default to materialize before the request header, or otherwise leaves provider behavior unchanged. Persona belongs to `dsh-system-prompt`: an agent-scoped `deployment:persona` may shadow the global default.
|
||||
|
||||
The cause is a TypeScript-enforced same-process input. An active `TurnCancellation` holder copies its discriminant into the runtime-only `AbortSignal.reason` and is retired before `turn/end` publication; the frozen `AbortSignal.reason` remains readable after that retirement. Only the loop reads the cause (`user`, `parent`, or lifecycle-only `disposed`) back off its own machine-private signal at settlement — there is no public reader, and a signal grants cooperating listeners no classification authority. Durable `turn/end` retains the coarse `{ kind: 'aborted' }` outcome; request provenance would require a separate durable event rather than overloading the terminal result.
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ interface LlmModelInfo {
|
||||
}
|
||||
```
|
||||
|
||||
对正确性敏感的元数据与参考目录分开解析,并归服务该确切路由的适配器所有。上下文容量和推理选项共用同一个确切模型结果,消费方因而无需重复执行权威模型解析。
|
||||
对正确性敏感的元数据与参考目录分开解析,并归服务该确切路由的适配器所有。上下文容量、适配器调用默认值和推理选项共用同一个确切模型结果,消费方因而无需重复执行权威模型解析。
|
||||
|
||||
```ts type-equiv
|
||||
/** Provider-owned context capacity for one exact provider/model route. */
|
||||
@@ -312,6 +312,8 @@ interface LlmModelReasoningInfo {
|
||||
interface LlmResolvedModelInfo extends LlmModelInfo {
|
||||
/** Provider-owned context capacity when known. */
|
||||
context?: LlmModelContext
|
||||
/** Adapter-configured per-request output cap materialized when callers omit one. */
|
||||
defaultMaxTokens?: number
|
||||
/** Adapter-owned selectable reasoning levels when exposed. */
|
||||
reasoning?: LlmModelReasoningInfo
|
||||
}
|
||||
@@ -398,9 +400,9 @@ interface ToolSchema {
|
||||
|
||||
### 请求信封:`LlmCallConfig` 与记录的 header
|
||||
|
||||
循环从已记录状态构建每个请求。`EpochHeader` 通过完整的 `request/header` 快照记录调用配置、渲染后的提示词以及权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)。结合派生历史,请求便可由会话日志重建。见 [session.md](session.md#the-request-header-event-requestheader) 与[可重建性 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
|
||||
循环从已记录状态构建每个请求。`EpochHeader` 通过完整的 `request/header` 快照记录调用配置、适配器默认值来源、渲染后的提示词以及权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)。结合派生历史,请求便可由会话日志重建。见 [session.md](session.md#the-request-header-event-requestheader) 与[可重建性 Agent Note(agent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
|
||||
|
||||
`agent/request` 接收冻结的调用配置种子,并可返回替代值以切换提供方、模型、推理强度或采样参数。waterfall 结束后,循环会在轮次信号控制下完成确切模型的能力准备,拒绝显式指定但不受支持的推理强度 ID(不自动调整),填入适配器配置的默认值,并记录最终生效值。准备完成的调用直至分派完成始终持有同一项适配器注册。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。
|
||||
`agent/request` 接收冻结的调用配置种子,并可返回替代值以切换提供方、模型、推理强度或采样参数。waterfall 开始前,循环会移除标记为适配器默认值的值,使确切模型准备过程填入所选路由的当前值;未带标记的显式设置仍保留在提议中。waterfall 结束后,准备过程会在轮次信号控制下拒绝显式指定但不受支持的推理强度 ID(不自动调整),并记录生效配置及其来源。准备完成的调用直至分派完成始终持有同一项适配器注册。到达 `llm/stream` 的请求会被深度冻结,因此变更会抛异常;请求还携带进程本地循环标识,使观察者不会把单独记录的冻结辅助调用误认成对话请求。
|
||||
|
||||
在协议格式上,循环构建的请求先读取 `system` 槽位(渲染后的提示词组装),再读取派生历史——边界快照,其尾部在轮次首步是最新的 `user/message`,在后续步骤是上一步的工具结果。开发不变式针对每个循环构建的请求精确重算此等式。
|
||||
|
||||
@@ -423,6 +425,17 @@ interface LlmCallConfig {
|
||||
}
|
||||
```
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
* Effective config fields supplied by exact-model adapter resolution rather
|
||||
* than by the caller's request proposal.
|
||||
*/
|
||||
interface LlmCallConfigAdapterDefaults {
|
||||
reasoningEffort?: true
|
||||
maxTokens?: true
|
||||
}
|
||||
```
|
||||
|
||||
## 会话
|
||||
|
||||
`Session` 是一份类型化 `SessionEvent` 的**仅追加日志**——唯一的真源。LLM(大语言模型)消息历史从日志*派生*(`deriveMessages()`),而非单独存储。事件词汇从 `SessionEventMap` 派生:
|
||||
@@ -666,7 +679,7 @@ interface Agent {
|
||||
}
|
||||
```
|
||||
|
||||
`AgentStatus` 为 `'idle' | 'running'`,`SessionId` 是品牌类型。dispose(资源释放)会把 agent 从注册表移除并发出 `agent/disposed`;它不是一个终态 status 值。`running` 描述整个驱动器的排空区间,可能跨越连续的排队轮次;它不能证明某个轮次仍然打开。对于需要在把输入作为 steering 加入当前提示词准入/轮次,还是提交为一个新的待准入提示词之间做选择的调用方,`acceptsNextStep` 才是更窄且准确的路由判断条件。`AgentOptions` 可合并扩展:core 声明 `provider?`、`model?` 与 `maxTokens?`(在 `agent/request` 后,分发要求 provider 与 model 都存在)。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时由提供方默认值控制。Persona 归 `dsh-system-prompt` 所有:agent 作用域的 `deployment:persona` 可以遮蔽全局默认值。
|
||||
`AgentStatus` 为 `'idle' | 'running'`,`SessionId` 是品牌类型。dispose(资源释放)会把 agent 从注册表移除并发出 `agent/disposed`;它不是一个终态 status 值。`running` 描述整个驱动器的排空区间,可能跨越连续的排队轮次;它不能证明某个轮次仍然打开。对于需要在把输入作为 steering 加入当前提示词准入/轮次,还是提交为一个新的待准入提示词之间做选择的调用方,`acceptsNextStep` 才是更窄且准确的路由判断条件。`AgentOptions` 可合并扩展:core 声明 `provider?`、`model?` 与 `maxTokens?`(在 `agent/request` 后,分发要求 provider 与 model 都存在)。提供 `maxTokens` 时,它必须是正安全整数,并限制每次对话模型请求的输出;省略时,系统会在写入请求 header 前填入确切模型的适配器默认值,否则提供方行为保持不变。Persona 归 `dsh-system-prompt` 所有:agent 作用域的 `deployment:persona` 可以遮蔽全局默认值。
|
||||
|
||||
cause 是由 TypeScript 强制约束的同进程输入。活跃的 `TurnCancellation` 持有者会把其判别字段复制到仅运行时的 `AbortSignal.reason`,并在发布 `turn/end` 前退役;冻结后的 `AbortSignal.reason` 仍可读取。只有 loop 会在结算时从自己机器私有的 signal 上读回 cause(`user`、`parent` 或仅用于生命周期的 `disposed`)——不存在公开的读取器,signal 也不授予协作监听器任何分类权限。持久 `turn/end` 保留粗粒度 `{ kind: 'aborted' }` 结果;若需记录请求 provenance,应使用单独的持久事件,而不是让终态结果承担额外含义。
|
||||
|
||||
|
||||
@@ -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/core-data-structures/llm-streaming.md
|
||||
llm-streaming.md: 6811611768a0ec577360a8ff82792899cf3b8fec
|
||||
llm-streaming.zh.md: 35374af6a20086f15384840689dba5aa24351750
|
||||
llm-streaming.md: e7500a7985ea1916e206c41e05855701b48fcf00
|
||||
llm-streaming.zh.md: 2b61815f2730afdfb93bc06b8ee8925d2f4cac25
|
||||
|
||||
@@ -162,13 +162,15 @@ declare class BlockAssembler {
|
||||
|
||||
## The seam
|
||||
|
||||
`LlmAdapter` is the provider seam: subclass, implement `stream()`, and register one adapter instance with `ctx.llm.registerAdapter(providers, adapter)`. `GenerateOptions.provider` selects the registered adapter; `GenerateOptions.model` is passed to that adapter and need not be registered at lifecycle start. Duplicate provider routes fail atomically. Optional `providerRetryPolicy()` is captured per route with normal defaults, while `providerInfo()` and asynchronous `listModels()` feed `LlmService.listProviders()` / `listModels()` with detached selector metadata. That catalog is advisory rather than a request whitelist: the adapter remains authoritative and may accept unlisted model ids. One asynchronous `resolveModel()` query returns exact model identity plus optional correctness-sensitive context capacity and ordered model-owned reasoning ids with an optional deployment default; absent fields mean unavailable metadata or capability, not invalid catalog membership. The resolver receives optional cancellation and must settle promptly after abort. `LlmService.resolveModelInfo()` validates and detaches the aggregate. The service validates and materializes reasoning through `resolveCallConfig()` at the final adapter boundary, so direct calls cannot bypass unsupported-effort rejection; direct dispatch captures one registration before awaiting that resolution. The agent loop instead uses `prepareCall()` to keep the same registration across model resolution, durable header logging, and dispatch. Adapter lookup happens at the terminal continuation of the `llm/stream` waterfall, so a listener may short-circuit the call or route a mutable one-shot request before lookup. The `block-start` / `block-end` `index` correlation and the assembler together mean an adapter only has to emit well-formed chunks — block reassembly is not each adapter's problem. The consumer surface (`ctx.llm.stream()`) and the `llm/stream` waterfall are described in [architecture.md § Content blocks and streaming](../architecture.md#content-blocks-and-streaming-dsh-llm).
|
||||
`LlmAdapter` is the provider seam: subclass, implement `stream()`, and register one adapter instance with `ctx.llm.registerAdapter(providers, adapter)`. `GenerateOptions.provider` selects the registered adapter; `GenerateOptions.model` is passed to that adapter and need not be registered at lifecycle start. Duplicate provider routes fail atomically. Optional `providerRetryPolicy()` is captured per route with normal defaults, while `providerInfo()` and asynchronous `listModels()` feed `LlmService.listProviders()` / `listModels()` with detached selector metadata. That catalog is advisory rather than a request whitelist: the adapter remains authoritative and may accept unlisted model ids. One asynchronous `resolveModel()` query returns exact model identity plus optional correctness-sensitive context capacity, an adapter-configured `defaultMaxTokens`, and ordered model-owned reasoning ids with an optional deployment default; absent fields mean unavailable metadata or provider-owned behavior, not invalid catalog membership. The resolver receives optional cancellation and must settle promptly after abort. `LlmService.resolveModelInfo()` validates and detaches the aggregate. At the final adapter boundary, `resolveCallConfig()` materializes the output default only when `maxTokens` is absent and validates and materializes reasoning, so direct calls cannot bypass either configured behavior; direct dispatch captures one registration before awaiting that resolution. The agent loop instead uses `prepareCall()` to keep the same registration across model resolution, durable header logging, and dispatch. Adapter lookup happens at the terminal continuation of the `llm/stream` waterfall, so a listener may short-circuit the call or route a mutable one-shot request before lookup. The `block-start` / `block-end` `index` correlation and the assembler together mean an adapter only has to emit well-formed chunks — block reassembly is not each adapter's problem. The consumer surface (`ctx.llm.stream()`) and the `llm/stream` waterfall are described in [architecture.md § Content blocks and streaming](../architecture.md#content-blocks-and-streaming-dsh-llm).
|
||||
|
||||
```ts type-equiv
|
||||
/** One model call whose config and adapter registration were resolved together. */
|
||||
interface PreparedLlmCall {
|
||||
/** Detached, deep-frozen config with any adapter-owned default materialized. */
|
||||
readonly config: LlmCallConfig
|
||||
/** Config fields materialized by the captured adapter rather than proposed by the caller. */
|
||||
readonly adapterDefaults: LlmCallConfigAdapterDefaults
|
||||
/**
|
||||
* Dispatch this call once through the registration captured during
|
||||
* preparation. The request's call-config fields must match {@link config};
|
||||
@@ -215,7 +217,7 @@ declare abstract class LlmAdapter {
|
||||
* @param model - exact model id passed to {@link GenerateOptions.model}.
|
||||
* @param _signal - cancellation for this exact-model lookup; asynchronous
|
||||
* implementations must settle promptly after it aborts.
|
||||
* @returns provider/model identity plus any context and reasoning metadata.
|
||||
* @returns provider/model identity plus any context, call-default, and reasoning metadata.
|
||||
*/
|
||||
resolveModel(
|
||||
provider: string,
|
||||
|
||||
@@ -162,13 +162,15 @@ declare class BlockAssembler {
|
||||
|
||||
## seam
|
||||
|
||||
`LlmAdapter` 是提供方 seam:创建子类、实现 `stream()`,再用 `ctx.llm.registerAdapter(providers, adapter)` 注册一个适配器实例。`GenerateOptions.provider` 选择已注册适配器;`GenerateOptions.model` 会传给该适配器,无需在生命周期启动时注册。重复提供方路由会原子失败。可选的 `providerRetryPolicy()` 会按路由捕获并填入 normal 默认值,`providerInfo()` 与异步 `listModels()` 方法则为 `LlmService.listProviders()` / `listModels()` 提供分离的 selector 元数据。该目录仅供参考,不是请求白名单:适配器仍是权威,并可接受未列出的模型 id。单次异步 `resolveModel()` 查询返回确切模型身份,以及可选的对正确性敏感的上下文容量、由模型持有的有序推理强度 ID 和部署默认值;字段缺失表示元数据或能力不可用,而不表示目录成员关系无效。解析器会接收可选的取消信号,并且必须在信号中止后迅速完成结算。`LlmService.resolveModelInfo()` 会校验聚合结果并返回分离值。服务通过最终适配器边界的 `resolveCallConfig()` 校验推理强度并填入默认值,因此直接调用也无法绕过对不支持推理强度的拒绝;直接分派会在等待解析前捕获一项适配器注册。agent loop 则使用 `prepareCall()`,使模型解析、请求头持久记录和分派全程使用同一项注册。适配器查找发生在 `llm/stream` waterfall(瀑布式事件)的终端 continuation,因此 listener 可以在查找前短路调用,或路由一个可变的一次性请求。`block-start` / `block-end` 的 `index` 关联与 assembler 共同意味着适配器只需 emit 格式正确的分片——块重组不是每个适配器各自的问题。消费方 surface(`ctx.llm.stream()`)与 `llm/stream` waterfall 见 [architecture.md § 内容块与流式传输](../architecture.md#content-blocks-and-streaming-dsh-llm)。
|
||||
`LlmAdapter` 是提供方 seam:创建子类、实现 `stream()`,再用 `ctx.llm.registerAdapter(providers, adapter)` 注册一个适配器实例。`GenerateOptions.provider` 选择已注册适配器;`GenerateOptions.model` 会传给该适配器,无需在生命周期启动时注册。重复提供方路由会原子失败。可选的 `providerRetryPolicy()` 会按路由捕获并填入 normal 默认值,`providerInfo()` 与异步 `listModels()` 方法则为 `LlmService.listProviders()` / `listModels()` 提供分离的 selector 元数据。该目录仅供参考,不是请求白名单:适配器仍是权威,并可接受未列出的模型 id。单次异步 `resolveModel()` 查询返回确切模型身份,以及可选的对正确性敏感的上下文容量、适配器配置的 `defaultMaxTokens`、由模型持有的有序推理强度 ID 和部署默认值;字段缺失表示元数据不可用或保留提供方持有的行为,而不表示目录成员关系无效。解析器会接收可选的取消信号,并且必须在信号中止后迅速完成结算。`LlmService.resolveModelInfo()` 会校验聚合结果并返回分离值。在最终适配器边界,`resolveCallConfig()` 仅在 `maxTokens` 缺失时填入输出默认值,并校验和填入推理强度,因此直接调用也无法绕过任何一项已配置行为;直接分派会在等待解析前捕获一项适配器注册。agent loop 则使用 `prepareCall()`,使模型解析、请求头持久记录和分派全程使用同一项注册。适配器查找发生在 `llm/stream` waterfall(瀑布式事件)的终端 continuation,因此 listener 可以在查找前短路调用,或路由一个可变的一次性请求。`block-start` / `block-end` 的 `index` 关联与 assembler 共同意味着适配器只需 emit 格式正确的分片——块重组不是每个适配器各自的问题。消费方 surface(`ctx.llm.stream()`)与 `llm/stream` waterfall 见 [architecture.md § 内容块与流式传输](../architecture.md#content-blocks-and-streaming-dsh-llm)。
|
||||
|
||||
```ts type-equiv
|
||||
/** One model call whose config and adapter registration were resolved together. */
|
||||
interface PreparedLlmCall {
|
||||
/** Detached, deep-frozen config with any adapter-owned default materialized. */
|
||||
readonly config: LlmCallConfig
|
||||
/** Config fields materialized by the captured adapter rather than proposed by the caller. */
|
||||
readonly adapterDefaults: LlmCallConfigAdapterDefaults
|
||||
/**
|
||||
* Dispatch this call once through the registration captured during
|
||||
* preparation. The request's call-config fields must match {@link config};
|
||||
@@ -215,7 +217,7 @@ declare abstract class LlmAdapter {
|
||||
* @param model - exact model id passed to {@link GenerateOptions.model}.
|
||||
* @param _signal - cancellation for this exact-model lookup; asynchronous
|
||||
* implementations must settle promptly after it aborts.
|
||||
* @returns provider/model identity plus any context and reasoning metadata.
|
||||
* @returns provider/model identity plus any context, call-default, and reasoning metadata.
|
||||
*/
|
||||
resolveModel(
|
||||
provider: string,
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/session.md
|
||||
session.md: 5389c2e2114094df5afdca25afd904b2cbbf8270
|
||||
session.zh.md: 29c4853213dfc886155e3f8d0991fa161e05c71a
|
||||
session.md: e70add64198efd57538d1a014f24533d5197e531
|
||||
session.zh.md: d83cba6fbcb4ffcf137203d5e3e55045f1444a8b
|
||||
|
||||
@@ -144,7 +144,7 @@ interface TodoItem {
|
||||
|
||||
### The request header event: `request/header`
|
||||
|
||||
The request envelope — the `EpochHeader` (call config + rendered system prompt + assembled tool schemas) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a later changed request records another full snapshot with reason `'change'`. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message.
|
||||
The request envelope — the `EpochHeader` (call config + adapter-default provenance + rendered system prompt + assembled tool schemas) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a later changed request records another full snapshot with reason `'change'`. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message.
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -155,6 +155,8 @@ The request envelope — the `EpochHeader` (call config + rendered system prompt
|
||||
interface EpochHeader {
|
||||
/** The conversation's call configuration (provider, model, reasoning effort, and sampling scalars). */
|
||||
config: LlmCallConfig
|
||||
/** Effective config fields materialized from the exact adapter rather than proposed by a caller. */
|
||||
adapterDefaults?: LlmCallConfigAdapterDefaults
|
||||
/** Rendered system prompt text; absent for a system-less request. */
|
||||
system?: string
|
||||
/** Assembled tool schemas; absent for a tool-less request. */
|
||||
|
||||
@@ -146,7 +146,7 @@ interface TodoItem {
|
||||
|
||||
### 请求头事件:`request/header`
|
||||
|
||||
请求信封(即 `EpochHeader`:调用配置 + 渲染后的系统提示词 + 已组装的工具 schema)会作为会话状态写入日志,因此每个对话请求都是日志的纯函数(见可重建性 Agent Note)。带有 reason `'initial'` 或 `'resume'` 的完整 `request/header` 快照记录每个 agent loop 实例的边界;之后请求发生变化时,系统会以 reason `'change'` 记录另一份完整快照。`foldRequestHeader(events)` 通过选择最新快照重建请求头。该事件不是 `SurfaceEventType`,不产生 LLM 消息。
|
||||
请求信封(即 `EpochHeader`:调用配置 + 适配器默认值来源 + 渲染后的系统提示词 + 已组装的工具 schema)会作为会话状态写入日志,因此每个对话请求都是日志的纯函数(见可重建性 Agent Note)。带有 reason `'initial'` 或 `'resume'` 的完整 `request/header` 快照记录每个 agent loop 实例的边界;之后请求发生变化时,系统会以 reason `'change'` 记录另一份完整快照。`foldRequestHeader(events)` 通过选择最新快照重建请求头。该事件不是 `SurfaceEventType`,不产生 LLM 消息。
|
||||
|
||||
```ts type-equiv
|
||||
/**
|
||||
@@ -157,6 +157,8 @@ interface TodoItem {
|
||||
interface EpochHeader {
|
||||
/** The conversation's call configuration (provider, model, reasoning effort, and sampling scalars). */
|
||||
config: LlmCallConfig
|
||||
/** Effective config fields materialized from the exact adapter rather than proposed by a caller. */
|
||||
adapterDefaults?: LlmCallConfigAdapterDefaults
|
||||
/** Rendered system prompt text; absent for a system-less request. */
|
||||
system?: string
|
||||
/** Assembled tool schemas; absent for a tool-less request. */
|
||||
|
||||
@@ -78,7 +78,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
}[T]
|
||||
```
|
||||
|
||||
Sources: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:286`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:315`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:347`](../packages/core/session/src/types.ts)
|
||||
Sources: [`packages/core/session/src/types.ts:282`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:289`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:318`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:350`](../packages/core/session/src/types.ts)
|
||||
|
||||
## Events
|
||||
|
||||
@@ -154,7 +154,7 @@ Source: [`packages/ui/user-approval/src/index.ts:67`](../packages/ui/user-approv
|
||||
|
||||
Types: [StreamChunk](core-data-structures/llm-streaming.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:212`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:215`](../packages/core/session/src/types.ts)
|
||||
|
||||
#### `assistant/message` — surface
|
||||
|
||||
@@ -170,7 +170,7 @@ Source: [`packages/core/session/src/types.ts:212`](../packages/core/session/src/
|
||||
|
||||
Types: [TokenUsage](core-data-structures/llm-streaming.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:219`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:222`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `command/*`
|
||||
|
||||
@@ -379,7 +379,7 @@ Source: [`packages/plan/plan-mode/src/index.ts:51`](../packages/plan/plan-mode/s
|
||||
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
|
||||
```
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:252`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:255`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `sandbox/*`
|
||||
|
||||
@@ -432,7 +432,7 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:33`](../packages/s
|
||||
'session/end-seed': Record<string, never>
|
||||
```
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:275`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:278`](../packages/core/session/src/types.ts)
|
||||
|
||||
#### `session/title` — log-only
|
||||
|
||||
@@ -468,7 +468,7 @@ Source: [`packages/session-title/session-title-llm/src/index.ts:43`](../packages
|
||||
'steering/message': { turn: number; message: UserMessage }
|
||||
```
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:245`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:248`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `step/*`
|
||||
|
||||
@@ -479,7 +479,7 @@ Source: [`packages/core/session/src/types.ts:245`](../packages/core/session/src/
|
||||
'step/end': { turn: number; step: number }
|
||||
```
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:201`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:204`](../packages/core/session/src/types.ts)
|
||||
|
||||
#### `step/start` — log-only
|
||||
|
||||
@@ -488,7 +488,7 @@ Source: [`packages/core/session/src/types.ts:201`](../packages/core/session/src/
|
||||
'step/start': { turn: number; step: number }
|
||||
```
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:199`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:202`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `todo/*`
|
||||
|
||||
@@ -501,7 +501,7 @@ Source: [`packages/core/session/src/types.ts:199`](../packages/core/session/src/
|
||||
|
||||
Types: [TodoItem](core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:247`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:250`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `tool/*`
|
||||
|
||||
@@ -518,7 +518,7 @@ Source: [`packages/core/session/src/types.ts:247`](../packages/core/session/src/
|
||||
|
||||
Types: [CallId](core-data-structures/core.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:225`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:228`](../packages/core/session/src/types.ts)
|
||||
|
||||
#### `tool/code-dispatch` — log-only
|
||||
|
||||
@@ -591,7 +591,7 @@ Source: [`packages/core/tools/src/code-mode.ts:33`](../packages/core/tools/src/c
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:237`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:240`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `turn/*`
|
||||
|
||||
@@ -609,7 +609,7 @@ Source: [`packages/core/session/src/types.ts:237`](../packages/core/session/src/
|
||||
|
||||
Types: [TurnEndReason](core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:197`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:200`](../packages/core/session/src/types.ts)
|
||||
|
||||
#### `turn/start` — log-only
|
||||
|
||||
@@ -622,7 +622,7 @@ Source: [`packages/core/session/src/types.ts:197`](../packages/core/session/src/
|
||||
|
||||
Types: [TurnTrigger](core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:190`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:193`](../packages/core/session/src/types.ts)
|
||||
|
||||
### `user/*`
|
||||
|
||||
@@ -640,4 +640,4 @@ Source: [`packages/core/session/src/types.ts:190`](../packages/core/session/src/
|
||||
'user/message': UserMessage
|
||||
```
|
||||
|
||||
Source: [`packages/core/session/src/types.ts:210`](../packages/core/session/src/types.ts)
|
||||
Source: [`packages/core/session/src/types.ts:213`](../packages/core/session/src/types.ts)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# carries ACP JSON-RPC.
|
||||
|
||||
# The DeepSeek adapter. Shipped default: full thinking at max effort on every
|
||||
# request (wire-only defaults; they never enter the request header).
|
||||
# request; exact-model resolution materializes request defaults before logging.
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
@@ -13,7 +13,6 @@
|
||||
baseURL: !!js process.env.DEEPSEEK_BASE_URL
|
||||
thinking: enabled
|
||||
reasoningEffort: max
|
||||
defaultContextWindow: 256000
|
||||
models:
|
||||
- id: deepseek-v4-flash
|
||||
- id: deepseek-v4-pro
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
baseURL: !!js process.env.DEEPSEEK_BASE_URL
|
||||
thinking: enabled
|
||||
reasoningEffort: max
|
||||
defaultContextWindow: 256000
|
||||
retryPolicy:
|
||||
mode: normal
|
||||
maxRetries: 2
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -18,7 +18,8 @@
|
||||
# The DeepSeek adapter. Swap to '@deepseek-ai/dsh-llm-pi-ai' for the pi-ai-backed
|
||||
# twin (a `providers` dict keyed by route; `reasoning: high` replaces
|
||||
# thinking/reasoningEffort). Shipped default: full thinking at max effort on
|
||||
# every request (wire-only defaults; they never enter the request header).
|
||||
# every request. Exact-model resolution materializes request defaults before
|
||||
# the request header is logged.
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
|
||||
17
examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml
vendored
Normal file
17
examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
- id: base
|
||||
name: '@cordisjs/plugin-include'
|
||||
config:
|
||||
path: ../../cordis.yml
|
||||
patches:
|
||||
- id: llm-deepseek
|
||||
config:
|
||||
apiKey: snapshot-key
|
||||
baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL
|
||||
thinking: disabled
|
||||
- id: cli-agent
|
||||
config:
|
||||
provider: deepseek-official
|
||||
model: deepseek-v4-flash
|
||||
persistenceRoot: './.sessions'
|
||||
workspaceContext: false
|
||||
persona: 'Keyless DeepSeek adapter defaults snapshot.'
|
||||
@@ -1,4 +1,6 @@
|
||||
import { readFile, readdir, writeFile } from 'node:fs/promises'
|
||||
import { createServer } from 'node:http'
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http'
|
||||
import { delimiter, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import {
|
||||
@@ -34,6 +36,7 @@ const ralphConfigPath = fileURLToPath(new URL('../ralph.cordis.snapshot.yml', im
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
const reasoningConfigPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url))
|
||||
const deepseekDefaultsConfigPath = fileURLToPath(new URL('./fixtures/deepseek-defaults.cordis.yml', import.meta.url))
|
||||
const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
|
||||
|
||||
interface JsonObject {
|
||||
@@ -45,6 +48,40 @@ interface PersistedLog {
|
||||
readonly header: JsonObject
|
||||
}
|
||||
|
||||
interface DeepSeekDefaultsServer {
|
||||
readonly url: string
|
||||
readonly requests: JsonObject[]
|
||||
close(): Promise<void>
|
||||
}
|
||||
|
||||
/** Serve one deterministic DeepSeek-compatible response while retaining its request body. */
|
||||
async function deepseekDefaultsServer(): Promise<DeepSeekDefaultsServer> {
|
||||
const requests: JsonObject[] = []
|
||||
const server = createServer((request: IncomingMessage, response: ServerResponse) => {
|
||||
let body = ''
|
||||
request.setEncoding('utf8')
|
||||
request.on('data', (chunk: string) => { body += chunk })
|
||||
request.on('end', () => {
|
||||
requests.push(JSON.parse(body) as JsonObject)
|
||||
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
||||
response.end([
|
||||
'data: {"choices":[{"delta":{"content":"DEFAULTS_OK"}}]}',
|
||||
'data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}',
|
||||
'data: [DONE]',
|
||||
'',
|
||||
].join('\n\n'))
|
||||
})
|
||||
})
|
||||
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
|
||||
const address = server.address()
|
||||
if (address === null || typeof address === 'string') throw new Error('DeepSeek defaults snapshot server has no port')
|
||||
return {
|
||||
url: `http://127.0.0.1:${address.port}`,
|
||||
requests,
|
||||
close: () => new Promise(resolve => server.close(() => { resolve() })),
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonl(content: string): JsonObject[] {
|
||||
return content.split('\n')
|
||||
.filter(line => line.trim().length > 0)
|
||||
@@ -244,6 +281,57 @@ describe('headless stream-json snapshots', () => {
|
||||
`)
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('logs and sends the DeepSeek adapter maxTokens default through the one-shot app', async () => {
|
||||
const server = await deepseekDefaultsServer()
|
||||
try {
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'DeepSeek adapter defaults headless stream-json snapshot',
|
||||
tempDirPrefix: 'headless-snapshot-deepseek-defaults-',
|
||||
binScript,
|
||||
configPath: deepseekDefaultsConfigPath,
|
||||
binArgs: [
|
||||
'--config',
|
||||
deepseekDefaultsConfigPath,
|
||||
'--output-format',
|
||||
'stream-json',
|
||||
'return the deterministic response',
|
||||
],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT_BASE_URL: server.url,
|
||||
NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.stderr).toBe('')
|
||||
expect(server.requests).toHaveLength(1)
|
||||
expect(server.requests[0]?.max_tokens).toBe(256_000)
|
||||
const header = (parseJsonl(result.stdout)
|
||||
.map(record => record.event)
|
||||
.find((event): event is JsonObject => (
|
||||
event !== null
|
||||
&& typeof event === 'object'
|
||||
&& !Array.isArray(event)
|
||||
&& 'type' in event
|
||||
&& event.type === 'request/header'
|
||||
))?.data as JsonObject | undefined)?.header as JsonObject | undefined
|
||||
expect(header?.config).toMatchInlineSnapshot(`
|
||||
{
|
||||
"maxTokens": 256000,
|
||||
"model": "deepseek-v4-flash",
|
||||
"provider": "deepseek-official",
|
||||
"reasoningEffort": "off",
|
||||
}
|
||||
`)
|
||||
expect(header?.adapterDefaults).toEqual({
|
||||
maxTokens: true,
|
||||
reasoningEffort: true,
|
||||
})
|
||||
} finally {
|
||||
await server.close()
|
||||
}
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('replays the advanced toolchain through the one-shot app', async () => {
|
||||
const prompt = await scenarioPrompt(advancedScenarioDir, 'advanced-toolchain')
|
||||
const fixtureFiles = [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":2,"time":0,"data":{"title":"say pong","messageSeqs":[1],"source":{"kind":"fallback"}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"high"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":5,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":6,"time":0,"data":{"turn":1,"reason":{"kind":"error","step":1,"failure":{"message":"llm-deepseek: no API key for provider route \"deepseek-official\"; store DEEPSEEK_API_KEY through the credentials service (the web Models page writes it), export DEEPSEEK_API_KEY in the launching environment, or — as a last resort — set a literal \"apiKey\" in the llm-deepseek settings section","code":"MISSING_CREDENTIAL"}}}}}
|
||||
{"type":"result","success":false,"sessionId":"{{sessionId}}","turn":1,"result":"","reason":{"kind":"error","step":1,"failure":{"message":"llm-deepseek: no API key for provider route \"deepseek-official\"; store DEEPSEEK_API_KEY through the credentials service (the web Models page writes it), export DEEPSEEK_API_KEY in the launching environment, or — as a last resort — set a literal \"apiKey\" in the llm-deepseek settings section","code":"MISSING_CREDENTIAL"}}}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
maxTokensAsSuccess: !!js "process.env.DSH_MAX_TOKENS_AS_SUCCESS === undefined ? true : JSON.parse(process.env.DSH_MAX_TOKENS_AS_SUCCESS)"
|
||||
|
||||
# The DeepSeek adapter. Shipped default: full thinking at max effort on every
|
||||
# request (wire-only defaults; they never enter the request header). The model
|
||||
# arrives per session over JSON-RPC, so it is not pinned here.
|
||||
# request; exact-model resolution materializes request defaults before logging.
|
||||
# The model arrives per session over JSON-RPC, so it is not pinned here.
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
config:
|
||||
|
||||
@@ -434,7 +434,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
{
|
||||
signature: 'async resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>',
|
||||
jsDoc: '/**\n * Validate a conversation call config against its exact model capability and\n * materialize an adapter-configured default. Unsupported explicit efforts\n * reject before provider I/O; no clamping or aliasing is performed. This\n * standalone query does not bind a later dispatch; use {@link prepareCall}\n * when logging and streaming must share one adapter registration.\n * @param config - provider/model route and optional request controls.\n * @param signal - optional cancellation for adapter-owned capability lookup.\n * @returns a detached config only when a default must be materialized.\n */',
|
||||
jsDoc: '/**\n * Validate a conversation call config against its exact model capability and\n * materialize adapter-configured defaults. Unsupported explicit efforts\n * reject before provider I/O; no clamping or aliasing is performed. This\n * standalone query does not bind a later dispatch; use {@link prepareCall}\n * when logging and streaming must share one adapter registration.\n * @param config - provider/model route and optional request controls.\n * @param signal - optional cancellation for adapter-owned capability lookup.\n * @returns a detached config only when a default must be materialized.\n */',
|
||||
},
|
||||
{
|
||||
signature: 'async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>',
|
||||
@@ -1861,7 +1861,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'EpochHeader',
|
||||
declaration: 'export interface EpochHeader {\n config: LlmCallConfig;\n system?: string;\n tools?: ToolSchema[];\n}',
|
||||
declaration: 'export interface EpochHeader {\n config: LlmCallConfig;\n adapterDefaults?: LlmCallConfigAdapterDefaults;\n system?: string;\n tools?: ToolSchema[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'FileDiff',
|
||||
@@ -2015,6 +2015,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'LlmCallConfig',
|
||||
declaration: 'export interface LlmCallConfig {\n provider: string;\n model: string;\n reasoningEffort?: ReasoningEffortId;\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'LlmCallConfigAdapterDefaults',
|
||||
declaration: 'export interface LlmCallConfigAdapterDefaults {\n reasoningEffort?: true;\n maxTokens?: true;\n}',
|
||||
},
|
||||
{
|
||||
name: 'LlmConfigurableProvider',
|
||||
declaration: 'export interface LlmConfigurableProvider {\n provider: string;\n displayName: string;\n settingsNs: string;\n settingsPath: readonly string[];\n}',
|
||||
@@ -2045,7 +2049,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'LlmResolvedModelInfo',
|
||||
declaration: 'export interface LlmResolvedModelInfo extends LlmModelInfo {\n context?: LlmModelContext;\n reasoning?: LlmModelReasoningInfo;\n}',
|
||||
declaration: 'export interface LlmResolvedModelInfo extends LlmModelInfo {\n context?: LlmModelContext;\n defaultMaxTokens?: number;\n reasoning?: LlmModelReasoningInfo;\n}',
|
||||
},
|
||||
{
|
||||
name: 'Message',
|
||||
@@ -2077,7 +2081,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'PreparedLlmCall',
|
||||
declaration: 'export interface PreparedLlmCall {\n readonly config: LlmCallConfig;\n stream(options: GenerateOptions): AsyncIterable<StreamChunk>;\n}',
|
||||
declaration: 'export interface PreparedLlmCall {\n readonly config: LlmCallConfig;\n readonly adapterDefaults: LlmCallConfigAdapterDefaults;\n stream(options: GenerateOptions): AsyncIterable<StreamChunk>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'PreparedReferencedMessage',
|
||||
|
||||
@@ -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/core/agent-loop/README.md
|
||||
README.md: a1617a1ef871f61157e0d70a06d055168170dced
|
||||
README.zh.md: 6ba945a41e700331929dabb557802c14256921fb
|
||||
README.md: afc00f1ecdd225f22da46b95827259a50c719766
|
||||
README.zh.md: 63aaab3b5af32b9bad70d74fbd57c8bd62c2ef7d
|
||||
|
||||
@@ -65,7 +65,7 @@ The driver owns one agent for its lifetime and runs inside `ctx.agents.withIniti
|
||||
|
||||
Every provider call that reaches a successful finish appends exactly one `assistant/message` completion anchor, including content-less calls and `max-tokens` finishes. The anchor records the assembled content as-is, retains exact chunk provenance (`[]` for a stream with no chunks), and includes usage when available; empty content stays out of derived message history.
|
||||
|
||||
After `agent/request` returns a provider/model call config, the loop asks `ctx.llm.prepareCall()` to validate any adapter-owned reasoning effort and materialize its configured default under the active turn signal. The prepared call retains the exact adapter registration across this asynchronous resolution, `request/header` logging, and terminal dispatch, so HMR cannot mix one adapter's capability result with another adapter's request. The effective config is logged before dispatch, so a listener can change effort between steps without hidden request drift. A route with no registered adapter preserves the proposed config so an `llm/stream` listener can own and short-circuit it; unhandled terminal dispatch still fails with `NO_ADAPTER`. A new loop instance restores the last effort only when its initial provider/model route exactly matches the logged route; a route change discards that opaque model-owned ID and resolves the new model independently.
|
||||
After `agent/request` returns a provider/model call config, the loop asks `ctx.llm.prepareCall()` to validate adapter-owned fields and materialize configured reasoning-effort and output-token defaults under the active turn signal. The prepared call retains the exact adapter registration across this asynchronous resolution, `request/header` logging, and terminal dispatch, so HMR cannot mix one adapter's capability result with another adapter's request. The header records the effective config and which fields came from the adapter. Before the next waterfall, the loop removes those marked fields from the proposal so the current exact route rematerializes its own defaults; unmarked explicit settings persist across steps and route changes. A route with no registered adapter preserves the proposed config so an `llm/stream` listener can own and short-circuit it; unhandled terminal dispatch still fails with `NO_ADAPTER`. A new loop instance applies the same provenance rule when resuming.
|
||||
|
||||
Plugin failure ends the current turn, not the loop. Only final adapter dispatch/iteration failures and terminal in-band error or aborted finishes enter `agent/request-error`; middleware, result processing, tools, and other extension failures close directly. Recovery receives the exact live error, immutable provider facts, immutable prior failures, the immutable retry policy of the adapter registration that served the request, and the turn signal after the failed step closes; the policy is absent if no final adapter served it. A handling listener returns `{ kind: 'retry' }`; the loop closes the failed turn with its error and opens one numbered retry turn without an intervening idle notification. Success clears the consecutive history, and an unhandled failure is terminal. AgentLoop owns one cancellation signal for the current admission or turn. An effective `cancel(cause)` clears pending work unless `keepInbox` is set and cooperatively aborts that signal; idle cancellation is a no-op. Durable `turn/end` records `aborted` for `user` and `parent`, while disposal records `disposed`; undispatched model tool calls receive synthetic `tool/call` and `ABORTED_BEFORE_DISPATCH` result pairs. The cancellation cause changes reporting, not how result context finalized after cancellation is handled. Disposal waits for signal-ignoring work before registry removal. The [explicit-cancellation decision](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md) owns the lifecycle and race contract.
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ interface Config {
|
||||
|
||||
每次提供方调用成功结束时,都会恰好追加一个 `assistant/message` 完成锚点,包括无内容调用和以 `max-tokens` 结束的调用。该锚点原样记录组装后的内容,保留确切的分片溯源(流没有分片时为 `[]`),并在用量可用时包含用量;空内容不会进入派生消息历史。
|
||||
|
||||
在 `agent/request` 返回提供方/模型调用配置后,循环会调用 `ctx.llm.prepareCall()`,在活跃轮次信号的控制下校验由适配器持有的推理(reasoning)强度,并填入其配置默认值。准备完成的调用会在这次异步解析、`request/header` 日志记录和最终分派期间保留同一项确切的适配器注册,因此 HMR(热模块替换)不会把某个适配器的能力解析结果与另一适配器的请求混用。生效配置会在分派前写入日志,因此监听器可以在步骤之间更改推理强度,而不会产生未记录的请求变化。没有已注册适配器的路由会保留原定配置,使 `llm/stream` 监听器可以接管并短路该请求;最终分派仍会以 `NO_ADAPTER` 拒绝未得到处理的路由。新循环实例仅在初始提供方/模型路由与日志路由完全一致时恢复上次的推理强度;路由变化会丢弃由前一模型持有的不透明 ID,并单独解析新模型。
|
||||
在 `agent/request` 返回提供方/模型调用配置后,循环会调用 `ctx.llm.prepareCall()`,在活跃轮次信号的控制下校验由适配器持有的字段,并填入配置的推理(reasoning)强度和输出 token 默认值。准备完成的调用会在这次异步解析、`request/header` 日志记录和最终分派期间保留同一项确切的适配器注册,因此 HMR(热模块替换)不会把某个适配器的能力解析结果与另一适配器的请求混用。请求 header 会记录生效配置以及哪些字段来自适配器。下一次 waterfall(瀑布式事件)前,循环会从提议中移除这些带标记字段,使当前精确路由重新填入自身默认值;未带标记的显式设置会跨步骤和路由变化保留。没有已注册适配器的路由会保留原定配置,使 `llm/stream` 监听器可以接管并短路该请求;最终分派仍会以 `NO_ADAPTER` 拒绝未得到处理的路由。新循环实例在恢复时会应用同一来源规则。
|
||||
|
||||
插件失败会结束当前轮次,而不是结束循环。只有最终适配器分发/迭代失败以及带内的终止错误或中止结束才进入 `agent/request-error`;中间件、结果处理、工具及其他扩展失败会直接关闭轮次。失败步骤关闭后,恢复逻辑会接收确切的实时错误、不可变的提供方事实、不可变的先前失败、为请求提供服务的适配器注册所对应的不可变重试策略,以及轮次信号;如果没有最终适配器为其提供服务,则该策略缺失。处理失败的监听器返回 `{ kind: 'retry' }`;循环用其错误关闭失败轮次,并在不插入空闲通知的情况下开启一个编号重试轮次。成功会清除连续失败历史;未被处理的失败是终态。AgentLoop 为当前接纳或轮次拥有一个取消信号。有效的 `cancel(cause)` 在未设置 `keepInbox` 时清除待处理工作,并以协作方式中止该信号;空闲取消是空操作。持久 `turn/end` 为 `user` 和 `parent` 记录 `aborted`,dispose(资源释放)则记录 `disposed`;未分发的模型工具调用会收到合成的 `tool/call` 与 `ABORTED_BEFORE_DISPATCH` 结果对。取消原因只改变报告方式,不改变对取消后已定案结果上下文的处理。dispose 会等待忽略信号的工作完成,然后才从注册表移除。[显式取消决策](../../../.agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md)规定生命周期与竞态契约。
|
||||
|
||||
@@ -92,7 +92,7 @@ interface Config {
|
||||
|
||||
#### Token 影响
|
||||
|
||||
每个步骤都会再次计入系统文本与 schema。逐 agent 作用域决定贡献,而权威组装 waterfall(瀑布式事件)可以改变最终请求,并使其监听器负责保持协议连贯。
|
||||
每个步骤都会再次计入系统文本与 schema。逐 agent 作用域决定贡献,而权威组装 waterfall 可以改变最终请求,并使其监听器负责保持协议连贯。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import type { GenerateOptions, LlmCallConfig, LlmFailure, Message, PreparedLlmCall, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm'
|
||||
import { canonicalHeader, headerEquals } from '@deepseek-ai/dsh-session'
|
||||
import type { AssistantMessage, Session, SessionId, TurnEndReason, TurnTrigger, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import type { AssistantMessage, EpochHeader, Session, SessionId, TurnEndReason, TurnTrigger, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import { executeToolCalls } from './tool-calls.ts'
|
||||
@@ -54,6 +54,15 @@ type StepOutcome =
|
||||
| { kind: 'completed'; continueTurn: boolean; concluded: boolean; maxTokens: boolean }
|
||||
| { kind: 'request-failed'; error: RequestError; failure: LlmFailure; retryPolicy: ResolvedRetryPolicy | undefined }
|
||||
|
||||
/** Remove adapter-derived values before plugins propose the next request config. */
|
||||
function requestProposal(header: EpochHeader): LlmCallConfig {
|
||||
if (header.adapterDefaults === undefined) return header.config
|
||||
const proposal = { ...header.config }
|
||||
if (header.adapterDefaults.reasoningEffort === true) delete proposal.reasoningEffort
|
||||
if (header.adapterDefaults.maxTokens === true) delete proposal.maxTokens
|
||||
return proposal
|
||||
}
|
||||
|
||||
/**
|
||||
* The concrete {@link Agent}: each `run()` owns one turn and repeats model
|
||||
* steps while tools or steering require another request.
|
||||
@@ -615,19 +624,21 @@ export class ReactLoopAgent implements Agent {
|
||||
): Promise<{ request: GenerateOptions; preparedCall?: PreparedLlmCall }> {
|
||||
const { session } = this
|
||||
|
||||
// A loop instance starts from its declared route, restoring only an opaque
|
||||
// effort owned by that exact model. Later steps fold the config it logged.
|
||||
const persistedConfig = session.requestHeader()?.config
|
||||
// A loop instance starts from its declared route, restoring only an explicit
|
||||
// effort owned by that exact model. Later steps re-resolve marked defaults.
|
||||
const persistedHeader = session.requestHeader()
|
||||
const persistedConfig = persistedHeader?.config
|
||||
const route = { provider: this.options.provider ?? '', model: this.options.model ?? '' }
|
||||
const reasoningEffort = persistedConfig?.provider === route.provider
|
||||
&& persistedConfig.model === route.model
|
||||
&& persistedHeader?.adapterDefaults?.reasoningEffort !== true
|
||||
? persistedConfig.reasoningEffort
|
||||
: undefined
|
||||
const maxTokens = this.options.maxTokens
|
||||
const seedConfig = deepFreeze(structuredClone(
|
||||
this.requestHeaderLogged
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion -- the instance logged the header it now folds
|
||||
? persistedConfig!
|
||||
? requestProposal(persistedHeader!)
|
||||
: {
|
||||
...route,
|
||||
...reasoningEffort === undefined ? {} : { reasoningEffort },
|
||||
@@ -657,6 +668,7 @@ export class ReactLoopAgent implements Agent {
|
||||
|
||||
const header = canonicalHeader({
|
||||
config,
|
||||
...preparedCall === undefined ? {} : { adapterDefaults: preparedCall.adapterDefaults },
|
||||
...system ? { system } : {},
|
||||
...tools.length > 0 ? { tools } : {},
|
||||
})
|
||||
|
||||
@@ -67,6 +67,7 @@ export class MockAdapter extends LlmAdapter {
|
||||
constructor(
|
||||
private script: (StreamChunk[] | ((options: GenerateOptions) => StreamChunk[]) | 'hang')[],
|
||||
private readonly reasoning?: LlmModelReasoningInfo,
|
||||
private readonly defaultMaxTokens?: number,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -80,6 +81,7 @@ export class MockAdapter extends LlmAdapter {
|
||||
id: model,
|
||||
name: model,
|
||||
...this.reasoning === undefined ? {} : { reasoning: this.reasoning },
|
||||
...this.defaultMaxTokens === undefined ? {} : { defaultMaxTokens: this.defaultMaxTokens },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
|
||||
|
||||
async function harness(adapter: MockAdapter, persona = 'stable base') {
|
||||
return harnessRoutes([['mock', adapter]], persona)
|
||||
}
|
||||
|
||||
async function harnessRoutes(
|
||||
adapters: readonly (readonly [provider: string, adapter: MockAdapter])[],
|
||||
persona = 'stable base',
|
||||
) {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(SessionStore)
|
||||
@@ -25,7 +32,7 @@ async function harness(adapter: MockAdapter, persona = 'stable base') {
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
for (const [provider, adapter] of adapters) ctx.llm.registerAdapter([provider], adapter)
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -134,6 +141,10 @@ describe('request stability across the loop', () => {
|
||||
ReasoningEffortId('high'),
|
||||
ReasoningEffortId('max'),
|
||||
])
|
||||
expect(headers.map(event => event.data.header.adapterDefaults)).toEqual([
|
||||
{ reasoningEffort: true },
|
||||
undefined,
|
||||
])
|
||||
expect(headers.map(event => event.data.reason)).toEqual(['initial', 'change'])
|
||||
|
||||
for (const [model, effort] of [
|
||||
@@ -158,6 +169,88 @@ describe('request stability across the loop', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('logs an adapter-owned maxTokens default before dispatch', async () => {
|
||||
const adapter = new MockAdapter([textResponse('bounded')], undefined, 256_000)
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('adapter-max-tokens'), {
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
})
|
||||
|
||||
send(agent, 'use the adapter output limit')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(adapter.requests[0]?.maxTokens).toBe(256_000)
|
||||
const header = agent.session.events.find(event => event.type === 'request/header')
|
||||
expect(header?.type === 'request/header' && header.data.header.config.maxTokens).toBe(256_000)
|
||||
expect(header?.type === 'request/header' && header.data.header.adapterDefaults)
|
||||
.toEqual({ maxTokens: true })
|
||||
})
|
||||
|
||||
it('rematerializes the selected adapter maxTokens default after a provider switch', async () => {
|
||||
const deepseek = new MockAdapter([textResponse('deepseek')], undefined, 256_000)
|
||||
const other = new MockAdapter([textResponse('other')], undefined, 8_192)
|
||||
const ctx = await harnessRoutes([
|
||||
['deepseek', deepseek],
|
||||
['other', other],
|
||||
])
|
||||
const agent = ctx.agentLoop.create(SessionId('adapter-max-tokens-switch'), {
|
||||
provider: 'deepseek',
|
||||
model: 'deepseek-model',
|
||||
})
|
||||
ctx.on('agent/request', async (_agent, turn, _step, _signal, next) => {
|
||||
const config = await next()
|
||||
return turn === 2
|
||||
? { ...config, provider: 'other', model: 'other-model' }
|
||||
: config
|
||||
})
|
||||
|
||||
send(agent, 'first')
|
||||
await waitForIdle(ctx, agent)
|
||||
send(agent, 'second')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(deepseek.requests[0]?.maxTokens).toBe(256_000)
|
||||
expect(other.requests[0]?.maxTokens).toBe(8_192)
|
||||
const headers = agent.session.events.filter(event => event.type === 'request/header')
|
||||
expect(headers.map(event => event.data.header.config.maxTokens)).toEqual([256_000, 8_192])
|
||||
expect(headers.map(event => event.data.header.adapterDefaults)).toEqual([
|
||||
{ maxTokens: true },
|
||||
{ maxTokens: true },
|
||||
])
|
||||
})
|
||||
|
||||
it('preserves an explicit agent maxTokens cap across a provider switch', async () => {
|
||||
const deepseek = new MockAdapter([textResponse('deepseek')], undefined, 256_000)
|
||||
const other = new MockAdapter([textResponse('other')], undefined, 8_192)
|
||||
const ctx = await harnessRoutes([
|
||||
['deepseek', deepseek],
|
||||
['other', other],
|
||||
])
|
||||
const agent = ctx.agentLoop.create(SessionId('explicit-max-tokens-switch'), {
|
||||
provider: 'deepseek',
|
||||
model: 'deepseek-model',
|
||||
maxTokens: 4_096,
|
||||
})
|
||||
ctx.on('agent/request', async (_agent, turn, _step, _signal, next) => {
|
||||
const config = await next()
|
||||
return turn === 2
|
||||
? { ...config, provider: 'other', model: 'other-model' }
|
||||
: config
|
||||
})
|
||||
|
||||
send(agent, 'first')
|
||||
await waitForIdle(ctx, agent)
|
||||
send(agent, 'second')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(deepseek.requests[0]?.maxTokens).toBe(4_096)
|
||||
expect(other.requests[0]?.maxTokens).toBe(4_096)
|
||||
const headers = agent.session.events.filter(event => event.type === 'request/header')
|
||||
expect(headers.map(event => event.data.header.config.maxTokens)).toEqual([4_096, 4_096])
|
||||
expect(headers.map(event => event.data.header.adapterDefaults)).toEqual([undefined, undefined])
|
||||
})
|
||||
|
||||
it('keeps exact-model resolution, request logging, and dispatch on one adapter registration', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
|
||||
@@ -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/core/agent/README.md
|
||||
README.md: 6bd5279ace93b6d2569833be6f102c854105c2eb
|
||||
README.zh.md: cdbb0c0b70124e0037a0f7a7a03ddedf1e3b78d3
|
||||
README.md: 0b55381ee484eb0044b1ebb88cfd137a987a9b3e
|
||||
README.zh.md: 9fb0100fb2c212627b8c14dad0aada0f73d11c10
|
||||
|
||||
@@ -14,7 +14,7 @@ Tracks live agents and carries the initiating Agent through asynchronous driver
|
||||
|
||||
The scoped-registration surface: `Agent.ctx` is the agent's scope context (`dsh-scope`, key = the agent) — register tools/sections/variables/listeners through it for that agent alone, all unwound on disposal. `agentEvents(ctx, agent)` is the fused dispatcher for ordinary agent-subject operations (carrier + injected subject in one move); its notification mode invokes every listener and contains both synchronous throws and returned-promise rejections. The registry lifecycle pair reuses one stable routing carrier. `assembleContextFor(agent)` builds the per-agent assembly context (`agent` + `scope` together). `installAgentLlmTarget(agentCtx, target)` snapshots a mutable provider/model/reasoning-effort selection during prompt assembly, applies the route to prompt variables, and applies the complete target to request routing for one step; an absent selected effort clears an inherited effort so the target uses adapter/provider defaults. `CreateAgentOptions.setup(agentCtx)` and `ResumeAgentOptions.setup(agentCtx)` compose a fresh or resumed agent's scoped world while both objects remain unpublished. Setup is trusted, composition-only same-process code: drive the agent only after creation resolves.
|
||||
|
||||
`AgentOptions` supplies the initial provider/model route and an optional positive `maxTokens` output cap. The concrete loop records the cap in the request header and applies it to each conversation-model request; callers that omit it leave provider defaults in control.
|
||||
`AgentOptions` supplies the initial provider/model route and an optional positive `maxTokens` output cap. The concrete loop resolves any exact-model adapter default, records the effective cap in the request header, and applies it to each conversation-model request; an explicit Agent option wins, while omission leaves the adapter or provider route default in control.
|
||||
|
||||
- `ctx.agents.register(agent: Agent): () => void` — record an **already-constructed** agent. Disposed with the calling fiber.
|
||||
- Advanced ordered lifecycle: `enter(agent, owner): () => void` enforces `agent.id === agent.session.id`, performs the authoritative ID collision check, and inserts without announcing; `owner` explicitly records the live creator-agent relation (or `undefined` for a root), independently of durable session lineage. `announce(agent)` emits `agent/created` exactly once. A detach requested synchronously by a creation listener is deferred until that dispatch unwinds, and every detach checks the captured entry object, so a stale capability cannot delete a later same-ID replacement. The async factory uses this split; ordinary plugins use `register()`.
|
||||
|
||||
@@ -14,7 +14,7 @@ Agent 接口、注册表、进程本地发起方作用域,以及 `agent/*` 事
|
||||
|
||||
带作用域的注册接口:`Agent.ctx` 是 agent 的作用域上下文(`dsh-scope`,键 = 该 agent)。通过它注册工具/段/变量/监听器,只对该 agent 生效,并在 dispose(资源释放)时全部撤销。`agentEvents(ctx, agent)` 是普通 agent 主体操作的融合分发器(一次完成载体 + 注入主体);其通知 mode 会调用每个监听器,并同时收容同步抛出和返回 Promise 的拒绝。注册表生命周期对复用一个稳定路由载体。`assembleContextFor(agent)` 构建按 agent 的组装上下文(同时包含 `agent` + `scope`)。`installAgentLlmTarget(agentCtx, target)` 在提示词组装期间快照可变的提供方/模型/推理(reasoning)强度选择,将路由应用到提示词变量,并将完整目标应用到一个步骤的请求路由;如果没有选定推理强度,则会清除继承的推理强度,使该目标使用适配器/提供方默认值。`CreateAgentOptions.setup(agentCtx)` 和 `ResumeAgentOptions.setup(agentCtx)` 在新建或恢复的 agent 尚未发布时,组合其带作用域的世界。Setup 是受信任、仅用于组合的同进程代码:只有创建完成后才能驱动 agent。
|
||||
|
||||
`AgentOptions` 提供初始的提供方/模型路由,以及可选的正数 `maxTokens` 输出上限。实体循环会把该上限记录到请求 header,并应用到每次对话模型请求;调用方省略时由提供方默认值控制。
|
||||
`AgentOptions` 提供初始的提供方/模型路由,以及可选的正数 `maxTokens` 输出上限。实体循环会解析确切模型的适配器默认值,把生效上限记录到请求 header,并应用到每次对话模型请求;显式 Agent 选项优先,省略时由适配器或提供方路由默认值控制。
|
||||
|
||||
- `ctx.agents.register(agent: Agent): () => void`:记录一个 **已经构造完成** 的 agent。随调用 fiber dispose。
|
||||
- 高级有序生命周期:`enter(agent, owner): () => void` 强制 `agent.id === agent.session.id`,执行权威 ID 冲突检查,并在不通知的情况下插入;`owner` 显式记录实时创建方 agent 关系(根 agent 为 `undefined`),与持久会话谱系无关。`announce(agent)` 恰好发出一次 `agent/created`。创建监听器同步请求的 detach 会延后到该次分发结束;每次 detach 都会检查捕获的条目对象,因此陈旧能力无法删除后续使用同一 ID 的替代项。异步工厂使用这一拆分;普通插件使用 `register()`。
|
||||
|
||||
@@ -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/core/session/README.md
|
||||
README.md: 9fa6cf5251d480be9d2388bdb32393fa2827168c
|
||||
README.zh.md: 5170d5d4b362a0f19ff6953e1636adef9aa7e8b8
|
||||
README.md: 132e627387aff54381d10e87a52dc1440d0a5b56
|
||||
README.zh.md: edf94f765b81a81dfe39662e967cd6dfdca140fa
|
||||
|
||||
@@ -64,7 +64,7 @@ Providers stream token-sized deltas, so a raw log stores hundreds of `assistant/
|
||||
|
||||
### Request-header reconstruction (`request-header.ts`)
|
||||
|
||||
`request/header` records a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, or `change`. `foldRequestHeader()` selects the latest snapshot; legacy delta events and the removed `fallback` reason are rejected. See the [reconstructable-requests Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
|
||||
`request/header` records a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, or `change`. Its optional `adapterDefaults` map marks effective `reasoningEffort` or `maxTokens` values materialized by exact-model resolution, allowing the next request proposal to distinguish them from explicit conversation settings. `foldRequestHeader()` selects the latest snapshot; legacy delta events and the removed `fallback` reason are rejected. See the [reconstructable-requests Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
|
||||
|
||||
A `user/message` stores the complete `UserMessage` directly, including the identity created before routing or prompt admission. It renders its `content` verbatim whether it is a direct human prompt, a synthetic injection, or an admitted goal round; its typed `source` is the only channel that tells them apart and carries any domain-specific durable facts. `assistant/message`, `tool/result`, and `steering/message` likewise store complete message values. Turn execution remains enclosed by `turn/start` and `turn/end`, while an idle injection may append and flush a `user/message` between turns without running the model.
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
|
||||
### 请求头重建(`request-header.ts`)
|
||||
|
||||
`request/header` 记录非历史请求封装的完整规范快照,其原因为 `initial`、`resume` 或 `change`。`foldRequestHeader()` 选择最新快照;旧版增量事件和已移除的 `fallback` 原因会被拒绝。详见[可重建请求 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
|
||||
`request/header` 记录非历史请求封装的完整规范快照,其原因为 `initial`、`resume` 或 `change`。其可选 `adapterDefaults` 映射会标记由精确模型解析填入的生效 `reasoningEffort` 或 `maxTokens` 值,使下一次请求提议能够将它们与显式对话设置区分开。`foldRequestHeader()` 选择最新快照;旧版增量事件和已移除的 `fallback` 原因会被拒绝。详见[可重建请求 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
|
||||
|
||||
`user/message` 会直接存储完整的 `UserMessage`,其中包括路由或提示词准入前创建的标识。无论它是直接人类提示词、合成注入,还是已准入的 Goal Round,都会原样呈现其 `content`;带类型的 `source` 是区分三者的唯一通道,并携带各领域专有的持久事实。`assistant/message`、`tool/result` 和 steering(中途引导)对应的 `steering/message` 也会存储完整的消息值。轮次执行仍由 `turn/start` 与 `turn/end` 包围,而空闲注入可以在轮次之间追加并刷新一条 `user/message`,无需运行模型。
|
||||
|
||||
|
||||
@@ -201,13 +201,18 @@ function assertCurrentLlmShape(event: Record<string, unknown>, index: number): v
|
||||
: undefined
|
||||
if (event['type'] === 'request/header') {
|
||||
const header = record?.['header']
|
||||
const config = typeof header === 'object' && header !== null ? (header as Record<string, unknown>)['config'] : undefined
|
||||
const headerRecord = typeof header === 'object' && header !== null && !Array.isArray(header)
|
||||
? header as Record<string, unknown>
|
||||
: undefined
|
||||
const config = headerRecord?.['config']
|
||||
if (!hasProviderModel(config)) throw new Error(`seed request/header at index ${index} lacks provider/model`)
|
||||
const reasoningEffort = (config as Record<string, unknown>)['reasoningEffort']
|
||||
const configRecord = config as Record<string, unknown>
|
||||
const reasoningEffort = configRecord['reasoningEffort']
|
||||
if (reasoningEffort !== undefined
|
||||
&& (typeof reasoningEffort !== 'string' || reasoningEffort.length === 0)) {
|
||||
throw new Error(`seed request/header at index ${index} has an invalid reasoningEffort`)
|
||||
}
|
||||
assertAdapterDefaults(headerRecord?.['adapterDefaults'], configRecord, index)
|
||||
}
|
||||
const type = event['type']
|
||||
if (type !== 'user/message' && type !== 'assistant/message'
|
||||
@@ -215,6 +220,26 @@ function assertCurrentLlmShape(event: Record<string, unknown>, index: number): v
|
||||
assertMessageEventShape(event, `seed ${type} at index ${index}`)
|
||||
}
|
||||
|
||||
/** Validate adapter-default provenance imported from a durable request header. */
|
||||
function assertAdapterDefaults(
|
||||
value: unknown,
|
||||
config: Record<string, unknown>,
|
||||
index: number,
|
||||
): void {
|
||||
if (value === undefined) return
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
throw new Error(`seed request/header at index ${index} has invalid adapterDefaults`)
|
||||
}
|
||||
const defaults = value as Record<string, unknown>
|
||||
const allowed = new Set(['reasoningEffort', 'maxTokens'])
|
||||
if (Object.keys(defaults).some(key => !allowed.has(key))
|
||||
|| Object.values(defaults).some(marker => marker !== true)
|
||||
|| defaults['reasoningEffort'] === true && config['reasoningEffort'] === undefined
|
||||
|| defaults['maxTokens'] === true && config['maxTokens'] === undefined) {
|
||||
throw new Error(`seed request/header at index ${index} has invalid adapterDefaults`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Validate only the event-specific invariants needed to safely replay a message. */
|
||||
function assertMessageEventShape(event: Record<string, unknown>, subject: string): void {
|
||||
const type = event['type']
|
||||
|
||||
@@ -19,8 +19,12 @@ import type { EpochHeader, SessionEvent } from './types.ts'
|
||||
* @returns the canonical header.
|
||||
*/
|
||||
export function canonicalHeader(header: EpochHeader): EpochHeader {
|
||||
const adapterDefaults = header.adapterDefaults
|
||||
return {
|
||||
config: header.config,
|
||||
...adapterDefaults?.reasoningEffort === true || adapterDefaults?.maxTokens === true
|
||||
? { adapterDefaults }
|
||||
: {},
|
||||
...header.system !== undefined && header.system.length > 0 ? { system: header.system } : {},
|
||||
...header.tools !== undefined && header.tools.length > 0 ? { tools: header.tools } : {},
|
||||
}
|
||||
@@ -38,7 +42,12 @@ function sameSchema(a: ToolSchema, b: ToolSchema): boolean {
|
||||
* @returns whether config, system, and tools all match.
|
||||
*/
|
||||
export function headerEquals(a: EpochHeader, b: EpochHeader): boolean {
|
||||
if (!callConfigEquals(a.config, b.config) || a.system !== b.system) return false
|
||||
if (
|
||||
!callConfigEquals(a.config, b.config)
|
||||
|| a.adapterDefaults?.reasoningEffort !== b.adapterDefaults?.reasoningEffort
|
||||
|| a.adapterDefaults?.maxTokens !== b.adapterDefaults?.maxTokens
|
||||
|| a.system !== b.system
|
||||
) return false
|
||||
const at = a.tools ?? []
|
||||
const bt = b.tools ?? []
|
||||
return at.length === bt.length && at.every((tool, i) => sameSchema(tool, bt[i] as ToolSchema))
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
AssistantMessage,
|
||||
CallId,
|
||||
LlmCallConfig,
|
||||
LlmCallConfigAdapterDefaults,
|
||||
LlmFailure,
|
||||
MessageSource,
|
||||
StreamChunk,
|
||||
@@ -163,6 +164,8 @@ export interface TodoItem {
|
||||
export interface EpochHeader {
|
||||
/** The conversation's call configuration (provider, model, reasoning effort, and sampling scalars). */
|
||||
config: LlmCallConfig
|
||||
/** Effective config fields materialized from the exact adapter rather than proposed by a caller. */
|
||||
adapterDefaults?: LlmCallConfigAdapterDefaults
|
||||
/** Rendered system prompt text; absent for a system-less request. */
|
||||
system?: string
|
||||
/** Assembled tool schemas; absent for a tool-less request. */
|
||||
|
||||
@@ -14,9 +14,24 @@ function tool(name: string, description = 'd'): ToolSchema {
|
||||
|
||||
describe('canonicalHeader', () => {
|
||||
it('normalizes empty optional fields to absence and preserves populated fields', () => {
|
||||
expect(canonicalHeader({ config: CONFIG, system: '', tools: [] })).toEqual({ config: CONFIG })
|
||||
const full = canonicalHeader({ config: CONFIG, system: 's', tools: [tool('a')] })
|
||||
expect(full).toEqual({ config: CONFIG, system: 's', tools: [tool('a')] })
|
||||
expect(canonicalHeader({
|
||||
config: CONFIG,
|
||||
adapterDefaults: {},
|
||||
system: '',
|
||||
tools: [],
|
||||
})).toEqual({ config: CONFIG })
|
||||
const full = canonicalHeader({
|
||||
config: { ...CONFIG, maxTokens: 256_000 },
|
||||
adapterDefaults: { maxTokens: true },
|
||||
system: 's',
|
||||
tools: [tool('a')],
|
||||
})
|
||||
expect(full).toEqual({
|
||||
config: { ...CONFIG, maxTokens: 256_000 },
|
||||
adapterDefaults: { maxTokens: true },
|
||||
system: 's',
|
||||
tools: [tool('a')],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -30,6 +45,14 @@ describe('headerEquals', () => {
|
||||
...base,
|
||||
config: { ...base.config, reasoningEffort: ReasoningEffortId('high') },
|
||||
})).toBe(false)
|
||||
expect(headerEquals(
|
||||
{ ...base, config: { ...base.config, maxTokens: 256_000 } },
|
||||
{
|
||||
...base,
|
||||
config: { ...base.config, maxTokens: 256_000 },
|
||||
adapterDefaults: { maxTokens: true },
|
||||
},
|
||||
)).toBe(false)
|
||||
expect(headerEquals(base, { ...base, system: 'other' })).toBe(false)
|
||||
expect(headerEquals(base, { ...base, tools: [] })).toBe(false)
|
||||
expect(headerEquals(base, { ...base, tools: [tool('a', 'changed')] })).toBe(false)
|
||||
|
||||
@@ -403,6 +403,40 @@ describe('Session', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('round-trips adapter-default provenance and rejects invalid durable values', () => {
|
||||
const valid = {
|
||||
type: 'request/header',
|
||||
seq: 0,
|
||||
time: 1,
|
||||
data: {
|
||||
header: {
|
||||
config: {
|
||||
provider: 'mock',
|
||||
model: 'model',
|
||||
maxTokens: 256_000,
|
||||
},
|
||||
adapterDefaults: { maxTokens: true },
|
||||
},
|
||||
reason: 'initial',
|
||||
},
|
||||
} as const
|
||||
expect(new Session(SessionId('adapter-defaults'), [valid]).events[0]).toEqual(valid)
|
||||
|
||||
for (const adapterDefaults of [
|
||||
null,
|
||||
[],
|
||||
{ unknown: true },
|
||||
{ maxTokens: false },
|
||||
{ reasoningEffort: true },
|
||||
]) {
|
||||
const invalid = structuredClone(valid) as unknown as SessionEvent
|
||||
if (invalid.type !== 'request/header') throw new Error('test fixture must be a request header')
|
||||
invalid.data.header.adapterDefaults = adapterDefaults as never
|
||||
expect(() => new Session(SessionId('invalid-adapter-defaults'), [invalid]))
|
||||
.toThrow('seed request/header at index 0 has invalid adapterDefaults')
|
||||
}
|
||||
})
|
||||
|
||||
it('isolates the log from mutation through a derived message (append-only contract)', () => {
|
||||
const session = new Session(SessionId('s4'))
|
||||
session.append('user/message', createUserMessage({
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md
|
||||
README.md: 46666459d524dab952d555cc7f196d45e53d606a
|
||||
README.zh.md: 4210974c274513dc47383a23711baa6b14515307
|
||||
README.md: 020aa65073495526be3f32912b7cd06667c52a2e
|
||||
README.zh.md: 4c655e90ba00340c056f6ac16159621f7a8c1ddb
|
||||
|
||||
@@ -19,6 +19,7 @@ The package root exposes the Cordis plugin contract and `DeepSeekAdapter`; wire
|
||||
baseURL: https://api.deepseek.com # optional; $DEEPSEEK_BASE_URL then the public API when omitted
|
||||
thinking: enabled # optional; provider default is enabled
|
||||
reasoningEffort: high # optional; off | high | max — omitted ⇒ high
|
||||
maxTokens: 256000 # optional positive per-request output cap; this is the default
|
||||
streamIdleTimeoutMs: 300000 # optional; positive finite Node timer delay; five-minute default
|
||||
retryPolicy: # optional; omission uses bounded normal defaults
|
||||
mode: always # normal | always
|
||||
@@ -26,18 +27,20 @@ The package root exposes the Cordis plugin contract and `DeepSeekAdapter`; wire
|
||||
initialDelayMs: 500
|
||||
maxDelayMs: 10000
|
||||
jitterRatio: 0.1
|
||||
defaultContextWindow: 256000 # optional positive-integer fallback for models without an exact value
|
||||
defaultContextWindow: 1000000 # optional positive-integer fallback; this is the default
|
||||
models: # optional; defaults to V4 Flash and V4 Pro
|
||||
- id: deepseek-v4-flash
|
||||
name: DeepSeek-V4-Flash
|
||||
- id: private-reasoner
|
||||
description: Company-hosted reasoning model
|
||||
contextWindow: 64000
|
||||
contextWindow: 512000
|
||||
```
|
||||
|
||||
The plugin registers the single provider route `deepseek-official` together with its resolved `retryPolicy`. A request selects it with `provider: deepseek-official`; its `model` is passed through as the wire `model` string, so changing DeepSeek models does not require lifecycle-time registration. Omitting `models` advertises `deepseek-v4-flash` as `DeepSeek-V4-Flash` and `deepseek-v4-pro` as `DeepSeek-V4-Pro`, each with a 256,000-token context window; an explicit list replaces those defaults, while `models: []` advertises none. Catalog entries are exposed through `ctx.llm.listModels('deepseek-official')` for clients such as ACP editors and the Web selector, but remain advisory: unlisted model ids still pass through unchanged. An omitted entry name defaults to its id.
|
||||
The plugin registers the single provider route `deepseek-official` together with its resolved `retryPolicy`. A request selects it with `provider: deepseek-official`; its `model` is passed through as the wire `model` string, so changing DeepSeek models does not require lifecycle-time registration. Omitting `models` advertises `deepseek-v4-flash` as `DeepSeek-V4-Flash` and `deepseek-v4-pro` as `DeepSeek-V4-Pro`, each with a 1,000,000-token context window; an explicit list replaces those defaults, while `models: []` advertises none. Catalog entries are exposed through `ctx.llm.listModels('deepseek-official')` for clients such as ACP editors and the Web selector, but remain advisory: unlisted model ids still pass through unchanged. An omitted entry name defaults to its id.
|
||||
|
||||
`contextWindow` is optional per configured model and is not exposed through the advisory catalog. `ctx.llm.resolveModelInfo('deepseek-official', model).context` returns an exact model value first, then `defaultContextWindow` for an entry without capacity or an unlisted pass-through id. When neither value exists, `context` is absent without invalidating routing. Pressure-sensitive plugins therefore get deployment-owned capacity without treating the model selector as authoritative. Registering another adapter for `deepseek-official` throws `LlmError('DUPLICATE_ADAPTER')`.
|
||||
`contextWindow` is optional per configured model and is not exposed through the advisory catalog. `ctx.llm.resolveModelInfo('deepseek-official', model).context` returns an exact model value first, then `defaultContextWindow` for an entry without capacity or an unlisted pass-through id. The adapter default is 1,000,000; pressure-sensitive plugins therefore get deployment-owned capacity without treating the model selector as authoritative. Registering another adapter for `deepseek-official` throws `LlmError('DUPLICATE_ADAPTER')`.
|
||||
|
||||
`maxTokens` is the adapter-configured output cap for conversation requests and defaults to 256,000. Exact-model resolution exposes it as `defaultMaxTokens`; `LlmService` materializes that value into `GenerateOptions.maxTokens` before the agent loop writes `request/header`, so the wire request remains reconstructable. An explicit request or `AgentOptions.maxTokens` value wins and is serialized as `max_tokens`. The adapter does not clamp this request budget against `contextWindow`; deployments with a smaller context or provider output limit must configure a compatible `maxTokens`.
|
||||
|
||||
The same exact-model result exposes ordered `off`, `high`, and `max` efforts under `reasoning` for every pass-through model when deployment policy permits thinking. `reasoningEffort` selects the deployment default and falls back to `high` when omitted. `agent/request` can replace it on each conversation step; the resolved value is logged in `request/header`. `high` and `max` enable thinking and serialize as the official top-level `reasoning_effort`; adapter-owned `off` instead serializes `thinking.type: disabled` and omits `reasoning_effort`. An unsupported value fails with `UNSUPPORTED_REASONING_EFFORT` before network I/O.
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:
|
||||
baseURL: https://api.deepseek.com # optional; $DEEPSEEK_BASE_URL then the public API when omitted
|
||||
thinking: enabled # optional; provider default is enabled
|
||||
reasoningEffort: high # optional; off | high | max — omitted ⇒ high
|
||||
maxTokens: 256000 # optional positive per-request output cap; this is the default
|
||||
streamIdleTimeoutMs: 300000 # optional; positive finite Node timer delay; five-minute default
|
||||
retryPolicy: # optional; omission uses bounded normal defaults
|
||||
mode: always # normal | always
|
||||
@@ -26,18 +27,20 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器:
|
||||
initialDelayMs: 500
|
||||
maxDelayMs: 10000
|
||||
jitterRatio: 0.1
|
||||
defaultContextWindow: 256000 # optional positive-integer fallback for models without an exact value
|
||||
defaultContextWindow: 1000000 # optional positive-integer fallback; this is the default
|
||||
models: # optional; defaults to V4 Flash and V4 Pro
|
||||
- id: deepseek-v4-flash
|
||||
name: DeepSeek-V4-Flash
|
||||
- id: private-reasoner
|
||||
description: Company-hosted reasoning model
|
||||
contextWindow: 64000
|
||||
contextWindow: 512000
|
||||
```
|
||||
|
||||
该插件注册唯一提供方路由 `deepseek-official`,同时注册解析后的 `retryPolicy`。请求使用 `provider: deepseek-official` 选择该路由;其 `model` 会作为协议 `model` 字符串原样传递,因此更改 DeepSeek 模型不需要生命周期时注册。省略 `models` 会公布 `deepseek-v4-flash`(名称为 `DeepSeek-V4-Flash`)和 `deepseek-v4-pro`(名称为 `DeepSeek-V4-Pro`),两者的上下文窗口均为 256,000 token;显式列表会替换这些默认值,`models: []` 则不公布任何模型。Catalog 配置项通过 `ctx.llm.listModels('deepseek-official')` 公开给 ACP(Agent Client Protocol)编辑器和 Web 选择器等客户端,但仍只提供建议:未列出模型 id 仍原样传递。省略配置项 name 默认为其 id。
|
||||
该插件注册唯一提供方路由 `deepseek-official`,同时注册解析后的 `retryPolicy`。请求使用 `provider: deepseek-official` 选择该路由;其 `model` 会作为协议 `model` 字符串原样传递,因此更改 DeepSeek 模型不需要生命周期时注册。省略 `models` 会公布 `deepseek-v4-flash`(名称为 `DeepSeek-V4-Flash`)和 `deepseek-v4-pro`(名称为 `DeepSeek-V4-Pro`),两者的上下文窗口均为 1,000,000 token;显式列表会替换这些默认值,`models: []` 则不公布任何模型。Catalog 配置项通过 `ctx.llm.listModels('deepseek-official')` 公开给 ACP(Agent Client Protocol)编辑器和 Web 选择器等客户端,但仍只提供建议:未列出模型 id 仍原样传递。省略配置项 name 默认为其 id。
|
||||
|
||||
`contextWindow` 对每个已配置模型都可选,不会通过建议 catalog 公开。`ctx.llm.resolveModelInfo('deepseek-official', model).context` 先返回精确模型值,再对不含容量的配置项或未列出原样传递 id 返回 `defaultContextWindow`。两者都不存在时,`context` 字段缺失但不会使路由失效。因此,压力敏感插件可以获得由部署决定的容量,不会将模型 selector 视为权威。为 `deepseek-official` 注册另一个适配器会抛出 `LlmError('DUPLICATE_ADAPTER')`。
|
||||
`contextWindow` 对每个已配置模型都可选,不会通过建议 catalog 公开。`ctx.llm.resolveModelInfo('deepseek-official', model).context` 先返回精确模型值,再对不含容量的配置项或未列出原样传递 id 返回 `defaultContextWindow`。适配器默认值为 1,000,000;因此,压力敏感插件可以获得由部署决定的容量,不会将模型 selector 视为权威。为 `deepseek-official` 注册另一个适配器会抛出 `LlmError('DUPLICATE_ADAPTER')`。
|
||||
|
||||
`maxTokens` 是适配器为对话请求配置的输出上限,默认值为 256,000。确切模型解析会将其公开为 `defaultMaxTokens`;`LlmService` 会在 agent loop(智能体循环)写入 `request/header` 前,将该值填入 `GenerateOptions.maxTokens`,从而仍可根据持久记录重建协议请求。显式的请求值或 `AgentOptions.maxTokens` 值优先,并会序列化为 `max_tokens`。适配器不会根据 `contextWindow` 自动调低该请求预算;上下文或提供方输出上限较小的部署必须配置与其相容的 `maxTokens`。
|
||||
|
||||
同一确切模型结果会在部署策略允许思考时,为每个原样传递模型在 `reasoning` 下公开有序的 `off`、`high` 和 `max` 推理(reasoning)强度。`reasoningEffort` 选择部署默认值,省略时回退为 `high`。`agent/request` 可以在每个会话步骤替换它;解析后的值会记录在 `request/header`。`high` 和 `max` 会启用思考,并序列化为官方顶层 `reasoning_effort`;适配器持有的 `off` 则序列化为 `thinking.type: disabled`,且省略 `reasoning_effort`。不支持的值会在网络 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败。
|
||||
|
||||
|
||||
@@ -56,8 +56,10 @@ export interface DeepSeekConnectionOptions {
|
||||
apiKeyEnv: CredentialRef
|
||||
/** Request defaults applied to every call (thinking mode, effort). */
|
||||
defaults: RequestDefaults
|
||||
/** Default per-request output cap; explicit request values win. */
|
||||
maxTokens: number
|
||||
/** Positive context capacity used when the selected model has no exact value. */
|
||||
defaultContextWindow?: number
|
||||
defaultContextWindow: number
|
||||
/** Advisory models exposed to discovery consumers; requests remain unrestricted. */
|
||||
models: readonly DeepSeekCatalogModel[]
|
||||
/** Maximum provider idle time while one stream read is outstanding. */
|
||||
@@ -81,6 +83,10 @@ export interface DeepSeekAdapterOptions {
|
||||
|
||||
/** Default maximum idle interval while an adapter stream read is outstanding. */
|
||||
export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000
|
||||
/** Default combined request/response context capacity. */
|
||||
export const DEFAULT_CONTEXT_WINDOW = 1_000_000
|
||||
/** Default per-request output-token cap. */
|
||||
export const DEFAULT_MAX_TOKENS = 256_000
|
||||
const STREAM_IDLE_TIMEOUT_CODE = 'LLM_STREAM_IDLE_TIMEOUT'
|
||||
const OFF_REASONING_EFFORT = ReasoningEffortId('off')
|
||||
const HIGH_REASONING_EFFORT = ReasoningEffortId('high')
|
||||
@@ -174,7 +180,8 @@ export class DeepSeekAdapter extends LlmAdapter {
|
||||
...configured === undefined
|
||||
? { provider, id: model, name: model }
|
||||
: modelInfo(provider, configured),
|
||||
...contextWindow === undefined ? {} : { context: { contextWindow } },
|
||||
context: { contextWindow },
|
||||
defaultMaxTokens: connection.maxTokens,
|
||||
...connection.defaults.thinking === 'disabled'
|
||||
? {
|
||||
reasoning: {
|
||||
|
||||
@@ -18,10 +18,20 @@ import type { RetryPolicyConfig } from '@deepseek-ai/dsh-llm'
|
||||
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import { deepEqualJson, installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
import { DEFAULT_STREAM_IDLE_TIMEOUT_MS, DeepSeekAdapter } from './adapter.ts'
|
||||
import {
|
||||
DEFAULT_CONTEXT_WINDOW,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_STREAM_IDLE_TIMEOUT_MS,
|
||||
DeepSeekAdapter,
|
||||
} from './adapter.ts'
|
||||
import type { DeepSeekCatalogModel, DeepSeekConnectionOptions } from './adapter.ts'
|
||||
|
||||
export { DeepSeekAdapter } from './adapter.ts'
|
||||
export {
|
||||
DEFAULT_CONTEXT_WINDOW,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_STREAM_IDLE_TIMEOUT_MS,
|
||||
DeepSeekAdapter,
|
||||
} from './adapter.ts'
|
||||
export type { DeepSeekAdapterOptions, DeepSeekCatalogModel, DeepSeekConnectionOptions } from './adapter.ts'
|
||||
export type { RequestDefaults } from './serialize.ts'
|
||||
export type * from './types.ts'
|
||||
@@ -35,8 +45,8 @@ const DEFAULT_API_KEY_ENV = 'DEEPSEEK_API_KEY'
|
||||
const PROVIDER = 'deepseek-official'
|
||||
|
||||
const DEFAULT_MODELS: DeepSeekCatalogModel[] = [
|
||||
{ id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', contextWindow: 256_000 },
|
||||
{ id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', contextWindow: 256_000 },
|
||||
{ id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', contextWindow: DEFAULT_CONTEXT_WINDOW },
|
||||
{ id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', contextWindow: DEFAULT_CONTEXT_WINDOW },
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -58,7 +68,9 @@ export interface Config {
|
||||
thinking?: 'enabled' | 'disabled'
|
||||
/** Default thinking effort (default `high`); `off` disables thinking per request. */
|
||||
reasoningEffort?: 'off' | 'high' | 'max'
|
||||
/** Positive context capacity used when the selected model has no exact value. */
|
||||
/** Default per-request output cap (default 256,000); explicit request values win. */
|
||||
maxTokens?: number
|
||||
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
|
||||
defaultContextWindow?: number
|
||||
/** Advisory models shown by discovery consumers; defaults to V4 Flash and V4 Pro. */
|
||||
models?: DeepSeekCatalogModel[]
|
||||
@@ -81,7 +93,8 @@ export const Config: z<Config> = z.object({
|
||||
baseURL: z.string(),
|
||||
thinking: z.union(['enabled', 'disabled']),
|
||||
reasoningEffort: z.union(['off', 'high', 'max']),
|
||||
defaultContextWindow: z.number().step(1).min(1),
|
||||
maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_TOKENS),
|
||||
defaultContextWindow: z.number().step(1).min(1).default(DEFAULT_CONTEXT_WINDOW),
|
||||
models: z.array(catalogModel).default(DEFAULT_MODELS),
|
||||
streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
||||
retryPolicy: RetryPolicySchema,
|
||||
@@ -141,6 +154,10 @@ export function resolveAdapterOptions(config: Config): ResolvedDeepSeekOptions {
|
||||
&& (!Number.isInteger(config.defaultContextWindow) || config.defaultContextWindow <= 0)) {
|
||||
throw new Error('llm-deepseek: defaultContextWindow must be a positive integer')
|
||||
}
|
||||
if (config.maxTokens !== undefined
|
||||
&& (!Number.isSafeInteger(config.maxTokens) || config.maxTokens <= 0)) {
|
||||
throw new Error('llm-deepseek: maxTokens must be a positive safe integer')
|
||||
}
|
||||
const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS
|
||||
if (!Number.isFinite(streamIdleTimeoutMs)
|
||||
|| streamIdleTimeoutMs <= 0
|
||||
@@ -157,9 +174,8 @@ export function resolveAdapterOptions(config: Config): ResolvedDeepSeekOptions {
|
||||
thinking: config.thinking,
|
||||
reasoningEffort: config.reasoningEffort,
|
||||
},
|
||||
...config.defaultContextWindow === undefined
|
||||
? {}
|
||||
: { defaultContextWindow: config.defaultContextWindow },
|
||||
maxTokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
|
||||
defaultContextWindow: config.defaultContextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
||||
models: resolveModels(config.models),
|
||||
streamIdleTimeoutMs,
|
||||
retryPolicy: resolveRetryPolicy(config.retryPolicy, 'llm-deepseek: retryPolicy'),
|
||||
|
||||
@@ -139,7 +139,10 @@ export function serializeMessages(messages: Message[]): WireMessage[] {
|
||||
* @param defaults - adapter-level thinking defaults; undefined fields put nothing on the wire.
|
||||
* @returns the chat-completions request body.
|
||||
*/
|
||||
export function serializeRequest(options: GenerateOptions, defaults: RequestDefaults = {}): WireRequest {
|
||||
export function serializeRequest(
|
||||
options: GenerateOptions,
|
||||
defaults: RequestDefaults = {},
|
||||
): WireRequest {
|
||||
const messages: WireMessage[] = []
|
||||
if (options.system !== undefined) {
|
||||
messages.push({ role: 'system', content: options.system })
|
||||
@@ -169,7 +172,7 @@ export function serializeRequest(options: GenerateOptions, defaults: RequestDefa
|
||||
: {},
|
||||
...tools !== undefined && tools.length > 0 ? { tools } : {},
|
||||
...options.temperature !== undefined ? { temperature: options.temperature } : {},
|
||||
...options.maxTokens !== undefined ? { max_tokens: options.maxTokens } : {},
|
||||
...options.maxTokens === undefined ? {} : { max_tokens: options.maxTokens },
|
||||
...options.stop !== undefined ? { stop: options.stop } : {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ describe('DeepSeekAdapter against a mock server', () => {
|
||||
// The wire request carried the auth header contents we configured.
|
||||
expect(server.requests[0]).toMatchObject({
|
||||
model: 'deepseek-v4-flash',
|
||||
max_tokens: 256_000,
|
||||
reasoning_effort: 'high',
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
@@ -167,6 +168,20 @@ describe('DeepSeekAdapter against a mock server', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the configured maxTokens default and preserves an explicit request cap', async () => {
|
||||
const server = await mockServer([
|
||||
{ kind: 'sse', events: textEvents },
|
||||
{ kind: 'sse', events: textEvents },
|
||||
])
|
||||
const ctx = await harness(server.url, { maxTokens: 32_000 })
|
||||
|
||||
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
|
||||
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [], maxTokens: 8_192 })
|
||||
|
||||
expect(server.requests[0]).toMatchObject({ max_tokens: 32_000 })
|
||||
expect(server.requests[1]).toMatchObject({ max_tokens: 8_192 })
|
||||
})
|
||||
|
||||
it('publishes only off and omits the wire effort when thinking is disabled', async () => {
|
||||
const server = await mockServer([{ kind: 'sse', events: textEvents }])
|
||||
const ctx = await harness(server.url, { thinking: 'disabled' })
|
||||
@@ -600,7 +615,8 @@ describe('plugin registration and config', () => {
|
||||
provider: 'deepseek-official',
|
||||
id: 'deepseek-v4-flash',
|
||||
name: 'DeepSeek-V4-Flash',
|
||||
context: { contextWindow: 256_000 },
|
||||
context: { contextWindow: 1_000_000 },
|
||||
defaultMaxTokens: 256_000,
|
||||
reasoning: {
|
||||
efforts: [
|
||||
{ id: ReasoningEffortId('off'), name: 'Off' },
|
||||
@@ -722,7 +738,10 @@ describe('plugin registration and config', () => {
|
||||
description: 'Higher reasoning budget',
|
||||
})
|
||||
await expect(ctx.llm.resolveModelInfo('deepseek-official', 'arbitrary-unlisted'))
|
||||
.resolves.not.toHaveProperty('context')
|
||||
.resolves.toMatchObject({
|
||||
context: { contextWindow: 1_000_000 },
|
||||
defaultMaxTokens: 256_000,
|
||||
})
|
||||
})
|
||||
|
||||
it('uses exact model capacity before the adapter-wide default', async () => {
|
||||
@@ -804,6 +823,23 @@ describe('plugin registration and config', () => {
|
||||
},
|
||||
)
|
||||
|
||||
it.each([0, 1.5, Number.MAX_SAFE_INTEGER + 1])(
|
||||
'rejects invalid adapter-wide maxTokens %s',
|
||||
async (maxTokens) => {
|
||||
expect(() => resolveAdapterOptions({ maxTokens }))
|
||||
.toThrow(/maxTokens must be a positive safe integer/)
|
||||
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await expect(ctx.plugin(LlmDeepSeek, {
|
||||
apiKey: 'k',
|
||||
baseURL: 'http://127.0.0.1:1',
|
||||
maxTokens,
|
||||
})).rejects.toThrow(/maxTokens/)
|
||||
expect(ctx.llm.listProviders()).toEqual([])
|
||||
},
|
||||
)
|
||||
|
||||
it('falls back to DEEPSEEK_API_KEY and DEEPSEEK_BASE_URL env vars', async () => {
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', 'env-key')
|
||||
vi.stubEnv('DEEPSEEK_BASE_URL', 'http://127.0.0.1:1')
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/llm/llm/README.md
|
||||
README.md: b338f4d07ae4c5e3dd9a04ad0765fb8b6d6a99a7
|
||||
README.zh.md: 11d4c92573e2bdba74d1f7e04b445a9039824267
|
||||
README.md: f4be9b298c730b7ec0a0faa4470890fe5e3f5af8
|
||||
README.zh.md: 9aa22ba861ee368523b03a5472ea783bbcbbd765
|
||||
|
||||
@@ -16,8 +16,8 @@ An adapter registry plus a single streaming call surface, interceptable via a wa
|
||||
- `ctx.llm.listConfigurableProviders(): LlmConfigurableProvider[]` List the declared directory in declaration order; configuration surfaces merge it with `listProviders()` to mark each entry live or dormant.
|
||||
- `ctx.llm.providerRetryPolicy(provider: string): ResolvedRetryPolicy` Return the provider-owned retry policy captured during registration, with normal defaults resolved.
|
||||
- `ctx.llm.listModels(provider: string): Promise<LlmModelInfo[]>` Discover the models one registered provider currently advertises.
|
||||
- `ctx.llm.resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>` Resolve validated exact-model identity plus available context and reasoning metadata from the owning adapter, with optional cancellation for asynchronous adapters.
|
||||
- `ctx.llm.resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>` Validate an explicit effort and materialize an adapter-configured default without clamping.
|
||||
- `ctx.llm.resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>` Resolve validated exact-model identity plus available context, output-default, and reasoning metadata from the owning adapter, with optional cancellation for asynchronous adapters.
|
||||
- `ctx.llm.resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>` Validate an explicit effort and materialize adapter-configured call defaults without clamping.
|
||||
- `ctx.llm.prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>` Resolve a config and capture its current adapter registration as one cancellable, one-shot call.
|
||||
- `ctx.llm.stream(options: GenerateOptions): AsyncIterable<StreamChunk>` Stream one model call as raw chunks (token-level deltas). Consumers assemble the chunks into blocks/messages with `BlockAssembler`.
|
||||
|
||||
@@ -27,9 +27,9 @@ Provider and model metadata is a discovery surface, not a routing whitelist. `re
|
||||
|
||||
Every topology commit point — adapter routes registering or disposing, directory entries appearing or withdrawing — emits the payload-free `llm/adapters-updated` event after the mutation, so consumers re-read `listProviders()`/`listModels()`/`listConfigurableProviders()` instead of polling. Observer failures are contained (logged, non-vetoing); only `INVARIANT`-coded failures rethrow after the fan-out.
|
||||
|
||||
Exact-model metadata is a separate correctness query, not a catalog decoration or global LLM setting. `resolveModelInfo()` asks the adapter that owns the exact provider/model route once; an adapter can describe an unlisted dynamic model, and absent `context` or `reasoning` fields mean only that those capabilities are unavailable. Invalid identity, context, or reasoning metadata fails with `INVALID_MODEL_INFO`, `INVALID_MODEL_CONTEXT`, or `INVALID_MODEL_REASONING`.
|
||||
Exact-model metadata is a separate correctness query, not a catalog decoration or global LLM setting. `resolveModelInfo()` asks the adapter that owns the exact provider/model route once; an adapter can describe an unlisted dynamic model, and absent `context`, `defaultMaxTokens`, or `reasoning` fields preserve unknown capacity, provider-owned output defaults, or unavailable reasoning capability. Invalid identity, context, output default, or reasoning metadata fails with `INVALID_MODEL_INFO`, `INVALID_MODEL_CONTEXT`, `INVALID_MODEL_MAX_TOKENS`, or `INVALID_MODEL_REASONING`.
|
||||
|
||||
Reasoning identifiers are opaque adapter-owned strings rather than a core enum. An adapter publishes its ordered selectable list, including an `off` id when that model's capability API exposes one. `resolveCallConfig()` accepts only an exact advertised identifier, materializes `defaultEffort` when present, and otherwise preserves the provider default. Asynchronous model resolvers receive the caller's signal and must settle promptly after cancellation. `prepareCall()` additionally retains the exact adapter registration through header logging and terminal dispatch, so HMR cannot combine one adapter's capability result with another adapter's request; reusing its one-shot handle or changing its call-config fields fails with `INVALID_PREPARED_CALL`. An unsupported explicit or configured effort fails with `UNSUPPORTED_REASONING_EFFORT` before provider I/O.
|
||||
`defaultMaxTokens` is an adapter-configured per-request output cap, not a model hard limit. `resolveCallConfig()` materializes it only when the request omits `maxTokens`; an explicit cap wins. Reasoning identifiers are opaque adapter-owned strings rather than a core enum: the same resolution accepts only an exact advertised identifier, materializes `defaultEffort` when present, and otherwise preserves the provider default. Asynchronous model resolvers receive the caller's signal and must settle promptly after cancellation. `prepareCall()` additionally reports which `maxTokens` and `reasoningEffort` fields it materialized in `adapterDefaults` and retains the exact adapter registration through header logging and terminal dispatch, so HMR cannot combine one adapter's capability result with another adapter's request; reusing its one-shot handle or changing its call-config fields fails with `INVALID_PREPARED_CALL`. An unsupported explicit or configured effort fails with `UNSUPPORTED_REASONING_EFFORT` before provider I/O.
|
||||
|
||||
### Events
|
||||
|
||||
@@ -39,7 +39,7 @@ Reasoning identifiers are opaque adapter-owned strings rather than a core enum.
|
||||
|
||||
### Extension points
|
||||
|
||||
- Subclass `LlmAdapter` and call `ctx.llm.registerAdapter(providers, adapter)` to add one or more provider routes. `GenerateOptions.provider` selects the adapter; `GenerateOptions.model` is adapter-owned and may be resolved dynamically. Override `providerRetryPolicy()` to supply provider-owned recovery configuration, `providerInfo()` and asynchronous `listModels()` to expose selector metadata, then implement `resolveModel()` when exact identity, capacity, or selectable reasoning efforts are available; an asynchronous resolver must honor its optional cancellation signal. The defaults use bounded normal retry policy, use the route and model ids as names, advertise no models, and return no capacity or reasoning metadata.
|
||||
- Subclass `LlmAdapter` and call `ctx.llm.registerAdapter(providers, adapter)` to add one or more provider routes. `GenerateOptions.provider` selects the adapter; `GenerateOptions.model` is adapter-owned and may be resolved dynamically. Override `providerRetryPolicy()` to supply provider-owned recovery configuration, `providerInfo()` and asynchronous `listModels()` to expose selector metadata, then implement `resolveModel()` when exact identity, capacity, an output default, or selectable reasoning efforts are available; an asynchronous resolver must honor its optional cancellation signal. The defaults use bounded normal retry policy, use the route and model ids as names, advertise no models, and return no capacity, output default, or reasoning metadata.
|
||||
- Wrap `llm/stream` via `ctx.on()` waterfall listeners for caching, logging, or routing. A wrapper that retries after emitting a chunk has no durable attempt boundary; shipped agent retry policy therefore uses `agent/request-error` instead.
|
||||
|
||||
### Messages (`message.ts`) and content blocks (`types.ts`)
|
||||
@@ -52,7 +52,7 @@ Streaming is a raw chunk protocol (`block-start`, `text-delta`, `reasoning-delta
|
||||
|
||||
### Call configuration (`call-config.ts`)
|
||||
|
||||
`LlmCallConfig` is the provider, model, optional adapter-owned reasoning effort, and sampling scalars of one conversation's requests (`provider`, `model`, `reasoningEffort`, `temperature`, `maxTokens`, `stop` — each mapping 1:1 onto the same-named `GenerateOptions` field). It is per-conversation state recorded in the session log as part of the request header (see the dsh-session `request/header` events), never a silently-adjustable per-call knob: the `agent/request` waterfall proposes a replacement, `prepareCall()` validates and defaults it under the turn signal, and the loop logs the effective value before using the prepared call's registration-bound stream. `callConfigEquals(a, b)` is the field-wise real-change detector; `deepFreeze(value)` is the ownership helper the loop applies to every built request before dispatch (`llm/stream` listeners and adapters read, never rewrite). `markAgentLoopRequest()` gives that exact object process-local loop provenance, and `isAgentLoopRequest()` lets observers distinguish it from independently logged auxiliary calls that may also be frozen and session-associated. `GenerateOptions.purpose` classifies logged auxiliary compaction and session-title calls so adapters can apply purpose-specific transport policy without changing ordinary conversation requests.
|
||||
`LlmCallConfig` is the provider, model, optional adapter-owned reasoning effort, and sampling scalars of one conversation's requests (`provider`, `model`, `reasoningEffort`, `temperature`, `maxTokens`, `stop` — each mapping 1:1 onto the same-named `GenerateOptions` field). It is per-conversation state recorded in the session log as part of the request header (see the dsh-session `request/header` events), never a silently-adjustable per-call knob: the `agent/request` waterfall proposes a replacement, `prepareCall()` validates it and materializes adapter defaults under the turn signal, and the loop logs the effective value plus adapter-default provenance before using the prepared call's registration-bound stream. The next proposal omits marked defaults so a changed route resolves its own values; unmarked explicit fields persist. `callConfigEquals(a, b)` is the field-wise real-change detector; `deepFreeze(value)` is the ownership helper the loop applies to every built request before dispatch (`llm/stream` listeners and adapters read, never rewrite). `markAgentLoopRequest()` gives that exact object process-local loop provenance, and `isAgentLoopRequest()` lets observers distinguish it from independently logged auxiliary calls that may also be frozen and session-associated. `GenerateOptions.purpose` classifies logged auxiliary compaction and session-title calls so adapters can apply purpose-specific transport policy without changing ordinary conversation requests.
|
||||
|
||||
### App attribution (`attribution.ts`)
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
- `ctx.llm.listConfigurableProviders(): LlmConfigurableProvider[]` 按声明顺序列出已声明的目录;配置界面将其与 `listProviders()` 合并,为每个条目标注存活或休眠。
|
||||
- `ctx.llm.providerRetryPolicy(provider: string): ResolvedRetryPolicy` 返回注册时捕获的提供方重试策略,并解析 normal 默认值。
|
||||
- `ctx.llm.listModels(provider: string): Promise<LlmModelInfo[]>` 发现某个已注册提供方当前公布的模型。
|
||||
- `ctx.llm.resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>` 从拥有精确路由的适配器解析经校验的确切模型身份、可用上下文和推理(reasoning)元数据;异步适配器可选地支持取消。
|
||||
- `ctx.llm.resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>` 校验显式推理强度,并填入适配器配置的默认值,但不自动调整。
|
||||
- `ctx.llm.resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>` 从拥有精确路由的适配器解析经校验的确切模型身份,以及可用上下文、输出默认值和推理(reasoning)元数据;异步适配器可选地支持取消。
|
||||
- `ctx.llm.resolveCallConfig(config: LlmCallConfig, signal?: AbortSignal): Promise<LlmCallConfig>` 校验显式推理强度,并填入适配器配置的调用默认值,但不自动调整。
|
||||
- `ctx.llm.prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall>` 解析配置并将其当前适配器注册捕获为一次可取消、一次性调用。
|
||||
- `ctx.llm.stream(options: GenerateOptions): AsyncIterable<StreamChunk>` 将一次模型调用流式输出为原始分片(token 级增量)。消费方使用 `BlockAssembler` 将分片组装为块/消息。
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
|
||||
每个拓扑提交点——适配器路由注册或 dispose、目录条目出现或撤回——都会在变更之后发出无载荷的 `llm/adapters-updated` 事件,消费方因此重读 `listProviders()`/`listModels()`/`listConfigurableProviders()` 而非轮询。观察者故障会被隔离(记录日志、不否决);只有带 `INVARIANT` 码的故障会在扇出后重新抛出。
|
||||
|
||||
确切模型元数据是独立的正确性查询,不是 catalog 装饰或全局 LLM 设置。`resolveModelInfo()` 会向拥有精确提供方/模型路由的适配器查询一次;适配器可以描述未列出的动态模型,缺少 `context` 或 `reasoning` 字段只表示相应能力不可用。无效的身份、上下文或推理元数据会以 `INVALID_MODEL_INFO`、`INVALID_MODEL_CONTEXT` 或 `INVALID_MODEL_REASONING` 失败。
|
||||
确切模型元数据是独立的正确性查询,不是 catalog 装饰或全局 LLM 设置。`resolveModelInfo()` 会向拥有精确提供方/模型路由的适配器查询一次;适配器可以描述未列出的动态模型,缺少 `context`、`defaultMaxTokens` 或 `reasoning` 字段会分别保留未知容量、提供方持有的输出默认值或不可用的推理能力。无效的身份、上下文、输出默认值或推理元数据会以 `INVALID_MODEL_INFO`、`INVALID_MODEL_CONTEXT`、`INVALID_MODEL_MAX_TOKENS` 或 `INVALID_MODEL_REASONING` 失败。
|
||||
|
||||
推理标识符是由适配器持有的不透明字符串,而非核心枚举。适配器会公布有序可选列表;模型能力 API 提供 `off` id 时,列表也会包含它。`resolveCallConfig()` 只接受与已公布标识符完全一致的值,在存在 `defaultEffort` 时填入它,否则保留提供方默认值。异步模型解析器会接收调用方的 signal,并且必须在取消后迅速结束。`prepareCall()` 还会让精确适配器注册跨越请求头记录和最终分派,因此 HMR(热模块替换)不会将一个适配器的能力结果与另一个适配器的请求混用;复用其一次性句柄或更改调用配置字段会以 `INVALID_PREPARED_CALL` 失败。不支持的显式或配置推理强度会在提供方 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败。
|
||||
`defaultMaxTokens` 是适配器配置的单次请求输出上限,不是模型硬上限。仅当请求省略 `maxTokens` 时,`resolveCallConfig()` 才会填入该值;显式上限优先。推理标识符是由适配器持有的不透明字符串,而非核心枚举:同一次解析只接受与已公布标识符完全一致的值,在存在 `defaultEffort` 时填入它,否则保留提供方默认值。异步模型解析器会接收调用方的 signal,并且必须在取消后迅速结束。`prepareCall()` 还会通过 `adapterDefaults` 报告它填入了哪些 `maxTokens` 和 `reasoningEffort` 字段,并让精确适配器注册跨越请求头记录和最终分派,因此 HMR(热模块替换)不会将一个适配器的能力结果与另一个适配器的请求混用;复用其一次性句柄或更改调用配置字段会以 `INVALID_PREPARED_CALL` 失败。不支持的显式或配置推理强度会在提供方 I/O 前以 `UNSUPPORTED_REASONING_EFFORT` 失败。
|
||||
|
||||
### 事件
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
### 扩展点
|
||||
|
||||
- 继承 `LlmAdapter` 并调用 `ctx.llm.registerAdapter(providers, adapter)`,添加一条或多条提供方路由。`GenerateOptions.provider` 选择适配器;`GenerateOptions.model` 属于适配器,可以动态解析。覆盖 `providerRetryPolicy()` 以提供由提供方持有的恢复配置,覆盖 `providerInfo()` 和异步 `listModels()` 以公开 selector 元数据;精确身份、容量或可选推理强度可用时,实现 `resolveModel()`;异步解析器必须响应其可选的取消 signal。默认实现使用有界的 normal 重试策略,将路由和模型 id 用作名称,不公布模型,也不返回容量或推理元数据。
|
||||
- 继承 `LlmAdapter` 并调用 `ctx.llm.registerAdapter(providers, adapter)`,添加一条或多条提供方路由。`GenerateOptions.provider` 选择适配器;`GenerateOptions.model` 属于适配器,可以动态解析。覆盖 `providerRetryPolicy()` 以提供由提供方持有的恢复配置,覆盖 `providerInfo()` 和异步 `listModels()` 以公开 selector 元数据;精确身份、容量、输出默认值或可选推理强度可用时,实现 `resolveModel()`;异步解析器必须响应其可选的取消 signal。默认实现使用有界的 normal 重试策略,将路由和模型 id 用作名称,不公布模型,也不返回容量、输出默认值或推理元数据。
|
||||
- 包装 `llm/stream` 时,通过 `ctx.on()` waterfall listener 实现缓存、日志或路由。发出分片后重试的包装层没有持久尝试边界;因此已发布 agent 重试策略改用 `agent/request-error`。
|
||||
|
||||
### 消息(`message.ts`)与内容块(`types.ts`)
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
### 调用配置(`call-config.ts`)
|
||||
|
||||
`LlmCallConfig` 是一个会话中各次请求的提供方、模型、可选的适配器持有推理强度和采样标量(`provider`、`model`、`reasoningEffort`、`temperature`、`maxTokens`、`stop`,每个都与同名 `GenerateOptions` 字段 1:1 映射)。它是作为请求标头一部分记录在会话日志中的每会话状态(见 dsh-session `request/header` 事件),绝不是可静默调整的每次调用旋钮:`agent/request` waterfall 会提议替换,`prepareCall()` 在轮次 signal 控制下校验并填入默认值,loop 随后记录生效值,再使用已准备调用中与注册绑定的流。`callConfigEquals(a, b)` 是逐字段真实变更检测器;`deepFreeze(value)` 是 loop 在 dispatch 前对每个已构建请求应用的所有权 helper(`llm/stream` listener 与适配器只读,绝不改写)。`markAgentLoopRequest()` 为该精确对象添加进程本地 loop 溯源,`isAgentLoopRequest()` 让观测方可以将其与同样可能冻结并关联会话、但独立记录的辅助调用区分。`GenerateOptions.purpose` 对已记录辅助压缩与会话标题调用分类,让适配器可以应用目的特定传输策略,而不改变普通会话请求。
|
||||
`LlmCallConfig` 是一个会话中各次请求的提供方、模型、可选的适配器持有推理强度和采样标量(`provider`、`model`、`reasoningEffort`、`temperature`、`maxTokens`、`stop`,每个都与同名 `GenerateOptions` 字段 1:1 映射)。它是作为请求标头一部分记录在会话日志中的每会话状态(见 dsh-session `request/header` 事件),绝不是可静默调整的每次调用旋钮:`agent/request` waterfall 会提议替换,`prepareCall()` 在轮次 signal 控制下校验它并填入适配器默认值,loop 随后记录生效值及适配器默认值来源,再使用已准备调用中与注册绑定的流。下一次提议会省略带标记的默认值,使变更后的路由解析自身的值;未带标记的显式字段会保留。`callConfigEquals(a, b)` 是逐字段真实变更检测器;`deepFreeze(value)` 是 loop 在 dispatch 前对每个已构建请求应用的所有权 helper(`llm/stream` listener 与适配器只读,绝不改写)。`markAgentLoopRequest()` 为该精确对象添加进程本地 loop 溯源,`isAgentLoopRequest()` 让观测方可以将其与同样可能冻结并关联会话、但独立记录的辅助调用区分。`GenerateOptions.purpose` 对已记录辅助压缩与会话标题调用分类,让适配器可以应用目的特定传输策略,而不改变普通会话请求。
|
||||
|
||||
### 应用归因(`attribution.ts`)
|
||||
|
||||
|
||||
@@ -27,6 +27,15 @@ export interface LlmCallConfig {
|
||||
stop?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective config fields supplied by exact-model adapter resolution rather
|
||||
* than by the caller's request proposal.
|
||||
*/
|
||||
export interface LlmCallConfigAdapterDefaults {
|
||||
reasoningEffort?: true
|
||||
maxTokens?: true
|
||||
}
|
||||
|
||||
/**
|
||||
* Field-wise equality over {@link LlmCallConfig} — the comparison a caller
|
||||
* runs to decide whether a proposed configuration is a real change (worth a
|
||||
|
||||
@@ -21,7 +21,7 @@ import { resolveRetryPolicy } from './retry-policy.ts'
|
||||
import type { ResolvedRetryPolicy } from './retry-policy.ts'
|
||||
import type { ProviderRequestId } from './brand.ts'
|
||||
import { callConfigEquals, deepFreeze } from './call-config.ts'
|
||||
import type { LlmCallConfig } from './call-config.ts'
|
||||
import type { LlmCallConfig, LlmCallConfigAdapterDefaults } from './call-config.ts'
|
||||
import { HarnessError } from './error.ts'
|
||||
import { bindAdapterFailureScope, markLlmAdapterFailure } from './adapter-failure.ts'
|
||||
import type { AdapterFailureScope } from './adapter-failure.ts'
|
||||
@@ -35,7 +35,7 @@ export * from './message.ts'
|
||||
export * from './retry-policy.ts'
|
||||
export { BlockAssembler } from './assembler.ts'
|
||||
export { callConfigEquals, deepFreeze, isAgentLoopRequest, markAgentLoopRequest } from './call-config.ts'
|
||||
export type { LlmCallConfig } from './call-config.ts'
|
||||
export type { LlmCallConfig, LlmCallConfigAdapterDefaults } from './call-config.ts'
|
||||
export { isLlmAdapterFailure, llmFailureOf, llmRetryPolicyOf } from './adapter-failure.ts'
|
||||
|
||||
declare module 'cordis' {
|
||||
@@ -125,6 +125,8 @@ export class LlmError extends HarnessError {
|
||||
export interface PreparedLlmCall {
|
||||
/** Detached, deep-frozen config with any adapter-owned default materialized. */
|
||||
readonly config: LlmCallConfig
|
||||
/** Config fields materialized by the captured adapter rather than proposed by the caller. */
|
||||
readonly adapterDefaults: LlmCallConfigAdapterDefaults
|
||||
/**
|
||||
* Dispatch this call once through the registration captured during
|
||||
* preparation. The request's call-config fields must match {@link config};
|
||||
@@ -178,7 +180,7 @@ export abstract class LlmAdapter {
|
||||
* @param model - exact model id passed to {@link GenerateOptions.model}.
|
||||
* @param _signal - cancellation for this exact-model lookup; asynchronous
|
||||
* implementations must settle promptly after it aborts.
|
||||
* @returns provider/model identity plus any context and reasoning metadata.
|
||||
* @returns provider/model identity plus any context, call-default, and reasoning metadata.
|
||||
*/
|
||||
resolveModel(
|
||||
provider: string,
|
||||
@@ -490,12 +492,21 @@ export class LlmService extends Service {
|
||||
'INVALID_MODEL_CONTEXT',
|
||||
)
|
||||
}
|
||||
const defaultMaxTokens = resolved.defaultMaxTokens
|
||||
if (defaultMaxTokens !== undefined
|
||||
&& (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) {
|
||||
throw new LlmError(
|
||||
`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`,
|
||||
'INVALID_MODEL_MAX_TOKENS',
|
||||
)
|
||||
}
|
||||
const info: LlmResolvedModelInfo = {
|
||||
provider,
|
||||
id: model,
|
||||
name: resolved.name,
|
||||
...resolved.description === undefined ? {} : { description: resolved.description },
|
||||
...context === undefined ? {} : { context: { contextWindow: context.contextWindow } },
|
||||
...defaultMaxTokens === undefined ? {} : { defaultMaxTokens },
|
||||
}
|
||||
const reasoning = resolved.reasoning
|
||||
if (reasoning === undefined) return info
|
||||
@@ -544,7 +555,7 @@ export class LlmService extends Service {
|
||||
|
||||
/**
|
||||
* Validate a conversation call config against its exact model capability and
|
||||
* materialize an adapter-configured default. Unsupported explicit efforts
|
||||
* materialize adapter-configured defaults. Unsupported explicit efforts
|
||||
* reject before provider I/O; no clamping or aliasing is performed. This
|
||||
* standalone query does not bind a later dispatch; use {@link prepareCall}
|
||||
* when logging and streaming must share one adapter registration.
|
||||
@@ -561,8 +572,12 @@ export class LlmService extends Service {
|
||||
config: LlmCallConfig,
|
||||
signal?: AbortSignal,
|
||||
): Promise<LlmCallConfig> {
|
||||
const reasoning = (await this.resolveModelInfoFor(registration, config.model, signal)).reasoning
|
||||
const requested = config.reasoningEffort
|
||||
const info = await this.resolveModelInfoFor(registration, config.model, signal)
|
||||
const defaulted = config.maxTokens === undefined && info.defaultMaxTokens !== undefined
|
||||
? { ...config, maxTokens: info.defaultMaxTokens }
|
||||
: config
|
||||
const reasoning = info.reasoning
|
||||
const requested = defaulted.reasoningEffort
|
||||
if (reasoning === undefined) {
|
||||
if (requested !== undefined) {
|
||||
throw new LlmError(
|
||||
@@ -570,17 +585,17 @@ export class LlmService extends Service {
|
||||
'UNSUPPORTED_REASONING_EFFORT',
|
||||
)
|
||||
}
|
||||
return config
|
||||
return defaulted
|
||||
}
|
||||
const effective = requested ?? reasoning.defaultEffort
|
||||
if (effective === undefined) return config
|
||||
if (effective === undefined) return defaulted
|
||||
if (!reasoning.efforts.some(effort => effort.id === effective)) {
|
||||
throw new LlmError(
|
||||
`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`,
|
||||
'UNSUPPORTED_REASONING_EFFORT',
|
||||
)
|
||||
}
|
||||
return requested === effective ? config : { ...config, reasoningEffort: effective }
|
||||
return requested === effective ? defaulted : { ...defaulted, reasoningEffort: effective }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -593,12 +608,20 @@ export class LlmService extends Service {
|
||||
*/
|
||||
async prepareCall(config: LlmCallConfig, signal?: AbortSignal): Promise<PreparedLlmCall> {
|
||||
const registration = this.registration(config.provider)
|
||||
const resolvedConfig = deepFreeze(structuredClone(
|
||||
await this.resolveCallConfigFor(registration, config, signal),
|
||||
))
|
||||
const resolved = await this.resolveCallConfigFor(registration, config, signal)
|
||||
const resolvedConfig = deepFreeze(structuredClone(resolved))
|
||||
const adapterDefaults = deepFreeze<LlmCallConfigAdapterDefaults>({
|
||||
...config.reasoningEffort === undefined && resolved.reasoningEffort !== undefined
|
||||
? { reasoningEffort: true }
|
||||
: {},
|
||||
...config.maxTokens === undefined && resolved.maxTokens !== undefined
|
||||
? { maxTokens: true }
|
||||
: {},
|
||||
})
|
||||
let dispatched = false
|
||||
return Object.freeze({
|
||||
config: resolvedConfig,
|
||||
adapterDefaults,
|
||||
stream: (options: GenerateOptions): AsyncIterable<StreamChunk> => {
|
||||
if (dispatched) {
|
||||
throw new LlmError('a prepared LLM call can only be dispatched once', 'INVALID_PREPARED_CALL')
|
||||
|
||||
@@ -182,6 +182,8 @@ export interface LlmModelReasoningInfo {
|
||||
export interface LlmResolvedModelInfo extends LlmModelInfo {
|
||||
/** Provider-owned context capacity when known. */
|
||||
context?: LlmModelContext
|
||||
/** Adapter-configured per-request output cap materialized when callers omit one. */
|
||||
defaultMaxTokens?: number
|
||||
/** Adapter-owned selectable reasoning levels when exposed. */
|
||||
reasoning?: LlmModelReasoningInfo
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ class CatalogAdapter extends ScriptedAdapter {
|
||||
private readonly models: readonly LlmModelInfo[],
|
||||
private readonly contexts: Readonly<Record<string, LlmModelContext>> = {},
|
||||
private readonly reasoning: Readonly<Record<string, LlmModelReasoningInfo>> = {},
|
||||
private readonly defaultMaxTokens: Readonly<Record<string, number>> = {},
|
||||
) {
|
||||
super(SCRIPT)
|
||||
}
|
||||
@@ -82,6 +83,7 @@ class CatalogAdapter extends ScriptedAdapter {
|
||||
name: model,
|
||||
...this.contexts[model] === undefined ? {} : { context: this.contexts[model] },
|
||||
...this.reasoning[model] === undefined ? {} : { reasoning: this.reasoning[model] },
|
||||
...this.defaultMaxTokens[model] === undefined ? {} : { defaultMaxTokens: this.defaultMaxTokens[model] },
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -904,7 +906,12 @@ describe('LlmService', () => {
|
||||
{ id: 'route', name: 'Route' },
|
||||
[],
|
||||
{},
|
||||
{ model: source },
|
||||
{
|
||||
model: source,
|
||||
providerDefault: {
|
||||
efforts: [{ id: ReasoningEffortId('standard'), name: 'Standard' }],
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
const resolved = await ctx.llm.resolveModelInfo('route', 'model')
|
||||
@@ -918,8 +925,54 @@ describe('LlmService', () => {
|
||||
})
|
||||
const explicit = { provider: 'route', model: 'model', reasoningEffort: ReasoningEffortId('ultra') }
|
||||
await expect(ctx.llm.resolveCallConfig(explicit)).resolves.toBe(explicit)
|
||||
const providerDefault = { provider: 'route', model: 'providerDefault' }
|
||||
await expect(ctx.llm.resolveCallConfig(providerDefault)).resolves.toBe(providerDefault)
|
||||
})
|
||||
|
||||
it('materializes an adapter-owned maxTokens default while preserving an explicit cap', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
ctx.llm.registerAdapter(['route'], new CatalogAdapter(
|
||||
{ id: 'route', name: 'Route' },
|
||||
[],
|
||||
{},
|
||||
{},
|
||||
{ model: 256_000 },
|
||||
))
|
||||
|
||||
await expect(ctx.llm.resolveModelInfo('route', 'model')).resolves.toMatchObject({
|
||||
defaultMaxTokens: 256_000,
|
||||
})
|
||||
await expect(ctx.llm.resolveCallConfig({ provider: 'route', model: 'model' })).resolves.toEqual({
|
||||
provider: 'route',
|
||||
model: 'model',
|
||||
maxTokens: 256_000,
|
||||
})
|
||||
const preparedDefault = await ctx.llm.prepareCall({ provider: 'route', model: 'model' })
|
||||
expect(preparedDefault.adapterDefaults).toEqual({ maxTokens: true })
|
||||
const explicit = { provider: 'route', model: 'model', maxTokens: 8_192 }
|
||||
await expect(ctx.llm.resolveCallConfig(explicit)).resolves.toBe(explicit)
|
||||
const preparedExplicit = await ctx.llm.prepareCall(explicit)
|
||||
expect(preparedExplicit.adapterDefaults).toEqual({})
|
||||
})
|
||||
|
||||
it.each([0, 1.5, Number.MAX_SAFE_INTEGER + 1])(
|
||||
'rejects invalid adapter-owned default maxTokens %s',
|
||||
async (defaultMaxTokens) => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
const adapter = new class extends ScriptedAdapter {
|
||||
override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
|
||||
return Promise.resolve({ provider, id: model, name: model, defaultMaxTokens })
|
||||
}
|
||||
}(SCRIPT)
|
||||
ctx.llm.registerAdapter(['route'], adapter)
|
||||
|
||||
await expect(ctx.llm.resolveModelInfo('route', 'model'))
|
||||
.rejects.toMatchObject({ code: 'INVALID_MODEL_MAX_TOKENS' })
|
||||
},
|
||||
)
|
||||
|
||||
it.each([
|
||||
[{ efforts: [] }, 'empty effort list'],
|
||||
[{ efforts: [{ id: '', name: 'Empty' }] }, 'empty id'],
|
||||
@@ -1064,6 +1117,8 @@ describe('LlmService', () => {
|
||||
ctx.llm.registerAdapter(['route'], adapter)
|
||||
const prepared = await ctx.llm.prepareCall({ provider: 'route', model: 'model' })
|
||||
expect(Object.isFrozen(prepared.config)).toBe(true)
|
||||
expect(Object.isFrozen(prepared.adapterDefaults)).toBe(true)
|
||||
expect(prepared.adapterDefaults).toEqual({ reasoningEffort: true })
|
||||
const stream = prepared.stream({
|
||||
...prepared.config,
|
||||
model: 'other',
|
||||
|
||||
@@ -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/sdk/sdk-protocol/README.md
|
||||
README.md: 62b26d4a82d358fa4efcb7ab84036e5f4848057f
|
||||
README.zh.md: 11677c6119c7da407d95ee38ad9f8f7a552c15de
|
||||
README.md: 6dfc749bb0610f2c94e1a23fa395a428e47126bc
|
||||
README.zh.md: 2c2284dcdac3029ffaf6cac65e4c1247a2328939
|
||||
|
||||
@@ -22,7 +22,7 @@ The shared wire protocol for the DeepSeek Harness SDK runtime: one newline-delim
|
||||
| server→client | `subagent.started` | `SubagentStartedNotification` |
|
||||
| server→client | `subagent.finished` | `SubagentFinishedNotification` (in-process runs only) |
|
||||
|
||||
`HarnessSdkRequestMap` and `HarnessSdkNotificationMap` index these by method name. `InitializeParams.maxTokens` is an optional positive safe integer that caps each conversation-model output for SDK-created agents and their in-process descendants; omission leaves the provider default in control. The notification payload types depend on `SessionEvent` (`dsh-session`), `ContentBlock` (`dsh-llm`), and `SubagentStopReason` (`dsh-subagent`) — the protocol streams full session-log envelopes, so the session vocabulary is part of the wire contract. `serverInfo.name` stays the wire-stable `deepseek-harness-sdk-runtime`.
|
||||
`HarnessSdkRequestMap` and `HarnessSdkNotificationMap` index these by method name. `InitializeParams.maxTokens` is an optional positive safe integer that caps each conversation-model output for SDK-created agents and their in-process descendants; omission allows the selected adapter's exact-model default to apply, or otherwise preserves provider behavior. The notification payload types depend on `SessionEvent` (`dsh-session`), `ContentBlock` (`dsh-llm`), and `SubagentStopReason` (`dsh-subagent`) — the protocol streams full session-log envelopes, so the session vocabulary is part of the wire contract. `serverInfo.name` stays the wire-stable `deepseek-harness-sdk-runtime`.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ DeepSeek Harness SDK 运行时的共享协议格式(wire format):一个按
|
||||
| server→client | `subagent.started` | `SubagentStartedNotification` |
|
||||
| server→client | `subagent.finished` | `SubagentFinishedNotification`(仅进程内运行) |
|
||||
|
||||
`HarnessSdkRequestMap` 与 `HarnessSdkNotificationMap` 按方法名索引这些类型。`InitializeParams.maxTokens` 是可选的正的安全整数,用于限制 SDK 创建的 agent(智能体)及其进程内后代的每次对话模型输出;省略时由提供方默认值控制。通知载荷类型依赖 `SessionEvent`(`dsh-session`)、`ContentBlock`(`dsh-llm`)与 `SubagentStopReason`(`dsh-subagent`)——协议以完整会话日志封套进行流式传输,因此会话词汇是协议格式契约的一部分。`serverInfo.name` 的协议值固定为 `deepseek-harness-sdk-runtime`。
|
||||
`HarnessSdkRequestMap` 与 `HarnessSdkNotificationMap` 按方法名索引这些类型。`InitializeParams.maxTokens` 是可选的正的安全整数,用于限制 SDK 创建的 agent(智能体)及其进程内后代的每次对话模型输出;省略时会应用所选适配器的确切模型默认值,否则提供方行为保持不变。通知载荷类型依赖 `SessionEvent`(`dsh-session`)、`ContentBlock`(`dsh-llm`)与 `SubagentStopReason`(`dsh-subagent`)——协议以完整会话日志封套进行流式传输,因此会话词汇是协议格式契约的一部分。`serverInfo.name` 的协议值固定为 `deepseek-harness-sdk-runtime`。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -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/subagent/subagent-dsh-sdk/README.md
|
||||
README.md: ea6526fb6a71fa271a05ef4a2902aec590ea7db7
|
||||
README.zh.md: a54419f94eee1878f5505f5bb08607007064dc2a
|
||||
README.md: a0c0811b585b69a634e49a2c838137655f316585
|
||||
README.zh.md: e068e27c7cb2cfd39bbf723fabbc29b17f6676f8
|
||||
|
||||
@@ -32,7 +32,7 @@ The provider advertises no start-time capabilities (`outputSchema`/`depthLimit`/
|
||||
| `cwd` | parent session cwd | Working-directory override; same validation as [`subagent-acp`](../subagent-acp/README.md). |
|
||||
| `provider` | `deepseek-official` | Provider route sent in the child's `initialize`. |
|
||||
| `model` | `deepseek-v4-flash` | Model sent in the child's `initialize`. |
|
||||
| `maxTokens` | provider default | Per-request output-token cap sent in the child's `initialize`; it applies to the child root agent and its in-process descendants. |
|
||||
| `maxTokens` | adapter/provider route default | Per-request output-token cap sent in the child's `initialize`; it applies to the child root agent and its in-process descendants. |
|
||||
| `env` | `{}` | Explicit child environment layered over a credential-scrubbed parent environment (e.g. the child's own `DEEPSEEK_API_KEY`, or `DSH_CORDIS_CONFIG`). |
|
||||
| `shutdownTimeoutMs` | `1000` | Bound on the protocol `shutdown` exchange during dispose. |
|
||||
| `disposeEofGraceMs` | `6000` | Grace after stdin EOF before platform termination. |
|
||||
|
||||
@@ -32,7 +32,7 @@ Provider 不宣告任何启动期能力(`outputSchema`/`depthLimit`/`toolFilte
|
||||
| `cwd` | 父会话 cwd | 工作目录覆盖;校验规则与 [`subagent-acp`](../subagent-acp/README.md) 相同。 |
|
||||
| `provider` | `deepseek-official` | 写入子进程 `initialize` 的提供方路由。 |
|
||||
| `model` | `deepseek-v4-flash` | 写入子进程 `initialize` 的模型。 |
|
||||
| `maxTokens` | 提供方默认值 | 写入子进程 `initialize` 的单次请求输出 token 上限;对子运行时的根 agent 及其进程内后代生效。 |
|
||||
| `maxTokens` | 适配器/提供方路由默认值 | 写入子进程 `initialize` 的单次请求输出 token 上限;对子运行时的根 agent 及其进程内后代生效。 |
|
||||
| `env` | `{}` | 在凭据擦除后的父环境之上叠加的显式子环境(例如子进程自己的 `DEEPSEEK_API_KEY`,或 `DSH_CORDIS_CONFIG`)。 |
|
||||
| `shutdownTimeoutMs` | `1000` | dispose 期间协议 `shutdown` 交换的时限。 |
|
||||
| `disposeEofGraceMs` | `6000` | stdin EOF 之后、平台终止之前的宽限。 |
|
||||
|
||||
@@ -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/ui/jsonrpc/README.md
|
||||
README.md: 18ecf396a9cb402a7c4dbe5150666445ff97f3d9
|
||||
README.zh.md: 4baeaa82e8637ec67d771c4a30f601bb8f2476dc
|
||||
README.md: ac47af28e69e647ba44a7718478db163d406f5dc
|
||||
README.zh.md: 2ab600dce52f471d8eef63848e6283217008dcf6
|
||||
|
||||
@@ -22,7 +22,7 @@ The plugin answers `shutdown`, disposes SDK-owned agents and subscriptions to qu
|
||||
|
||||
## Wire notes
|
||||
|
||||
`initialize.serverInfo.name` is the wire-stable `deepseek-harness-sdk-runtime`. An optional positive `initialize.maxTokens` becomes the request output cap of each SDK-created agent and its in-process descendants; invalid values reject initialization, while omission sends no cap and preserves provider defaults. A session accepts one in-flight prompt; overlap fails immediately, other sessions remain independent, and the session is reusable after settlement. `session.finished` reports that prompt's message-triggered turn outcome; later between-turn records still stream as `session.event` notifications but cannot replace the prompt status. Persistence roots and persona come from `cordis.yml`.
|
||||
`initialize.serverInfo.name` is the wire-stable `deepseek-harness-sdk-runtime`. An optional positive `initialize.maxTokens` becomes the request output cap of each SDK-created agent and its in-process descendants; invalid values reject initialization, while omission sends no SDK cap and allows the selected adapter or provider route default to apply. A session accepts one in-flight prompt; overlap fails immediately, other sessions remain independent, and the session is reusable after settlement. `session.finished` reports that prompt's message-triggered turn outcome; later between-turn records still stream as `session.event` notifications but cannot replace the prompt status. Persistence roots and persona come from `cordis.yml`.
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Stdout 只承载 JSON-RPC 帧。部署不得组合 stdout logger;诊断应写
|
||||
|
||||
## 协议说明
|
||||
|
||||
`initialize.serverInfo.name` 的协议稳定值为 `deepseek-harness-sdk-runtime`。可选的正整数 `initialize.maxTokens` 会成为每个 SDK 创建的 agent 及其进程内后代的请求输出上限;非法值会使初始化失败,省略时则不发送上限并保留提供方默认值。一个会话只接受一个进行中的提示词;重叠请求会立即失败,其他会话保持独立,当前请求结算后该会话可再次使用。`session.finished` 报告由该提示词消息触发的轮次结果;后续轮次间记录仍会作为 `session.event` 通知流式发出,但不能替换该提示词的状态。持久化根目录和 persona 由 `cordis.yml` 提供。
|
||||
`initialize.serverInfo.name` 的协议稳定值为 `deepseek-harness-sdk-runtime`。可选的正整数 `initialize.maxTokens` 会成为每个 SDK 创建的 agent 及其进程内后代的请求输出上限;非法值会使初始化失败,省略时则不发送 SDK 上限,并应用所选适配器或提供方路由的默认值。一个会话只接受一个进行中的提示词;重叠请求会立即失败,其他会话保持独立,当前请求结算后该会话可再次使用。`session.finished` 报告由该提示词消息触发的轮次结果;后续轮次间记录仍会作为 `session.event` 通知流式发出,但不能替换该提示词的状态。持久化根目录和 persona 由 `cordis.yml` 提供。
|
||||
|
||||
## 模型体验
|
||||
|
||||
|
||||
@@ -86,6 +86,11 @@
|
||||
"symbol": "LlmCallConfig",
|
||||
"source": "packages/llm/llm/src/call-config.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "LlmCallConfigAdapterDefaults",
|
||||
"source": "packages/llm/llm/src/call-config.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/core.md",
|
||||
"symbol": "SessionEvent",
|
||||
|
||||
Reference in New Issue
Block a user