Merge remote-tracking branch 'origin/feat/gui-full-access-confirmation' into feat/gui-full-access-confirmation

This commit is contained in:
ZiyaZhang
2026-07-30 19:32:27 -07:00
269 files changed
+3748 -1463

No files matched your search

@@ -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`;显式配置优于未记录的提供方回退值。
@@ -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-client-locale-full-rollout.md
2026-07-30-client-locale-full-rollout.md: c080d9f240d4533ecd9694ceecfada8662c46425
2026-07-30-client-locale-full-rollout.zh.md: 062d982e3d7ea62f3ca4c8fedb842e8336f0852c
@@ -0,0 +1,45 @@
# Agent Note: Full client copy rollout onto the typed locale seat, and the non-translation boundary
Status: implemented
English | [中文](2026-07-30-client-locale-full-rollout.zh.md)
## Problem
After the typed locale standard seat landed (`locale:` on register → framework-injected typed `t`), only four early adopters rode it; every other client package still shipped hardcoded, mixed-language literals. Migrating the rest required mechanisms and boundary decisions the early adopters never touched: how registration-time text (nav rows, view-tab labels) refreshes on a language switch; how the zero-cordis ui-primitives atoms receive copy; and which strings deliberately stay untranslated — an unrecorded boundary invites a future agent to "complete" the localization.
## Decision
**Registration-time text rides a label thunk.** A list registration's `label` accepts `SlotLabel = string | (() => string)`; owners projecting ledger rows resolve through `resolveSlotLabel` (never reading `options.label` raw) and make the read point follow the locale revision (outlets subscribe to the revision themselves; off-ledger projections such as the ui-settings nav fold the revision into their cache key and subscribe to both sources). Thunks evaluate per read, so a language switch causes zero ledger churn — no re-registration, versions stay put, and every `locale/change` re-registration wiring is deleted.
**Component copy rides the standard `t` seat; deep children take `t` as a plain prop** typed `XxxProps['t']`. The dictionary canon is unchanged: `zh satisfies Record<string, string>` is the key source and `en satisfies Record<XxxKey, string>` locks bilingual balance.
**Zero-cordis atoms (ui-primitives) take copy as props**: `labels` on `TerminalBlock`/`JsonTree`, `copyLabel`/`copiedLabel` on `CodeBlock`, `codeLabels` on `MarkdownText`, `truncatedLabel` on `JsonBlock`, `label` on `ConnectionBanner`, `closeLabel` on `Modal` — defaults are the previous hardcoded strings, so a consumer passing nothing renders byte-identical output. Localized plugins pass dictionary-driven labels from their own `t` seat; call sites passing object props memoize them on the `t` identity (`MarkdownText` caches its component table on the `codeLabels` identity).
**The non-translation boundary (deliberate decisions, not debt):**
- **Error/failure strings stay English**: client-authored fallbacks (`command failed`, plan-toggle failures), RpcError messages, and wire `error.message (code)` pass-throughs render verbatim.
- **Design literals stay out of the dictionaries**: tool-row variant titles (Think/Bash/…), SYSTEM/USER-style kind badges, the Plan chip wordmark, the whole StatsLine — identical in both languages.
- **ui-trajectory is deferred wholesale** (a developer inspection surface, terminology-dense, ruled separately).
- **Boot copy stays hardcoded** (AppRoot renders before the locale service exists).
**Derivation layers stay pure; localization happens at render.** ui-workspace's `relativeTime` returns structured `{unit, n}` composed with dictionary templates by the renderer; blank sessions and the Ungrouped bucket keep their stored titles, with the renderer substituting localized copy off the `blank` flag / absent `workspaceId`; **blank rows are excluded from search entirely** (a bilingual display title cannot match a single-language query stably). Dates use no Intl: format templates live in the dictionaries (message clock `clock.md`/`clock.ymd`, workspace hover `date.ymd`) and the formatters take `t` as a parameter, staying pure.
**Test and e2e doctrine**: `makeTranslate(...dicts)` (dsh-client-test-runtime) mirrors the service lookup chain (first-dict-wins, key fallback, `{name}` interpolation); component specs stub the `t` seat with it, typed against real props seats. Web e2e uniformly opens through `newEnglishPage` (pins `dsh.locale=en` before boot) and the built-boot snapshot pins the same — goldens are immune to localization migrations; the settings language-switch scenario deliberately bypasses the helper to cover the zh default.
The "apply layer subscribes to `locale/change` and re-registers for fresh labels" mechanism in the [settings/locale/theme layering note](../../proposed/architecture/2026-07-25-client-settings-locale-theme.md) is superseded by this decision (thunk + revision lifecycle).
## Alternatives considered
- **Keep labels as strings and re-register on switch** (the early adopters' original shape): boot already registers once per package, and `locale/change` listeners re-registering amplifies into a storm; ledger version churn also busts every version-keyed projection cache. Thunks move the refresh cost to read points that already follow the revision.
- **A locale context/injection channel for ui-primitives**: breaks the zero-cordis boundary (atoms would depend on the runtime) and drags unlocalized consumers (ui-trajectory) along. Props let each consumer decide independently.
- **Error strings in the dictionaries**: the error surface is a debugging surface — verbatim English is what gets searched and compared in reports; wire pass-throughs are untranslatable anyway, and half-translation manufactures mixed-language text.
- **`toLocaleString()`/Intl for dates**: follows the browser/OS language, not the app locale, guaranteeing mixed text after a switch; the dictionary templates are tiny and isomorphic to the message clock.
- **Blank rows matching search (against localized or stored titles)**: either choice yields "visible but unfindable" in one language; placeholder rows carry no information, so whole-row exclusion is the stable semantic.
## Consequences
- A language switch refreshes the whole UI instantly with zero re-registration; adopting a new package is three steps (dictionary + declare-merge + `locale: NS`), no hand-written glue.
- Cost: list-label consumers must know `resolveSlotLabel` (a raw `options.label` read can now hold a function); the `SlotLabel` type catches most misuse statically.
- ui-primitives' Chinese defaults still render Chinese under the English locale **until a consumer passes labels** — the unmigrated JsonTree consumer (ui-trajectory) showing its English defaults happens to match that package's all-English status quo.
- Pinning e2e to English means the zh default is covered mainly by package-level component specs and the settings language-switch scenario; browser e2e no longer asserts zh copy.
@@ -0,0 +1,45 @@
# Agent Note: client 文案全量接入 typed locale 席位与不翻译边界
Status: implemented
[English](2026-07-30-client-locale-full-rollout.md) | 中文
## Problem
typed locale 标准席位(`locale:` 注册声明 → 框架注入强类型 `t`)落地后,只有四个先行包接入;其余 client 包的文案仍是硬编码的中英混杂字面量。全量迁移需要几个先行包没有触及的机制与边界决定:注册期文本(导航行、视图 tab 的 label)在语言切换时如何刷新;zero-cordis 的 ui-primitives 原子组件如何拿到文案;哪些字符串**刻意不**本地化——没有记录的边界会诱使后来者"补完"翻译。
## Decision
**注册期文本走 label thunk。** ui-slots 的 list 注册项 `label` 接受 `SlotLabel = string | (() => string)`owner 投影 ledger 行时必须经 `resolveSlotLabel` 解析(不裸读 `options.label`),并让读取点跟随 locale revisionoutlet 自身订阅 revisionledger 外的投影如 ui-settings 导航把 revision 并进缓存键、订阅双源)。thunk 每次读取时求值,语言切换零 ledger churn——没有重注册、version 不动,`locale/change` 重注册接线全部删除。
**组件文案走标准 `t` 席位;深层子组件用 prop 下传**,类型写 `XxxProps['t']`。字典规范形态不变:`zh satisfies Record<string, string>` 为 key 源、`en satisfies Record<XxxKey, string>` 锁双语平衡。
**zero-cordis 原子组件(ui-primitives)文案 props 化**`TerminalBlock`/`JsonTree``labels``CodeBlock``copyLabel`/`copiedLabel``MarkdownText``codeLabels``JsonBlock``truncatedLabel``ConnectionBanner``label``Modal``closeLabel`——默认值即原硬编码字符串,不传 props 的消费者渲染逐字节不变。已本地化的插件从自己的 `t` 席位传字典驱动的 label;传对象 props 的调用点按 `t` 身份 memo`MarkdownText` 的组件表按 `codeLabels` 身份缓存)。
**不翻译边界(刻意决定,不是欠账):**
- **错误/失败类字符串一律英文**:client 自产的兜底串(`command failed`、plan 切换失败)、RpcError message、wire 透出的 `error.message (code)` 原样呈现。
- **设计字面量不进字典**tool 行 variant 标题(Think/Bash/…)、SYSTEM/USER 类 kind 徽标、Plan chip 字标、StatsLine 全部指标——中英界面显示一致。
- **ui-trajectory 整包缓做**(开发者检查面,术语密集,单独裁决)。
- **boot 文案保持硬编码**AppRoot 渲染早于 locale 服务可用)。
**派生层保持纯函数,本地化只在渲染层**ui-workspace 的 `relativeTime` 返回结构化 `{unit, n}` 由渲染组合字典模板;blank 会话/未分组桶的存储标题不变,渲染按 `blank` 标志/`workspaceId` 缺席替换本地化文案;**搜索态 blank 行一律排除**(双语标题无法与单语查询稳定匹配)。日期不引 Intl:格式模板进字典(消息时钟 `clock.md`/`clock.ymd`workspace hover `date.ymd`),格式化函数吃 `t` 参数保持纯。
**测试与 e2e 口径**`makeTranslate(...dicts)`dsh-client-test-runtime)镜像服务查找链(首个命中字典胜出、key 兜底、`{name}` 插值),组件测试的 `t` 桩统一用它并以真实 props 席位定型。web e2e 统一 `newEnglishPage`boot 前钉 `dsh.locale=en`),built-boot snapshot 同样钉 en——golden 对语言迁移免疫;settings 语言切换用例刻意绕开该 helper 覆盖 zh 默认态。
[settings/locale/theme 分层 Note](../../proposed/architecture/2026-07-25-client-settings-locale-theme.md) 中"apply 层订阅 `locale/change` 重注册刷新 label"的机制已被本决定取代(thunk + revision 生命周期)。
## Alternatives considered
- **label 保持 string、语言切换时重注册**(先行包的旧形态):boot 每包一次注册已很重,`locale/change` 监听者重注册会放大成风暴;ledger version 抖动还会击穿一切按 version 缓存的投影。thunk 把刷新成本移到读取点,读取点本来就跟随 revision。
- **给 ui-primitives 造 locale context/注入通道**:破坏 zero-cordis 边界(原子组件从此依赖运行时),且强迫未本地化消费者(ui-trajectory)陪跑。props 化让每个消费者独立决定。
- **错误串进字典**:错误面是排障面,英文原样最利于搜索与上报比对;且 wire 透出串本就不可译,半译反而制造混合语言。
- **日期用 `toLocaleString()`/Intl**:跟随浏览器/OS 语言而非应用语言,切换后必然产生混合文本;字典模板量小且与消息时钟同构。
- **blank 行参与搜索(匹配本地化标题或存储标题)**:任一选择都在某个语言下"看得见搜不到";占位行本无信息量,整体排除语义最稳。
## Consequences
- 语言切换全 UI 即时刷新且零重注册;新包接入 = 字典 + declare-merge + `locale: NS` 三步,无手写胶水。
- 代价:list label 的消费方必须知道 `resolveSlotLabel`(裸读 `options.label` 拿到函数);类型上 `SlotLabel` 已挡住多数误用。
- ui-primitives 的中文默认值在英文语言下依旧是中文,**直到消费点传 label**——未迁移包(ui-trajectory 的 JsonTree)显示英文默认恰好符合其整包英文现状。
- e2e 英文钉死意味着 zh 默认态主要靠包级组件测试与 settings 语言切换用例覆盖,浏览器 e2e 不再验证 zh 文案。
@@ -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 进程中的每个会话拥有相同、可重现的预算。
@@ -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-29-web-message-icon-actions-and-clock.md
2026-07-29-web-message-icon-actions-and-clock.md: ccc9b127d2fcf276fa415ec0ede8b062e45f3df5
2026-07-29-web-message-icon-actions-and-clock.zh.md: fb4b82a22478b2de38755368ec48a5dca420e79d
2026-07-29-web-message-icon-actions-and-clock.md: f43f7f9c9687e4494993d7e225d11cf6446a9954
2026-07-29-web-message-icon-actions-and-clock.zh.md: 866fae79f6ad3ea2cb80e5443d2cf5f763562d29
@@ -10,9 +10,9 @@ The web chat user bubble already had copy / branch / edit IconActions but no clo
## Decision
**User bubbles prepend a date-aware local clock to the existing IconActions row; finalized assistant *content* nodes (non-empty text blocks) append a copy / branch / clock row with `margin-top: 16px`; both seats stay visible whenever mounted and re-format at the next local midnight.**
**User bubbles prepend a date-aware local clock to the existing IconActions row; the last content-text assistant of each turn appends a copy / branch / clock row with `margin-top: 16px`; both seats stay visible whenever mounted and re-format at the next local midnight.**
Both seats format `node.time` through `formatMessageClock`: same calendar day → `HH:mm`, earlier this year → `M月D日 HH:mm`, other years → `YYYY年M月D日 HH:mm`. `useCalendarDay` is a component-local day tick (timeout to the next local midnight) so memoized rows re-render when the calendar day changes without a new framework hook. `MessageItem` places the label before copy (figma `388:20051`). `AssistantMarkdown` places it after branch (figma `43:32997`) only when `streaming` is false, the event time is known, and the node has non-empty text content; Think-only nodes and the streaming tail omit the row. Copy writes joined text blocks. Both message rows pass their event's `seq` to the same fork callback; [Web session fork actions](2026-07-27-web-session-fork-actions.md) define the real mutation contract. Clipboard write and the clock helpers live in `message-chrome.ts`. The assembled surface is pinned by `apps/web/tests/message-actions.e2e.ts` (cold-seeded history + aria golden); aria normalization collapses every clock shape to `{{clock}}`.
Both seats format `node.time` through `formatMessageClock`: same calendar day → `HH:mm`, earlier this year → `M月D日 HH:mm`, other years → `YYYY年M月D日 HH:mm`. `useCalendarDay` is a component-local day tick (timeout to the next local midnight) so memoized rows re-render when the calendar day changes without a new framework hook. `MessageItem` places the label before copy (figma `388:20051`). `ChatView` derives turn-tail seqs via `assistantActionsSeqs` and withholds `time` for mid-turn content; `AssistantMarkdown` places the row after branch (figma `43:32997`) only when `streaming` is false, the event time is known, and the node has non-empty text content. Think-only nodes, mid-turn narration, and the streaming tail omit the row. Copy writes joined text blocks. Both message rows pass their event's `seq` to the same fork callback; [Web session fork actions](2026-07-27-web-session-fork-actions.md) define the real mutation contract. Clipboard write and the clock helpers live in `message-chrome.ts`. The assembled surface is pinned by `apps/web/tests/message-actions.e2e.ts` (cold-seeded history + aria golden); aria normalization collapses every clock shape to `{{clock}}`.
## Alternatives considered
@@ -20,6 +20,8 @@ Both seats format `node.time` through `formatMessageClock`: same calendar day
**Put IconActions under every finalized assistant node (including Think-only).** Rejected: copy has nothing useful to write without text content, and repeating the chrome under every step/Think row clutters the flow; only content output owns the seat.
**Put IconActions under every content-text assistant in a multi-step turn.** Rejected: mid-turn narration (text before tools) is not the settled answer; repeating copy/branch/clock under each step clutters the flow. Only the last content assistant of the turn owns the seat.
**Hover-reveal the action row on hover-capable pointers.** Rejected: once the row exists it should stay discoverable; opacity hiding made the chrome easy to miss and required parent hover selectors that duplicated the mount gate.
**Let the IconActions decision also define session fork semantics.** Rejected: this note owns only message chrome, clocks, and mount gating; boundary selection, failure behavior, and switching semantics belong to the separate [Web session fork actions](2026-07-27-web-session-fork-actions.md), keeping presentation components from becoming a second home for session mutation.
@@ -28,4 +30,4 @@ Both seats format `node.time` through `formatMessageClock`: same calendar day
## Consequences
Settled assistant content answers expose copy, branch, and the event clock as soon as the row mounts; Think-only nodes stay chrome-free. User and assistant clocks share the same day/year widening rules and refresh after midnight without a message mutation. Per-message paging remains a deferred footer seat in the package README. Package tests pin the three clock shapes, the midnight widen, the content-only assistant gate, and the respective event `seq` values passed by the user and assistant branch buttons; the web e2e scenario pins the assembled IconActions chrome.
Each turn's last settled content answer exposes copy, branch, and the event clock as soon as the row mounts; mid-turn content and Think-only nodes stay chrome-free. User and assistant clocks share the same day/year widening rules and refresh after midnight without a message mutation. Per-message paging remains a deferred footer seat in the package README. Package tests pin the three clock shapes, the midnight widen, the content-only assistant gate, the turn-tail seq gate, and the respective event `seq` values passed by the user and assistant branch buttons; the web e2e scenario pins the assembled IconActions chrome.
@@ -10,9 +10,9 @@ Web 聊天的用户气泡已有复制/分支/编辑 IconActions,但没有
## 决策
**用户气泡在既有 IconActions 行前追加感知日期的本地时钟;已定稿的 assistant *内容*节点(非空 text 块)在正文下追加带 `margin-top: 16px` 的复制/分支/时钟;两边只要挂载就保持可见,并在下一个本地午夜重新格式化。**
**用户气泡在既有 IconActions 行前追加感知日期的本地时钟;每个轮次中最后一条带 text 内容的 assistant 在正文下追加带 `margin-top: 16px` 的复制/分支/时钟;两边只要挂载就保持可见,并在下一个本地午夜重新格式化。**
两边都通过 `formatMessageClock` 格式化 `node.time`:同一日历日 → `HH:mm`,同年更早 → `M月D日 HH:mm`,跨年 → `YYYY年M月D日 HH:mm``useCalendarDay` 是组件本地的日刻度(定时到下一个本地午夜),因此 memo 行在日历日变化时会重渲染,且不新增框架 hook。`MessageItem` 把标签放在复制之前(figma `388:20051`)。`AssistantMarkdown`放在分支之后(figma `43:32997`),且仅在 `streaming` 为 false、已知事件时间、且节点含非空 text 内容时渲染纯 Think 节点与流式尾部省略该行。复制写入拼接后的 text 块。两种消息行都把自己的事件 `seq` 交给同一个 fork 回调;真实 mutation 契约由 [Web session fork 操作](2026-07-27-web-session-fork-actions.md)定义。剪贴板写入与时钟辅助函数放在 `message-chrome.ts`。组装面由 `apps/web/tests/message-actions.e2e.ts`(冷 seed 历史 + aria golden)钉住;aria 归一化把每种时钟形态折叠为 `{{clock}}`
两边都通过 `formatMessageClock` 格式化 `node.time`:同一日历日 → `HH:mm`,同年更早 → `M月D日 HH:mm`,跨年 → `YYYY年M月D日 HH:mm``useCalendarDay` 是组件本地的日刻度(定时到下一个本地午夜),因此 memo 行在日历日变化时会重渲染,且不新增框架 hook。`MessageItem` 把标签放在复制之前(figma `388:20051`)。`ChatView` 通过 `assistantActionsSeqs` 推导轮次尾部的 seq,并不为轮次中间的内容传入 `time``AssistantMarkdown`该行放在分支之后(figma `43:32997`),且仅在 `streaming` 为 false、已知事件时间、且节点含非空 text 内容时渲染纯 Think 节点、轮次中间的叙述与流式尾部省略该行。复制写入拼接后的 text 块。两种消息行都把自己的事件 `seq` 交给同一个 fork 回调;真实 mutation 契约由 [Web session fork 操作](2026-07-27-web-session-fork-actions.md)定义。剪贴板写入与时钟辅助函数放在 `message-chrome.ts`。组装面由 `apps/web/tests/message-actions.e2e.ts`(冷 seed 历史 + aria golden)钉住;aria 归一化把每种时钟形态折叠为 `{{clock}}`
## 曾考虑的方案
@@ -20,6 +20,8 @@ Web 聊天的用户气泡已有复制/分支/编辑 IconActions,但没有
**给每个已定稿 assistant 节点(含纯 Think)都挂 IconActions。** 否决:没有 text 内容时复制没有可写内容,且在每一步/Think 下重复 chrome 会打乱流程;只有内容输出拥有该座位。
**给多步骤轮次中的每一条带 text 内容的 assistant 都挂 IconActions。** 否决:轮次中间的叙述(工具调用前的 text)不是已定稿答案;在每一步下重复复制/分支/时钟会打乱流程。只有该轮次中最后一条内容 assistant 拥有该座位。
**在具备 hover 能力的指针上用 hover 才揭示操作行。** 否决:行一旦存在就应保持可发现;用 opacity 隐藏容易漏看,且需要父级 hover 选择器重复挂载门控。
**由 IconActions 决策同时定义 session fork 语义。** 否决:本笔记只拥有消息 chrome、时钟与挂载门控;边界选择、失败行为和切换语义属于独立的 [Web session fork 操作](2026-07-27-web-session-fork-actions.md),避免展示组件成为 session mutation 的第二正家。
@@ -28,4 +30,4 @@ Web 聊天的用户气泡已有复制/分支/编辑 IconActions,但没有
## 后果
已定稿的 assistant 内容回答在行挂载后立刻暴露复制、分支与事件时钟;纯 Think 节点不带 chrome。用户与 assistant 时钟共用同一套跨天/跨年加宽规则,并在午夜后无需消息变更即可刷新。逐消息分页仍是包 README 中的暂缓 footer 座位。包级测试钉住三种时钟形态、午夜加宽、assistant 仅内容门控,以及 user/assistant 分支按钮各自传递的事件 `seq`;Web e2e 场景钉住组装后的 IconActions chrome。
每个轮次中最后一条已定稿的内容回答在行挂载后立刻暴露复制、分支与事件时钟;轮次中间的内容与纯 Think 节点不带 chrome。用户与 assistant 时钟共用同一套跨天/跨年加宽规则,并在午夜后无需消息变更即可刷新。逐消息分页仍是包 README 中的暂缓 footer 座位。包级测试钉住三种时钟形态、午夜加宽、assistant 仅内容门控、轮次尾部 seq 门控,以及 user/assistant 分支按钮各自传递的事件 `seq`;Web e2e 场景钉住组装后的 IconActions chrome。
@@ -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/feature/2026-07-31-web-telemetry-default-mount.md
2026-07-31-web-telemetry-default-mount.md: 6c1fdaa8719ee01726b51db9a469ff659cbac476
2026-07-31-web-telemetry-default-mount.zh.md: b447832527ba9731097cd0776060db11ee4dfc30
@@ -0,0 +1,39 @@
# Agent Note: Default session-telemetry mount (OTel reporting) in the dsh web composition
Status: implemented
English | [中文](2026-07-31-web-telemetry-default-mount.zh.md)
## Problem
The telemetry seam and OTel backend ([revival Note](2026-07-23-session-telemetry-otel-revival.md)) had never been wired into any deployment composition since completion: no roster row, no switch, no cadence ruling, and zero observability over user sessions for the internal deployment. A deployment decision was needed: which surfaces report, to where, on what cadence, how to opt out, and how CI stays isolated.
## Decision
The shared `dsh` core (`apps/cli/config/base.cordis.yml`) mounts the `telemetry-otel` row by default with a baked-in production endpoint, so every surface — TUI, web, and headless — reports; this is the **internal-testing deployment stance** — reporting is on when an endpoint exists, and users opt out through the environment. Each surface's exit path drains the queue: web/headless dispose on SIGINT/SIGTERM (headless gained those handlers in this change), and the TUI's normal exit runs `disposeRootAndExit` (root dispose, 5s bounded — above the ~1s drain ceiling configured here) while its `/resume` handoff disposes the root before `execve`.
| Ruling | Value | Rationale |
|---|---|---|
| Mount surface | base.cordis.yml (TUI + web + headless) | One deployment stance for every surface; per-surface divergence would need a reason, and none exists |
| Endpoint | `DSH_TELEMETRY_OTLP_URL`, default `https://harness-telemetry.deepseeksvc.com/v1/logs` | Internal collector; the env override serves local/dev runs |
| Opt-out switch | any non-empty `DSH_TELEMETRY_DISABLED` (including `0`/`false`) disables | A privacy switch prefers off-by-mistake over on-by-mistake; a row can only be disabled at AppCLIEntry's patch layer (config has no disable semantic, and the switch must precede the load-time `exporter.url` validation) |
| Cadence | `processor.scheduledDelayMillis: 10000` (10s/batch) | Streaming while the session runs, never exit-time-only; a crash loses at most the last unexported interval |
| Exit-drain bound | `exporter.timeoutMillis: 1000` + `maxExportBatchSize: 2048` (== maxQueueSize) + `exportTimeoutMillis: 1500` | Dispose must release within ~1s against an unreachable collector: timeoutMillis doubles as the per-attempt socket timeout and the retry deadline (1s effectively disables the SDK's 5-try backoff), and aligning batch size with the queue cap makes the drain a single batch; SDK defaults can stall 40s+ |
| Compression | `compression: gzip` | Event bodies carry full content; cross-datacenter bandwidth |
| CI isolation | top-level `env: DSH_TELEMETRY_DISABLED: '1'` in all 8 GitHub workflows | Every CI channel that boots the web composition (e2e/snapshot/built smokes) must not stream test sessions to the production endpoint |
The keyless integration test `apps/cli/tests/telemetry-web.e2e.ts` pins the deployment-level behavior: an in-test OTLP collector plus a mock LLM server, a real `dsh web` boot, asserting ledger coverage, seq monotonicity, the first-of-step chunk projection, and the ops `shutdown` marker arriving through the SIGINT drain.
## Alternatives considered
**No default mount; deployments add the row themselves (continuing the SDK stance).** Rejected for this stage: this repo's web/headless composition IS the internal deployment, and default-on reporting is that deployment's product requirement; the SDK stance survives in the seam packages (unmounted = nothing leaves).
**A config field instead of an env patch for the switch.** Infeasible: cordis rows have no config-level disable semantic, and `exporter.url` validation fails loud at plugin construction, so the switch must take effect before the Loader — AppCLIEntry's patch layer is the only seat.
**A `Promise.race` timeout backstop around exit.** Deferred: the parameter set already bounds the worst-case drain to ~1.5-3s (typically <100ms), measured SIGINT-to-exit 110ms-1.1s; the unbounded drip-feed-response risk stays under observation, and on real evidence the race lands inside the backend's `shutdown()` (never the coordinator — that would decide loss semantics for every backend).
## Consequences
- A developer running `dsh web` without a local collector POSTs to the production endpoint every 10s (silent failure when unreachable; no OTel diag logger is registered); local development sets `DSH_TELEMETRY_DISABLED=1` or points `DSH_TELEMETRY_OTLP_URL` locally.
- **No redaction rule is mounted yet**: exports are the raw captured copy (full user/assistant message text, tool arguments and results, the system prompt, the local `session.cwd` path). Crossing a trust boundary requires `telemetry/record` rules first — the redaction rule, identity Resource attributes (hostname / anonymous user id / surface), and the usage-metrics track are the explicit follow-ups of this decision.
- Test rigs reusing this tree (e.g. `apps/web/tests/scaffold.ts`) must explicitly disable the row, or fixture sessions stream to whatever collector the environment happens to name.
@@ -0,0 +1,39 @@
# Agent Note: dsh web 组合默认挂载会话遥测(OTel 上报)
Status: implemented
[English](2026-07-31-web-telemetry-default-mount.md) | 中文
## Problem
遥测 seam 与 OTel backend[revival Note](2026-07-23-session-telemetry-otel-revival.md))自完成以来从未接入任何部署组合:没有 roster 行、没有开关、没有节奏口径,内部部署对用户会话零可观测。需要一个部署决策:哪些 surface 上报、报到哪、什么节奏、怎么关、CI 怎么隔离。
## Decision
`dsh` 共享核心(`apps/cli/config/base.cordis.yml`)默认挂载 `telemetry-otel` 行,内置生产 endpoint,因此所有 surface——TUI、web、headless——都上报;这是**内部测试期的部署立场**——有 endpoint 就报,用户可经环境变量退出。各 surface 的退出路径都会排空队列:web/headless 在 SIGINT/SIGTERM 上 disposeheadless 的信号处理是本次补上的),TUI 的正常退出走 `disposeRootAndExit`(根 dispose,5s 兜底——高于此处配置的 ~1s drain 上界),其 `/resume` 移交也在 `execve` 前 dispose 根。
| 决策项 | 取值 | 理由 |
|---|---|---|
| 挂载面 | base.cordis.ymlTUI + web + headless | 所有 surface 一个部署立场;按 surface 分化需要理由,而当前没有 |
| endpoint | `DSH_TELEMETRY_OTLP_URL`,缺省 `https://harness-telemetry.deepseeksvc.com/v1/logs` | 内部 collectorenv 覆盖供本地/联调 |
| 退出开关 | `DSH_TELEMETRY_DISABLED` 非空(含 `0`/`false`)即关 | 隐私向开关取「宁关勿误开」;行级 disable 只能在 AppCLIEntry 的 patch 层做(config 无 disable 语义,且必须先于 `exporter.url` 的加载期校验生效) |
| 上报节奏 | `processor.scheduledDelayMillis: 10000`(10s/批) | 流式回流,非退出才报;崩溃至多丢最后一个未导出间隔 |
| 退出 drain 上界 | `exporter.timeoutMillis: 1000` + `maxExportBatchSize: 2048== maxQueueSize` + `exportTimeoutMillis: 1500` | collector 不可达时 dispose 必须 ~1s 内放行:timeoutMillis 同时是单次 socket 超时与重试 deadline1s 等效关掉 SDK 5 次 backoff),批大小对齐队列上限使 drain 恒为单批;默认参数下最坏可卡 40s+ |
| 压缩 | `compression: gzip` | 事件 body 含全文,跨机房带宽 |
| CI 隔离 | 全部 8 个 GitHub workflow 顶层 `env: DSH_TELEMETRY_DISABLED: '1'` | CI 启动 web 组合的所有通道(e2e/snapshot/built smoke)不得向生产 endpoint 泄测试会话 |
集成测试 `apps/cli/tests/telemetry-web.e2e.ts`(keyless)钉住部署级行为:测试内 OTLP collector + mock LLM,真启动 `dsh web`,断言 ledger 覆盖、seq 单调、chunk 首条投影、以及 SIGINT drain 后 ops `shutdown` 标记到达。
## Alternatives considered
**默认不挂载,部署方自行加行(SDK 立场的延续)。** 否决于当前阶段:本仓的 web/headless 组合就是内部部署本身,「上报默认开」是这个部署的产品要求;SDK 立场仍由 seam 包保持(不挂 = 零外发)。
**开关做成 config 字段而非 env patch。** 不可行:cordis 行没有 config 层的 disable 语义,且 `exporter.url` 校验在插件构造期 fail-loud,开关必须在 Loader 之前生效——AppCLIEntry patch 层是唯一落点。
**退出时 `Promise.race` 兜底超时。** 暂缓:参数组合已把最坏 drain 压到 ~1.5-3s(典型 <100ms),实测 SIGINT→退出 110ms-1.1sdrip-feed 慢滴响应的无界等待风险留观,出现实证再在 backend `shutdown()` 内加 race(不放 coordinator——那会替所有 backend 决定丢失语义)。
## Consequences
- 无本地 collector 的开发者跑 `dsh web` 会对生产 endpoint 每 10s 发一次 POST(联不通则静默失败,OTel diag logger 未注册);本地开发设 `DSH_TELEMETRY_DISABLED=1``DSH_TELEMETRY_OTLP_URL` 指本地。
- **当前零脱敏规则挂载**:导出即原始捕获副本(用户/助手消息全文、工具参数与结果、system prompt、`session.cwd` 本地路径)。跨信任边界前必须挂 `telemetry/record` 规则——脱敏规则、身份 Resource 维度(hostname/匿名 user id/surface)、使用数据 metrics 轨三件是本决策明确的后续工作。
- 复用这棵树的测试载具(如 `apps/web/tests/scaffold.ts`)须显式关停该行,否则 fixture 会话会流向 env 里碰巧存在的 collector。
@@ -1,6 +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
2026-07-25-client-settings-locale-theme.md: 87077b3fd3f0bd8a3375a71aebf947cbd9961799
2026-07-25-client-settings-locale-theme.zh.md: a64a4afdf6565a527a25136694aa79305eeabb3c
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-07-25-client-settings-locale-theme.md
2026-07-25-client-settings-locale-theme.md: c86d6ac053f7bb87ce758613a5f3a0a34951e428
2026-07-25-client-settings-locale-theme.zh.md: 05edbb3c550828832a390e3cf4fad3262b5be196
@@ -55,7 +55,7 @@ root
└─ models (order 10) ui-models 注册
```
Section and item contributions both use declaration-aware deferral (ui-slots' `deferRegistration()`: ledger-judged presence, `refresh()` for localized labels, one-call disposal) and do not depend on the client manifest's apply order. The SlotMap types split homes: trigger/header/close/section have their canonical home in the ui-settings contract (the consumers, general and models, both depend on the shell — no cycle); `settings.general.item`'s canonical home is the locale package — it is the lowest common dependency of all item registrants (a settings row always carries copy), while the declarer general's contract is unreachable from locale/ui-theme (it would form a cycle); ui-theme consumes it through a re-export seam.
Section and item contributions both use declaration-aware deferral (ui-slots' `deferRegistration()`: ledger-judged presence, one-call disposal; localized labels ride the label thunk from the [full-rollout note](../../implemented/architecture/2026-07-30-client-locale-full-rollout.md), not `refresh()`) and do not depend on the client manifest's apply order. The SlotMap types split homes: trigger/header/close/section have their canonical home in the ui-settings contract (the consumers, general and models, both depend on the shell — no cycle); `settings.general.item`'s canonical home is the locale package — it is the lowest common dependency of all item registrants (a settings row always carries copy), while the declarer general's contract is unreachable from locale/ui-theme (it would form a cycle); ui-theme consumes it through a re-export seam.
### Future work: promote slot declarations to first-class injectable waits
@@ -55,7 +55,7 @@ root
└─ models (order 10) ui-models 注册
```
section/item contribution 均使用 declaration-aware deferralui-slots 的 `deferRegistration()`ledger 判在位、`refresh()` 换本地化 label、一键 dispose),不依赖 client manifest 的 apply 顺序。SlotMap 类型分家:trigger/header/close/section 正家在 ui-settings contract(消费者 general/models 均依赖壳,无环);`settings.general.item` 正家在 locale 包——它是全部 item 注册方的最低公共依赖(设置行必带文案),而声明方 general 的 contract 对 locale/ui-theme 不可达(会成环);ui-theme 经 re-export seam 消费。
section/item contribution 均使用 declaration-aware deferralui-slots 的 `deferRegistration()`ledger 判在位、一键 dispose;本地化 label 走 [全量接入 Note](../../implemented/architecture/2026-07-30-client-locale-full-rollout.md) 的 label thunk,不再 `refresh()`),不依赖 client manifest 的 apply 顺序。SlotMap 类型分家:trigger/header/close/section 正家在 ui-settings contract(消费者 general/models 均依赖壳,无环);`settings.general.item` 正家在 locale 包——它是全部 item 注册方的最低公共依赖(设置行必带文案),而声明方 general 的 contract 对 locale/ui-theme 不可达(会成环);ui-theme 经 re-export seam 消费。
### Future work:坑位声明升格为可 inject 的一等等待物
@@ -28,6 +28,11 @@ concurrency:
permissions:
contents: read
env:
# CI runs must never report to the production telemetry endpoint baked
# into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
DSH_TELEMETRY_DISABLED: '1'
jobs:
# Job-level conditions cannot inspect `matrix`, so validate target names and
# construct the matrix before the dependent jobs.
+3
View File
@@ -24,6 +24,9 @@ permissions:
env:
PRIMARY_NODE_VERSION: '24'
# CI runs must never report to the production telemetry endpoint baked
# into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
DSH_TELEMETRY_DISABLED: '1'
jobs:
+3
View File
@@ -23,6 +23,9 @@ permissions:
env:
PRIMARY_NODE_VERSION: '24'
# CI runs must never report to the production telemetry endpoint baked
# into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
DSH_TELEMETRY_DISABLED: '1'
jobs:
build:
+5
View File
@@ -46,6 +46,11 @@ concurrency:
permissions:
contents: read
env:
# CI runs must never report to the production telemetry endpoint baked
# into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
DSH_TELEMETRY_DISABLED: '1'
jobs:
e2e:
runs-on: ubuntu-latest
+5
View File
@@ -10,6 +10,11 @@ on:
permissions:
contents: read
env:
# CI runs must never report to the production telemetry endpoint baked
# into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
DSH_TELEMETRY_DISABLED: '1'
jobs:
expected-filenames:
name: no golden filenames
+5
View File
@@ -19,6 +19,11 @@ concurrency:
permissions:
contents: read
env:
# CI runs must never report to the production telemetry endpoint baked
# into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
DSH_TELEMETRY_DISABLED: '1'
defaults:
run:
working-directory: native/landlock-run
+5
View File
@@ -19,6 +19,11 @@ on:
permissions:
contents: read
env:
# CI runs must never report to the production telemetry endpoint baked
# into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
DSH_TELEMETRY_DISABLED: '1'
jobs:
e2e:
runs-on: ubuntu-latest
+5
View File
@@ -19,6 +19,11 @@ concurrency:
permissions:
contents: read
env:
# CI runs must never report to the production telemetry endpoint baked
# into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
DSH_TELEMETRY_DISABLED: '1'
jobs:
# Keyless real-kernel sandbox proofs (sandbox Agent Note § Testing): each ladder
# rung is only provable on a host where it enforces, so this job fans out
+2 -2
View File
@@ -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 apps/cli/README.md
README.md: d783d75cc9747d13887386fcf7609a6778e5dfb5
README.zh.md: 3f5ce7e7a3a302fd9e255c1042ccb7b03deb59d9
README.md: e56b726029c5bba9ba769c6dd3493d913f0129d7
README.zh.md: 24ff9a6e8d48016d213e877e23768332d86cccde
+2
View File
@@ -24,6 +24,8 @@ The shipped TUI and Web compositions register the native DeepSeek adapter plus p
`DSH_TOOLS_MODE` selects the tool presentation mode for the whole Web/headless process: `native` (the schema default when unset), `code` (the `run_code`-only Code Mode wire), or `both`; any other value fails loud at boot through the `dsh-tools` config schema. It is a TEMPORARY seam — process-wide because Loader composition is static — and is removed once the web UI owns per-session tool-mode selection; the TUI surface ignores it (its config tree pins its own mode).
Every `dsh` surface — TUI, Web, and headless — reports session telemetry by default (the row lives in the shared `base.cordis.yml`): every session-log event streams as OTLP/HTTP log records to `https://harness-telemetry.deepseeksvc.com/v1/logs` on a 10-second batch cadence. `DSH_TELEMETRY_OTLP_URL` points the exporter at a different collector; setting `DSH_TELEMETRY_DISABLED` to ANY non-empty value — including `0` or `false` — disables the row before it loads (a privacy switch prefers off-by-mistake over on-by-mistake). No redaction rule is mounted in this composition yet: exported records are the raw captured copy, including message text, tool arguments and results, and the session's working-directory path. The deployment rulings live in the [web-telemetry-default-mount Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md).
## Install (developer machine)
Symlink the source-running launcher onto your PATH; it resolves the checkout through its own real path, so code changes apply on the next launch with no build step:
+2
View File
@@ -24,6 +24,8 @@ Web 和无头界面启动 `base.cordis.yml` 与 `web.cordis.yml`,随后应用
`DSH_TOOLS_MODE` 为整个 Web/无头进程选择工具呈现模式:可选值为 `native`(未设置时的 schema 默认值)、`code`(仅含 `run_code` 的 Code Mode 协议接口)或 `both`;任何其他值都会经由 `dsh-tools` 配置 schema 在启动时明确报错。它是一个临时 seam:Loader 组合是静态的,因此该设置作用于整个进程;待 Web UI 负责逐会话工具模式选择后便会移除。TUI 界面会忽略该变量(其配置树固定了自身模式)。
每个 `dsh` 界面——TUI、Web 与无头——都默认上报会话遥测(该行位于共享的 `base.cordis.yml`):每条会话日志事件以 OTLP/HTTP 日志记录的形式、按 10 秒批处理节奏流向 `https://harness-telemetry.deepseeksvc.com/v1/logs``DSH_TELEMETRY_OTLP_URL` 可将 exporter 指向其他 collector;将 `DSH_TELEMETRY_DISABLED` 设为**任意非空值**——包括 `0``false`——都会在该行加载前将其关停(隐私开关取「宁可误关、不可误开」)。该组合当前未挂载任何脱敏规则:导出记录即原始捕获副本,包含消息正文、工具参数与结果、以及会话工作目录路径。部署口径见 [web-telemetry-default-mount Agent Note](../../.agents/notes/implemented/feature/2026-07-31-web-telemetry-default-mount.md)。
## 安装(开发机)
将从源码运行的启动器符号链接到 PATH 上;它通过自身真实路径解析 checkout,因此代码更改会在下次启动时生效,无需构建:
+3
View File
@@ -38,6 +38,8 @@ flowchart LR
cfg --> plugin_tui_session_persistence_jsonl
plugin_tui_session_query_sqlite["session-query-sqlite<br/>@deepseek-ai/dsh-session-query-sqlite"]
cfg --> plugin_tui_session_query_sqlite
plugin_tui_telemetry_otel["telemetry-otel<br/>@deepseek-ai/dsh-session-telemetry-otel"]
cfg --> plugin_tui_telemetry_otel
plugin_tui_subprocess["subprocess<br/>@deepseek-ai/dsh-subprocess-local"]
cfg --> plugin_tui_subprocess
plugin_tui_bash_local["bash-local<br/>@deepseek-ai/dsh-bash-local"]
@@ -123,6 +125,7 @@ flowchart LR
| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
| `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
| `session-query-sqlite` | `@deepseek-ai/dsh-session-query-sqlite` |
| `telemetry-otel` | `@deepseek-ai/dsh-session-telemetry-otel` |
| `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
| `bash-local` | `@deepseek-ai/dsh-bash-local` |
| `tool-bash` | `@deepseek-ai/dsh-tool-bash` |
+32
View File
@@ -94,6 +94,38 @@
config:
path: !!js launcherSessionQueryPath ?? './.sessions/session-query.db'
# Session telemetry, on for every dsh surface: mirrors every session-log
# event (assistant/chunk projected to first-of-step) plus ops markers onto
# OTLP/HTTP log records, streaming on the batch processor's cadence
# (10s/batch here) — not at exit; a crash loses at most the last unexported
# interval. No telemetry/record redaction rule is mounted yet, so exports
# are the raw captured copy; the deployment stance, env seams, and
# follow-ups are pinned in the web-telemetry-default-mount Agent Note.
# DSH_TELEMETRY_OTLP_URL overrides the production endpoint, and a non-empty
# DSH_TELEMETRY_DISABLED — any value, including '0'/'false' — opts the
# process out (the launchers patch the row disabled; config cannot disable
# a row). The exporter/processor values bound the shutdown drain to ~1s
# against an unreachable collector: exporter.timeoutMillis is both the
# per-attempt socket timeout and the retry deadline (1s effectively
# disables the SDK's 5-try backoff), maxExportBatchSize == maxQueueSize
# (both explicit) makes the drain a single batch, and exportTimeoutMillis
# is the processor's own cap on that one export cycle — the second bound
# when the exporter's clock alone does not fire. Every surface's exit path
# drains it: web/headless dispose on SIGINT/SIGTERM, and the TUI's normal
# exit and /resume handoff both dispose the root.
- id: telemetry-otel
name: '@deepseek-ai/dsh-session-telemetry-otel'
config:
exporter:
url: !!js process.env.DSH_TELEMETRY_OTLP_URL ?? 'https://harness-telemetry.deepseeksvc.com/v1/logs'
compression: gzip
timeoutMillis: 1000
processor:
scheduledDelayMillis: 10000
maxQueueSize: 2048
maxExportBatchSize: 2048
exportTimeoutMillis: 1500
- id: subprocess
name: '@deepseek-ai/dsh-subprocess-local'
+2 -2
View File
@@ -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
+3
View File
@@ -81,6 +81,7 @@
"@deepseek-ai/dsh-session-projection-cache": "workspace:^",
"@deepseek-ai/dsh-session-query-sqlite": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-session-telemetry-otel": "workspace:^",
"@deepseek-ai/dsh-session-title": "workspace:^",
"@deepseek-ai/dsh-session-title-first-message-llm": "workspace:^",
"@deepseek-ai/dsh-settings-local": "workspace:^",
@@ -123,7 +124,9 @@
"js-yaml": "^4.2.0"
},
"devDependencies": {
"@deepseek-ai/dsh-llm-mock-server": "workspace:^",
"@types/js-yaml": "^4.0.9",
"execa": "^10.0.0",
"node-pty": "1.1.0"
}
}
+41
View File
@@ -24,6 +24,9 @@ import type {} from '@deepseek-ai/dsh-host-webserver'
const PROFILE_DIR = '.dsh-tmp-profile'
const PROFILE_FILE = 'config.json'
/** The session-telemetry row id the DSH_TELEMETRY_DISABLED switch targets (mounted in web.cordis.yml). */
const TELEMETRY_ROW_ID = 'telemetry-otel'
/** The webserver schema's all-interfaces bind literal: gates LAN-authority derivation here and the printed LAN URL in web.ts. */
const ALL_INTERFACES_HOST = '0.0.0.0'
@@ -59,6 +62,38 @@ export function resolveLanTrust(
return { lanAddresses, trustedHosts: [...lanAddresses, ...extra] }
}
/**
* Resolve the telemetry opt-out switch into its boot patch. ANY non-empty
* value (including `'0'`/`'false'`) disables: a privacy switch prefers
* off-by-mistake over on-by-mistake. Throws when the switch is set but the
* row is absent — a silently no-op "disabled" privacy switch would keep
* exporting while the user believes it is off.
* @param disabledEnv - the raw `DSH_TELEMETRY_DISABLED` value (`undefined` when unset).
* @param hasRow - whether the composition carries the {@link TELEMETRY_ROW_ID} row.
* @returns the disable patch, or `undefined` when telemetry stays enabled.
*/
export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: boolean): PatchOptions | undefined {
if ((disabledEnv ?? '') === '') return undefined
if (!hasRow) {
throw new Error(`dsh: DSH_TELEMETRY_DISABLED is set but row "${TELEMETRY_ROW_ID}" is not in this composition`)
}
return { id: TELEMETRY_ROW_ID, disabled: true }
}
/**
* Whether a config file carries the telemetry row, parsed under the same
* `!!js`-tolerant dialect the boot uses — the `hasRow` input for launchers
* that compose their patch lists outside {@link AppCLIEntry} (the TUI).
* @param file - absolute path of the config or overlay file.
* @returns true when a top-level (or inserted) row has the telemetry id.
*/
export function configHasTelemetryRow(file: string): boolean {
const doc = yaml.load(readFileSync(file, 'utf8'), { schema: includeYamlSchema })
if (!Array.isArray(doc)) throw new Error(`dsh: ${file} is not a top-level entry list`)
return (doc as { id?: string; insert?: { id?: string }[] }[]).some(row =>
row.id === TELEMETRY_ROW_ID || (row.insert ?? []).some(inserted => inserted.id === TELEMETRY_ROW_ID))
}
/** One profile-json key mapped onto a yml row's config field. */
interface ProfileMapping {
jsonPath: string
@@ -203,6 +238,12 @@ export class AppCLIEntry {
if (yml === undefined) throw new Error(`dsh: patch target row "${id}" not found in ${this.options.configPath}`)
return { id, config: { ...(yml.config ?? {}) as Record<string, unknown>, ...bag } }
})
// Telemetry opt-out: a row can only be turned off at the patch layer
// (config cannot disable an entry), and the switch must hold BEFORE the
// plugin constructs — its exporter.url validation is load-time fail-loud.
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
if (telemetryPatch !== undefined) this.patches.push(telemetryPatch)
}
/** Shared Loader boot; the dev HMR row mounts before await so the fail-loud sweep covers it. */
+11
View File
@@ -82,6 +82,17 @@ export async function runHeadless(task: string): Promise<void> {
})
const { ctx, port } = await entry.run()
const dispose = async (): Promise<void> => { await ctx.fiber.dispose() }
// Signal exits must still dispose the tree: the composition mounts
// exit-drained plugins (telemetry's queued tail and shutdown marker would
// otherwise be lost), and Node's default signal exit skips disposal.
let signalled = false
const disposeAndExit = (code: number): void => {
if (signalled) return
signalled = true
void dispose().finally(() => { process.exit(code) })
}
process.on('SIGTERM', () => { disposeAndExit(143) })
process.on('SIGINT', () => { disposeAndExit(130) })
// The headless session is web-observable while it runs (same composition).
process.stderr.write(`dsh: observing at http://127.0.0.1:${String(port)}\n`)
const api = new InProcessApiClient(toFetchHandler(ctx.apiProxy))
+17 -6
View File
@@ -31,6 +31,7 @@ import {
resolveConfigPath,
} from '@deepseek-ai/dsh-app-boot'
import { SessionId } from '@deepseek-ai/dsh-session'
import { configHasTelemetryRow, resolveTelemetryPatch } from './app-cli-entry.ts'
import { SESSION_QUERY_SQLITE_PATH_KEY } from '@deepseek-ai/dsh-session-query-sqlite'
import { CONFIGURED_AGENT_IDENTITIES_KEY } from '@deepseek-ai/dsh-agent-loop'
import type { Context } from 'cordis'
@@ -196,16 +197,26 @@ export async function runTui(
// demo or test config would silently run on the user's provider and model.
// `--config-replace` additionally discards the base and the surface overlay.
const replaceTree = configReplace !== undefined
const patches = replaceTree ? [] : [
...loadOverlayPatches(NAME, TUI_OVERLAY),
...resolvedConfig === undefined
? loadPersonalPatches(NAME) ?? []
: loadOverlayPatches(NAME, resolveConfigPath(resolvedConfig, undefined)),
const bootConfig = resolvedConfigReplace === undefined ? BASE_CONFIG : resolveConfigPath(resolvedConfigReplace, undefined)
// Same opt-out semantics as the web surface (resolveTelemetryPatch: any
// non-empty value disables; setting the switch against a tree without the
// row fails loud rather than silently no-opping a privacy switch). The row
// presence is checked against the tree actually booting, so a
// --config-replace tree is judged on its own rows, not the shipped base's.
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, configHasTelemetryRow(bootConfig))
const patches = [
...replaceTree ? [] : [
...loadOverlayPatches(NAME, TUI_OVERLAY),
...resolvedConfig === undefined
? loadPersonalPatches(NAME) ?? []
: loadOverlayPatches(NAME, resolveConfigPath(resolvedConfig, undefined)),
],
...telemetryPatch === undefined ? [] : [telemetryPatch],
]
const queryIndexPath = join(tmpdir(), SESSION_QUERY_DB)
const ctx = await boot(
NAME,
resolvedConfigReplace === undefined ? BASE_CONFIG : resolveConfigPath(resolvedConfigReplace, undefined),
bootConfig,
patches,
(hostCtx) => {
// The launcher owns session identity and the exit line: a config-mounted
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'
import { resolveTelemetryPatch } from '../src/app-cli-entry.ts'
describe('resolveTelemetryPatch', () => {
it('keeps telemetry enabled when the switch is unset or empty', () => {
expect(resolveTelemetryPatch(undefined, true)).toBeUndefined()
expect(resolveTelemetryPatch('', true)).toBeUndefined()
})
it('disables on ANY non-empty value, including falsy-looking ones', () => {
for (const value of ['1', '0', 'false', 'no']) {
expect(resolveTelemetryPatch(value, true)).toEqual({ id: 'telemetry-otel', disabled: true })
}
})
it('fails loud when the switch is set but the row is absent', () => {
expect(() => resolveTelemetryPatch('1', false)).toThrow('DSH_TELEMETRY_DISABLED is set but row "telemetry-otel" is not in this composition')
})
it('ignores a missing row while the switch is unset', () => {
expect(resolveTelemetryPatch(undefined, false)).toBeUndefined()
})
})
+3 -1
View File
@@ -130,7 +130,9 @@ function smoke(overrides: Partial<TuiPtySmokeOptions> & { label: string }): Prom
tempDirPrefix: 'dsh-tui-smoke-',
binScript: dshBinScript,
tsconfigPath,
env: { DEEPSEEK_API_KEY: 'keyless-tui-no-call' },
// Telemetry now mounts in the shared base: keep fixture sessions from
// POSTing to the production endpoint when run outside CI's workflow env.
env: { DEEPSEEK_API_KEY: 'keyless-tui-no-call', DSH_TELEMETRY_DISABLED: '1' },
// Artifact CI builds and smokes concurrently on a contended runner.
...(process.env.DSH_EXAMPLE_MODE === 'lib' ? { timeoutMs: 60_000 } : {}),
...overrides,
+1
View File
@@ -3,6 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>DeepSeek Harness</title>
</head>
<body>
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="50.000000" height="50.000000" viewBox="0 0 50 50" fill="none">
<path id="path" d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z" fill="#000" fill-opacity="1.000000" fill-rule="nonzero"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

+22 -5
View File
@@ -11,7 +11,24 @@ import {
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, saveFailureShot } from './support.ts'
import { saveFailureShot } from './support.ts'
/**
* connectFreshWorkspace twin over the product default Chinese locale (the
* shared helper's anchors assume the English page every other scenario
* boots; this scenario deliberately keeps zh, so the localized picker
* copy is the anchor set).
*/
async function connectFreshWorkspaceZh(page: Page, name = 'workspace'): Promise<void> {
await page.getByRole('button', { name: '选择工作区' }).click()
await page.getByRole('menuitem', { name: '新建工作区' }).click()
const dialog = page.getByRole('dialog', { name: '新建工作区' })
await dialog.waitFor({ timeout: 10_000 })
await dialog.getByLabel('新工作区名称').fill(name)
await dialog.getByRole('button', { name: '创建工作区' }).click()
await page.locator('textarea:enabled[placeholder="描述你想要构建的内容"]')
.waitFor({ timeout: 15_000 })
}
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/access-confirmation', import.meta.url))
const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
@@ -36,7 +53,7 @@ describe('web e2e: Full access confirmation', () => {
tripwire = watchConsole(page)
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
await connectFreshWorkspace(page)
await connectFreshWorkspaceZh(page)
}, 120_000)
afterAll(async () => {
@@ -46,7 +63,7 @@ describe('web e2e: Full access confirmation', () => {
it('requires acknowledgement before the composer picker can enable Full access', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-full-access-confirmation'))
const access = page.locator('button[aria-label^="Access mode"]').first()
const access = page.locator('button[aria-label^="访问模式"]').first()
await access.waitFor({ timeout: 10_000 })
// Normalize the starting preset through the real command path. The
@@ -55,7 +72,7 @@ describe('web e2e: Full access confirmation', () => {
await access.click()
await page.getByRole('menuitem', { name: 'Workspace Write' }).click()
await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 })
.toBe('Access mode, current: Workspace Write')
.toBe('访问模式,当前:Workspace Write')
}
await access.click()
@@ -75,7 +92,7 @@ describe('web e2e: Full access confirmation', () => {
expect(await enable.isEnabled()).toBe(true)
await enable.click()
await expect.poll(() => access.getAttribute('aria-label'), { timeout: 10_000 })
.toBe('Access mode, current: Full access')
.toBe('访问模式,当前:Full access')
expect(await dialog.count()).toBe(0)
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
+1 -1
View File
@@ -151,7 +151,7 @@ describe('web e2e: approval takeover keeps its actions reachable', () => {
await page.setViewportSize(original)
}
await panel.getByRole('button', { name: '允许一次' }).click()
await panel.getByRole('button', { name: 'Allow once' }).click()
const sessionId = await settled
if (MODE === 'record') {
+3
View File
@@ -60,6 +60,9 @@ let unmount: (() => void) | undefined
beforeEach(() => {
localStorage.clear()
// English pinned before boot: role/text locators stay deterministic across
// localized component migrations (the newEnglishPage e2e convention).
localStorage.setItem('dsh.locale', 'en')
document.title = 'DeepSeek Harness'
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
@@ -98,7 +98,7 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
expect(await page.getByText('Details', { exact: true }).isVisible()).toBe(false)
await compareOrRefreshGolden(HANDLES_EXPECTED, await handleSnapshot(page), MODE)
const sidebarBefore = await sidebarTrack(page)
@@ -118,18 +118,18 @@ describe.skipIf(MODE === 'record')('web e2e: details panel follows the current S
await appFrame(page).waitFor({ timeout: 30_000 })
await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
expect(await page.getByText('Details', { exact: true }).isVisible()).toBe(false)
await page.getByRole('button', { name: /^(?:New session|新.*会话)$/ }).last().click()
await page.getByText("Let's start building", { exact: false }).waitFor({ timeout: 15_000 })
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
expect(await page.getByText('Details', { exact: true }).isVisible()).toBe(false)
const original = page.locator('[role=treeitem]').filter({ hasText: 'Reply with the single word' }).first()
await original.click()
await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
expect(await page.getByText('Details', { exact: true }).isVisible()).toBe(false)
const ungrouped = page.getByText('Ungrouped', { exact: true })
const ungroupedRow = ungrouped.locator('..').locator('..')
+7 -7
View File
@@ -64,14 +64,14 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
await expect.poll(() => page.getByText('DONE', { exact: true }).count(), { timeout: 15_000 }).toBe(1)
// Focus-reveal the footers (hover:hover keeps them opacity-hidden until
// hover/focus-within). User has three actions; each finalized assistant
// text node has copy + branch.
const copyButtons = page.getByRole('button', { name: '复制' })
// hover/focus-within). User has three actions; each turn's last content
// assistant has copy + branch.
const copyButtons = page.getByRole('button', { name: 'Copy' })
await expect.poll(() => copyButtons.count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(2)
await copyButtons.first().focus()
await expect.poll(() => page.getByRole('button', { name: '在新对话中分支' }).count(), { timeout: 5_000 })
await expect.poll(() => page.getByRole('button', { name: 'Branch into a new conversation' }).count(), { timeout: 5_000 })
.toBeGreaterThanOrEqual(2)
await expect.poll(() => page.getByRole('button', { name: '编辑' }).count(), { timeout: 5_000 }).toBe(1)
await expect.poll(() => page.getByRole('button', { name: 'Edit' }).count(), { timeout: 5_000 }).toBe(1)
}, 60_000)
it.skipIf(MODE === 'record')('matches the conversation aria golden with IconActions and clocks', async () => {
@@ -81,7 +81,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
}).waitFor({ timeout: 10_000 })
// Keep a footer focused so opacity-hidden actions stay in the a11y tree
// as an active/focused control during the capture.
await page.getByRole('button', { name: '复制' }).first().focus()
await page.getByRole('button', { name: 'Copy' }).first().focus()
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
.split(SEED_ID).join('{{seededId}}')
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
@@ -91,7 +91,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-fork'))
// Exercise the assistant action specifically; package coverage pins the
// user action separately at its own event seq.
await page.getByRole('button', { name: '在新对话中分支' }).last().click()
await page.getByRole('button', { name: 'Branch into a new conversation' }).last().click()
await expect.poll(
() => scaffold.ctx.agents.list().find(agent => agent.session.header.parentSession === SessionId(SEED_ID)),
{ timeout: 15_000 },
+2 -2
View File
@@ -253,7 +253,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
}
})
expect(dot.state).toBe('done')
expect(dot.label).toBe('已完成')
expect(dot.label).toBe('Done')
expect(dot.beforePrompt).toBe(true)
expect(dot.insideCard).toBe(true)
expect(dot.leftOfPrompt).toBe(true)
@@ -270,7 +270,7 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'])
await card.locator('[class*="_copyButton_"]').first().click()
await expect.poll(() => card.locator('[class*="_copyButton_"]').first().textContent(), { timeout: 5_000 })
.toBe('复制成功')
.toBe('Copied')
expect(await page.evaluate(() => navigator.clipboard.readText())).toContain('NAVIGATION_OK')
}, 60_000)
+7 -7
View File
@@ -81,7 +81,7 @@ describe('web e2e: queue row actions', () => {
await input.fill(text)
await input.press('Enter')
}
const queueHeader = page.getByRole('button', { name: '2 条排队消息' })
const queueHeader = page.getByRole('button', { name: '2 queued messages' })
await expect.poll(() => queueHeader.getAttribute('aria-expanded'), { timeout: 10_000 })
.toBe('false')
const collapsedSnapshot = await captureStableAria(
@@ -92,21 +92,21 @@ describe('web e2e: queue row actions', () => {
await compareOrRefreshGolden(COLLAPSED_EXPECTED, collapsedSnapshot, MODE)
await queueHeader.click()
await expect.poll(
() => page.getByRole('button', { name: '删除排队消息' }).count(),
() => page.getByRole('button', { name: 'Remove queued message' }).count(),
{ timeout: 10_000 },
).toBe(2)
const editRow = page.getByText(EDIT, { exact: true }).locator('..')
await editRow.getByRole('button', { name: '编辑排队消息' }).click()
const editor = page.getByRole('textbox', { name: '编辑排队消息' })
await editRow.getByRole('button', { name: 'Edit queued message' }).click()
const editor = page.getByRole('textbox', { name: 'Edit queued message' })
await editor.fill(EDITED)
const editingSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(EDITING_EXPECTED, editingSnapshot, MODE)
await page.getByRole('button', { name: '保存排队消息' }).click()
await page.getByRole('button', { name: 'Save queued message' }).click()
await page.getByText(EDITED, { exact: true }).waitFor()
const removeRow = page.getByText(REMOVE, { exact: true }).locator('..')
await removeRow.getByRole('button', { name: '删除排队消息' }).click()
await removeRow.getByRole('button', { name: 'Remove queued message' }).click()
await expect.poll(() => page.getByText(REMOVE, { exact: true }).count()).toBe(0)
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
@@ -116,7 +116,7 @@ describe('web e2e: queue row actions', () => {
expect(tripwire.warnings).toEqual([])
const editedRow = page.getByText(EDITED, { exact: true }).locator('..')
await editedRow.getByRole('button', { name: '删除排队消息' }).click()
await editedRow.getByRole('button', { name: 'Remove queued message' }).click()
await expect.poll(() => page.getByText(EDITED, { exact: true }).count()).toBe(0)
await page.getByRole('button', { name: 'Stop generating' }).click()
await settled
+4
View File
@@ -221,6 +221,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
// workspace, keeping the composition untouched.
{ id: 'workspace-context', disabled: true },
{ id: 'session-title-llm', disabled: true },
// Fixture sessions must never leave the process: the shipped row defaults
// to the production OTLP endpoint (or whatever DSH_TELEMETRY_OTLP_URL
// names in the ambient environment).
{ id: 'telemetry-otel', disabled: true },
{ id: 'webserver', config: { host: '127.0.0.1', port: 0, distIndex: DIST_INDEX } },
{ id: 'settings', config: { dshHome: harnessHome } },
{ id: 'credentials', config: { dshHome: harnessHome } },
+3 -3
View File
@@ -147,7 +147,7 @@ describe('web e2e: seeded history renders through cold resume', () => {
}],
},
}))
await page.getByRole('button', { name: '上下文注入' }).waitFor({ timeout: 10_000 })
await page.getByRole('button', { name: 'Context injection' }).waitFor({ timeout: 10_000 })
}, 60_000)
it.skipIf(MODE === 'record')('matches the historical conversation aria golden', async () => {
@@ -165,7 +165,7 @@ describe('web e2e: seeded history renders through cold resume', () => {
it.skipIf(MODE === 'record')('matches the Figma context disclosure geometry', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-context-injection'))
const disclosure = page.getByRole('button', { name: '上下文注入' })
const disclosure = page.getByRole('button', { name: 'Context injection' })
expect(await disclosure.getAttribute('aria-expanded')).toBe('false')
const collapsedIcon = disclosure.locator('svg').first()
const collapsedIconBox = await collapsedIcon.boundingBox()
@@ -238,7 +238,7 @@ describe('web e2e: seeded history renders through cold resume', () => {
// where neither half repeats the other (the dispatched `/` and its
// argument stay out of the title, and the settlement text never restates
// the command's own name).
await page.getByRole('button', { name: 'Access mode, current: Danger Full Access' }).click()
await page.getByRole('button', { name: 'Access mode, current: Full access' }).click()
await page.getByRole('menuitem', { name: 'Workspace Write' }).click()
await page.getByRole('button', { name: 'Access mode, current: Workspace Write' }).waitFor({ timeout: 10_000 })
// Scoped to the row itself, so unrelated page text that happens to read
@@ -1,4 +1,4 @@
- text: 等待审批
- group "审批详情": "escalate sandbox to workspace-write: Need to write the notes.txt file as requested by the user. echo 'tok63z tokc7y tokibx tokofw tokujv tok10nu tok16rt tok1cvs tok1izr tok1p3q tok1v7p tok21bo tok2a4 tok8e3 tokei2 tokkm1 tokqq0 tokwtz tok12xy tok191x tok1f5w tok1l9v tok1rdu tok1xht tok23ls tok4k8 tokao7 tokgs6 tokmw5 tokt04 tokz43 tok1582 tok1bc1 tok1hg0 tok1njz tok1tny tok1zrx tokqd tok6uc tokcyb tokj2a tokp69 tokva8 tok11e7 tok17i6 tok1dm5 tok1jq4 tok1pu3 tok1vy2 tok2221 tok30h tok94g tokf8f toklce tokrgd tokxkc tok13ob tok19sa tok1fw9 tok1m08 tok1s47 tok1y86 tok24c5 tok5al tokbek tokhij toknmi toktqh tokzug tok15yf tok1c2e tok1i6d tok1oac tok1ueb tok20ia tok1gq tok7kp tokdoo tokjsn tokpwm tokw0l tok124k tok188j tok1eci tok1kgh tok1qkg tok1wof tok22se tok3qu tok9ut tokfys tokm2r toks6q tokyap tok14eo tok1ain tok1gmm tok1mql tok1suk tok1yyj tok252i tok60y tokc4x toki8w tokocv tokugu tok10kt tok16os tok1csr tok1iwq tok1p0p tok1v4o tok218n tok273 tok8b2 tokef1 tokkj0 tokqmz tokwqy tok12ux tok18yw tok1f2v tok1l6u tok1rat tok1xes tok23ir tok4h7 tokal6 tokgp5 tokmt4 toksx3 tokz12 tok1551 tok1b90 tok1hcz tok1ngy tok1tkx tok1zow toknc tok6rb tokcva tokiz9 tokp38 tokv77 tok11b6 tok17f5 tok1dj4 tok1jn3 tok1pr2 tok1vv1 tok21z0 tok2xg tok91f tokf5e tokl9d tokrdc tokxhb tok13la tok19p9 tok1ft8 tok1lx7 tok1s16 tok1y55 tok2494 tok57k tokbbj tokhfi toknjh tokktng tokzrf tok15ve tok1bzd tok1i3c tok1o7b tok1uba tok20f9 tok1dp tok7ho tokdln tokjpm tokptl tokvxk tok121j tok185i tok1e9h tok1kdg tok1qhf tok1wle tok22pd tok3nt tok9rs tokfvr toklzq toks3p toky7o tok14bn tok1afm tok1gjl tok1mnk tok1srj tok1yvi tok24zh tok5xx tokc1w toki5v toko9u tokudt tok10hs tok16lr tok1cpq tok1itp tok1oxo tok1v1n tok215m tok242 tok881 tokec0 tokfz tokqjy tokwnx' > notes.txt"
- button "拒绝"
- button "允许一次"
- text: Waiting for approval
- group "Approval details": "escalate sandbox to workspace-write: Need to write the notes.txt file as requested by the user. echo 'tok63z tokc7y tokibx tokofw tokujv tok10nu tok16rt tok1cvs tok1izr tok1p3q tok1v7p tok21bo tok2a4 tok8e3 tokei2 tokkm1 tokqq0 tokwtz tok12xy tok191x tok1f5w tok1l9v tok1rdu tok1xht tok23ls tok4k8 tokao7 tokgs6 tokmw5 tokt04 tokz43 tok1582 tok1bc1 tok1hg0 tok1njz tok1tny tok1zrx tokqd tok6uc tokcyb tokj2a tokp69 tokva8 tok11e7 tok17i6 tok1dm5 tok1jq4 tok1pu3 tok1vy2 tok2221 tok30h tok94g tokf8f toklce tokrgd tokxkc tok13ob tok19sa tok1fw9 tok1m08 tok1s47 tok1y86 tok24c5 tok5al tokbek tokhij toknmi toktqh tokzug tok15yf tok1c2e tok1i6d tok1oac tok1ueb tok20ia tok1gq tok7kp tokdoo tokjsn tokpwm tokw0l tok124k tok188j tok1eci tok1kgh tok1qkg tok1wof tok22se tok3qu tok9ut tokfys tokm2r toks6q tokyap tok14eo tok1ain tok1gmm tok1mql tok1suk tok1yyj tok252i tok60y tokc4x toki8w tokocv tokugu tok10kt tok16os tok1csr tok1iwq tok1p0p tok1v4o tok218n tok273 tok8b2 tokef1 tokkj0 tokqmz tokwqy tok12ux tok18yw tok1f2v tok1l6u tok1rat tok1xes tok23ir tok4h7 tokal6 tokgp5 tokmt4 toksx3 tokz12 tok1551 tok1b90 tok1hcz tok1ngy tok1tkx tok1zow toknc tok6rb tokcva tokiz9 tokp38 tokv77 tok11b6 tok17f5 tok1dj4 tok1jn3 tok1pr2 tok1vv1 tok21z0 tok2xg tok91f tokf5e tokl9d tokrdc tokxhb tok13la tok19p9 tok1ft8 tok1lx7 tok1s16 tok1y55 tok2494 tok57k tokbbj tokhfi toknjh tokktng tokzrf tok15ve tok1bzd tok1i3c tok1o7b tok1uba tok20f9 tok1dp tok7ho tokdln tokjpm tokptl tokvxk tok121j tok185i tok1e9h tok1kdg tok1qhf tok1wle tok22pd tok3nt tok9rs tokfvr toklzq toks3p toky7o tok14bn tok1afm tok1gjl tok1mnk tok1srj tok1yvi tok24zh tok5xx tokc1w toki5v toko9u tokudt tok10hs tok16lr tok1cpq tok1itp tok1oxo tok1v1n tok215m tok242 tok881 tokec0 tokfz tokqjy tokwnx' > notes.txt"
- button "Reject"
- button "Allow once"
@@ -5,11 +5,11 @@
- tab "Chat" [selected]
- tab "Trajectory"
- text: "Using ONE run_code program: run bash `echo CODE_ROUND_OK`, then read the file missing.txt catching its error in the program. Return an object with both outcomes. Then reply DONE and stop. {{clock}}"
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- 'button "Think The user wants me to write a single `run_code` program that:"':
- img
@@ -27,9 +27,9 @@
- img
- text: Think The program ran successfully. Let me now reply DONE as instructed.
- paragraph: DONE
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- text: {{clock}}
- textbox "Message the agent"
@@ -5,11 +5,11 @@
- tab "Chat" [selected]
- tab "Trajectory"
- text: "Use only Cordis tools. First call cordis_inspect with what \"temporary\". Then call cordis_mount with this exact code: \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\". Read its returned id and call cordis_unmount with that exact id. After all three calls succeed, reply exactly CORDIS_UI_DONE and stop. {{clock}}"
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- button "Think The user wants me to:":
- img
@@ -26,7 +26,7 @@
- button [expanded]:
- img
- text: Mount temporary Plugin typescript
- button "复制"
- button "Copy"
- code: "return { name: \"snapshot-noop\", apply(ctx) {} }"
- 'button "Think The id is \"dyn-1\". Now step 3: call cordis_unmount with that id."':
- img
@@ -41,9 +41,9 @@
- img
- text: Think All three calls succeeded. I should now reply exactly "CORDIS_UI_DONE" and stop.
- paragraph: CORDIS_UI_DONE
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- text: {{clock}}
- textbox "Message the agent"
@@ -5,28 +5,28 @@
- tab "Chat" [selected]
- tab "Trajectory"
- text: "Use the bash tool to run exactly: echo WEB_E2E_OK. Then reply with the single word DONE and stop. {{clock}}"
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- button "Think The user wants me to run a simple bash command and reply with \"DONE\".":
- img
- img
- text: Think The user wants me to run a simple bash command and reply with "DONE".
- img
- text: Bash Echo the test string 已完成 workspace echo WEB_E2E_OK
- button "复制"
- text: Bash Echo the test string Done workspace echo WEB_E2E_OK
- button "Copy"
- text: WEB_E2E_OK
- button "Think The command executed successfully and output \"WEB_E2E_OK\". I just need to reply with \"DONE\".":
- img
- img
- text: Think The command executed successfully and output "WEB_E2E_OK". I just need to reply with "DONE".
- paragraph: DONE
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- text: {{clock}}
- textbox "Message the agent"
@@ -34,6 +34,6 @@
- text: DeepSeek-V4-Flash
- img
- button "Send message" [disabled]
- text: 详情
- button "关闭详情"
- text: 点击消息流中的工具行查看详情
- text: Details
- button "Close details"
- text: Click a tool row in the message flow to view its details
@@ -5,20 +5,20 @@
- tab "Chat" [selected]
- tab "Trajectory"
- text: Reply with the single word LIGHTHOUSE and stop. {{clock}}
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- button "Think The user wants me to reply with a single word. Let me comply.":
- img
- img
- text: Think The user wants me to reply with a single word. Let me comply.
- paragraph: LIGHTHOUSE
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- text: {{clock}}
- textbox "Message the agent"
@@ -5,17 +5,17 @@
- tab "Chat" [selected]
- tab "Trajectory"
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- paragraph: partial
- text: 已停止
- button "复制":
- text: Stopped
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- text: {{clock}}
- textbox "Message the agent"
@@ -5,11 +5,11 @@
- tab "Chat" [selected]
- tab "Trajectory"
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- textbox "Message the agent"
- button "Add attachment":
@@ -5,20 +5,20 @@
- tab "Chat" [selected]
- tab "Trajectory"
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.":
- img
- img
- text: Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.
- paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures.
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- text: {{clock}}
- textbox "Message the agent"
@@ -4,13 +4,13 @@
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}"
- button "复制":
- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. 7/25 {{clock}}"
- button "Copy":
- img
- tooltip "复制"
- button "在新对话中分支":
- tooltip "Copy"
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.":
- img
@@ -27,11 +27,11 @@
- img
- text: Think Both files have been read. a.txt contains "alpha" and b.txt contains "beta". I'll now reply with DONE as instructed.
- paragraph: DONE
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- text: {{clock}}
- text: 7/25 {{clock}}
- textbox "Message the agent"
- button "Add attachment":
- img
@@ -1,3 +1,3 @@
- text: 已完成 {{workspace}} echo NAVIGATION_OK
- button "复制"
- text: Done {{workspace}} echo NAVIGATION_OK
- button "Copy"
- text: NAVIGATION_OK
@@ -6,11 +6,11 @@
- tab "Trajectory"
- img
- text: "plan Plan mode on. Use /plan off to leave. Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop. {{clock}}"
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- 'button "Think The user wants me to plan a small change to add a `--greeting` flag to a CLI. They explicitly told me not to read or write any files, and to call exit_plan_mode with a short plan. Let me do that directly."':
- img
@@ -20,11 +20,6 @@
- text: Since the user has explicitly asked me not to read or write any files and to go straight to planning, I'll proceed with
- code: exit_plan_mode
- text: .
- button "复制":
- img
- button "在新对话中分支":
- img
- text: {{clock}}
- button:
- img
- img
@@ -34,9 +29,9 @@
- img
- text: "Think The plan was approved. The user's last instruction says: \"Once the plan is approved, reply with the single word DONE and stop.\" So I should just reply with DONE and stop."
- paragraph: DONE
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- text: {{clock}}
- textbox "Message the agent"
@@ -5,11 +5,11 @@
- tab "Chat" [selected]
- tab "Trajectory"
- text: "Use the ask_user_question tool to ask me exactly one question with id \"color\", question \"Which color do you prefer?\", header \"Pick one\", and two options: label \"Blue\" with description \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\", and label \"Green\" with description \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\" After I answer, reply with the single word DONE and stop. {{clock}}"
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- button "Think The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that.":
- img
@@ -24,9 +24,9 @@
- img
- text: Think The user answered "Blue". I should now reply with the single word DONE and stop.
- paragraph: DONE
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- text: {{clock}}
- textbox "Message the agent"
@@ -5,14 +5,14 @@
- tab "Chat" [selected]
- tab "Trajectory"
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- paragraph: partial
- button "2 条排队消息"
- button "2 queued messages"
- textbox "Message the agent"
- button "Add attachment":
- img
@@ -5,26 +5,26 @@
- tab "Chat" [selected]
- tab "Trajectory"
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- paragraph: partial
- button "2 条排队消息" [disabled] [expanded]
- button "2 queued messages" [disabled] [expanded]
- list:
- listitem:
- text: Queue item to remove
- button "编辑排队消息":
- button "Edit queued message":
- img
- button "删除排队消息":
- button "Remove queued message":
- img
- listitem:
- textbox "编辑排队消息": Edited queue item
- button "保存排队消息":
- textbox "Edit queued message": Edited queue item
- button "Save queued message":
- img
- button "取消编辑":
- button "Cancel editing":
- img
- textbox "Message the agent"
- button "Add attachment":
@@ -5,19 +5,19 @@
- tab "Chat" [selected]
- tab "Trajectory"
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- paragraph: partial
- list:
- listitem:
- text: Edited queue item
- button "编辑排队消息":
- button "Edit queued message":
- img
- button "删除排队消息":
- button "Remove queued message":
- img
- textbox "Message the agent"
- button "Add attachment":
@@ -4,12 +4,12 @@
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}"
- button "复制":
- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. 7/25 {{clock}}"
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.":
- img
@@ -26,15 +26,15 @@
- img
- text: Think Both files have been read. a.txt contains "alpha" and b.txt contains "beta". I'll now reply with DONE as instructed.
- paragraph: DONE
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- text: {{clock}}
- button "上下文注入":
- text: 7/25 {{clock}}
- button "Context injection":
- img
- img
- text: 上下文注入
- text: Context injection
- img
- text: permission preset workspace-write
- textbox "Message the agent"
@@ -4,12 +4,12 @@
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"
- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}"
- button "复制":
- text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. 7/25 {{clock}}"
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- button "Think The user wants me to read a.txt and b.txt, then reply with \"DONE\". Let me do both reads in parallel.":
- img
@@ -26,15 +26,15 @@
- img
- text: Think Both files have been read. a.txt contains "alpha" and b.txt contains "beta". I'll now reply with DONE as instructed.
- paragraph: DONE
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- text: {{clock}}
- button "上下文注入":
- text: 7/25 {{clock}}
- button "Context injection":
- img
- img
- text: 上下文注入
- text: Context injection
- textbox "Message the agent"
- button "Add attachment":
- img
@@ -5,11 +5,11 @@
- tab "Chat" [selected]
- tab "Trajectory"
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- button "Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.":
- img
@@ -5,11 +5,11 @@
- tab "Chat" [selected]
- tab "Trajectory"
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}}
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- button "编辑":
- button "Edit":
- img
- button "Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.":
- img
@@ -18,15 +18,15 @@
- button:
- img
- img
- text: "Ask question 1/1 answered 插话 Interjection: include the word BANANA in your final reply."
- text: "Ask question 1/1 answered Interjection Interjection: include the word BANANA in your final reply."
- button "Think The user selected \"Yes\" and wants me to include the word \"BANANA\" in my final reply. Let me acknowledge their answer.":
- img
- img
- text: Think The user selected "Yes" and wants me to include the word "BANANA" in my final reply. Let me acknowledge their answer.
- paragraph: Great, let's move forward. BANANA!
- button "复制":
- button "Copy":
- img
- button "在新对话中分支":
- button "Branch into a new conversation":
- img
- text: {{clock}}
- textbox "Message the agent"
+3 -3
View File
@@ -121,9 +121,9 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
// exists yet and no interjection bubble renders — the composer still
// blocks, alone. The DOM is stable here (no further SSE frames can
// arrive until the question is answered), making this state capturable.
expect(await page.getByText('插话').count()).toBe(0)
expect(await page.getByText('Interjection', { exact: true }).count()).toBe(0)
expect(await page.getByText(STEER, { exact: true }).count()).toBe(0)
expect(await page.getByRole('button', { name: '编辑排队消息' }).count()).toBe(0)
expect(await page.getByRole('button', { name: 'Edit queued message' }).count()).toBe(0)
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(MID_EXPECTED, snapshot, MODE)
}
@@ -157,7 +157,7 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => {
// Visible: the badged interjection bubble plus the reply that obeys it
// (steer text + final reply each contain the marker word).
await expect.poll(() => page.getByText('插话').count(), { timeout: 15_000 }).toBe(1)
await expect.poll(() => page.getByText('Interjection', { exact: true }).count(), { timeout: 15_000 }).toBe(1)
await expect.poll(() => page.getByText('Interjection:', { exact: false }).count(), { timeout: 10_000 }).toBe(1)
await expect.poll(() => page.getByText('BANANA', { exact: false }).count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(2)
expect(await page.locator('[data-question-key]').count()).toBe(0)
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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)).
+2 -2
View File
@@ -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))。
+4 -2
View File
@@ -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`
+3 -3
View File
@@ -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 -2
View File
@@ -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
+17 -4
View File
@@ -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.
+17 -4
View File
@@ -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 Noteagent 决策记录)](../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md)。
循环从已记录状态构建每个请求。`EpochHeader` 通过完整的 `request/header` 快照记录调用配置、适配器默认值来源、渲染后的提示词以及权威返回工具顺序(由 `toolOrder` 配置;未配置时按字典序)。结合派生历史,请求便可由会话日志重建。见 [session.md](session.md#the-request-header-event-requestheader) 与[可重建性 Agent Noteagent 决策记录)](../../.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
+4 -2
View File
@@ -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 -2
View File
@@ -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
+3 -1
View File
@@ -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. */
+3 -1
View File
@@ -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. */
+1 -1
View File
@@ -71,7 +71,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `internal/dispatch` | - | [`commands`](../packages/ui/commands), [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/ui/permission), [`plan-mode`](../packages/plan/plan-mode), [`pty-local`](../packages/pty/pty-local), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session-title/session-title), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`workflow`](../packages/workflow/workflow) |
| `internal/plugin` | - | `hmr`, `loader`, `modules`, `webserver` |
| `internal/status` | - | [`agent`](../packages/core/agent) |
| `locale/change` | `locale` (`emit`) | `locale`, `ui-models`, `ui-settings-general` |
| `locale/change` | `locale` (`emit`) | `locale` |
| `models/changed` | `runtime` (`emit`) | `ui-models` |
| `settings/changed` | `runtime` (`emit`) | `ui-models` |
| `slash/input-begin-command` | - | `ui-conversation` |
+29 -25
View File
@@ -316,10 +316,6 @@ flowchart TD
pkg_client_ui_settings --> pkg_invariants
pkg_client_ui_trajectory --> pkg_client_ui_primitives
pkg_client_ui_trajectory --> pkg_invariants
pkg_client_ui_workspace --> pkg_client_runtime
pkg_client_ui_workspace --> pkg_client_ui_primitives
pkg_client_ui_workspace --> pkg_client_ui_slots
pkg_client_ui_workspace --> pkg_invariants
pkg_credentials --> pkg_brand
pkg_credentials --> pkg_invariants
pkg_helper --> pkg_brand
@@ -389,20 +385,15 @@ flowchart TD
pkg_client_ui_theme --> pkg_client_ui_primitives
pkg_client_ui_theme --> pkg_client_ui_slots
pkg_client_ui_theme --> pkg_invariants
pkg_client_ui_workspace --> pkg_client_locale
pkg_client_ui_workspace --> pkg_client_runtime
pkg_client_ui_workspace --> pkg_client_ui_primitives
pkg_client_ui_workspace --> pkg_client_ui_slots
pkg_client_ui_workspace --> pkg_invariants
pkg_credentials_local --> pkg_atomic_write
pkg_credentials_local --> pkg_credentials
pkg_credentials_local --> pkg_invariants
pkg_credentials_local --> pkg_paths
pkg_host_directory_picker_browse --> pkg_client_locale
pkg_host_directory_picker_browse --> pkg_client_runtime
pkg_host_directory_picker_browse --> pkg_client_ui_primitives
pkg_host_directory_picker_browse --> pkg_client_ui_slots
pkg_host_directory_picker_browse --> pkg_client_ui_workspace
pkg_host_directory_picker_browse --> pkg_invariants
pkg_host_directory_picker_native --> pkg_client_runtime
pkg_host_directory_picker_native --> pkg_client_ui_slots
pkg_host_directory_picker_native --> pkg_client_ui_workspace
pkg_host_directory_picker_native --> pkg_invariants
pkg_lsp --> pkg_brand
pkg_lsp --> pkg_invariants
pkg_lsp --> pkg_llm
@@ -479,10 +470,16 @@ flowchart TD
pkg_code_runtime_worker --> pkg_invariants
pkg_code_runtime_worker --> pkg_session
pkg_code_runtime_worker --> pkg_timeout
pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
pkg_host_directory_picker_auto --> pkg_host_webserver
pkg_host_directory_picker_auto --> pkg_invariants
pkg_host_directory_picker_browse --> pkg_client_locale
pkg_host_directory_picker_browse --> pkg_client_runtime
pkg_host_directory_picker_browse --> pkg_client_ui_primitives
pkg_host_directory_picker_browse --> pkg_client_ui_slots
pkg_host_directory_picker_browse --> pkg_client_ui_workspace
pkg_host_directory_picker_browse --> pkg_invariants
pkg_host_directory_picker_native --> pkg_client_runtime
pkg_host_directory_picker_native --> pkg_client_ui_slots
pkg_host_directory_picker_native --> pkg_client_ui_workspace
pkg_host_directory_picker_native --> pkg_invariants
pkg_lsp_local --> pkg_brand
pkg_lsp_local --> pkg_invariants
pkg_lsp_local --> pkg_llm
@@ -560,6 +557,7 @@ flowchart TD
pkg_user_interaction --> pkg_invariants
pkg_user_interaction --> pkg_llm
pkg_client_ui_command --> pkg_client_connection
pkg_client_ui_command --> pkg_client_locale
pkg_client_ui_command --> pkg_client_runtime
pkg_client_ui_command --> pkg_client_ui_conversation
pkg_client_ui_command --> pkg_client_ui_primitives
@@ -573,6 +571,10 @@ flowchart TD
pkg_tmux_context --> pkg_bash
pkg_tmux_context --> pkg_invariants
pkg_tmux_context --> pkg_session
pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse
pkg_host_directory_picker_auto --> pkg_host_directory_picker_native
pkg_host_directory_picker_auto --> pkg_host_webserver
pkg_host_directory_picker_auto --> pkg_invariants
pkg_pty --> pkg_agent
pkg_pty --> pkg_brand
pkg_pty --> pkg_invariants
@@ -651,6 +653,7 @@ flowchart TD
pkg_permission --> pkg_session_projection
pkg_permission --> pkg_user_approval
pkg_client_ui_goal --> pkg_client_connection
pkg_client_ui_goal --> pkg_client_locale
pkg_client_ui_goal --> pkg_client_runtime
pkg_client_ui_goal --> pkg_client_ui_conversation
pkg_client_ui_goal --> pkg_client_ui_primitives
@@ -926,6 +929,7 @@ flowchart TD
pkg_tui --> pkg_tools
pkg_tui --> pkg_user_interaction
pkg_client_ui_plan --> pkg_client_connection
pkg_client_ui_plan --> pkg_client_locale
pkg_client_ui_plan --> pkg_client_runtime
pkg_client_ui_plan --> pkg_client_ui_conversation
pkg_client_ui_plan --> pkg_client_ui_slots
@@ -1057,7 +1061,6 @@ flowchart TD
| [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) |
| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`client-ui-primitives`](../packages/client/ui-primitives), [`invariants`](../packages/support/invariants) |
| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`credentials`](../packages/credentials/credentials) | `credentials` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`helper`](../packages/sdk/helper) | `sdk` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess) |
| [`telemetry`](../packages/sdk/telemetry) | `sdk` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
@@ -1078,9 +1081,8 @@ flowchart TD
| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) |
| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
| [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
| [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) |
@@ -1103,7 +1105,8 @@ flowchart TD
| [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) |
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) |
| [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`sandbox-policy`](../packages/sandbox/sandbox-policy) | `sandbox` | [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
@@ -1123,9 +1126,10 @@ flowchart TD
| [`commands`](../packages/ui/commands) | `ui` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) |
| [`user-approval`](../packages/ui/user-approval) | `ui` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`user-interaction`](../packages/ui/user-interaction) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) |
| [`client-ui-command`](../packages/client/ui-command) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`client-ui-command`](../packages/client/ui-command) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) |
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) |
| [`pty`](../packages/pty/pty) | `pty` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) |
| [`scripts`](../packages/sdk/scripts) | `sdk` | [`app-boot`](../packages/ui/app-boot), [`invariants`](../packages/support/invariants) |
| [`session-projection-cache`](../packages/session-projection/session-projection-cache) | `session-projection` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-projection`](../packages/session-projection/session-projection), [`storage-domain`](../packages/storage/storage-domain) |
@@ -1142,7 +1146,7 @@ flowchart TD
| [`session-title-llm`](../packages/session-title/session-title-llm) | `session-title` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session-title/session-title), [`timeout`](../packages/util/timeout) |
| [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/ui/user-approval) |
| [`permission`](../packages/ui/permission) | `ui` | [`bash`](../packages/bash/bash), [`commands`](../packages/ui/commands), [`invariants`](../packages/support/invariants), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session-projection/session-projection), [`user-approval`](../packages/ui/user-approval) |
| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) |
| [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) |
| [`client-ui-model`](../packages/client/ui-model) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-command`](../packages/client/ui-command), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) |
| [`pty-local`](../packages/pty/pty-local) | `pty` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`pty`](../packages/pty/pty), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess) |
| [`tasks-local`](../packages/tasks/tasks-local) | `tasks` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tasks`](../packages/tasks/tasks), [`timeout`](../packages/util/timeout) |
@@ -1184,7 +1188,7 @@ flowchart TD
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools) |
| [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`tui`](../packages/ui/tui) | `ui` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`commands`](../packages/ui/commands), [`compact`](../packages/compact/compact), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference), [`session-title`](../packages/session-title/session-title), [`skill`](../packages/skill/skill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) |
| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`plan-mode`](../packages/plan/plan-mode) |
| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`plan-mode`](../packages/plan/plan-mode) |
| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session-title/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks-local`](../packages/tasks/tasks-local), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) |
| [`sdk-protocol`](../packages/sdk/sdk-protocol) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
+14 -14
View File
@@ -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)
+1 -2
View File
@@ -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
-1
View File
@@ -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.
+2 -1
View File
@@ -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:
@@ -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"}}}
+2 -2
View File
@@ -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:
@@ -37,6 +37,7 @@ export { FixtureSession, TestSessions } from './sessions.ts'
export { TestWorkspaces } from './workspaces.ts'
export { conversationSnapshot, workspaceListState } from './fixtures.ts'
export type { SessionBehaviorOverrides, SessionFixture, Stabilizer } from './fixtures.ts'
export { makeTranslate } from './translate.ts'
/** Erased register face for the internal root call (the public declare seam holds the typing). */
type ErasedRegister = (options: object, component: unknown) => () => void
@@ -0,0 +1,32 @@
/**
* Test double of the locale lookup chain: a translate stub over plain
* dictionaries, mirroring LocaleService's resolution order (first dictionary
* that owns the key wins, then the key itself stays visible) and its
* `{name}` template interpolation. Specs stub the framework-injected `t`
* seat with `makeTranslate(zh, commonZh)` instead of re-implementing the
* chain per suite.
*/
/**
* Build a translate stub resolving through `dicts` in order (namespace
* first, then the shared common vocabulary), falling back to the key.
* @param dicts - dictionaries consulted in order.
* @returns the translate function (assignable to any `XxxProps['t']` seat).
*/
export function makeTranslate(
...dicts: readonly Record<string, string>[]
): (key: string, params?: Record<string, unknown>) => string {
return (key, params) => {
let template = key
for (const dict of dicts) {
const hit = dict[key]
if (hit !== undefined) {
template = hit
break
}
}
if (!params) return template
return template.replace(/\{(\w+)\}/g, (match, name: string) =>
name in params ? String(params[name]) : match)
}
}
+4
View File
@@ -25,6 +25,7 @@
"dshClient": {
"inject": [
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-ui-slash",
"@deepseek-ai/dsh-client-ui-conversation"
],
@@ -40,6 +41,7 @@
},
"peerDependencies": {
"@deepseek-ai/dsh-client-connection": "^0.0.1",
"@deepseek-ai/dsh-client-locale": "^0.0.1",
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
"@deepseek-ai/dsh-client-ui-conversation": "^0.0.1",
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1",
@@ -51,7 +53,9 @@
},
"devDependencies": {
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-client-locale": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
"@deepseek-ai/dsh-client-ui-slash": "workspace:^",
@@ -13,6 +13,7 @@ import { useEffect, useRef } from 'react'
import { useSyncExternalStore } from 'react'
import clsx from 'clsx'
import { IconCheckOutline16, RiskConfirmation, useAnchoredMaxHeight } from '@deepseek-ai/dsh-client-ui-primitives'
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
import { filterOptions } from './popup.ts'
import type { PopupSelectController } from './popup.ts'
import css from './PopupSelectView.module.css'
@@ -26,12 +27,15 @@ export interface PopupSelectInjected {
popup: PopupSelectController
}
/** Full shell props: injected face + the locale seat. */
export type PopupSelectViewProps = PopupSelectInjected & PropsLocale<'command'>
/**
* Render the popupSelect shell overlay entry.
* @param props - injected face: the session's shell controller.
* @param props - injected face: the session's shell controller; `t` rides the standard locale seat.
* @returns the select card while open; null while closed.
*/
export function PopupSelectView({ popup }: PopupSelectInjected) {
export function PopupSelectView({ popup, t }: PopupSelectViewProps) {
const state = useSyncExternalStore(
fn => popup.state.subscribe(fn),
() => popup.state.getSnapshot(),
@@ -106,15 +110,15 @@ export function PopupSelectView({ popup }: PopupSelectInjected) {
ref={cardRef}
className={css.card}
style={{ maxHeight }}
aria-label={`/${String(state.command)} options`}
aria-label={t('overlay.aria', { command: String(state.command) })}
onKeyDown={onKeyDown}
>
<input
ref={searchRef}
className={css.search}
type="text"
placeholder="Search…"
aria-label="Filter options"
placeholder={t('search.placeholder')}
aria-label={t('search.aria')}
value={state.search}
readOnly={state.submitting}
onChange={(ev) => { popup.setSearch(ev.currentTarget.value) }}
@@ -123,15 +127,15 @@ export function PopupSelectView({ popup }: PopupSelectInjected) {
<div className={css.error} role="alert">
<span className={css.errorText}>{state.error}</span>
{state.status === 'failed' && (
<button type="button" className={css.retry} onClick={() => { popup.retry() }}>Retry</button>
<button type="button" className={css.retry} onClick={() => { popup.retry() }}>{t('retry')}</button>
)}
</div>
)}
{state.status === 'pending' && <div className={css.status}>Loading options</div>}
{state.submitting && <div className={css.status}>Applying</div>}
{state.status === 'ready' && rows.length === 0 && <div className={css.status}>No options</div>}
{state.status === 'pending' && <div className={css.status}>{t('status.loading')}</div>}
{state.submitting && <div className={css.status}>{t('status.applying')}</div>}
{state.status === 'ready' && rows.length === 0 && <div className={css.status}>{t('status.empty')}</div>}
{state.status === 'ready' && (
<div role="listbox" aria-label={`/${String(state.command)} matches`} className={css.viewport}>
<div role="listbox" aria-label={t('listbox.aria', { command: String(state.command) })} className={css.viewport}>
{rows.map((option, index) => (
<div
key={option.id}
+19 -3
View File
@@ -10,19 +10,23 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
// key's owner) into this program so the overlay registration below typechecks
// against the real declaration — no runtime edge to ui-conversation.
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
// Type-only: pulls the locale plugin's Context merge (ctx.locale).
import type {} from '@deepseek-ai/dsh-client-locale/client'
import { CommandService } from './service.ts'
import type { PopupSelectInjected } from './PopupSelectView.tsx'
import { PopupSelectView } from './PopupSelectView.tsx'
import { en, zh, type CommandKey } from './locales.ts'
export { CommandService } from './service.ts'
export { CommandDirectory } from './directory.ts'
export type { CommandDescriptor, DirectoryStatus } from './directory.ts'
export { filterOptions, PopupSelectController } from './popup.ts'
export type { PopupSelectDeps, PopupSpec, PopupState, TokenSegment } from './popup.ts'
export type { PopupSelectInjected } from './PopupSelectView.tsx'
export type { PopupSelectInjected, PopupSelectViewProps } from './PopupSelectView.tsx'
export type {
CommandContribution, CommandDecoration, CommandServiceContract, CommandUiSpec, SelectConfirmation, SelectOption,
} from './contract.ts'
export type { CommandKey } from './locales.ts'
declare module 'cordis' {
interface Context {
@@ -30,8 +34,18 @@ declare module 'cordis' {
}
}
/** Required services: the '/' source registry plus the scope + wire faces the service reads. */
export const inject = ['slash', 'sessions', 'connection']
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface LocaleNamespaceMap {
/** The popupSelect shell's copy. */
command: CommandKey
}
}
/** Dictionary namespace owned by this plugin. */
const NS = 'command'
/** Required services: the '/' source registry plus the scope + wire faces the service reads, and the copy's locale registry. */
export const inject = ['slash', 'sessions', 'connection', 'locale']
/**
* Client plugin body: mount the service, then register the popupSelect shell
@@ -39,6 +53,7 @@ export const inject = ['slash', 'sessions', 'connection']
* @param ctx - client root context.
*/
export function apply(ctx: ClientContext): void {
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-command: dictionaries')
ctx.plugin(CommandService)
// Conditional mount, same seam as ui-slash's MenuView registration:
// 'conversation.input.overlay' is declared by the conversation composer
@@ -51,6 +66,7 @@ export function apply(ctx: ClientContext): void {
name: 'conversation.input.overlay',
id: 'command-popup',
order: 1,
locale: NS,
inject: (sessionId): PopupSelectInjected => {
const actx = sessions.scope(sessionId)
if (actx === undefined) throw new Error(`ui-command: session "${String(sessionId)}" resolved no scope`)
Loaded 100 of 269 files, more files were not shown because too many files have changed in this diff. Show more