From 4f5c73003263418c76dc88e2bb8ea6f164f0950c Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:38:10 -0700 Subject: [PATCH 001/160] feat(web): combine plugin settings into tabs --- .../2026-08-11-plugin-settings-tabs.i18n.yaml | 6 ++ .../2026-08-11-plugin-settings-tabs.md | 37 +++++++ .../2026-08-11-plugin-settings-tabs.zh.md | 37 +++++++ ...6-08-10-web-plugin-configuration.i18n.yaml | 4 +- .../2026-08-10-web-plugin-configuration.md | 4 +- .../2026-08-10-web-plugin-configuration.zh.md | 4 +- apps/web/tests/plugin-config.e2e.ts | 11 +- apps/web/tests/settings-chrome.e2e.ts | 2 + .../created.expected.md | 3 - .../damaged.expected.md | 3 - .../section.expected.md | 3 - .../models-settings/configured.expected.md | 3 - .../models-settings/declared-edit.expected.md | 3 - .../models-settings/declared.expected.md | 3 - .../models-settings/empty.expected.md | 3 - .../models.expected.md | 3 - .../dismissed.expected.md | 3 - .../plugin-config/section.expected.md | 37 +++---- .../settings-chrome/dialog.expected.md | 3 - packages/client/README.i18n.yaml | 4 +- packages/client/README.md | 4 +- packages/client/README.zh.md | 4 +- .../client/ui-plugin-config/README.i18n.yaml | 4 +- packages/client/ui-plugin-config/README.md | 6 +- packages/client/ui-plugin-config/README.zh.md | 6 +- packages/client/ui-plugin-config/package.json | 2 +- .../src/client/ConfigurablePluginsTab.tsx | 25 +++++ .../src/client/PluginConfigSection.module.css | 51 ++++++++- .../src/client/PluginConfigSection.tsx | 100 ++++++++++++++---- .../ui-plugin-config/src/client/index.ts | 84 +++++++++++---- .../ui-plugin-config/src/client/locales.ts | 18 ++-- packages/client/ui-plugin-config/src/index.ts | 6 +- .../tests/apply.client.spec.ts | 23 +++- .../tests/section.client.spec.tsx | 66 +++++++++--- packages/client/ui-plugins/README.i18n.yaml | 4 +- packages/client/ui-plugins/README.md | 6 +- packages/client/ui-plugins/README.zh.md | 6 +- packages/client/ui-plugins/package.json | 2 +- .../client/PluginSettingsSection.module.css | 7 -- .../src/client/PluginSettingsSection.tsx | 46 ++++---- .../client/ui-plugins/src/client/index.ts | 12 +-- .../client/ui-plugins/src/client/locales.ts | 6 +- packages/client/ui-plugins/src/index.ts | 4 +- .../tests/browser-plugin.client.spec.tsx | 22 ++-- .../tests/components.client.spec.tsx | 14 ++- packages/client/ui-settings/README.i18n.yaml | 4 +- packages/client/ui-settings/README.md | 2 +- packages/client/ui-settings/README.zh.md | 2 +- .../ui-settings/src/client/contract/slots.ts | 15 +++ .../client/ui-settings/src/client/index.ts | 2 +- 50 files changed, 512 insertions(+), 217 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md create mode 100644 .agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.zh.md create mode 100644 packages/client/ui-plugin-config/src/client/ConfigurablePluginsTab.tsx diff --git a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.i18n.yaml new file mode 100644 index 0000000000..cdfdc3afde --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.i18n.yaml @@ -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-08-11-plugin-settings-tabs.md +2026-08-11-plugin-settings-tabs.md: 987e9f49e750a026c38a0d73c255cd335c53a4fe +2026-08-11-plugin-settings-tabs.zh.md: 887ca20bc09871d4bac5650a5d5eb71d5fcb81ee diff --git a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md new file mode 100644 index 0000000000..987e9f49e7 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md @@ -0,0 +1,37 @@ +# Agent Note: Feature-owned tabs in Plugins settings + +Status: implemented + +English | [中文](2026-08-11-plugin-settings-tabs.zh.md) + +## Problem + +Plugin configuration and the read-only Loader inventory each registered a top-level `settings.section`. They described the same Plugins domain but occupied two navigation rows, split search and configuration into unrelated pages, and gave the Settings shell no principled way to present them together. Combining their components directly would instead make one feature plugin import and own the other feature's data lifecycle. + +## Decision + +`@deepseek-ai/dsh-client-ui-plugin-config` owns the single `settings.section` contribution with id `plugins`. It renders the shared title and compact tab chrome, declares the root-scoped list slot `settings.plugins.tab`, and projects that ledger's id, order, and locale-following label into its tabs. The slot's canonical type lives in `ui-settings`, so a tab contributor depends on the Settings domain contract rather than on another feature plugin. + +The section owner contributes a `configurable` tab that declares the existing nested `settings.plugin.item` list. Configuration cards keep their namespace bindings, draft state, validation, and writes unchanged. `@deepseek-ai/dsh-client-ui-plugins` contributes an `all` tab to `settings.plugins.tab`; its Host Loader observer, generated Remote namespace, DTO, search semantics, and read-only disclosure cards remain unchanged. + +The first ordered tab is selected by default. A tab mounts only when first selected and then remains mounted but hidden while the Plugins section stays mounted. This delays the inventory RPC until the user opens **Plugin list** and preserves drafts, search text, disclosure state, and the fetched snapshot while switching tabs. Closing Settings unmounts the section, so reopening it obtains a fresh inventory snapshot when that tab is selected again. + +Both registrations use `ctx.slots.inject()`. If the section declarer unloads, the tab declaration and every contribution collapse with it; redeclaration lets each feature re-register without a static import or activation-order dependency. + +## Alternatives considered + +**Keep two Settings navigation rows and only rename them.** Rejected because the duplication is structural, not copy-related: both pages still represent the same Plugins domain and compete for navigation space. + +**Import the inventory component into `ui-plugin-config`.** Rejected because the configuration plugin would then own another plugin's Remote dependency and lifecycle. It would also turn an optional browser contribution into a package-level dependency. + +**Hard-code the two tab labels and components in the section owner.** Rejected because a third feature would require editing the owner, and HMR teardown could leave chrome for a contribution that no longer exists. The slot ledger already provides identity, ordering, localization, and cascade semantics. + +**Move Plugins aggregation into `ui-settings-general`.** Rejected because the Settings shell owns generic navigation and modal chrome, not feature content. Adding Plugins-specific tabs there would make every future Plugins view a shell change. + +## Consequences + +Settings has one Plugins navigation row, ordered before Agent Presets, with **Plugin configuration** and **Plugin list** tabs. Agent Presets remains an independent section because it edits per-session agent compositions rather than the live Host Loader tree. + +Feature ownership remains explicit: `ui-plugin-config` owns the Plugins page and editable cards, `ui-plugins` owns the read-only inventory view, and the Host/RPC path does not change. A new Plugins view can join by registering one `settings.plugins.tab` contribution. + +The aggregation depends on the section owner being composed: without `ui-plugin-config`, `ui-plugins` waits for a tab declaration and renders nothing. That is an intentional composition dependency carried by the slot registry rather than a static package import. diff --git a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.zh.md b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.zh.md new file mode 100644 index 0000000000..887ca20bc0 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.zh.md @@ -0,0 +1,37 @@ +# Agent Note: “插件”设置中的功能自有标签页 + +Status: implemented + +[English](2026-08-11-plugin-settings-tabs.md) | 中文 + +## 问题 + +插件配置与只读 Loader 清单各自注册了一个顶层 `settings.section`。两者描述同一个“插件”领域,却占据两行导航,把搜索与配置拆成互不相关的页面,也没有给 Settings 外壳一个有原则的聚合方式。若直接合并两者的组件,则会让一个功能插件 import 并拥有另一个功能的数据生命周期。 + +## 决策 + +`@deepseek-ai/dsh-client-ui-plugin-config` 拥有唯一一个 id 为 `plugins` 的 `settings.section` 贡献。它渲染共享标题和紧凑标签栏,声明根级列表 slot `settings.plugins.tab`,并把该记录中的 id、order 与跟随语言的 label 投影成标签页。该 slot 的规范类型位于 `ui-settings`,因此标签页贡献方依赖设置领域约定,而不是依赖另一个功能插件。 + +分区拥有方贡献 `configurable` 标签页,由它声明既有的嵌套 `settings.plugin.item` 列表。配置卡片原有的命名空间绑定、草稿状态、校验与写入均保持不变。`@deepseek-ai/dsh-client-ui-plugins` 向 `settings.plugins.tab` 贡献 `all` 标签页;它的 Host Loader 观察器、生成的 Remote 命名空间、DTO、搜索语义与只读折叠卡片均保持不变。 + +默认选择顺序中的第一个标签页。某个标签页只有首次被选择时才挂载,之后在“插件”分区保持挂载期间只隐藏而不卸载。这样会把清单 RPC 延迟到用户打开**插件列表**时,并在切换标签页时保留草稿、搜索文本、折叠状态和已读取的快照。关闭 Settings 会卸载该分区,因此再次打开后,重新选择该标签页时会取得新的清单快照。 + +两项注册都使用 `ctx.slots.inject()`。分区声明方卸载时,标签 slot 及其全部贡献随之折叠;重新声明后,每项功能都能重新注册,无需静态 import,也不依赖激活顺序。 + +## 备选方案 + +**保留两行 Settings 导航,只改名称。** 否决,因为重复是结构问题,而非文案问题:两个页面仍然代表同一个“插件”领域,并继续争夺导航空间。 + +**把清单组件 import 进 `ui-plugin-config`。** 否决,因为配置插件会因此拥有另一个插件的 Remote 依赖与生命周期,也会把可选的浏览器贡献变成包级依赖。 + +**在分区拥有方硬编码两个标签页的名称和组件。** 否决,因为第三项功能需要修改拥有方,HMR teardown 也可能留下已不存在贡献的界面框架。slot 记录已经提供标识、顺序、本地化与级联语义。 + +**把“插件”聚合移入 `ui-settings-general`。** 否决,因为 Settings 外壳拥有通用导航与模态界面框架,而不拥有功能内容。把“插件”专属标签页放在那里,会让今后每一种“插件”视图都需要修改外壳。 + +## 影响 + +Settings 只有一行“插件”导航,排在“Agent 预设”之前,包含**插件配置**与**插件列表**两个标签页。“Agent 预设”仍是独立分区,因为它编辑每个会话的 agent 组装,而非实时 Host Loader 树。 + +功能所有权保持明确:`ui-plugin-config` 拥有“插件”页面与可编辑卡片,`ui-plugins` 拥有只读清单视图,Host/RPC 路径不变。新的“插件”视图只需注册一个 `settings.plugins.tab` 贡献即可加入。 + +该聚合依赖分区拥有方被组装:没有 `ui-plugin-config` 时,`ui-plugins` 会等待标签 slot 的声明且不渲染任何内容。这是通过 slot 注册表承载的有意组合依赖,而不是静态包 import。 diff --git a/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.i18n.yaml index a27cb812e9..bea499a69a 100644 --- a/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.md -2026-08-10-web-plugin-configuration.md: 7375f496c7af1a695243444fe56aca7262d3dedd -2026-08-10-web-plugin-configuration.zh.md: 59d65db39bcc2306983f2a26dcf252164d7a6f37 +2026-08-10-web-plugin-configuration.md: 29c69695141d4fd00f5b323232522c5298ce4e9b +2026-08-10-web-plugin-configuration.zh.md: d7df9c332fc3cbca20ec75cb543600601bbac141 diff --git a/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.md b/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.md index 7375f496c7..29c6969514 100644 --- a/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.md +++ b/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.md @@ -12,7 +12,7 @@ The seam that made the Models page possible was already general: any plugin may ## Decision -Three host-plane plugins register their own settings namespace, and one browser-side section renders whatever the deployment exposes. +Three host-plane plugins register their own settings namespace, and one browser-side Plugins section aggregates feature-owned tabs. Its configurable tab renders whatever editable settings the deployment exposes. **Layering, unchanged.** A section resolves as schema defaults → the plugin's composition entry → the user layer. Each plugin passes its `cordis.yml` entry as the `base` and reads its config through a source thunk, so a stored change reaches the next use and a detaching settings provider leaves the composition entry running. Constraints the schema cannot express — positive and finite, the timer bound on `graceMs`, the parallel cap being a positive integer — become the section validator, so a bad value is refused at the write instead of at the next command. @@ -24,7 +24,7 @@ Three host-plane plugins register their own settings namespace, and one browser- **Exposure stays a Host allowlist.** The three namespaces join `WEB_SETTINGS_NAMESPACES`; registration alone still never crosses the transport, and a namespace absent from that list answers `settings-not-exposed` exactly as an unregistered one does. -**The section knows no namespace.** `dsh-client-ui-plugin-config` declares a `settings.plugin.item` slot and renders the cards registered into it, so a plugin that ships a browser half owns its card and its controls. Each card binds its namespace through the client settings scope, which gained the two things a form needs: the raw `user` layer, whose key PRESENCE is what marks a field overridden, and `unset`, which clears one field back to the composition layer. A card renders nothing while its namespace is unavailable, so a deployment that does not compose the owning plugin shows no trace of it. +**The configurable tab knows no namespace.** `dsh-client-ui-plugin-config` owns the Plugins section, contributes its `configurable` page through `settings.plugins.tab`, and declares a nested `settings.plugin.item` slot there. It renders the cards registered into that nested slot, so a plugin that ships a browser half owns its card and its controls. Each card binds its namespace through the client settings scope, which gained the two things a form needs: the raw `user` layer, whose key PRESENCE is what marks a field overridden, and `unset`, which clears one field back to the composition layer. A card renders nothing while its namespace is unavailable, so a deployment that does not compose the owning plugin shows no trace of it. **A card stages its edits and writes them on save.** Controls hold no draft of their own: the card's form owns the staged text, every control renders it, and only **Save** turns it into document mutations. A settings write is durable and revision-fenced, so a control that committed as it settled spent a revision on a value the user had not decided to store and could not preview; the reset stages the composed default the same way. Because the Host's validators own the constraints no schema can express, the form reads the section back after writing and reports a save that did not land instead of predicting the outcome, keeping those drafts for the user to correct. The credential control is staged with the rest even though it writes through the credentials domain, so one save covers everything the card shows. diff --git a/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.zh.md b/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.zh.md index 59d65db39b..d7df9c332f 100644 --- a/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.zh.md +++ b/.agents/notes/implemented/feature/2026-08-10-web-plugin-configuration.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -三个宿主平面插件各自注册 settings 命名空间,一个浏览器侧分区渲染该部署所暴露的一切。 +三个宿主平面插件各自注册 settings 命名空间,一个浏览器侧“插件”分区聚合由各功能持有的标签页。它的“可配置”标签页渲染该部署所暴露的一切可编辑设置。 **分层不变。** 一个分节按 schema 默认值 → 插件的组装条目 → 用户层解析。每个插件把自己的 `cordis.yml` 条目作为 `base` 传入,并通过 source thunk 读取配置,因此存储的变更会作用于下一次使用,而脱离的 settings 提供方会让组装条目继续运行。schema 无法表达的约束——正有限、`graceMs` 的定时器上界、并行上限必须是正整数——成为分节的校验器,因此错误的值在写入时被拒绝,而不是到下一条命令时才失败。 @@ -24,7 +24,7 @@ Status: implemented **暴露仍是 Host 的白名单。** 这三个命名空间加入 `WEB_SETTINGS_NAMESPACES`;仅有注册依然不会跨越传输边界,而不在该名单中的命名空间会与未注册的命名空间得到完全相同的 `settings-not-exposed`。 -**该分区不认识任何命名空间。** `dsh-client-ui-plugin-config` 声明 `settings.plugin.item` slot 并渲染注册进来的卡片,因此带浏览器半侧的插件拥有自己的卡片与控件。每张卡片通过客户端 settings scope 绑定其命名空间,而该 scope 补上了表单所需的两样东西:原始 `user` 层——键的**存在**才标记字段被覆盖——以及把单个字段清回组装层的 `unset`。命名空间不可用时卡片什么都不渲染,因此未组装该插件的部署不会显示它的任何痕迹。 +**“可配置”标签页不认识任何命名空间。** `dsh-client-ui-plugin-config` 拥有“插件”分区,通过 `settings.plugins.tab` 贡献自己的 `configurable` 页面,并在其中声明嵌套的 `settings.plugin.item` slot。它渲染注册进这个嵌套 slot 的卡片,因此带浏览器半侧的插件拥有自己的卡片与控件。每张卡片通过客户端 settings scope 绑定其命名空间,而该 scope 补上了表单所需的两样东西:原始 `user` 层——键的**存在**才标记字段被覆盖——以及把单个字段清回组装层的 `unset`。命名空间不可用时卡片什么都不渲染,因此未组装该插件的部署不会显示它的任何痕迹。 **卡片暂存修改,保存时才写入。** 控件不持有自己的草稿:暂存文本归卡片的表单所有,所有控件渲染的都是它,只有**保存**才把它变成文档变更。settings 写入是持久且带 revision 栅栏的,因此「失焦即提交」的控件会为用户尚未决定存储、也无从预览的值花掉一个 revision;重置同样只是暂存组装默认值。schema 表达不了的约束归 Host 的校验器所有,所以表单在写入后回读分节、报告没有落盘的保存,而不是自行预测结果,并保留这些草稿供用户修改。密钥控件虽然经由 credentials 领域写入,也和其余字段一起暂存,因此一次保存覆盖卡片上的全部内容。 diff --git a/apps/web/tests/plugin-config.e2e.ts b/apps/web/tests/plugin-config.e2e.ts index 15956877f4..3c97f4b414 100644 --- a/apps/web/tests/plugin-config.e2e.ts +++ b/apps/web/tests/plugin-config.e2e.ts @@ -1,5 +1,5 @@ -// Web e2e scenario: the Plugins settings section — the cards a deployment's -// exposed host-plane namespaces produce, one field edited through the real +// Web e2e scenario: the configurable tab in Plugins settings — the cards a +// deployment's exposed host-plane namespaces produce, one field edited through the real // wire down to `$DSH_HOME/settings.yaml`, and the override badge and reset // that layering produces. Zero model calls: everything is client state plus // the settings document on a blank frame, so there is no fixture and a stray @@ -56,9 +56,12 @@ describe('web e2e: plugin configuration section', () => { await page.getByRole('button', { name: '设置', exact: true }).click() const dialog = page.getByRole('dialog', { name: '设置' }) await dialog.waitFor({ timeout: 10_000 }) - await dialog.getByRole('button', { name: '插件配置', exact: true }).click() + await dialog.getByRole('button', { name: '插件', exact: true }).click() await expect - .poll(() => dialog.getByRole('button', { name: '插件配置', exact: true }).getAttribute('aria-current'), { timeout: 5_000 }) + .poll(() => dialog.getByRole('button', { name: '插件', exact: true }).getAttribute('aria-current'), { timeout: 5_000 }) + .toBe('true') + await expect + .poll(() => dialog.getByRole('tab', { name: '插件配置', exact: true }).getAttribute('aria-selected'), { timeout: 5_000 }) .toBe('true') return dialog } diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts index 5d057a7bed..d84f314bf5 100644 --- a/apps/web/tests/settings-chrome.e2e.ts +++ b/apps/web/tests/settings-chrome.e2e.ts @@ -99,6 +99,7 @@ describe('web e2e: settings modal and General preferences', () => { // an unrelated plugin does not rewrite this surface's golden. await dialog.getByRole('button', { name: '插件', exact: true }).click() await dialog.getByRole('heading', { name: '插件', exact: true }).waitFor({ timeout: 10_000 }) + await dialog.getByRole('tab', { name: '插件列表', exact: true }).click() const pluginRow = dialog.locator(PLUGIN_ROW_SELECTOR) await pluginRow.waitFor({ timeout: 10_000 }) const expectedPluginCount = [...scaffold.ctx.loader.entries()] @@ -109,6 +110,7 @@ describe('web e2e: settings modal and General preferences', () => { expect(await dialog.locator('[data-plugin-count]').getAttribute('data-plugin-count')) .toBe(String(expectedPluginCount)) expect(await dialog.getByRole('button', { name: '插件', exact: true }).getAttribute('aria-current')).toBe('true') + expect(await dialog.getByRole('tab', { name: '插件列表', exact: true }).getAttribute('aria-selected')).toBe('true') expect(await dialog.getByRole('button', { name: '模型' }).getAttribute('aria-current')).toBeNull() const pluginsSnapshot = await captureStableAria( page, diff --git a/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md index 40b9497831..c29b1cf478 100644 --- a/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md +++ b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md @@ -13,9 +13,6 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件配置": - - img - - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md index c3e9035098..15d4cc8f94 100644 --- a/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md +++ b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md @@ -13,9 +13,6 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件配置": - - img - - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md index e411e8ea28..312958187b 100644 --- a/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md +++ b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md @@ -13,9 +13,6 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件配置": - - img - - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/models-settings/configured.expected.md b/apps/web/tests/snapshots/models-settings/configured.expected.md index 66cc7c88b6..3c3be0922c 100644 --- a/apps/web/tests/snapshots/models-settings/configured.expected.md +++ b/apps/web/tests/snapshots/models-settings/configured.expected.md @@ -13,9 +13,6 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件配置": - - img - - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/models-settings/declared-edit.expected.md b/apps/web/tests/snapshots/models-settings/declared-edit.expected.md index 86f8b77fe8..1d538bcfe4 100644 --- a/apps/web/tests/snapshots/models-settings/declared-edit.expected.md +++ b/apps/web/tests/snapshots/models-settings/declared-edit.expected.md @@ -13,9 +13,6 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件配置": - - img - - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/models-settings/declared.expected.md b/apps/web/tests/snapshots/models-settings/declared.expected.md index b9e5dca61f..df48328fd3 100644 --- a/apps/web/tests/snapshots/models-settings/declared.expected.md +++ b/apps/web/tests/snapshots/models-settings/declared.expected.md @@ -13,9 +13,6 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件配置": - - img - - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/models-settings/empty.expected.md b/apps/web/tests/snapshots/models-settings/empty.expected.md index 03e87a7a51..cea14113ac 100644 --- a/apps/web/tests/snapshots/models-settings/empty.expected.md +++ b/apps/web/tests/snapshots/models-settings/empty.expected.md @@ -13,9 +13,6 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件配置": - - img - - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md b/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md index 562b54d837..5ebf0b1456 100644 --- a/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md +++ b/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md @@ -13,9 +13,6 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件配置": - - img - - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/onboarding-usable-provider/dismissed.expected.md b/apps/web/tests/snapshots/onboarding-usable-provider/dismissed.expected.md index b3e1141abc..84c7358b54 100644 --- a/apps/web/tests/snapshots/onboarding-usable-provider/dismissed.expected.md +++ b/apps/web/tests/snapshots/onboarding-usable-provider/dismissed.expected.md @@ -13,9 +13,6 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件配置": - - img - - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/plugin-config/section.expected.md b/apps/web/tests/snapshots/plugin-config/section.expected.md index 18cdef13d5..54ac42b4a7 100644 --- a/apps/web/tests/snapshots/plugin-config/section.expected.md +++ b/apps/web/tests/snapshots/plugin-config/section.expected.md @@ -13,25 +13,26 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件配置": - - img - - text: 插件配置 - button "打开配置文件" - button "关闭": - img - text: 关闭 - - heading "插件配置" [level=2] - - paragraph: 配置本部署已安装的插件。 - - list: - - listitem: - - 'button "展开设置: 终端"': - - text: 终端 限制 agent 运行的每一条命令。 - - img - - listitem: - - 'button "展开设置: Agent 循环"': - - text: Agent 循环 Agent 如何派发工具调用。 - - img - - listitem: - - 'button "展开设置: 网页搜索"': - - text: 网页搜索 DeepSeek 搜索提供方。 - - img + - heading "插件" [level=2] + - paragraph: 配置和查看本部署已安装的插件。 + - tablist "插件视图": + - tab "插件配置" [selected] + - tab "插件列表" + - tabpanel "插件配置": + - list: + - listitem: + - 'button "展开设置: 终端"': + - text: 终端 限制 agent 运行的每一条命令。 + - img + - listitem: + - 'button "展开设置: Agent 循环"': + - text: Agent 循环 Agent 如何派发工具调用。 + - img + - listitem: + - 'button "展开设置: 网页搜索"': + - text: 网页搜索 DeepSeek 搜索提供方。 + - img diff --git a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md index 2a7c767bf8..89cff5df3f 100644 --- a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md +++ b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md @@ -13,9 +13,6 @@ - button "Agent 预设": - img - text: Agent 预设 - - button "插件配置": - - img - - text: 插件配置 - button "打开配置文件" - button "关闭": - img diff --git a/packages/client/README.i18n.yaml b/packages/client/README.i18n.yaml index cd9f668fad..be4a65a085 100644 --- a/packages/client/README.i18n.yaml +++ b/packages/client/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/README.md -README.md: 236531281c17ef982982e97caad99491584bd0b5 -README.zh.md: e619ffaa6341f509537342bde90344141d4c8f64 +README.md: b9452d1f763be5be6953cb7973da8a2c909ed979 +README.zh.md: 1225101cff05a3f5655c4660d3d6d86be47d9a30 diff --git a/packages/client/README.md b/packages/client/README.md index 236531281c..b9452d1f76 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -35,13 +35,13 @@ The browser side of the dsh web GUI: shell boot, browser-host communication, sha | [`ui-model/`](ui-model/README.md) | Provides model selection in conversation surfaces. | | [`ui-permission/`](ui-permission/README.md) | Configures default permissions and switches the current session's access. | | [`ui-plan/`](ui-plan/README.md) | Presents active plan-mode status and its exit control. | -| [`ui-plugin-config/`](ui-plugin-config/README.md) | The Plugins settings section: host-plane plugin configuration as expandable cards. | +| [`ui-plugin-config/`](ui-plugin-config/README.md) | Owns the Plugins settings section, its tab extension point, and configurable host-plane plugin cards. | | [`ui-question/`](ui-question/README.md) | Presents interactive questions requested by the agent. | | [`ui-agent-preset/`](ui-agent-preset/README.md) | Selects a session's agent preset and authors preset compositions. | | [`ui-settings/`](ui-settings/README.md) | Hosts the settings interface and its extension areas. | | [`ui-settings-general/`](ui-settings-general/README.md) | Provides the general settings section. | | [`ui-models/`](ui-models/README.md) | Provides model-provider configuration and DeepSeek onboarding. | -| [`ui-plugins/`](ui-plugins/README.md) | Shows the current Host Loader entries in a read-only Settings section. | +| [`ui-plugins/`](ui-plugins/README.md) | Contributes the read-only Host Loader inventory tab to Plugins settings. | Each child reference owns its contract and detailed behavior. The [slot system standard](../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md) and [web client architecture note](../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md) own the cross-package composition and loading decisions. diff --git a/packages/client/README.zh.md b/packages/client/README.zh.md index e619ffaa63..1225101cff 100644 --- a/packages/client/README.zh.md +++ b/packages/client/README.zh.md @@ -35,13 +35,13 @@ dsh web GUI 的浏览器侧:shell 启动、浏览器与宿主通信、共享 U | [`ui-model/`](ui-model/README.md) | 在会话界面中提供模型选择。 | | [`ui-permission/`](ui-permission/README.md) | 配置默认权限并切换当前会话的访问模式。 | | [`ui-plan/`](ui-plan/README.md) | 展示生效中的 plan mode 状态及其退出控件。 | -| [`ui-plugin-config/`](ui-plugin-config/README.md) | 插件设置分区:把宿主平面的插件配置呈现为可展开卡片。 | +| [`ui-plugin-config/`](ui-plugin-config/README.md) | 拥有“插件”设置分区、它的标签页扩展点,以及可配置的宿主平面插件卡片。 | | [`ui-question/`](ui-question/README.md) | 展示 agent 请求的交互式问题。 | | [`ui-agent-preset/`](ui-agent-preset/README.md) | 选择会话的 agent 预设,并创作预设组装。 | | [`ui-settings/`](ui-settings/README.md) | 承载设置界面及其扩展区域。 | | [`ui-settings-general/`](ui-settings-general/README.md) | 提供常规设置分区。 | | [`ui-models/`](ui-models/README.md) | 提供模型提供方配置与 DeepSeek 配置引导。 | -| [`ui-plugins/`](ui-plugins/README.md) | 在只读设置分区中展示当前 Host Loader 条目。 | +| [`ui-plugins/`](ui-plugins/README.md) | 向“插件”设置贡献只读的 Host Loader 清单标签页。 | 每个子文档负责自身的约定和详细行为。[slot 系统标准](../../.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md)与 [Web 客户端架构 Agent Note](../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md)负责跨包组合与加载决策。 diff --git a/packages/client/ui-plugin-config/README.i18n.yaml b/packages/client/ui-plugin-config/README.i18n.yaml index d112523b42..9b3486623f 100644 --- a/packages/client/ui-plugin-config/README.i18n.yaml +++ b/packages/client/ui-plugin-config/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-plugin-config/README.md -README.md: 7e530d70f6573d619378e43b0245345b45d6db18 -README.zh.md: fd4f980fcf71c00c2357017fb40c76a9ca7a72cc +README.md: 1bf8dd60b966ee0c0e28ae931556182c5c2b2e87 +README.zh.md: bd87e06202b2b083a6e9e089eb91681deea9d678 diff --git a/packages/client/ui-plugin-config/README.md b/packages/client/ui-plugin-config/README.md index 7e530d70f6..1bf8dd60b9 100644 --- a/packages/client/ui-plugin-config/README.md +++ b/packages/client/ui-plugin-config/README.md @@ -2,17 +2,17 @@ English | [中文](README.zh.md) -The **Plugins** settings section: one expandable card per Host plugin whose configuration a user owns. A card shows the plugin's name and what it governs; expanding it in place reveals hand-written controls bound to that plugin's settings namespace, each field marking whether the user overrode it and offering a reset back to the value the deployment composed. +The **Plugins** settings section and its **Plugin configuration** tab. The section owns the heading and compact tab chrome; feature plugins contribute pages through `settings.plugins.tab`. This package's own tab shows one expandable card per Host plugin whose configuration a user owns. A card shows the plugin's name and what it governs; expanding it in place reveals hand-written controls bound to that plugin's settings namespace, each field marking whether the user overrode it and offering a reset back to the value the deployment composed. ## What appears here -A card renders only when its namespace is both registered by a live Host plugin and served to the browser. A deployment that does not compose the owning plugin — or serves the namespace to no client — renders nothing for it rather than an empty or disabled card, so the section reflects what this deployment actually runs. +A card renders only when its namespace is both registered by a live Host plugin and served to the browser. A deployment that does not compose the owning plugin — or serves the namespace to no client — renders nothing for it rather than an empty or disabled card, so the configurable tab reflects what this deployment actually runs. The first batch covers the shell executor (`bash`), the agent loop's tool-call parallelism (`agent-loop`), and the DeepSeek search provider (`web-search-deepseek`). ## Extension point -The section declares `settings.plugin.item`, a root list slot. A plugin that ships a browser half registers its own card into that slot and owns its controls; this package neither enumerates namespaces nor renders a form it was not given. Ordering follows the slot's `order`. +The section declares `settings.plugins.tab`, a root list slot whose labels become ordered tabs. It keeps a tab mounted after its first selection, so local drafts and read-only snapshots survive tab switches. The package registers its own `configurable` contribution, which declares the nested `settings.plugin.item` list slot. A plugin that ships a browser half registers its own card into that nested slot and owns its controls; this package neither enumerates namespaces nor renders a form it was not given. Both levels follow the contribution's `order`. ## Writes diff --git a/packages/client/ui-plugin-config/README.zh.md b/packages/client/ui-plugin-config/README.zh.md index fd4f980fcf..bd87e06202 100644 --- a/packages/client/ui-plugin-config/README.zh.md +++ b/packages/client/ui-plugin-config/README.zh.md @@ -2,17 +2,17 @@ [English](README.md) | 中文 -**插件**设置分区:每个配置由用户拥有的 Host 插件占一张可展开卡片。卡片展示插件名称及其管辖范围;就地展开后是绑定到该插件 settings 命名空间的手写控件,每个字段标注用户是否覆盖过它,并提供重置回部署组装值的入口。 +**插件**设置分区及其**插件配置**标签页。该分区拥有标题与紧凑的标签栏;功能插件通过 `settings.plugins.tab` 贡献页面。本包自己的标签页为每个配置由用户拥有的 Host 插件展示一张可展开卡片。卡片展示插件名称及其管辖范围;就地展开后是绑定到该插件 settings 命名空间的手写控件,每个字段标注用户是否覆盖过它,并提供重置回部署组装值的入口。 ## 这里会出现什么 -只有当某个命名空间既被存活的 Host 插件注册、又被服务给浏览器时,它的卡片才会渲染。未组装该插件的部署——或未向任何客户端服务该命名空间的部署——不会渲染空卡片或禁用卡片,而是什么都不渲染,因此这一分区反映的是该部署实际运行的东西。 +只有当某个命名空间既被存活的 Host 插件注册、又被服务给浏览器时,它的卡片才会渲染。未组装该插件的部署——或未向任何客户端服务该命名空间的部署——不会渲染空卡片或禁用卡片,而是什么都不渲染,因此“插件配置”标签页反映的是该部署实际运行的东西。 第一批覆盖 shell 执行器(`bash`)、agent 循环的工具调用并行度(`agent-loop`)以及 DeepSeek 搜索提供方(`web-search-deepseek`)。 ## 扩展点 -本分区声明了根级列表 slot `settings.plugin.item`。带浏览器半侧的插件把自己的卡片注册进该 slot 并拥有其控件;本包既不枚举命名空间,也不渲染未被交给它的表单。排序遵循 slot 的 `order`。 +本分区声明根级列表 slot `settings.plugins.tab`,其标签会成为有序标签页。某个标签页首次被选择后会保持挂载,因此本地草稿与只读快照在切换标签页时不会丢失。本包注册自己的 `configurable` 贡献,由它声明嵌套的 `settings.plugin.item` 列表 slot。带浏览器半侧的插件把自己的卡片注册进这个嵌套 slot 并拥有其控件;本包既不枚举命名空间,也不渲染未被交给它的表单。两层排序都遵循贡献的 `order`。 ## 写入 diff --git a/packages/client/ui-plugin-config/package.json b/packages/client/ui-plugin-config/package.json index dd4d808789..9637702da7 100644 --- a/packages/client/ui-plugin-config/package.json +++ b/packages/client/ui-plugin-config/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-client-ui-plugin-config", - "description": "Plugin configuration section: host-plane plugin settings as expandable cards", + "description": "Plugins settings section with feature-owned tabs and configurable host-plane plugin cards", "version": "0.0.1-rc.2", "publishConfig": { "access": "restricted" diff --git a/packages/client/ui-plugin-config/src/client/ConfigurablePluginsTab.tsx b/packages/client/ui-plugin-config/src/client/ConfigurablePluginsTab.tsx new file mode 100644 index 0000000000..1531e2ad0a --- /dev/null +++ b/packages/client/ui-plugin-config/src/client/ConfigurablePluginsTab.tsx @@ -0,0 +1,25 @@ +/** Configurable Host plugins contributed to the shared Plugins section. */ + +import type { InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +import type {} from './slot-contract.ts' +import css from './PluginConfigSection.module.css' + +/** Registration-side business face for the configurable tab. */ +export interface ConfigurablePluginsTabInjected { + /** How many cards the slot ledger held when the tab registration mounted. */ + cardCount: number +} + +/** Props the renderer binds for the configurable tab. */ +export type ConfigurablePluginsTabProps = + PropsRuntime<'settings.plugins.tab'> + & PropsLocale<'settings.pluginConfig'> + & PropsRenderSlots<'settings.plugin.item'> + & InjectFace + +/** Render cards registered by plugins that expose editable settings. */ +export function ConfigurablePluginsTab({ t, renderSlot, cardCount }: ConfigurablePluginsTabProps) { + return cardCount === 0 + ?

{t('empty')}

+ : +} diff --git a/packages/client/ui-plugin-config/src/client/PluginConfigSection.module.css b/packages/client/ui-plugin-config/src/client/PluginConfigSection.module.css index 45c9a78f00..f5a162eee8 100644 --- a/packages/client/ui-plugin-config/src/client/PluginConfigSection.module.css +++ b/packages/client/ui-plugin-config/src/client/PluginConfigSection.module.css @@ -1,10 +1,10 @@ -/* Plugin configuration section: heading, intro, and the card list. */ +/* Plugins section: compact tabs plus the configurable plugin card list. */ .section { display: flex; flex-direction: column; gap: 12px; - max-width: 720px; + max-width: 760px; color: var(--dsw-alias-label-primary); } @@ -20,6 +20,53 @@ color: var(--dsw-alias-label-tertiary); } +.tabs { + display: flex; + align-items: flex-end; + gap: 22px; + border-bottom: 1px solid var(--dsw-alias-border-l2); + margin-top: 2px; +} + +.tab { + position: relative; + border: 0; + padding: 7px 1px 9px; + background: transparent; + color: var(--dsw-alias-label-tertiary); + font: inherit; + font-size: 13px; + line-height: 20px; + cursor: pointer; +} + +.tab:hover, +.tab[data-active='true'] { + color: var(--dsw-alias-label-primary); +} + +.tab[data-active='true']::after, +.tab:focus-visible::after { + position: absolute; + right: 0; + bottom: -1px; + left: 0; + height: 2px; + border-radius: 2px 2px 0 0; + background: var(--dsw-alias-label-primary); + content: ''; +} + +.tab:focus-visible { + outline: none; + color: var(--dsw-alias-label-primary); +} + +.panel { + min-width: 0; + padding-top: 2px; +} + .cards { list-style: none; margin: 0; diff --git a/packages/client/ui-plugin-config/src/client/PluginConfigSection.tsx b/packages/client/ui-plugin-config/src/client/PluginConfigSection.tsx index 68de45eff3..a75816b92e 100644 --- a/packages/client/ui-plugin-config/src/client/PluginConfigSection.tsx +++ b/packages/client/ui-plugin-config/src/client/PluginConfigSection.tsx @@ -1,49 +1,105 @@ -/** - * Plugin configuration section: the shell around the per-plugin cards. It - * enumerates nothing itself — cards arrive through the `settings.plugin.item` - * slot it declares, so a plugin that ships a browser half owns its own card - * and this section never learns what a namespace means. - */ +/** Plugins settings section: localized tabs around feature-owned pages. */ -import type { InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' -import type {} from './slot-contract.ts' +import { useEffect, useId, useState } from 'react' +import type { + HostObservable, InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime, +} from '@deepseek-ai/dsh-client-ui-slots' import type { PluginConfigKey } from './locales.ts' import css from './PluginConfigSection.module.css' +/** One tab projected from a `settings.plugins.tab` contribution. */ +export interface PluginSettingsTabRow { + id: string + order: number + label: string +} + /** Registration-side business face for the section. */ export interface PluginConfigSectionInjected { - /** How many cards the slot ledger currently holds; zero renders the empty line. */ - cardCount: number + hooks: { + /** Ordered, locale-aware projection of the Plugins tab ledger. */ + tabs: HostObservable + } } /** Props the renderer binds for the section. */ export type PluginConfigSectionProps = PropsRuntime<'settings.section'> & PropsLocale<'settings.pluginConfig'> - & PropsRenderSlots<'settings.plugin.item'> + & PropsRenderSlots<'settings.plugins.tab'> & InjectFace -/** - * Render the plugin configuration section. - * @param props - runtime slot rendering, locale copy, and the card count. - * @returns the section. - */ -export function PluginConfigSection(props: PluginConfigSectionProps) { - const { t, renderSlot, cardCount } = props +/** Render one Plugins page whose contents arrive from feature-owned tabs. */ +export function PluginConfigSection({ t, renderSlot, useTabs }: PluginConfigSectionProps) { + const tabsId = useId() + const rows = useTabs(value => value) + const [activeId, setActiveId] = useState() + const [visitedIds, setVisitedIds] = useState>(() => new Set()) + const active = rows.find(row => row.id === activeId)?.id ?? rows[0]?.id + + // A tab mounts only when first selected, then stays mounted while hidden so + // local drafts, disclosure state, search, and the inventory snapshot survive + // switching between the two views. + useEffect(() => { + if (active === undefined) return + setVisitedIds((previous) => { + if (previous.has(active)) return previous + return new Set([...previous, active]) + }) + }, [active]) + return (

{t('title')}

{t('intro')}

- {cardCount === 0 - ?

{t('empty')}

- :
    {renderSlot('settings.plugin.item', {})}
} + {rows.length === 0 ?

{t('empty')}

: ( + <> +
+ {rows.map((row) => { + const selected = row.id === active + return ( + + ) + })} +
+ {rows + .filter(row => row.id === active || visitedIds.has(row.id)) + .map((row) => { + const selected = row.id === active + return ( + + ) + })} + + )}
) } declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { - /** Plugin configuration section and card copy. */ + /** Plugins section, configurable-tab, and card copy. */ 'settings.pluginConfig': PluginConfigKey } } diff --git a/packages/client/ui-plugin-config/src/client/index.ts b/packages/client/ui-plugin-config/src/client/index.ts index f3425634ef..a83bff8c11 100644 --- a/packages/client/ui-plugin-config/src/client/index.ts +++ b/packages/client/ui-plugin-config/src/client/index.ts @@ -1,13 +1,12 @@ /** - * Plugin configuration surface, browser half — one settings section holding - * an expandable card per Host plugin whose configuration a user owns. + * Plugins settings surface, browser half — one section whose feature-owned + * tabs include configurable Host plugin cards and read-only inventory. * - * The section owns no knowledge of any namespace: it declares the - * `settings.plugin.item` slot and renders whatever cards were registered into - * it, so a plugin that ships a browser half contributes its own card and its - * own controls. The three cards this package registers are the host-plane - * sections the deployment already exposes; each binds its namespace through - * the client settings scope, which keeps them unaware of one another. + * The section declares `settings.plugins.tab`; its own `configurable` tab then + * declares `settings.plugin.item` and renders whatever cards were registered + * into it. The three cards this package ships are the host-plane sections the + * deployment already exposes; each binds its namespace through the client + * settings scope, which keeps them unaware of one another and of other tabs. */ import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client' @@ -18,11 +17,15 @@ import type {} from '@deepseek-ai/dsh-client-locale/client' // through the service, never a value import (client bundle purity gate). import type {} from '@deepseek-ai/dsh-client-ui-settings/client' import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client' +import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' // Type-only: the ctx.remote Context merge and the forwarded-event key face. import type {} from '@deepseek-ai/dsh-api-remotes/client' import { AgentLoopCard } from './AgentLoopCard.tsx' import { BashCard } from './BashCard.tsx' +import { ConfigurablePluginsTab } from './ConfigurablePluginsTab.tsx' +import type { ConfigurablePluginsTabInjected } from './ConfigurablePluginsTab.tsx' import { PluginConfigSection } from './PluginConfigSection.tsx' +import type { PluginConfigSectionInjected, PluginSettingsTabRow } from './PluginConfigSection.tsx' import { WebSearchCard } from './WebSearchCard.tsx' import { AGENT_LOOP_NS, AgentLoopCardController } from './agent-loop-store.ts' import { BASH_NS, BashCardController } from './bash-store.ts' @@ -30,6 +33,7 @@ import { WEB_SEARCH_NS, WebSearchCardController } from './web-search-store.ts' import { en, zh } from './locales.ts' export type { PluginConfigSectionInjected, PluginConfigSectionProps } from './PluginConfigSection.tsx' +export type { ConfigurablePluginsTabInjected, ConfigurablePluginsTabProps } from './ConfigurablePluginsTab.tsx' export type { PluginCardProps } from './PluginCard.tsx' export type { SettingsPluginItemOwnerProps } from './slot-contract.ts' export type { FieldProps } from './fields.tsx' @@ -67,23 +71,67 @@ export function apply(ctx: ClientContext): void { 'ui-plugin-config: credential invalidations', ) - // The section renders the empty line rather than an empty list when no plugin - // contributed a card. The count is read once: the renderer caches a root - // entry's inject face per registration, so this reports what was registered - // when the section mounted, not what is visible now. Both gaps are bounded by - // this deployment always registering the three cards below — a card that - // arrives later would not raise the count, and a namespace this deployment - // does not expose leaves its card rendering nothing inside a non-empty list. + let tabsVersion = -1 + let tabsRevision = -1 + let tabs: readonly PluginSettingsTabRow[] = [] + const sectionInjected = (): PluginConfigSectionInjected => ({ + hooks: { + tabs: { + getSnapshot: () => { + const version = ctx.slots.getVersion('settings.plugins.tab') + const revision = ctx.locale.getSnapshot().revision + if (version !== tabsVersion || revision !== tabsRevision) { + tabsVersion = version + tabsRevision = revision + tabs = ctx.slots.entries('settings.plugins.tab') + .map(entry => ({ + /* v8 ignore next -- list-slot registration requires id */ + id: entry.options.id ?? '', + order: entry.options.order ?? 0, + label: resolveSlotLabel(entry.options.label) ?? '', + })) + .sort((a, b) => a.order - b.order) + } + return tabs + }, + subscribe: (listener) => { + const offLedger = ctx.slots.subscribe('settings.plugins.tab', listener) + const offLocale = ctx.locale.subscribe(listener) + return () => { + offLedger() + offLocale() + } + }, + }, + }, + }) + + // This package owns the one Plugins navigation entry and the tab chrome; + // feature plugins contribute pages without competing for Settings nav rows. ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'plugins', - order: 30, + order: 15, label: () => t('nav'), locale: NS, - inject: () => ({ cardCount: ctx.slots.entries('settings.plugin.item').length }), - children: { 'settings.plugin.item': { kind: 'list', scope: 'root' } }, + inject: sectionInjected, + children: { 'settings.plugins.tab': { kind: 'list', scope: 'root' } }, }, PluginConfigSection)) + // The existing configuration page is one ordinary tab. It keeps ownership + // of the card slot and the three shipped card contributions below. + ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({ + name: 'settings.plugins.tab', + id: 'configurable', + order: 0, + label: () => t('configurableTab'), + locale: NS, + inject: (): ConfigurablePluginsTabInjected => ({ + cardCount: ctx.slots.entries('settings.plugin.item').length, + }), + children: { 'settings.plugin.item': { kind: 'list', scope: 'root' } }, + }, ConfigurablePluginsTab)) + ctx.slots.inject('settings.plugin.item', function* () { yield ctx.slots.register({ name: 'settings.plugin.item', diff --git a/packages/client/ui-plugin-config/src/client/locales.ts b/packages/client/ui-plugin-config/src/client/locales.ts index 18fc943f7b..763b9986c4 100644 --- a/packages/client/ui-plugin-config/src/client/locales.ts +++ b/packages/client/ui-plugin-config/src/client/locales.ts @@ -2,7 +2,7 @@ /** Locale keys these surfaces render. */ export type PluginConfigKey = - | 'nav' | 'title' | 'intro' | 'empty' + | 'nav' | 'title' | 'intro' | 'tabs' | 'configurableTab' | 'empty' | 'overridden' | 'reset' | 'readOnly' | 'expand' | 'collapse' | 'save' | 'saving' | 'discard' | 'unsaved' | 'saveFailed' | 'invalidNumber' | 'bashTitle' | 'bashDescription' | 'bashTimeoutMs' | 'bashTimeoutMsHint' @@ -14,9 +14,11 @@ export type PluginConfigKey = /** English copy. */ export const en: Record = { - nav: 'Plugin config', - title: 'Plugin configuration', - intro: 'Configure the plugins this deployment installed.', + nav: 'Plugins', + title: 'Plugins', + intro: 'Configure and inspect the plugins installed in this deployment.', + tabs: 'Plugin views', + configurableTab: 'Plugin configuration', empty: 'This deployment exposes no plugin settings.', overridden: 'Overridden', reset: 'Reset to default', @@ -53,9 +55,11 @@ export const en: Record = { /** Simplified Chinese copy. */ export const zh: Record = { - nav: '插件配置', - title: '插件配置', - intro: '配置本部署已安装的插件。', + nav: '插件', + title: '插件', + intro: '配置和查看本部署已安装的插件。', + tabs: '插件视图', + configurableTab: '插件配置', empty: '本部署没有开放任何插件设置。', overridden: '已覆盖', reset: '恢复默认', diff --git a/packages/client/ui-plugin-config/src/index.ts b/packages/client/ui-plugin-config/src/index.ts index 0bb50fba35..ce0f6e034d 100644 --- a/packages/client/ui-plugin-config/src/index.ts +++ b/packages/client/ui-plugin-config/src/index.ts @@ -1,7 +1,7 @@ /** - * Plugin configuration surface, node half. The empty apply exists so the - * plugin appears in the host cordis.yml / Loader; the browser half ships the - * settings section through exports["./client"], discovered from the + * Plugins settings surface, node half. The empty apply exists so the plugin + * appears in the host cordis.yml / Loader; the browser half owns the section + * and its configurable tab through exports["./client"], discovered from the * package.json dsh.client declaration. Every section this page edits is owned * by the Host plugin that registered it, so this package registers no * namespace of its own. diff --git a/packages/client/ui-plugin-config/tests/apply.client.spec.ts b/packages/client/ui-plugin-config/tests/apply.client.spec.ts index a880446e8c..8c5e768c45 100644 --- a/packages/client/ui-plugin-config/tests/apply.client.spec.ts +++ b/packages/client/ui-plugin-config/tests/apply.client.spec.ts @@ -8,6 +8,9 @@ import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' import { TestRemote, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' import { SettingsScopeService } from '@deepseek-ai/dsh-client-ui-settings/client' import { apply, inject } from '@deepseek-ai/dsh-client-ui-plugin-config/client' +import type { + ConfigurablePluginsTabInjected, PluginConfigSectionInjected, +} from '@deepseek-ai/dsh-client-ui-plugin-config/client' // The service reads its initial locale from the browser; these specs assert // the shipped Chinese copy, so they state the browser they assume. @@ -46,16 +49,20 @@ describe('ui-plugin-config apply', () => { expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'settingsScope']) }) - it('registers the section and declares the per-plugin card slot', async () => { + it('registers one Plugins section and declares the tab and card slots', async () => { const { ctx, slots } = await bench() declareRoot(slots) await ctx.plugin({ inject: [...inject], apply }).await() const section = slots.entries('settings.section')[0]! - expect(section.options).toMatchObject({ id: 'plugins', order: 30 }) + expect(section.options).toMatchObject({ id: 'plugins', order: 15 }) // The nav label is a locale-following thunk; owners resolve it at read time. - expect(resolveSlotLabel(section.options.label)).toBe('插件配置') + expect(resolveSlotLabel(section.options.label)).toBe('插件') + expect(slots.spec('settings.plugins.tab')).toMatchObject({ kind: 'list', scope: 'root' }) + const tab = slots.entries('settings.plugins.tab')[0]! + expect(tab.options).toMatchObject({ id: 'configurable', order: 0 }) + expect(resolveSlotLabel(tab.options.label)).toBe('插件配置') expect(slots.spec('settings.plugin.item')).toMatchObject({ kind: 'list', scope: 'root' }) }) @@ -69,13 +76,18 @@ describe('ui-plugin-config apply', () => { .toEqual(['bash', 'agent-loop', 'web-search']) }) - it('injects a live card count and one business face per card', async () => { + it('injects a live tab projection, a card count, and one business face per card', async () => { const { ctx, slots } = await bench() declareRoot(slots) await ctx.plugin({ inject: [...inject], apply }).await() const section = slots.entries('settings.section')[0]! - expect((section as { inject?: () => unknown }).inject?.()).toEqual({ cardCount: 3 }) + const sectionFace = (section.inject as unknown as () => PluginConfigSectionInjected)() + expect(sectionFace.hooks.tabs.getSnapshot()).toEqual([ + { id: 'configurable', order: 0, label: '插件配置' }, + ]) + const tab = slots.entries('settings.plugins.tab')[0]! + expect((tab.inject as unknown as () => ConfigurablePluginsTabInjected)()).toEqual({ cardCount: 3 }) for (const entry of slots.entries('settings.plugin.item')) { const face = (entry as { inject?: () => unknown }).inject?.() as { hooks: Record } // Each card injects exactly one snapshot store plus its own actions. @@ -129,6 +141,7 @@ describe('ui-plugin-config apply', () => { await fiber.dispose() expect(slots.entries('settings.section')).toHaveLength(0) + expect(slots.spec('settings.plugins.tab')).toBeUndefined() expect(slots.spec('settings.plugin.item')).toBeUndefined() }) }) diff --git a/packages/client/ui-plugin-config/tests/section.client.spec.tsx b/packages/client/ui-plugin-config/tests/section.client.spec.tsx index 3945092587..8e0986892f 100644 --- a/packages/client/ui-plugin-config/tests/section.client.spec.tsx +++ b/packages/client/ui-plugin-config/tests/section.client.spec.tsx @@ -13,8 +13,10 @@ import { AgentLoopCard } from '../src/client/AgentLoopCard.tsx' import type { AgentLoopCardProps } from '../src/client/AgentLoopCard.tsx' import { BashCard } from '../src/client/BashCard.tsx' import type { BashCardProps } from '../src/client/BashCard.tsx' +import { ConfigurablePluginsTab } from '../src/client/ConfigurablePluginsTab.tsx' +import type { ConfigurablePluginsTabProps } from '../src/client/ConfigurablePluginsTab.tsx' import { PluginConfigSection } from '../src/client/PluginConfigSection.tsx' -import type { PluginConfigSectionProps } from '../src/client/PluginConfigSection.tsx' +import type { PluginConfigSectionProps, PluginSettingsTabRow } from '../src/client/PluginConfigSection.tsx' import { WebSearchCard } from '../src/client/WebSearchCard.tsx' import type { WebSearchCardProps } from '../src/client/WebSearchCard.tsx' import type { AgentLoopCardState } from '../src/client/agent-loop-store.ts' @@ -46,13 +48,24 @@ function cardActions() { return { edit: vi.fn(), resetField: vi.fn(), save: vi.fn(), discard: vi.fn() } } -function renderSection(cardCount: number, cards = 'cards') { +function renderSection(rows: readonly PluginSettingsTabRow[]) { + const props = { + t, + useTabs: (selector: (value: readonly PluginSettingsTabRow[]) => unknown) => selector(rows), + renderSlot: (_name: string, _owner: unknown, options: { only?: string }) => ( + {options.only} + ), + } as unknown as PluginConfigSectionProps + render() +} + +function renderConfigurable(cardCount: number, cards = 'cards') { const props = { t, cardCount, renderSlot: () =>
  • {cards}
  • , - } as unknown as PluginConfigSectionProps - render() + } as unknown as ConfigurablePluginsTabProps + render() } function renderBash(state: Partial = {}) { @@ -69,26 +82,53 @@ function renderBash(state: Partial = {}) { } describe('PluginConfigSection', () => { + it('says so when no plugin contributed a tab', () => { + renderSection([]) + + expect(screen.getByText(en.empty)).toBeTruthy() + expect(screen.queryByRole('tab')).toBeNull() + }) + + it('defaults to the first ordered tab and mounts another only after selection', () => { + renderSection([ + { id: 'configurable', order: 0, label: en.configurableTab }, + { id: 'all', order: 10, label: 'Plugin list' }, + ]) + + const configurable = screen.getByRole('tab', { name: en.configurableTab }) + const all = screen.getByRole('tab', { name: 'Plugin list' }) + expect(configurable.getAttribute('aria-selected')).toBe('true') + expect(screen.getByText('configurable')).toBeTruthy() + expect(screen.queryByText('all')).toBeNull() + + fireEvent.click(all) + expect(all.getAttribute('aria-selected')).toBe('true') + expect(screen.getByText('all')).toBeTruthy() + expect(screen.getByText('configurable').closest('[role="tabpanel"]')).toHaveProperty('hidden', true) + }) + + it('leads with its own heading and intro', () => { + renderSection([{ id: 'configurable', order: 0, label: en.configurableTab }]) + + expect(screen.getByRole('heading', { name: en.title })).toBeTruthy() + expect(screen.getByText(en.intro)).toBeTruthy() + }) +}) + +describe('ConfigurablePluginsTab', () => { it('says so when no plugin contributed a card', () => { - renderSection(0) + renderConfigurable(0) expect(screen.getByText(en.empty)).toBeTruthy() expect(screen.queryByText('cards')).toBeNull() }) it('renders the card list once a plugin contributed one', () => { - renderSection(1) + renderConfigurable(1) expect(screen.getByText('cards')).toBeTruthy() expect(screen.queryByText(en.empty)).toBeNull() }) - - it('leads with its own heading and intro', () => { - renderSection(1) - - expect(screen.getByRole('heading', { name: en.title })).toBeTruthy() - expect(screen.getByText(en.intro)).toBeTruthy() - }) }) describe('BashCard', () => { diff --git a/packages/client/ui-plugins/README.i18n.yaml b/packages/client/ui-plugins/README.i18n.yaml index 62085c3d6b..04e1faa8c5 100644 --- a/packages/client/ui-plugins/README.i18n.yaml +++ b/packages/client/ui-plugins/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-plugins/README.md -README.md: bb487d5e2cbd34406d83867997ede4d70b190d70 -README.zh.md: 48a11911509ea260aa9727d55c0b4df6efbfb1c9 +README.md: a663665cc2d7ce26a2724da9aecc25f51515327a +README.zh.md: 852ecbe6546f76a76fc6d9d3a8bb891539b0844f diff --git a/packages/client/ui-plugins/README.md b/packages/client/ui-plugins/README.md index bb487d5e2c..a663665cc2 100644 --- a/packages/client/ui-plugins/README.md +++ b/packages/client/ui-plugins/README.md @@ -2,9 +2,9 @@ English | [中文](README.zh.md) -Read-only Plugins section for Web Settings. The browser plugin registers one localized `settings.section` contribution with id `plugin-inventory`, after Models, and lets the Settings shell supply its ordinary fallback icon. It performs no Remote read during plugin activation; mounting the section lazily calls `ctx.remote.pluginInventory.list()` through [`api-remotes`](../../api/remotes/README.md). +Read-only **Plugin list** tab for Web Settings. The browser plugin registers one localized `settings.plugins.tab` contribution with id `all`; the Plugins section owns the navigation entry and tab chrome. It performs no Remote read during plugin activation. Selecting the tab for the first time mounts it and lazily calls `ctx.remote.pluginInventory.list()` through [`api-remotes`](../../api/remotes/README.md). -The page renders a searchable two-column catalog of compact disclosure cards. Each collapsed card uses the local Loader id as its title, a colored root-Fiber status dot, and a small effective-enablement tag. Expanding one card reveals its Loader-tree entry value without a redundant field label, followed by the effective configuration and Cordis status. Loading, empty, no-match, and generic failure states stay local to the mounted component, and a failed read can be retried without exposing transport details. The registration uses `ctx.slots.inject()`, so it follows late Settings declaration, redeclaration, locale changes, and teardown without owning another global store. +The tab renders a searchable two-column catalog of compact disclosure cards. Each collapsed card uses the short module name as its title and a small effective-enablement tag; enabled entries also show a colored root-fiber status dot. Expanding one card reveals its Loader-tree entry id without a redundant field label, followed by the effective configuration and, for enabled entries, Cordis status. Disabled entries omit the redundant unmounted runtime state. The entry id remains the React key, disclosure identity, detail value, and an additional search target; it is never classified by string shape. Loading, empty, no-match, and generic failure states stay local to the mounted component, and a failed read can be retried without exposing transport details. The registration uses `ctx.slots.inject()`, so it follows late tab declaration, redeclaration, locale changes, and teardown without importing the section owner. ## Model Experience @@ -16,5 +16,5 @@ None; this package neither assembles nor sends a provider request. ## Known Limitations and Deferred Work -- **One snapshot per mount or retry** — the page does not subscribe to Loader changes or automatically refetch after reconnect; reopening the section obtains a new snapshot. +- **One snapshot per Settings mount or retry** — the tab does not subscribe to Loader changes or automatically refetch after reconnect; switching tabs preserves the current snapshot, while reopening Settings obtains a new one. - **Read-only Loader view** — local search does not add provenance, current-browser activation diagnosis, grouping by source, or plugin mutation controls. diff --git a/packages/client/ui-plugins/README.zh.md b/packages/client/ui-plugins/README.zh.md index 48a1191150..852ecbe654 100644 --- a/packages/client/ui-plugins/README.zh.md +++ b/packages/client/ui-plugins/README.zh.md @@ -2,9 +2,9 @@ [English](README.md) | 中文 -Web 设置中的只读“插件”分区。浏览器插件在“模型”之后注册一个 id 为 `plugin-inventory` 的本地化 `settings.section` 贡献,并由 Settings shell 提供常规的回退图标。插件激活期间不会读取 Remote;挂载该分区时,组件才通过 [`api-remotes`](../../api/remotes/README.md) 懒调用 `ctx.remote.pluginInventory.list()`。 +Web 设置中的只读**插件列表**标签页。浏览器插件注册一个 id 为 `all` 的本地化 `settings.plugins.tab` 贡献;“插件”分区拥有导航入口与标签栏。插件激活期间不会读取 Remote;首次选择该标签页时才挂载组件,并通过 [`api-remotes`](../../api/remotes/README.md) 懒调用 `ctx.remote.pluginInventory.list()`。 -页面以可搜索的双列紧凑折叠卡片展示清单。每张收起的卡片使用 Loader 本地 id 作为标题,以彩色圆点表示根 Fiber 状态,以小标签表示有效启停状态。展开卡片后会直接展示 Loader 树条目值,不附加重复的字段标题,并列出有效配置状态与 Cordis 状态。加载、空结果、无匹配结果与通用失败状态只属于已挂载组件;读取失败后可以重试,且不会暴露传输细节。注册使用 `ctx.slots.inject()`,因此能跟随 Settings 的延迟声明、重新声明、本地化变化与 teardown,而不拥有另一份全局 store。 +该标签页以可搜索的双列紧凑折叠卡片展示清单。每张收起的卡片使用模块短名称作为标题,以小标签表示有效启停状态;已启用的条目还会以彩色圆点表示根 fiber 状态。展开卡片后会直接展示 Loader 树条目 id,不附加重复的字段标题,并列出有效配置状态;已启用的条目还会列出 Cordis 状态,已停用的条目则省略重复的“未挂载”运行状态。条目 id 仍作为 React key、展开标识、详情值与额外的搜索目标;代码不按字符串形状对它分类。加载、空结果、无匹配结果与通用失败状态只属于已挂载组件;读取失败后可以重试,且不会暴露传输细节。注册使用 `ctx.slots.inject()`,因此能跟随标签 slot 的延迟声明、重新声明、本地化变化与 teardown,而无需 import 分区拥有方。 ## 模型体验 @@ -16,5 +16,5 @@ Web 设置中的只读“插件”分区。浏览器插件在“模型”之后 ## 已知限制与暂缓事项 -- **每次挂载或重试只读取一份快照** —— 页面不订阅 Loader 变化,也不会在重连后自动重新读取;重新打开分区会取得新快照。 +- **每次 Settings 挂载或重试只读取一份快照** —— 标签页不订阅 Loader 变化,也不会在重连后自动重新读取;切换标签页会保留当前快照,重新打开 Settings 则会取得新快照。 - **只读 Loader 视图** —— 本地搜索不会额外引入来源、按来源分组、当前浏览器激活诊断或插件修改控件。 diff --git a/packages/client/ui-plugins/package.json b/packages/client/ui-plugins/package.json index 07fb9d162c..965a1ef415 100644 --- a/packages/client/ui-plugins/package.json +++ b/packages/client/ui-plugins/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-client-ui-plugins", - "description": "Read-only Cordis Loader plugin inventory in Web settings", + "description": "Read-only Cordis Loader inventory tab in Web Plugins settings", "version": "0.0.1-rc.2", "publishConfig": { "access": "restricted" diff --git a/packages/client/ui-plugins/src/client/PluginSettingsSection.module.css b/packages/client/ui-plugins/src/client/PluginSettingsSection.module.css index 9429b60bb5..de10a3bd36 100644 --- a/packages/client/ui-plugins/src/client/PluginSettingsSection.module.css +++ b/packages/client/ui-plugins/src/client/PluginSettingsSection.module.css @@ -7,19 +7,12 @@ color: var(--dsw-alias-label-primary); } -.heading h2, .catalogHeading h3, .status, .failure p { margin: 0; } -.heading h2 { - font-size: 16px; - line-height: 24px; - font-weight: 600; -} - .status, .failure { font-size: 13px; diff --git a/packages/client/ui-plugins/src/client/PluginSettingsSection.tsx b/packages/client/ui-plugins/src/client/PluginSettingsSection.tsx index 87d6486000..4febea4814 100644 --- a/packages/client/ui-plugins/src/client/PluginSettingsSection.tsx +++ b/packages/client/ui-plugins/src/client/PluginSettingsSection.tsx @@ -19,7 +19,7 @@ type PluginFiberPhase = PluginInventoryEntry['fiberPhase'] /** Full component props assembled by the Settings slot renderer. */ export type PluginSettingsSectionProps = - PropsRuntime<'settings.section'> + PropsRuntime<'settings.plugins.tab'> & PropsLocale<'settings.plugins'> & InjectFace @@ -62,7 +62,7 @@ function matches(entry: PluginInventoryEntry, normalizedQuery: string): boolean /** Render the read-only current Loader inventory. */ export function PluginSettingsSection({ list, t }: PluginSettingsSectionProps): ReactNode { - const titleId = useId() + const catalogId = useId() const [request, setRequest] = useState(0) const [query, setQuery] = useState('') const [expanded, setExpanded] = useState(null) @@ -97,10 +97,7 @@ export function PluginSettingsSection({ list, t }: PluginSettingsSectionProps): } return ( -
    -
    -

    {t('title')}

    -
    +
    {state.status === 'loading' ?

    {t('loading')}

    : null} {state.status === 'error' ? (
    @@ -134,8 +131,9 @@ export function PluginSettingsSection({ list, t }: PluginSettingsSectionProps): {filteredEntries.map((entry) => { const status = phaseLabel(entry.fiberPhase, t) const title = moduleShortName(entry.moduleName) + const configuration = t(entry.enabled ? 'enabledTag' : 'disabledTag') const open = expanded === entry.entryId - const detailId = `${titleId}-details-${encodeURIComponent(entry.entryId)}` + const detailId = `${catalogId}-details-${encodeURIComponent(entry.entryId)}` return (
  • { setExpanded(current => current === entry.entryId ? null : entry.entryId) }} > {title} - + {entry.enabled ? ( + + ) : null} - {t(entry.enabled ? 'enabledTag' : 'disabledTag')} + {configuration} @@ -174,12 +174,14 @@ export function PluginSettingsSection({ list, t }: PluginSettingsSectionProps):
    {t('configuration')}
    -
    {t(entry.enabled ? 'enabledTag' : 'disabledTag')}
    -
    -
    -
    {t('cordis')}
    -
    {status}
    +
    {configuration}
    + {entry.enabled ? ( +
    +
    {t('cordis')}
    +
    {status}
    +
    + ) : null}
  • ) : null} @@ -190,6 +192,6 @@ export function PluginSettingsSection({ list, t }: PluginSettingsSectionProps): ) : null}
    ) : null} -
    + ) } diff --git a/packages/client/ui-plugins/src/client/index.ts b/packages/client/ui-plugins/src/client/index.ts index ccf12ab989..f2261086b7 100644 --- a/packages/client/ui-plugins/src/client/index.ts +++ b/packages/client/ui-plugins/src/client/index.ts @@ -22,7 +22,7 @@ export const NS = 'settings.plugins' /** Services required by the Settings registration and generated Remote face. */ export const inject = ['slots', 'locale', 'remote', 'remote.pluginInventory'] -/** Register the lazy plugin inventory page below Models in Settings. */ +/** Contribute the lazy inventory tab to the Plugins settings section. */ export function apply(ctx: ClientContext): void { ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-plugins: dictionaries') @@ -36,11 +36,11 @@ export function apply(ctx: ClientContext): void { } const injected = (): PluginSettingsSectionInjected => ({ list }) - ctx.slots.inject('settings.section', () => ctx.slots.register({ - name: 'settings.section', - id: 'plugin-inventory', - order: 15, - label: () => t('nav'), + ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({ + name: 'settings.plugins.tab', + id: 'all', + order: 10, + label: () => t('tab'), locale: NS, inject: injected, }, PluginSettingsSection)) diff --git a/packages/client/ui-plugins/src/client/locales.ts b/packages/client/ui-plugins/src/client/locales.ts index c505296f38..a95d7b95cb 100644 --- a/packages/client/ui-plugins/src/client/locales.ts +++ b/packages/client/ui-plugins/src/client/locales.ts @@ -2,8 +2,7 @@ /** Simplified Chinese dictionary and key source of truth. */ export const zh = { - nav: '插件', - title: '插件', + tab: '插件列表', loading: '正在读取插件…', error: '暂时无法读取插件。', retry: '重试', @@ -28,8 +27,7 @@ export type PluginsKey = keyof typeof zh /** English dictionary checked against the Chinese key set. */ export const en = { - nav: 'Plugins', - title: 'Plugins', + tab: 'Plugin list', loading: 'Reading plugins…', error: 'Plugins are temporarily unavailable.', retry: 'Retry', diff --git a/packages/client/ui-plugins/src/index.ts b/packages/client/ui-plugins/src/index.ts index 489544a421..473386a9ec 100644 --- a/packages/client/ui-plugins/src/index.ts +++ b/packages/client/ui-plugins/src/index.ts @@ -1,4 +1,4 @@ -/** Host loader entry for the browser implementation exported from `./client`. */ +/** Host loader entry for the inventory-tab browser implementation exported from `./client`. */ -/** Host plugin body — no host-side behavior for the plugin settings section. */ +/** Host plugin body — no host-side behavior for the plugin inventory tab. */ export function apply(): void {} diff --git a/packages/client/ui-plugins/tests/browser-plugin.client.spec.tsx b/packages/client/ui-plugins/tests/browser-plugin.client.spec.tsx index d9d8a43cd8..e0feb50e69 100644 --- a/packages/client/ui-plugins/tests/browser-plugin.client.spec.tsx +++ b/packages/client/ui-plugins/tests/browser-plugin.client.spec.tsx @@ -38,7 +38,7 @@ async function bench() { function declare(slots: SlotsService): () => void { return slots.register({ name: 'root', - children: { 'settings.section': { kind: 'list', scope: 'root' } }, + children: { 'settings.plugins.tab': { kind: 'list', scope: 'root' } }, } as never, () => null) } @@ -47,16 +47,16 @@ describe('ui-plugins browser plugin', () => { expect(inject).toEqual(['slots', 'locale', 'remote', 'remote.pluginInventory']) }) - it('registers a localized section without reading the Remote eagerly', async () => { + it('registers a localized tab without reading the Remote eagerly', async () => { const b = await bench() declare(b.slots) await b.ctx.plugin({ inject: [...inject], apply }).await() - const entry = b.slots.entries('settings.section')[0]! + const entry = b.slots.entries('settings.plugins.tab')[0]! expect(entry.component).toBe(PluginSettingsSection) - expect(entry.options).toMatchObject({ id: 'plugin-inventory', order: 15 }) + expect(entry.options).toMatchObject({ id: 'all', order: 10 }) expect(entry.locale).toBe(NS) - expect(resolveSlotLabel(entry.options.label)).toBe('插件') + expect(resolveSlotLabel(entry.options.label)).toBe('插件列表') expect(b.list).not.toHaveBeenCalled() const injected = (entry.inject as unknown as () => PluginSettingsSectionInjected)() @@ -71,22 +71,22 @@ describe('ui-plugins browser plugin', () => { const b = await bench() const fiber = b.ctx.plugin({ inject: [...inject], apply }) await fiber.await() - expect(b.slots.entries('settings.section')).toHaveLength(0) + expect(b.slots.entries('settings.plugins.tab')).toHaveLength(0) const stop = declare(b.slots) - await vi.waitFor(() => { expect(b.slots.entries('settings.section')).toHaveLength(1) }) + await vi.waitFor(() => { expect(b.slots.entries('settings.plugins.tab')).toHaveLength(1) }) b.locale.setLocale('en') - expect(resolveSlotLabel(b.slots.entries('settings.section')[0]!.options.label)).toBe('Plugins') + expect(resolveSlotLabel(b.slots.entries('settings.plugins.tab')[0]!.options.label)).toBe('Plugin list') stop() - expect(b.slots.entries('settings.section')).toHaveLength(0) + expect(b.slots.entries('settings.plugins.tab')).toHaveLength(0) declare(b.slots) await vi.waitFor(() => { - expect(b.slots.entries('settings.section')[0]?.component).toBe(PluginSettingsSection) + expect(b.slots.entries('settings.plugins.tab')[0]?.component).toBe(PluginSettingsSection) }) await fiber.dispose() - expect(b.slots.entries('settings.section')).toHaveLength(0) + expect(b.slots.entries('settings.plugins.tab')).toHaveLength(0) expect(() => b.locale.register(NS, 'zh', {})).not.toThrow() await b.ctx.fiber.dispose() }) diff --git a/packages/client/ui-plugins/tests/components.client.spec.tsx b/packages/client/ui-plugins/tests/components.client.spec.tsx index 9da8a79b0d..bf104abcdb 100644 --- a/packages/client/ui-plugins/tests/components.client.spec.tsx +++ b/packages/client/ui-plugins/tests/components.client.spec.tsx @@ -12,16 +12,12 @@ afterEach(cleanup) type Snapshot = Awaited> const t = ((key: PluginsKey): string => en[key]) as PluginSettingsSectionProps['t'] -const unusedHook = (() => { throw new Error('unused by plugin inventory') }) as never function props(list: PluginSettingsSectionInjected['list']): PluginSettingsSectionProps { return { - close: vi.fn(), - useSessions: unusedHook, - useWorkspaces: unusedHook, t, list, - } + } as PluginSettingsSectionProps } const SNAPSHOT = { @@ -36,7 +32,7 @@ const SNAPSHOT = { } as unknown as Snapshot describe('PluginSettingsSection', () => { - it('renders searchable two-column-card semantics with dots and tags', async () => { + it('renders runtime status only for enabled plugins', async () => { const deferred = Promise.withResolvers() const list = vi.fn(() => deferred.promise) const view = render() @@ -56,10 +52,10 @@ describe('PluginSettingsSection', () => { 'Loading', 'Mount failed', 'Unloading', - 'Not mounted', ]) { expect(screen.getByRole('img', { name: value })).toBeTruthy() } + expect(screen.queryByRole('img', { name: 'Not mounted' })).toBeNull() const active = screen.getByRole('button', { name: 'hmr, Mounted, Enabled' }) expect(active.getAttribute('aria-expanded')).toBe('false') fireEvent.click(active) @@ -75,8 +71,10 @@ describe('PluginSettingsSection', () => { target: { value: 'disabled-entry' }, }) expect(view.container.querySelector('[data-loader-entry]')).toBeNull() - fireEvent.click(screen.getByRole('button', { name: 'directory-picker-native, Not mounted, Disabled' })) + fireEvent.click(screen.getByRole('button', { name: 'directory-picker-native, Disabled' })) expect(screen.getAllByText(en.disabledTag)).toHaveLength(2) + expect(screen.queryByText(en.cordis)).toBeNull() + expect(screen.queryByText(en.unobserved)).toBeNull() }) it('filters by module name or Loader entry id', async () => { diff --git a/packages/client/ui-settings/README.i18n.yaml b/packages/client/ui-settings/README.i18n.yaml index 392f2b52a1..ec7c333250 100644 --- a/packages/client/ui-settings/README.i18n.yaml +++ b/packages/client/ui-settings/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-settings/README.md -README.md: 8bf085fd02e3c76148674065bb4f5708e9a6e8d8 -README.zh.md: f7b6f18809c64be6830ea23c3968e9af70b70c41 +README.md: 950585c4957cd59fe3a38dc37cdd4084f7c5541c +README.zh.md: dce8dbf5c8e0939142fed8a3df84c47acd7c8a1e diff --git a/packages/client/ui-settings/README.md b/packages/client/ui-settings/README.md index 8bf085fd02..950585c495 100644 --- a/packages/client/ui-settings/README.md +++ b/packages/client/ui-settings/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The settings domain's base layer, with two roles and no presentation of its own. It provides `ctx.settingsScope`, the Host transport every preference row binds its durable namespace section through, and it declares the settings slot types registrants fill: `settings.trigger` / `settings.header` / `settings.close` (chrome content), `settings.action` (ordered content-header actions), `settings.section` (one page per feature), and `settings.onboarding` (ordered feature-owned pages). It depends on no `ui-*` presentation package, so any feature that owns a preference can reach it; the settings SHELL — the `sidebar.settings` occupant, its navigation, and the chrome — lives in ui-settings-general, because a shell dependency on ui-sidebar would close a reference graph cycle through ui-layout and ui-theme. The shell's own contract types live beside the shell for the same reason. +The settings domain's base layer, with two roles and no presentation of its own. It provides `ctx.settingsScope`, the Host transport every preference row binds its durable namespace section through, and it declares the settings slot types registrants fill: `settings.trigger` / `settings.header` / `settings.close` (chrome content), `settings.action` (ordered content-header actions), `settings.section` (one page per feature), `settings.plugins.tab` (feature-owned pages inside the Plugins section), and `settings.onboarding` (ordered feature-owned pages). It depends on no `ui-*` presentation package, so any feature that owns a preference can reach it; the settings SHELL — the `sidebar.settings` occupant, its navigation, and the chrome — lives in ui-settings-general, because a shell dependency on ui-sidebar would close a reference graph cycle through ui-layout and ui-theme. The shell's own contract types live beside the shell for the same reason. The plugin injects nothing and waits for nothing: `ctx.settingsScope.bind(spec)` resolves the wire face through the CALLER's context at call time, so the bound scope's disposer belongs to the calling fiber, and the caller injects `connection` for the transport and `remote` for the invalidation. Listeners exist before the first background read starts, so a row's activation never blocks on the settings transport. A bound scope reloads on the forwarded `settings/document-updated` event for its own namespace and on `connection/reset`. Writes carry one field path and the last known namespace revision as `expectedRevision`; a rejected or failed write re-reads unless a newer write already superseded it, and a stale read never publishes over a newer one. Without a `decode` in the spec, a section that is not a plain object, fails its rehydrated schema, or carries a schema envelope this client cannot rehydrate publishes no value at all, so a row renders its own absent state instead of a half-decoded one. diff --git a/packages/client/ui-settings/README.zh.md b/packages/client/ui-settings/README.zh.md index f7b6f18809..dce8dbf5c8 100644 --- a/packages/client/ui-settings/README.zh.md +++ b/packages/client/ui-settings/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -设置领域的底座,承担两项职责,本身不含任何呈现内容。它提供 `ctx.settingsScope`——每个偏好设置行绑定自己那份持久化命名空间分区所用的宿主传输层;并声明由注册方填充的设置 slot 类型:`settings.trigger`/`settings.header`/`settings.close`(界面框架内容)、`settings.action`(内容标题栏中的有序操作)、`settings.section`(每项功能一页)和 `settings.onboarding`(由各功能持有的有序页面)。它不依赖任何 `ui-*` 呈现包,因此任何持有偏好设置的功能都能够到它;设置**外壳**——`sidebar.settings` 占位方、它的导航与界面框架——位于 ui-settings-general,因为外壳一旦依赖 ui-sidebar,就会经 ui-layout 与 ui-theme 闭合出一条引用图环路。外壳自身的契约类型出于同一原因与外壳放在一起。 +设置领域的底座,承担两项职责,本身不含任何呈现内容。它提供 `ctx.settingsScope`——每个偏好设置行绑定自己那份持久化命名空间分区所用的宿主传输层;并声明由注册方填充的设置 slot 类型:`settings.trigger`/`settings.header`/`settings.close`(界面框架内容)、`settings.action`(内容标题栏中的有序操作)、`settings.section`(每项功能一页)、`settings.plugins.tab`(“插件”分区内由各功能持有的页面)和 `settings.onboarding`(由各功能持有的有序页面)。它不依赖任何 `ui-*` 呈现包,因此任何持有偏好设置的功能都能够到它;设置**外壳**——`sidebar.settings` 占位方、它的导航与界面框架——位于 ui-settings-general,因为外壳一旦依赖 ui-sidebar,就会经 ui-layout 与 ui-theme 闭合出一条引用图环路。外壳自身的契约类型出于同一原因与外壳放在一起。 该插件不注入任何服务、也不等待任何服务:`ctx.settingsScope.bind(spec)` 在调用时经**调用方**的 context 解析线路面,因此绑定所得 scope 的 disposer 归调用方 fiber 所有,而由调用方注入 `connection` 取得传输层、注入 `remote` 取得失效通知。监听器在首次后台读取启动之前就已存在,因此某一行的激活绝不会阻塞在设置传输层上。已绑定的 scope 会在收到属于自己命名空间的转发 `settings/document-updated` 事件时、以及在 `connection/reset` 时重新读取。写入携带单一字段路径以及最近已知的命名空间 revision 作为 `expectedRevision`;被拒绝或失败的写入会重新读取,除非已有更新的写入取代了它,而过期的读取绝不会覆盖发布更新的结果。若 spec 未提供 `decode`,则分区不是普通对象、未通过其重建后的 schema 校验、或携带本客户端无法重建的 schema 信封时,一律不发布任何值,于是行渲染自己的缺失状态,而不是一份半解码的值。 diff --git a/packages/client/ui-settings/src/client/contract/slots.ts b/packages/client/ui-settings/src/client/contract/slots.ts index e648fb2244..79ec682959 100644 --- a/packages/client/ui-settings/src/client/contract/slots.ts +++ b/packages/client/ui-settings/src/client/contract/slots.ts @@ -51,6 +51,15 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { * item registrant; the shell neither declares nor renders it.) */ 'settings.section': { kind: 'list'; scope: 'root'; owner: SettingsSectionOwnerProps } + /** + * One page inside the Plugins settings section. The section owner renders + * localized entry labels as tabs and mounts each contribution inside its + * corresponding tab panel. Options: `id` (tab key), `order` (tab order), + * and `label` (registrant-localized tab text). Declared at runtime by the + * feature that owns the Plugins section; the type lives here so inventory + * and configuration plugins collaborate without depending on one another. + */ + 'settings.plugins.tab': { kind: 'list'; scope: 'root'; owner: SettingsPluginsTabOwnerProps } /** * Root-scoped onboarding steps contributed by settings features. The * shell mounts one ordered step at a time; the active registrant either @@ -83,6 +92,12 @@ export interface SettingsGeneralItemOwnerProps { children?: never } +/** Owner share of a Plugins tab (the section supplies nothing). */ +export interface SettingsPluginsTabOwnerProps { + /** Marker field: tab owner props are intentionally empty. */ + children?: never +} + /** Owner share of the trigger content seat: the sidebar column state. */ export interface SettingsTriggerOwnerProps { /** Whether the sidebar renders wide content (false = 56px rail, icon only). */ diff --git a/packages/client/ui-settings/src/client/index.ts b/packages/client/ui-settings/src/client/index.ts index 8a6fbf25c9..2a670a7396 100644 --- a/packages/client/ui-settings/src/client/index.ts +++ b/packages/client/ui-settings/src/client/index.ts @@ -13,7 +13,7 @@ import { SettingsScopeService } from './settings-scope.ts' export type { SettingsGeneralItemOwnerProps, SettingsHeaderOwnerProps, SettingsOnboardingOwnerProps, - SettingsSectionOwnerProps, SettingsTriggerOwnerProps, + SettingsPluginsTabOwnerProps, SettingsSectionOwnerProps, SettingsTriggerOwnerProps, } from './contract/slots.ts' export { SettingsScopeController, SettingsScopeService } from './settings-scope.ts' From 28fcda2751c73882c3b0e4471f2d4d2813e7e0de Mon Sep 17 00:00:00 2001 From: pku-xht Date: Wed, 12 Aug 2026 17:07:34 +0800 Subject: [PATCH 002/160] feat(agent-presets): enable background Codex and Claude Code subagent tasks --- ...8-10-fork-children-stay-one-shot.i18n.yaml | 4 +- .../2026-08-10-fork-children-stay-one-shot.md | 4 +- ...26-08-10-fork-children-stay-one-shot.zh.md | 4 +- ...code-and-codex-subagent-backends.i18n.yaml | 4 +- ...claude-code-and-codex-subagent-backends.md | 29 ++++---- ...ude-code-and-codex-subagent-backends.zh.md | 29 ++++---- ...bagent-one-shot-background-tasks.i18n.yaml | 6 ++ ...duct-subagent-one-shot-background-tasks.md | 69 +++++++++++++++++++ ...t-subagent-one-shot-background-tasks.zh.md | 69 +++++++++++++++++++ .../agent-presets/code/agent.cordis.yml | 4 +- .../agent-presets/cordis/agent.cordis.yml | 4 +- .../editing-cordis-compositions/SKILL.md | 6 +- .../agent-presets/standard/agent.cordis.yml | 4 +- apps/cli/tests/web-agent-presets.e2e.ts | 16 +++++ examples/acp-agent/cordis.yml | 3 +- .../product-subagent-both.cordis.snapshot.yml | 4 +- .../product-subagent-both.cordis.yml | 8 +-- ...product-subagent-codex.cordis.snapshot.yml | 2 +- .../product-subagent-codex.cordis.yml | 4 +- .../subagent/subagent-claude-code/cordis.yml | 6 +- .../subagent/subagent-claude-code/driver.ts | 5 ++ .../subagent/subagent-codex/cordis.yml | 4 +- .../subagent/subagent-codex/driver.ts | 5 ++ .../tool-schemas.expected.json | 12 +++- .../tool-schemas.expected.json | 6 +- examples/headless-agent/cordis.yml | 3 +- .../subagent-claude-code/README.i18n.yaml | 4 +- .../subagent/subagent-claude-code/README.md | 20 ++++-- .../subagent-claude-code/README.zh.md | 20 ++++-- .../tests/loader-composition.e2e.ts | 7 +- .../subagent/subagent-codex/README.i18n.yaml | 4 +- packages/subagent/subagent-codex/README.md | 20 ++++-- packages/subagent/subagent-codex/README.zh.md | 20 ++++-- .../tests/loader-composition.e2e.ts | 5 +- .../tool-subagent/tests/tool-subagent.spec.ts | 6 +- 35 files changed, 318 insertions(+), 102 deletions(-) create mode 100644 .agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.i18n.yaml create mode 100644 .agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.md create mode 100644 .agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.zh.md diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml index ef424471d8..282779ce62 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md -2026-08-10-fork-children-stay-one-shot.md: 030bed3c1b516a54afd5f97b00000ed7ceaf64d8 -2026-08-10-fork-children-stay-one-shot.zh.md: 4b033a70c3674315c491e85be67a1d357aeac51b +2026-08-10-fork-children-stay-one-shot.md: f799f9f7affe7dfecbbe12d0988cf6f9b936d43e +2026-08-10-fork-children-stay-one-shot.zh.md: 5c55e96296ed6678ac82afc0e40150d443f74c96 diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md index 030bed3c1b..f799f9f7af 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md @@ -12,7 +12,7 @@ The child-scoped `report` return channel is now the largest such addition, and s ## Decision -Every shipped composition binds the fork delegation tool to `backgroundMode: one-shot`: [the base bundle](../../../../packages/bundle/base/cordis.patch.yml), [the ACP example](../../../../examples/acp-agent/cordis.yml), and [the headless example](../../../../examples/headless-agent/cordis.yml). The base bundle leaves `run_in_background` available, because it mounts a task service; the two examples set `enableRunInBackground: false`, because they mount none and a one-shot background start would otherwise fail at call time on a missing `tasks` service. +Every shipped composition binds the fork delegation tool to `backgroundMode: one-shot`: [the base bundle](../../../../packages/bundle/base/cordis.patch.yml), [the ACP example](../../../../examples/acp-agent/cordis.yml), and [the headless example](../../../../examples/headless-agent/cordis.yml). All three have access to the generic Task runtime: the base bundle leaves `run_in_background` available, while the two examples set `enableRunInBackground: false` as an explicit foreground-only composition choice. One-shot children — foreground and background alike — are created through `SubagentService.start()`, which never enters the continuable activation-setup registry, so neither `report` nor its prompt section is installed. A forked one-shot child's system prompt and tool schemas therefore equal its parent's, apart from the `persona` and `toolFilter` deltas a deployment opts into per delegation tool. @@ -38,7 +38,7 @@ The reintroduction condition is recorded as a `TODO(fork-continuable-prefix-reus ## Consequences -- No shipped composition creates a continuable forked child; `subagent_fork` returns a result to its caller's turn, and `send_message` addresses only spawned children. +- No shipped composition creates a continuable forked child. The base bundle may return a one-shot Task id for `subagent_fork`, while the ACP and headless examples return the result to the caller's turn; `send_message` addresses only spawned children. - A forked child's request prefix stays byte-identical to its parent's unless the deployment configures `persona` or `toolFilter` on the fork delegation tool, so the token cost of seeding buys provider-side reuse again. - The fork provider's continuable path has no production caller and no assembled-composition coverage. It keeps its package-level tests, and the seam still accepts it, so a bundle or `--patch` overlay can reintroduce it with no code change and no warning. - `subagent_fork`'s model-visible schema changes: the continuable background wording is replaced by the one-shot task wording in the base bundle, and disappears entirely from the two examples. The affected keyless snapshot tool-schema sidecars are re-recorded in the same change. diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md index 4b033a70c3..5c55e96296 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md @@ -12,7 +12,7 @@ fork 与 spawn 的唯一区别是 child 的 Session 会以 parent 已完成轮 ## 决策 -所有随附组合都把 fork 委派工具绑定为 `backgroundMode: one-shot`:[base 组合包](../../../../packages/bundle/base/cordis.patch.yml)、[ACP 示例](../../../../examples/acp-agent/cordis.yml)与[headless 示例](../../../../examples/headless-agent/cordis.yml)。base 组合包保留 `run_in_background`,因为它挂载了 task 服务;两个示例设置 `enableRunInBackground: false`,因为它们都不挂载 task 服务,否则一次 one-shot 后台启动会在调用时因缺少 `tasks` 服务而失败。 +所有随附组合都把 fork 委派工具绑定为 `backgroundMode: one-shot`:[base 组合包](../../../../packages/bundle/base/cordis.patch.yml)、[ACP 示例](../../../../examples/acp-agent/cordis.yml)与[headless 示例](../../../../examples/headless-agent/cordis.yml)。三者都能使用通用 Task 运行时:base 组合包保留 `run_in_background`,两个示例则把 `enableRunInBackground: false` 作为显式的仅前台组装选择。 one-shot child——前台与后台皆然——经由 `SubagentService.start()` 创建,该路径从不进入可继续的 activation setup 注册表,因此 `report` 与它的提示词 section 都不会被安装。于是一个 fork 出的 one-shot child 的系统提示词与工具 schema 与其 parent 相同,只差部署逐个委派工具主动选择的 `persona` 与 `toolFilter` 增量。 @@ -38,7 +38,7 @@ one-shot child——前台与后台皆然——经由 `SubagentService.start()` ## 后果 -- 没有任何随附组合会创建可继续的 fork child;`subagent_fork` 把结果返回给调用方的轮次,而 `send_message` 只寻址 spawn 出的 child。 +- 没有任何随附组合会创建可继续的 fork child。base 组合包中的 `subagent_fork` 可以返回 one-shot Task id,而 ACP 与 headless 示例会把结果返回给调用方的轮次;`send_message` 只寻址 spawn 出的 child。 - 除非部署在 fork 委派工具上配置了 `persona` 或 `toolFilter`,fork child 的请求前缀与其 parent 逐字节相同,因此初始内容的 token 成本重新换来了提供方侧的复用。 - fork 提供方的可继续路径没有生产调用方,也没有整体组装层面的覆盖。它保留自己的包内测试,seam 也仍然接受它,因此某个组合包或 `--patch` 覆盖层可以无需改动代码、也不会有任何警告地把它重新引入。 - `subagent_fork` 面向模型的 schema 发生变化:base 组合包中可继续的后台措辞被 one-shot 的 task 措辞取代,在两个示例中则完全消失。受影响的无密钥快照工具 schema 伴随文件在同一次改动中重新记录。 diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml index 84cb091651..278981d220 100644 --- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md -2026-08-04-claude-code-and-codex-subagent-backends.md: ccc96d6c998c4ab958a7eea1e502d036d16ec90d -2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 740eeb633e336d5b01cb0b84fb656612e690959d +2026-08-04-claude-code-and-codex-subagent-backends.md: 99801d6bbd9c67e6c9215d4e10c77fb70d5e7525 +2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 0944f1356ff334cc284def91d5ce0924ce950b83 diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md index ccc96d6c99..99801d6bbd 100644 --- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md +++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md @@ -12,24 +12,25 @@ The product integrations must not become second owners for task text, cwd, cance ## Decision -The harness publishes two sibling one-shot provider packages: `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [shared-profile-host placement decision](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md) supersedes the original opt-in composition placement. Loading either provider starts no product process, and each tool accepts only a standalone text task; product selection and background execution are not model arguments. +The harness publishes two sibling one-shot provider packages: `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [shared-profile-host placement decision](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md) owns provider placement, and the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice. Loading either provider starts no product process, and each tool accepts only a standalone text task; product selection remains deployment configuration. -Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools disable background execution and use `maxDepth: 'provider-managed'`, leaving recursion policy with the out-of-process product instead of sending a limit the provider cannot enforce. Every call creates a fresh product process and a non-resumable product conversation. The shared subagent service continues to own request resolution, lifecycle events, result settlement, and foreground collection; the shared subprocess service owns credential scrubbing, process-tree termination, and whole-tree exit observation. +Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools use `backgroundMode: 'one-shot'` and `maxDepth: 'provider-managed'`: the consumer keeps foreground collection as the default and may place the same run in the generic Task runtime, while recursion policy stays with the out-of-process product. Every call creates a fresh product process and a non-resumable product conversation. `ctx.subagents` owns named-request resolution and paired lifecycle events; `dsh-tool-subagent` owns model-visible scheduling and foreground-versus-Task adaptation; `ctx.tasks` and `dsh-tool-tasks` own Task ids, state, output, controls, notices, and parent-owner cancellation; each product provider owns native result mapping, while `dsh-subprocess` owns credential scrubbing, process-tree termination, and whole-tree exit observation. ```text -fixed tool → shared subagent service → product provider → official product process - ← final answer / explicit error / cancellation ← terminal product fact - → foreground disposal → shared process-tree termination → whole-tree exit +fixed tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> product process + foreground <- final product outcome + background -> ctx.tasks / dsh-tool-tasks -> Task id / state / notice / controls + both -> provider disposal -> dsh-subprocess -> whole-tree exit ``` ### Ownership and lifecycle -| Phase | Shared owner | Product-specific responsibility | Observable result | +| Layer | Owner | Responsibility | Observable result | | --- | --- | --- | --- | -| Resolve | `dsh-tool-subagent` and `ctx.subagents` | Validate the product's text-only input and derive native startup parameters | Unsupported context or malformed input fails before a run is published | -| Start | `dsh-subprocess` owns every acquired process tree | Reach the smallest native point at which the product conversation and process can both be controlled | `start()` publishes one existing `SubagentRun`, or cleans up and rejects | -| Run | The product owns its native protocol facts; the holder owns their mapping | Submit exactly one task and derive an existing shared stop reason; Codex uses `max-tokens` only for explicit context exhaustion | The parent receives only a final answer or an explicit failure | -| Dispose | The foreground consumer requests release; `dsh-subprocess` proves exit | Close the native protocol and express any best-effort native cancellation | Disposal is idempotent and returns only after the whole process tree exits | +| Delegation lifecycle | `ctx.subagents` | Resolve the named provider request and pair lifecycle events around the published `SubagentRun` | Unsupported context or malformed input fails before a run is published; start and terminal events remain paired | +| Scheduling and adaptation | `dsh-tool-subagent` | Interpret `run_in_background`, choose foreground collection or one-shot Task registration, and map the shared stop reason | Foreground returns the product outcome; background returns a Task id after registration | +| Task state and control | `ctx.tasks` and `dsh-tool-tasks` | Own Task state, output, cancellation, owner cleanup, completion notices, and model-facing controls | The exact parent can collect, list, or stop background work and receives its completion notice | +| Native run and teardown | Product provider and `dsh-subprocess` | Produce one native result, close the product protocol, request best-effort native cancellation, and prove process-tree exit | Foreground return and Task settlement both wait for idempotent disposal and whole-tree exit | ## Codex provider @@ -59,7 +60,7 @@ The credentialed Claude Code e2e uses the official DeepSeek Claude Code contract ## Distribution and evidence -Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The Loader tier boots the README-shaped user configuration, verifies both fixed foreground-only tools in one context, and starts neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret. +Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The Loader tier boots the README-shaped user configuration, verifies both fixed one-shot tools expose optional background scheduling alongside generic task controls, and starts neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret. The Codex evidence pins `@openai/codex@0.147.0` and `codex-cli 0.147.0`. Its real-product spec observes the exact Bearer key, original task, byte-exact final answer, unattended command rejection with no file side effect, local cancellation, and whole-tree exit. Production still supplies `codex` on `PATH`. @@ -83,12 +84,12 @@ The project owner's distribution authorization is scoped to the official `@anthr **Plugin-managed login, product home, models, settings, or permissions.** Those choices would create another authority beside each product's native configuration and enlarge a one-shot provider into account management. The providers expose only an explicit environment overlay and teardown grace; unattended interaction fails closed. -**Continuation, progress, background collection, and shared parent context.** The delivered user result is one self-contained task and one final answer. Product sessions, resume, follow-up, intermediate messages, parent transcript transfer, structured output, and background collection need separate user contracts and are not prebuilt. +**Continuation, progress, product-native background state, and shared parent context.** The provider payload remains one final answer for one self-contained task. The generic Task layer may add its id, status, notice, collection, and cancellation results, but product sessions, resume, follow-up, intermediate messages, parent transcript transfer, structured output, and provider-specific background state need separate user contracts and are not prebuilt. ## Consequences -Users delegate through two stable foreground tools backed by the official product integrations. Their Profile placement and per-Preset exposure are owned by the [shared-host placement decision](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md); this note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of task settlement and process-tree quiescence. +Users delegate through two stable one-shot tools backed by the official product integrations. Their Profile placement and per-Preset exposure are owned by the [shared-host placement decision](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md), and their foreground-default optional Task scheduling is owned by the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md). This note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of task settlement and process-tree quiescence. -Every delegation pays for a fresh product process and independent model context, and only final text reaches the parent. Product-native configuration makes behavior depend on the deployment's installed product, account state, and workspace settings. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout. +Every delegation pays for a fresh product process and independent model context. The product payload reaching the parent is final text only; background scheduling additionally exposes generic Task ids, status, completion notices, and collection or cancellation results. Product-native configuration makes behavior depend on the deployment's installed product, account state, and workspace settings. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout. Compatibility is pinned by package-level unit coverage, keyless real-product loopback tests, credentialed DeepSeek nonce tests, public Loader composition, built-package and NodeNext consumer checks, generated documentation and notices, and the repository CI matrix. A supported product or DeepSeek endpoint/model baseline change must refresh those facts; production performs no separate runtime version probe. diff --git a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md index 740eeb633e..0944f1356f 100644 --- a/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md +++ b/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.zh.md @@ -12,24 +12,25 @@ Status: implemented ## 决策 -harness 交付两个同级的一次性提供方包:`codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[共享 profile 宿主归属决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)取代原先由用户选择启用的组装位置。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择与后台执行都不作为模型参数。 +harness 交付两个同级的一次性提供方包:`codex` 与 `claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[共享 profile 宿主归属决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)负责提供方位置,[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责模型可见的调度选择。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品选择仍属于部署配置。 -这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动能力,并传递父会话 cwd,但不会复制父级对话。文档所示的工具会禁用后台执行,并使用 `maxDepth: 'provider-managed'`,将递归策略留给进程外产品,而不是发送提供方无法强制执行的限制。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。共享 subagent 服务继续负责请求解析、生命周期事件、结果结算和前台收集;共享子进程服务负责凭证清洗、进程树终止以及整棵进程树的退出观测。 +这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动能力,并传递父会话 cwd,但不会复制父级对话。文档所示的工具使用 `backgroundMode: 'one-shot'` 与 `maxDepth: 'provider-managed'`:消费方默认在前台收集结果,也可把同一次运行放入通用 Task 运行时,而递归策略仍由进程外产品负责。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。`ctx.subagents` 负责具名请求解析与成对生命周期事件;`dsh-tool-subagent` 负责模型可见的调度以及前台与 Task 适配;`ctx.tasks` 和 `dsh-tool-tasks` 负责 Task id、状态、输出、控制、通知与父级 owner 取消;各产品提供方负责原生结果映射,`dsh-subprocess` 则负责凭证清洗、进程树终止以及整棵进程树的退出观测。 ```text -fixed tool → shared subagent service → product provider → official product process - ← final answer / explicit error / cancellation ← terminal product fact - → foreground disposal → shared process-tree termination → whole-tree exit +fixed tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> product process + foreground <- final product outcome + background -> ctx.tasks / dsh-tool-tasks -> Task id / state / notice / controls + both -> provider disposal -> dsh-subprocess -> whole-tree exit ``` ### 归属与生命周期 -| 阶段 | 共享责任方 | 产品特定职责 | 可观察结果 | +| 层级 | 责任方 | 职责 | 可观察结果 | | --- | --- | --- | --- | -| 解析 | `dsh-tool-subagent` 与 `ctx.subagents` | 验证产品的纯文本输入并推导原生启动参数 | 不受支持的上下文或格式错误的输入会在发布运行前报错 | -| 启动 | `dsh-subprocess` 负责每棵已获取的进程树 | 到达能够同时控制产品对话与进程的最小原生控制点 | `start()` 发布一个已存在的 `SubagentRun`,否则清理后拒绝调用 | -| 运行 | 产品负责其原生协议事实;持有方负责映射这些事实 | 只提交一项任务,并推导出一种现有的共享停止原因;Codex 仅在明确发生上下文耗尽时使用 `max-tokens` | 父级只会收到最终回答或明确失败 | -| dispose(资源释放) | 前台消费方请求释放;`dsh-subprocess` 证明进程已退出 | 关闭原生协议,并发出尽力而为的原生取消请求 | 释放操作具有幂等性,且仅在整棵进程树退出后才返回 | +| 委派生命周期 | `ctx.subagents` | 解析具名提供方请求,并为已发布的 `SubagentRun` 配对生命周期事件 | 不受支持的上下文或格式错误的输入会在发布运行前报错;启动与终态事件保持成对 | +| 调度与适配 | `dsh-tool-subagent` | 解释 `run_in_background`,选择前台收集或 one-shot Task 登记,并映射共享停止原因 | 前台返回产品结果;后台在登记完成后返回 Task id | +| Task 状态与控制 | `ctx.tasks` 与 `dsh-tool-tasks` | 负责 Task 状态、输出、取消、owner 清理、完成通知与面向模型的控制工具 | 准确父级可以收集、列出或停止后台工作,并收到完成通知 | +| 原生运行与清理 | 产品提供方与 `dsh-subprocess` | 产生一个原生结果、关闭产品协议、请求尽力而为的原生取消,并证明进程树退出 | 前台返回与 Task 结算都会等待幂等资源释放和整棵进程树退出 | ## Codex 提供方 @@ -59,7 +60,7 @@ Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端 ## 分发与证据 -每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 组合 e2e 和一项带密钥 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Loader 层级会启动 README 所示形态的用户配置,在同一个上下文中验证两个固定且只支持前台执行的工具,并且不会启动任何产品进程。带密钥层级会使用仅在运行时提供的密钥启动同一生产提供方与真实产品,要求从固定的 DeepSeek 官方服务取得唯一随机数,并再次证明完全停稳;仅当本地操作者未提供密钥时才会自行跳过,而受信任的 CI 会预检该 secret。 +每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 组合 e2e 和一项带密钥 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Loader 层级会启动 README 所示形态的用户配置,在同一个上下文中验证两个固定一次性工具会与通用任务控制工具一起公开可选后台调度,而且不会启动任何产品进程。带密钥层级会使用仅在运行时提供的密钥启动同一生产提供方与真实产品,要求从固定的 DeepSeek 官方服务取得唯一随机数,并再次证明完全停稳;仅当本地操作者未提供密钥时才会自行跳过,而受信任的 CI 会预检该 secret。 Codex 证据锁定 `@openai/codex@0.147.0` 与 `codex-cli 0.147.0`。其真实产品测试会观测确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、不会产生文件副作用的无人值守命令拒绝、本地取消以及整棵进程树退出。生产环境仍提供 `codex`,并通过 `PATH` 解析。 @@ -83,12 +84,12 @@ Claude Code 证据锁定 Agent SDK 0.3.220,并使用 SDK 按平台分发的 Cl **由插件管理登录、产品主目录、模型、设置或权限。** 这些选择会在每个产品的原生配置之外建立另一套权威来源,并将一次性提供方扩张为账户管理功能。提供方只公开显式环境覆盖项和清理宽限期;无人值守交互会以默认拒绝方式失败。 -**续接、进度、后台收集和共享父级上下文。** 已交付的用户结果是一项自包含任务和一个最终回答。产品会话、恢复、后续交互、中间消息、父级 transcript(文本记录)传递、结构化输出和后台收集都需要独立的用户约定,当前实现不会预先构建这些功能。 +**续接、进度、产品原生后台状态和共享父级上下文。** 提供方载荷仍是一项自包含任务的一个最终回答。通用 Task 层可以额外提供 id、状态、通知、收集与取消结果,但产品会话、恢复、后续交互、中间消息、父级 transcript(文本记录)传递、结构化输出和提供方专属后台状态都需要独立的用户约定,当前实现不会预先构建这些功能。 ## 后果 -用户通过官方产品集成支持的两个稳定前台工具进行委派。它们在 Profile 中的归属和按 Preset 暴露方式由[共享宿主归属决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)负责;本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占任务结算与进程树完全停稳的责任。 +用户通过官方产品集成支持的两个稳定一次性工具进行委派。它们在 Profile 中的归属和按 Preset 暴露方式由[共享宿主归属决策](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md)负责,默认前台且可选通用 Task 的调度方式由[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.md)负责。本说明规定的提供方生命周期会保留原生设置与行为,而共享服务继续独占任务结算与进程树完全停稳的责任。 -每次委派都要承担新建产品进程和独立模型上下文的开销,且只有最终文本会到达父级。产品原生配置使行为取决于部署环境中安装的产品、账户状态和工作区设置。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。 +每次委派都要承担新建产品进程和独立模型上下文的开销。到达父级的产品载荷仍只有最终文本;后台调度还会额外公开通用 Task id、状态、完成通知以及收集或取消结果。产品原生配置使行为取决于部署环境中安装的产品、账户状态和工作区设置。带密钥 e2e 运行还会消耗外部 API 配额,并依赖 DeepSeek 官方端点;对协议、失败、取消与审批的确定性覆盖仍由无密钥层级承担。提供方不会恢复会话、以流式方式传送进度、接受新的人工交互、回滚工具或文件副作用,也不会施加按实际经过时间触发的超时。 兼容性由包级单元测试覆盖率、无密钥真实产品回环测试、带密钥 DeepSeek 随机数测试、公开 Loader 组合、已构建包与 NodeNext 消费方检查、生成的文档与声明以及仓库 CI 矩阵共同锁定。更改受支持的产品基线或 DeepSeek 端点/模型基线时必须刷新这些事实;生产环境不会另行执行运行时版本探测。 diff --git a/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.i18n.yaml b/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.i18n.yaml new file mode 100644 index 0000000000..9ea93b886c --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.i18n.yaml @@ -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-08-12-product-subagent-one-shot-background-tasks.md +2026-08-12-product-subagent-one-shot-background-tasks.md: bcc4d9b947e5e666161fd2c2b447fa2edf86bd69 +2026-08-12-product-subagent-one-shot-background-tasks.zh.md: 3aa8266788c48cd5301dde65dab2cb9d58b4145b diff --git a/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.md b/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.md new file mode 100644 index 0000000000..bcc4d9b947 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.md @@ -0,0 +1,69 @@ +# Agent Note: Product one-shot subagents use generic background Tasks + +Status: implemented + +English | [中文](2026-08-12-product-subagent-one-shot-background-tasks.zh.md) + +## Problem + +The Codex and Claude Code providers already run one self-contained task and return one final answer, while `dsh-tool-subagent` already adapts any one-shot provider to the generic background Task runtime. The shipped product-tool rows disabled that route, so an agent could only wait for the product answer even when the delegation was independent of its next action. + +Exposing background execution must not add a product session, product-specific task state, another cancellation owner, or another result protocol. The same provider run must remain responsible for one native process or query and one final answer, while the existing Task registry remains responsible for ids, collection, cancellation, owner cleanup, and completion notices. + +## Decision + +The `standard`, `code`, and `cordis` Agent Presets configure the dormant `subagent_codex` and `subagent_claude_code` rows with `backgroundMode: one-shot`. Removing a row's `disabled` field exposes the existing optional `run_in_background` argument. Omission or `false` waits in the foreground; explicit `true` returns a parent-owned Task id after synchronous Task preflight and registration, without waiting for provider startup or completion. + +The [generic one-shot background adapter](2026-07-08-background-subagent-tasks.md) owns background registration and settlement. It starts the same [`SubagentRun`](2026-06-21-subagent-capability-seam.md), uses a Task-owned cancellation signal across provider startup and execution, waits for `run.result` and `run.dispose()`, maps the terminal result into the Task, and lets `task_output`, `task_list`, `task_kill`, and the existing completion notice expose that state. The [product provider decision](2026-08-04-claude-code-and-codex-subagent-backends.md) continues to own native protocols, answer selection, local cancellation, and process-tree quiescence. + +No provider configuration, service interface, event, wire field, persistence format, or product identifier is added. Foreground and background differ only in which existing consumer waits for the same one-shot run. + +### Ownership and lifecycle + +```text +product tool call + -> omitted / false: tool call waits -> final answer or error -> run disposal + -> true: Task preflight + owner cleanup + -> starter begins provider startup under Task-owned signal + -> Task record/id published and returned (startup remains pending) + -> provider result + run disposal -> Task settlement + notice + -> task_output reads / task_kill cancels + -> parent disposal: Task owner cleanup cancels -> run disposal -> process exit +``` + +| Fact or resource | Owner | Product-tool responsibility | Observable result | +| --- | --- | --- | --- | +| Product selection and exposure | Agent Preset | Bind one fixed tool name to one fixed provider | Enabling one row exposes only that product tool | +| Foreground or background choice | `dsh-tool-subagent` | Resolve `run_in_background` under `one-shot` policy | Omission is foreground; explicit `true` returns a Task id | +| Task id, state, output, cancellation, and notice | `ctx.tasks` and `dsh-tool-tasks` | Register and present the existing one-shot run | Generic task tools collect or stop the run for the exact parent | +| Native answer and process quiescence | Product provider and `dsh-subprocess` | Produce one final result and release one process tree | Task settlement and foreground return both wait for disposal | + +## Published composition + +Full profiles keep both product providers on the host and keep both product-tool rows disabled in each full preset. The host task registry is shared across sessions, while each preset contributes the generic task controls to its own agent scope. A user copies a preset and removes `disabled` from either or both product rows; no product process starts during composition. + +A custom composition that enables one-shot background execution must provide the complete generic Task capability: `dsh-tasks-local` as the provider and `dsh-tool-tasks` as the model-facing consumer. A product tool without that runtime can still execute in the foreground, but an explicit background request fails the existing Task preflight instead of publishing an uncollectable id. + +The ACP product compositions use the same fixed product rows and generic task controls. Their keyless schema snapshots expose `description`, `prompt`, and optional `run_in_background` for each enabled product tool without invoking Codex, Claude Code, or an external model. + +## Verification + +The shipped Web composition boots four user-preset variants—neither product, Codex, Claude Code, and both—and checks that each enabled product tool exposes `run_in_background` alongside `task_output`, `task_list`, and `task_kill`. The two package-owned Loader compositions run with an empty `PATH`, inspect the same schemas and controls, and prove that loading the providers starts no product process. ACP keyless snapshots pin the assembled product schemas, while the existing `dsh-tool-subagent` and task suites pin foreground defaulting, Task registration, final-output collection, cancellation, completion notices, owner disposal, and provider disposal. + +## Alternatives considered + +**Keep the product tools foreground-only.** This preserves the smallest schema but prevents agents from scheduling independent product work even though the generic one-shot Task adapter already owns the required lifecycle. + +**Make product delegations background by default.** A one-shot Task requires later collection, unlike a continuable child with its own durable conversation id and settlement delivery. Foreground remains the compatible default, and background remains an explicit scheduling choice. + +**Use Codex or Claude Code native session state as the background owner.** That would create provider-specific ids, status, cancellation, and recovery semantics beside the generic Task registry. The providers remain one-shot result producers and keep native ids private. + +**Add product-specific output, wait, or kill tools.** Separate controls would duplicate the generic task protocol and teach a different collection workflow for each provider. The existing `task_*` tools already cover the required operations. + +**Add continuable product sessions at the same time.** Resume, follow-up, progress, and persisted product sessions require new product contracts and lifecycle ownership. This decision exposes only the already implemented one-shot background route. + +## Consequences + +Agents can continue useful work while Codex or Claude Code handles an independent one-shot task, then collect the final answer or cancel it through the same Task controls used by other background producers. Foreground callers retain their existing result and error behavior. + +Every product delegation still starts a fresh native process or query, produces final text as its only product payload, and ends with provider disposal and whole-tree exit. A background call additionally exposes the generic Task id, status, completion notice, and collection or cancellation results. Background Tasks are process-local and parent-owned: they do not survive parent disposal, do not expose intermediate product activity, and do not make a product conversation resumable. Custom compositions that expose the background argument must also keep the generic Task provider and controls available. diff --git a/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.zh.md b/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.zh.md new file mode 100644 index 0000000000..3aa8266788 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-12-product-subagent-one-shot-background-tasks.zh.md @@ -0,0 +1,69 @@ +# Agent Note: 产品 one-shot subagent 使用通用后台 Task + +Status: implemented + +[English](2026-08-12-product-subagent-one-shot-background-tasks.md) | 中文 + +## 问题 + +Codex 与 Claude Code 提供方已经能够运行一项自包含任务并返回一个最终回答,而 `dsh-tool-subagent` 也已经能够把任意 one-shot 提供方接入通用后台 Task 运行时。随附产品工具行禁用了这条路径,因此即使委托与 agent 的下一步操作彼此独立,agent 也只能等待产品回答。 + +公开后台执行不得增加产品会话、产品专属任务状态、另一取消责任方或另一结果协议。同一个提供方运行必须继续负责一个原生进程或 query 和一个最终回答,而现有任务注册表继续负责 id、收集、取消、owner 清理与完成通知。 + +## 决策 + +`standard`、`code` 与 `cordis` Agent Preset 使用 `backgroundMode: one-shot` 配置休眠的 `subagent_codex` 与 `subagent_claude_code` 行。删除某一行的 `disabled` 字段后,现有可选参数 `run_in_background` 就会公开。省略该参数或传入 `false` 时会在前台等待;显式传入 `true` 时会在同步完成 Task 预检与登记后返回由父级拥有的 Task id,而不会等待提供方启动或完成。 + +[通用 one-shot 后台适配器](2026-07-08-background-subagent-tasks.md)负责后台登记与结算。它会启动同一个 [`SubagentRun`](2026-06-21-subagent-capability-seam.md),让 Task 自有的取消信号覆盖提供方启动与执行,等待 `run.result` 和 `run.dispose()`,把终态结果映射进 Task,并由 `task_output`、`task_list`、`task_kill` 与现有完成通知公开该状态。[产品提供方决策](2026-08-04-claude-code-and-codex-subagent-backends.md)继续负责原生协议、答案选择、本地取消与进程树完全停稳。 + +本决策不新增提供方配置、服务接口、事件、协议字段、持久化格式或产品标识符。前台与后台的区别仅在于由哪个现有消费方等待同一个 one-shot 运行。 + +### 归属与生命周期 + +```text +product tool call + -> omitted / false: tool call waits -> final answer or error -> run disposal + -> true: Task preflight + owner cleanup + -> starter begins provider startup under Task-owned signal + -> Task record/id published and returned (startup remains pending) + -> provider result + run disposal -> Task settlement + notice + -> task_output reads / task_kill cancels + -> parent disposal: Task owner cleanup cancels -> run disposal -> process exit +``` + +| 事实或资源 | 责任方 | 产品工具职责 | 可观察结果 | +| --- | --- | --- | --- | +| 产品选择与公开 | Agent Preset | 把一个固定工具名绑定到一个固定提供方 | 启用一行只会公开对应产品工具 | +| 前台或后台选择 | `dsh-tool-subagent` | 按 `one-shot` 策略解析 `run_in_background` | 省略参数时在前台运行;显式传入 `true` 时返回 task id | +| task id、状态、输出、取消与通知 | `ctx.tasks` 与 `dsh-tool-tasks` | 登记并展示现有 one-shot 运行 | 通用任务工具为准确父级收集或停止运行 | +| 原生答案与进程完全停稳 | 产品提供方与 `dsh-subprocess` | 产生一个最终结果并释放一棵进程树 | Task 结算与前台返回都会等待资源释放 | + +## 发布组装 + +完整 profile 把两个产品提供方保留在宿主,并在每个完整 preset 中让两个产品工具行保持禁用。宿主任务注册表由各会话共享,而每个 preset 会把通用任务控制工具贡献到自身 agent 作用域。用户复制一个 preset,再从任一或两个产品行删除 `disabled`;组装期间不会启动产品进程。 + +自定义组装若启用 one-shot 后台执行,就必须提供完整的通用 Task 能力:由 `dsh-tasks-local` 充当提供方,由 `dsh-tool-tasks` 充当面向模型的消费方。没有该运行时的产品工具仍可在前台执行,但显式后台请求会在现有 Task 预检中失败,不会发布无法收集的 id。 + +ACP 产品组装使用相同的固定产品行与通用任务控制工具。其无密钥 schema 快照会为每个已启用产品工具公开 `description`、`prompt` 和可选的 `run_in_background`,而不会调用 Codex、Claude Code 或外部模型。 + +## 验证 + +随附 Web 组装会启动四种用户 preset 变体——不启用产品、只启用 Codex、只启用 Claude Code,以及同时启用两者——并检查每个已启用产品工具都会与 `task_output`、`task_list` 和 `task_kill` 一起公开 `run_in_background`。两个由包负责的 Loader 组装会在空 `PATH` 下运行,检查相同 schema 与控制工具,并证明加载提供方不会启动产品进程。ACP 无密钥快照会固定组装后的产品 schema,而现有 `dsh-tool-subagent` 与任务测试套件会固定前台默认值、Task 登记、最终输出收集、取消、完成通知、owner 资源释放与提供方资源释放。 + +## 曾考虑的替代方案 + +**让产品工具继续只支持前台运行。** 这种方案保留最小 schema,却会阻止 agent 调度独立产品工作,即使通用 one-shot Task 适配器已经负责所需生命周期。 + +**让产品委托默认在后台运行。** one-shot Task 需要后续收集,这不同于拥有自身持久会话 id 与结算交付的可续接子级。前台继续作为兼容默认值,后台继续作为显式调度选择。 + +**让 Codex 或 Claude Code 原生会话状态负责后台生命周期。** 这会在通用任务注册表之外建立提供方专属 id、状态、取消与恢复语义。提供方继续只产生 one-shot 结果,并把原生 id 保持为私有事实。 + +**增加产品专属 output、wait 或 kill 工具。** 独立控制工具会复制通用任务协议,并为每个提供方教授不同的收集工作流。现有 `task_*` 工具已经覆盖所需操作。 + +**同时增加可续接产品会话。** 恢复、后续交互、进度与持久化产品会话需要新的产品约定和生命周期归属。本决策只公开已经实现的 one-shot 后台路径。 + +## 后果 + +agent 可以在 Codex 或 Claude Code 处理独立 one-shot 任务时继续推进其他工作,随后通过其他后台 producer 共用的 Task 控制工具收集最终回答或取消运行。前台调用方继续获得既有结果与错误行为。 + +每次产品委托仍会启动一个全新的原生进程或 query,把最终文本作为唯一产品载荷,并以提供方资源释放和整棵进程树退出结束。后台调用还会额外公开通用 Task id、状态、完成通知以及收集或取消结果。后台 Task 仅存在于当前进程且由父级拥有:它不会在父级资源释放后继续存活,不会公开产品中间活动,也不会让产品对话变得可恢复。公开后台参数的自定义组装还必须让通用 Task 提供方与控制工具保持可用。 diff --git a/apps/cli/config/agent-presets/code/agent.cordis.yml b/apps/cli/config/agent-presets/code/agent.cordis.yml index 992b1a3eb5..5b5191cd7f 100644 --- a/apps/cli/config/agent-presets/code/agent.cordis.yml +++ b/apps/cli/config/agent-presets/code/agent.cordis.yml @@ -207,7 +207,7 @@ config: provider: codex toolName: subagent_codex - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed - id: tool-subagent-claude-code @@ -216,7 +216,7 @@ config: provider: claude-code toolName: subagent_claude_code - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed - id: workflow-workerthread diff --git a/apps/cli/config/agent-presets/cordis/agent.cordis.yml b/apps/cli/config/agent-presets/cordis/agent.cordis.yml index d05063846f..eb05a66d11 100644 --- a/apps/cli/config/agent-presets/cordis/agent.cordis.yml +++ b/apps/cli/config/agent-presets/cordis/agent.cordis.yml @@ -194,7 +194,7 @@ config: provider: codex toolName: subagent_codex - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed - id: tool-subagent-claude-code @@ -203,7 +203,7 @@ config: provider: claude-code toolName: subagent_claude_code - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed - id: workflow-workerthread diff --git a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md index d897cafb7a..344abba03c 100644 --- a/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md +++ b/apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md @@ -134,7 +134,7 @@ Copy these disabled templates from a shipped full preset and remove `disabled` o config: provider: codex toolName: subagent_codex - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed - id: tool-subagent-claude-code @@ -143,11 +143,11 @@ Copy these disabled templates from a shipped full preset and remove `disabled` o config: provider: claude-code toolName: subagent_claude_code - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed ``` -The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. The host must provide `codex` or `claude` on `PATH`; the preset does not install, authenticate, select a model for, or probe either product. +The two rows are independent. Leaving both disabled preserves the copied preset, enabling one exposes only that product tool, and enabling both exposes both. `backgroundMode: one-shot` keeps omitted or `false` calls in the foreground and lets explicit `run_in_background: true` return a generic Task id. Full presets already carry `tool-tasks`, while the host carries the task registry; retain both when making a custom composition so `task_output`, `task_list`, `task_kill`, cancellation, and completion notices stay available. The host must provide `codex` or `claude` on `PATH`; the preset does not install, authenticate, select a model for, or probe either product. ## What not to move into a preset diff --git a/apps/cli/config/agent-presets/standard/agent.cordis.yml b/apps/cli/config/agent-presets/standard/agent.cordis.yml index b57b18eef7..6ffdf6b1b1 100644 --- a/apps/cli/config/agent-presets/standard/agent.cordis.yml +++ b/apps/cli/config/agent-presets/standard/agent.cordis.yml @@ -206,7 +206,7 @@ config: provider: codex toolName: subagent_codex - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed - id: tool-subagent-claude-code @@ -215,7 +215,7 @@ config: provider: claude-code toolName: subagent_claude_code - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed - id: workflow-workerthread diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index fcbb00d33f..ac966c3bb2 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -122,6 +122,16 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis const toolNames = (ctx: Context, agent?: Agent): string[] => ctx.tools.schemas(agent).map(schema => schema.name).sort() +function toolParameterNames(ctx: Context, agent: Agent, toolName: string): string[] { + const schema = ctx.tools.schemas(agent).find(tool => tool.name === toolName) + if (schema === undefined) throw new Error(`missing tool schema ${toolName}`) + const properties = schema.parameters.properties + if (typeof properties !== 'object' || properties === null || Array.isArray(properties)) { + throw new Error(`${toolName} has invalid parameter properties`) + } + return Object.keys(properties).sort() +} + function enablePresetTool(composition: string, id: string): string { const row = ` - id: ${id}\n` const start = composition.indexOf(row) @@ -475,6 +485,12 @@ describe('product subagent rows in user presets', () => { const tools = toolNames(productCtx, handle.agent) expect(tools.filter(name => name === 'subagent_codex' || name === 'subagent_claude_code')) .toEqual(productTools) + expect(tools).toEqual(expect.arrayContaining(['task_kill', 'task_list', 'task_output'])) + for (const productTool of productTools) { + expect(toolParameterNames(productCtx, handle.agent, productTool)).toEqual([ + 'description', 'prompt', 'run_in_background', + ]) + } } finally { await handle.dispose() } diff --git a/examples/acp-agent/cordis.yml b/examples/acp-agent/cordis.yml index a73d6d36e4..fd16694d5d 100644 --- a/examples/acp-agent/cordis.yml +++ b/examples/acp-agent/cordis.yml @@ -123,7 +123,8 @@ # Fork stays one-shot because a continuable child's `report` tool and prompt # section precede the inherited history a fork reuses; `run_in_background` is off -# because this example mounts no task service. See .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. +# as an explicit foreground-only choice even though agent-spine-demo mounts the +# generic Task runtime. See .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. - id: tool-subagent-fork name: '@deepseek-ai/dsh-tool-subagent' config: diff --git a/examples/acp-agent/product-subagent-both.cordis.snapshot.yml b/examples/acp-agent/product-subagent-both.cordis.snapshot.yml index 73d22eae27..0f8760cb91 100644 --- a/examples/acp-agent/product-subagent-both.cordis.snapshot.yml +++ b/examples/acp-agent/product-subagent-both.cordis.snapshot.yml @@ -27,12 +27,12 @@ config: provider: codex toolName: subagent_codex - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed - id: tool-subagent-claude-code name: '@deepseek-ai/dsh-tool-subagent' config: provider: claude-code toolName: subagent_claude_code - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed diff --git a/examples/acp-agent/product-subagent-both.cordis.yml b/examples/acp-agent/product-subagent-both.cordis.yml index ce9d054058..6a75bec332 100644 --- a/examples/acp-agent/product-subagent-both.cordis.yml +++ b/examples/acp-agent/product-subagent-both.cordis.yml @@ -1,5 +1,5 @@ -# Add both native product providers and the same independent foreground tool -# rows an Agent Preset may contribute. Loading the composition starts neither +# Add both native product providers and the same independent one-shot tool rows +# an Agent Preset may contribute. Loading the composition starts neither # product; the scenario pins both model-visible schemas. - id: base name: '@deepseek-ai/cordis-plugin-include' @@ -16,12 +16,12 @@ config: provider: codex toolName: subagent_codex - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed - id: tool-subagent-claude-code name: '@deepseek-ai/dsh-tool-subagent' config: provider: claude-code toolName: subagent_claude_code - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed diff --git a/examples/acp-agent/product-subagent-codex.cordis.snapshot.yml b/examples/acp-agent/product-subagent-codex.cordis.snapshot.yml index 8b8cd8604c..69171c7dbf 100644 --- a/examples/acp-agent/product-subagent-codex.cordis.snapshot.yml +++ b/examples/acp-agent/product-subagent-codex.cordis.snapshot.yml @@ -25,5 +25,5 @@ config: provider: codex toolName: subagent_codex - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed diff --git a/examples/acp-agent/product-subagent-codex.cordis.yml b/examples/acp-agent/product-subagent-codex.cordis.yml index 55f9985b7d..2a95679e14 100644 --- a/examples/acp-agent/product-subagent-codex.cordis.yml +++ b/examples/acp-agent/product-subagent-codex.cordis.yml @@ -1,4 +1,4 @@ -# Add the native Codex product provider and its preset-shaped foreground tool to +# Add the native Codex product provider and its preset-shaped one-shot tool to # the real ACP composition. The model is told not to call it; the scenario pins # the assembled request schema without starting Codex. - id: base @@ -14,5 +14,5 @@ config: provider: codex toolName: subagent_codex - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/cordis.yml b/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/cordis.yml index fcdc0c4fb6..45b62f880f 100644 --- a/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/cordis.yml +++ b/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/cordis.yml @@ -1,4 +1,4 @@ -# Test-only composition of both public opt-in providers and foreground tools. +# Test-only composition of both public opt-in providers and one-shot task tools. # The owning e2e boots this tree but never invokes a model or product process. - id: fixture name: './fixture.ts' @@ -20,7 +20,7 @@ config: provider: codex toolName: subagent_codex - enableRunInBackground: false + backgroundMode: one-shot maxDepth: 'provider-managed' - id: tool-subagent-claude-code @@ -28,7 +28,7 @@ config: provider: claude-code toolName: subagent_claude_code - enableRunInBackground: false + backgroundMode: one-shot maxDepth: 'provider-managed' - id: agent-spine diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/driver.ts b/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/driver.ts index d7540e1a2d..19b0cfc172 100644 --- a/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/driver.ts +++ b/examples/acp-agent/tests/fixtures/subagent/subagent-claude-code/driver.ts @@ -53,11 +53,16 @@ try { required: tool.parameters.required, } }) + const taskTools = ctx.tools.schemas() + .map(schema => schema.name) + .filter(name => name === 'task_kill' || name === 'task_list' || name === 'task_output') + .sort() process.stdout.write(`${JSON.stringify({ registeredProviders: ctx.subagents.list(), providers, tools, + taskTools, starts, })}\n`) } finally { diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml index e770b11c95..6afe2b888d 100644 --- a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml +++ b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/cordis.yml @@ -1,4 +1,4 @@ -# Test-only composition of the public opt-in provider and foreground tool. +# Test-only composition of the public opt-in provider and one-shot task tool. # The owning e2e boots this tree but never invokes the model or Codex. - id: fixture name: './fixture.ts' @@ -17,7 +17,7 @@ config: provider: codex toolName: subagent_codex - enableRunInBackground: false + backgroundMode: one-shot maxDepth: 'provider-managed' - id: agent-spine diff --git a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts index 873f5e36de..59bb8eb54a 100644 --- a/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts +++ b/examples/acp-agent/tests/fixtures/subagent/subagent-codex/driver.ts @@ -31,6 +31,10 @@ try { if (typeof properties !== 'object' || properties === null || Array.isArray(properties)) { throw new Error('subagent_codex tool has invalid parameter properties') } + const taskTools = ctx.tools.schemas() + .map(schema => schema.name) + .filter(name => name === 'task_kill' || name === 'task_list' || name === 'task_output') + .sort() process.stdout.write(`${JSON.stringify({ providers: ctx.subagents.list(), @@ -44,6 +48,7 @@ try { parameterNames: Object.keys(properties).sort(), required: tool.parameters.required, }, + taskTools, starts, })}\n`) } finally { diff --git a/examples/acp-agent/tests/snapshots/product-subagent-both/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/product-subagent-both/tool-schemas.expected.json index a1a46ab6b5..6855c0b127 100644 --- a/examples/acp-agent/tests/snapshots/product-subagent-both/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/product-subagent-both/tool-schemas.expected.json @@ -256,7 +256,7 @@ }, { "name": "subagent_claude_code", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the subagent and returns its result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the result by default. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -267,6 +267,10 @@ "prompt": { "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." + }, + "run_in_background": { + "type": "boolean", + "description": "Whether to run as a background task and return its id. Defaults to false; collect with task_output or stop with task_kill." } }, "required": [ @@ -277,7 +281,7 @@ }, { "name": "subagent_codex", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the subagent and returns its result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the result by default. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -288,6 +292,10 @@ "prompt": { "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." + }, + "run_in_background": { + "type": "boolean", + "description": "Whether to run as a background task and return its id. Defaults to false; collect with task_output or stop with task_kill." } }, "required": [ diff --git a/examples/acp-agent/tests/snapshots/product-subagent-codex/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/product-subagent-codex/tool-schemas.expected.json index 449f17784c..78cec432c7 100644 --- a/examples/acp-agent/tests/snapshots/product-subagent-codex/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/product-subagent-codex/tool-schemas.expected.json @@ -256,7 +256,7 @@ }, { "name": "subagent_codex", - "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the subagent and returns its result.", + "description": "Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This call waits for the result by default. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.", "parameters": { "type": "object", "properties": { @@ -267,6 +267,10 @@ "prompt": { "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." + }, + "run_in_background": { + "type": "boolean", + "description": "Whether to run as a background task and return its id. Defaults to false; collect with task_output or stop with task_kill." } }, "required": [ diff --git a/examples/headless-agent/cordis.yml b/examples/headless-agent/cordis.yml index 61a0190142..f4ccbf5ec3 100644 --- a/examples/headless-agent/cordis.yml +++ b/examples/headless-agent/cordis.yml @@ -120,7 +120,8 @@ # Fork stays one-shot because a continuable child's `report` tool and prompt # section precede the inherited history a fork reuses; `run_in_background` is off -# because this example mounts no task service. See .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. +# as an explicit foreground-only choice even though agent-spine-demo mounts the +# generic Task runtime. See .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. - id: tool-subagent-fork name: '@deepseek-ai/dsh-tool-subagent' config: diff --git a/packages/subagent/subagent-claude-code/README.i18n.yaml b/packages/subagent/subagent-claude-code/README.i18n.yaml index bbb3c9cf1a..168a921e7c 100644 --- a/packages/subagent/subagent-claude-code/README.i18n.yaml +++ b/packages/subagent/subagent-claude-code/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent-claude-code/README.md -README.md: 17b14e847baea3eadda7129b5e49f5e65b668cc8 -README.zh.md: 2f59144d5bd9f26a58773e6dd53909b2b0e8da14 +README.md: 556cde7da4467659d02709c43325e60fa7de6a74 +README.zh.md: bda00d7be4467e739604eeba0020850cb1a99d50 diff --git a/packages/subagent/subagent-claude-code/README.md b/packages/subagent/subagent-claude-code/README.md index 17b14e847b..556cde7da4 100644 --- a/packages/subagent/subagent-claude-code/README.md +++ b/packages/subagent/subagent-claude-code/README.md @@ -31,7 +31,7 @@ The provider advertises no optional start-time capabilities and reports `inherit Production resolves `claude` from the subprocess execution world's credential-scrubbed `PATH`, with explicit `env` entries applied, and passes the resulting path to the SDK as `pathToClaudeCodeExecutable`. On Windows, a resolved `.cmd` or `.bat` path is carried as a quoted, per-spawn environment value that `cmd.exe /v:off` expands once, so valid path metacharacters remain data. The pinned SDK's fixed flags then occupy cmd's command tail and contain no cmd metacharacters; they are not ordinary Windows argv. Native settings and authentication remain authoritative. The plugin does not install another CLI, select a model, create a product home, log in, or probe an account. Credential-shaped ambient variables are removed before the explicit `env` overlay is applied, so an API key or token intended for the child must be supplied there. Non-credential endpoint variables such as `ANTHROPIC_BASE_URL`, along with ordinary ambient values such as `PATH` and `HOME`, remain inherited unless overridden. -Shipped profiles load this provider once on the host and start no Claude process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to agents composed from the copy. A custom host composition can still use both rows directly. +Shipped profiles load this provider once on the host and start no Claude process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_claude_code` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Task id for `task_output` or `task_kill`. Shipped full profiles already provide the task registry and controls; a custom composition must load the same generic task provider and consumer when it enables this background route. ```yaml - id: subagent-claude-code @@ -40,13 +40,19 @@ Shipped profiles load this provider once on the host and start no Claude process env: ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY +- id: tasks + name: '@deepseek-ai/dsh-tasks-local' + +- id: tool-tasks + name: '@deepseek-ai/dsh-tool-tasks' + - id: tool-subagent-claude-code name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: claude-code toolName: subagent_claude_code - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed ``` @@ -72,19 +78,19 @@ The child pays for an independent Claude Code context and query. Child tokens do Independent of the parent request cache. Reuse depends only on Claude Code's own model, instructions, tools, native settings, and fresh query. -### Parent tool result, indirectly +### Parent scheduling and results, indirectly #### What the model sees -Through `dsh-tool-subagent`, the parent sees only the strict final Claude Code answer or the consumer's exact error for a non-completed result. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, and product ids are not copied into the parent Session. +Through `dsh-tool-subagent`, a foreground call gives the parent the strict final Claude Code answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a bounded completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, and product ids are not copied into the parent Session. #### Token effect -Parent input grows only by the final answer or error retained in the tool result. This provider adds no parent tool schema by itself. +Foreground input grows by the retained final answer or error. Background input also includes the start acknowledgement, completion notice, and any `task_output`, `task_kill`, or later status results; child tokens still do not enter the parent context. This provider adds no parent tool schema by itself. #### KV Cache effect -Append-only: the new tool result follows the reusable parent request prefix. +Append-only: foreground adds one result after the reusable parent prefix, while background appends the Task acknowledgement, notice, and later control or collection results. Background scheduling can add a notice-driven turn, but none of these messages rewrites the earlier prefix. ## Known Limitations and Deferred Work @@ -93,6 +99,6 @@ Append-only: the new tool result follows the reusable parent request prefix. - **Product installation and account state remain native** — a missing or incompatible `claude`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer or login flow. - **The SDK platform CLI remains in the install closure** — production ignores it in favor of the host `claude`, but the current SDK optional dependency is still installed and supplies the keyless compatibility fixture. Removing that payload belongs to the separate product installation-closure follow-up. - **No human interaction path** — `AskUserQuestion` is disabled and other interactive callbacks are absent, so tasks requiring new approval or input fail instead of suspending. -- **Final text only** — reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local. +- **Product payload is final text only** — reasoning, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local; generic Task ids, notices, and status come from the shared task runtime. - **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider. - **No wall-clock timeout or side-effect rollback** — the caller cancels long work, and files or external systems changed before cancellation are not restored. diff --git a/packages/subagent/subagent-claude-code/README.zh.md b/packages/subagent/subagent-claude-code/README.zh.md index 2f59144d5b..bda00d7be4 100644 --- a/packages/subagent/subagent-claude-code/README.zh.md +++ b/packages/subagent/subagent-claude-code/README.zh.md @@ -31,7 +31,7 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK 生产环境从子进程执行世界清除凭证后的 `PATH` 解析 `claude`,再应用显式 `env` 条目,并把所得路径作为 `pathToClaudeCodeExecutable` 交给 SDK。在 Windows 上,解析到的 `.cmd` 或 `.bat` 路径会作为带引号、仅供本次 spawn 使用的环境值交给 `cmd.exe /v:off` 展开一次,因此合法路径中的元字符仍只是数据。锁定版本的 SDK 随后把固定命令行选项放在 cmd 的命令尾部;这些选项不含 cmd 元字符,也并不是普通的 Windows argv。原生设置与身份验证继续是权威来源。本插件不安装另一份 CLI、不选择模型、不创建产品主目录、不执行登录,也不探测账户。具有凭证特征的环境变量会在显式 `env` 覆盖生效前被清除,因此供子进程使用的 API 密钥或 token 必须在该配置中显式提供。除非被覆盖,`ANTHROPIC_BASE_URL` 等非凭证端点变量以及 `PATH` 和 `HOME` 等普通环境变量仍会被继承。 -随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Claude 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_claude_code`。自定义宿主组装仍可直接使用两条配置行。 +随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Claude 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_claude_code`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Task ID,供 `task_output` 或 `task_kill` 使用。随附完整 profile 已提供任务注册表和控制工具;自定义组装若启用该后台路径,也必须加载同一通用任务提供方与消费方。 ```yaml - id: subagent-claude-code @@ -40,13 +40,19 @@ SDK 接收由文本块原样拼接成的任务。提供方会完整迭代 SDK env: ANTHROPIC_API_KEY: !!js process.env.ANTHROPIC_API_KEY +- id: tasks + name: '@deepseek-ai/dsh-tasks-local' + +- id: tool-tasks + name: '@deepseek-ai/dsh-tool-tasks' + - id: tool-subagent-claude-code name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: claude-code toolName: subagent_claude_code - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed ``` @@ -72,19 +78,19 @@ Claude Code 子任务会在一个全新的 SDK query 中接收独立文本任务 这与父请求缓存相互独立。能否复用只取决于 Claude Code 自身的模型、指令、工具、原生设置和全新 query。 -### 父级工具结果(间接) +### 父级调度与结果(间接) #### 模型看到的内容 -通过 `dsh-tool-subagent`,父级模型只会看到符合严格成功条件的 Claude Code 最终答案,或者在结果未完成时看到消费方给出的原样错误。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。 +通过 `dsh-tool-subagent`,前台调用会让父级模型看到符合严格成功条件的 Claude Code 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id;随后通用任务控制面会送达有界完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。 #### 对 token 的影响 -父级输入只会增加工具结果中保留的最终答案或错误内容。本提供方自身不添加父级工具 schema。 +前台输入会增加工具结果中保留的最终答案或错误内容。后台输入还会包含启动确认、完成通知,以及 `task_output`、`task_kill` 或后续状态结果;子任务 token 仍不会进入父级上下文。本提供方自身不添加父级工具 schema。 #### 对 KV Cache 的影响 -仅追加:新的工具结果接在可复用的父请求前缀之后。 +仅追加:前台会在可复用的父请求前缀后增加一个结果,后台则会继续追加 Task 启动确认、通知以及后续控制或收集结果。后台调度可能增加一个由通知唤醒的轮次,但这些消息都不会改写更早的前缀。 ## 已知限制与后续工作 @@ -93,6 +99,6 @@ Claude Code 子任务会在一个全新的 SDK query 中接收独立文本任务 - **产品安装与账户状态仍由原生机制管理**:`claude` 缺失或不兼容、配置错误或身份验证失败都会呈现为启动错误或运行错误;本插件不提供安装程序或登录流程。 - **SDK 平台 CLI 仍在安装闭包内**:生产环境会忽略它,改用宿主提供的 `claude`,但当前 SDK 的可选依赖仍会安装,并提供无密钥兼容性 fixture。移除该载荷属于独立的产品安装闭包后续项。 - **没有人工交互路径**:`AskUserQuestion` 被禁用,其他交互回调也不存在,因此需要新审批或输入的任务会失败而不会挂起。 -- **仅返回最终文本**:推理、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部。 +- **产品载荷仅包含最终文本**:推理、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部;通用 Task id、通知与状态来自共享任务运行时。 - **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。 - **没有按实际经过时间触发的超时或副作用回滚**:长时间运行的工作由调用方取消,且取消前已更改的文件或外部系统不会恢复原状。 diff --git a/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts b/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts index 51a2ea0025..54d91a3e1f 100644 --- a/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts +++ b/packages/subagent/subagent-claude-code/tests/loader-composition.e2e.ts @@ -15,7 +15,7 @@ const configPath = join(fixtureDir, 'cordis.yml') const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url)) describe('product-provider public Loader composition', () => { - it('loads both opt-in packages and foreground tools without starting either product', async () => { + it('loads both opt-in packages, one-shot task tools, and task controls without starting either product', async () => { const { stdout, stderr } = await runLoaderSmoke({ label: 'product-provider Loader composition', tempDirPrefix: 'dsh-product-provider-loader-', @@ -57,15 +57,16 @@ describe('product-provider public Loader composition', () => { tools: [ { name: 'subagent_codex', - parameterNames: ['description', 'prompt'], + parameterNames: ['description', 'prompt', 'run_in_background'], required: ['description', 'prompt'], }, { name: 'subagent_claude_code', - parameterNames: ['description', 'prompt'], + parameterNames: ['description', 'prompt', 'run_in_background'], required: ['description', 'prompt'], }, ], + taskTools: ['task_kill', 'task_list', 'task_output'], starts: 0, }) }, LOADER_SMOKE_TEST_TIMEOUT_MS) diff --git a/packages/subagent/subagent-codex/README.i18n.yaml b/packages/subagent/subagent-codex/README.i18n.yaml index 1cfb9645c2..9c9c7e8648 100644 --- a/packages/subagent/subagent-codex/README.i18n.yaml +++ b/packages/subagent/subagent-codex/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent-codex/README.md -README.md: 3d59ca1eaf3db9dd9d9d2cd451692ebd2a956ef4 -README.zh.md: b60cb1bba9b2d7b3f61c544c1600862a0ad6ce5b +README.md: 6a882c128ebd372ee148a4315a237d4eacf6d42b +README.zh.md: beb5411f6a449efc7a45c171d217f98657805740 diff --git a/packages/subagent/subagent-codex/README.md b/packages/subagent/subagent-codex/README.md index 3d59ca1eaf..6a882c128e 100644 --- a/packages/subagent/subagent-codex/README.md +++ b/packages/subagent/subagent-codex/README.md @@ -27,7 +27,7 @@ The provider advertises no optional start-time capabilities and reports `inherit Production resolves `codex` from `PATH` and uses the host's native Codex configuration and authentication. The plugin does not install Codex, select a model, create `CODEX_HOME`, log in, or probe a version. Credential-shaped ambient variables are removed by the subprocess seam, so an API key intended for the child must be supplied explicitly in `env`; ordinary ambient values such as `PATH` and `HOME` remain available unless overridden. -Shipped profiles load this provider once on the host and start no Codex process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. A custom host composition can still use both rows directly. +Shipped profiles load this provider once on the host and start no Codex process until a tool call. Full Agent Presets carry the tool row below with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Task id for `task_output` or `task_kill`. Shipped full profiles already provide the task registry and controls; a custom composition must load the same generic task provider and consumer when it enables this background route. ```yaml - id: subagent-codex @@ -36,13 +36,19 @@ Shipped profiles load this provider once on the host and start no Codex process env: OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY +- id: tasks + name: '@deepseek-ai/dsh-tasks-local' + +- id: tool-tasks + name: '@deepseek-ai/dsh-tool-tasks' + - id: tool-subagent-codex name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: codex toolName: subagent_codex - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed ``` @@ -66,19 +72,19 @@ The child pays for an independent Codex context and turn. Child tokens do not en Independent of the parent request cache. Reuse depends only on Codex's own provider, model, instructions, tools, and ephemeral-thread request. -### Parent tool result, indirectly +### Parent scheduling and results, indirectly #### What the model sees -Through `dsh-tool-subagent`, the parent sees only the selected final Codex answer or the consumer's exact error for a non-completed result. Codex commentary, reasoning, tool activity, stderr, workspace diffs, and product ids are not copied into the parent Session. +Through `dsh-tool-subagent`, a foreground call gives the parent the selected final Codex answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a bounded completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Codex commentary, reasoning, tool activity, stderr, workspace diffs, usage, and product ids are not copied into the parent Session. #### Token effect -Parent input grows only by the final answer or error retained in the tool result. This provider adds no parent tool schema by itself. +Foreground input grows by the retained final answer or error. Background input also includes the start acknowledgement, completion notice, and any `task_output`, `task_kill`, or later status results; child tokens still do not enter the parent context. This provider adds no parent tool schema by itself. #### KV Cache effect -Append-only: the new tool result follows the reusable parent request prefix. +Append-only: foreground adds one result after the reusable parent prefix, while background appends the Task acknowledgement, notice, and later control or collection results. Background scheduling can add a notice-driven turn, but none of these messages rewrites the earlier prefix. ## Known Limitations and Deferred Work @@ -86,6 +92,6 @@ Append-only: the new tool result follows the reusable parent request prefix. - **Host-managed product installation and account state** — a missing or incompatible `codex`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer, login flow, or runtime version gate. - **Compatibility is pinned by development evidence** — upgrading from the verified 0.147.0 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, keyless real-product, and credentialed DeepSeek nonce tests. - **No human approval path** — known unattended approval requests are denied and unknown server requests fail closed; deployments cannot configure an allow policy through this package. -- **Final text only** — reasoning, commentary, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local. +- **Product payload is final text only** — reasoning, commentary, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local; generic Task ids, notices, and status come from the shared task runtime. - **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider. - **No wall-clock timeout or side-effect rollback** — the caller cancels long work, and files or external systems changed before cancellation are not restored. diff --git a/packages/subagent/subagent-codex/README.zh.md b/packages/subagent/subagent-codex/README.zh.md index b60cb1bba9..beb5411f6a 100644 --- a/packages/subagent/subagent-codex/README.zh.md +++ b/packages/subagent/subagent-codex/README.zh.md @@ -27,7 +27,7 @@ 生产环境会从 `PATH` 中解析 `codex`,并使用宿主机原生的 Codex 配置与身份验证。本插件不安装 Codex、不选择模型、不创建 `CODEX_HOME`、不执行登录,也不探测版本。子进程 seam 会移除具有凭证特征的环境变量,因此供子进程使用的 API 密钥必须在 `env` 中显式提供;除非被覆盖,`PATH` 和 `HOME` 等普通环境变量值仍然可用。 -随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Codex 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`。自定义宿主组装仍可直接使用两条配置行。 +随附 profile 会在宿主上加载一次该提供方,而且在工具被调用前不会启动 Codex 进程。完整 Agent Preset 携带下列工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Task ID,供 `task_output` 或 `task_kill` 使用。随附完整 profile 已提供任务注册表和控制工具;自定义组装若启用该后台路径,也必须加载同一通用任务提供方与消费方。 ```yaml - id: subagent-codex @@ -36,13 +36,19 @@ env: OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY +- id: tasks + name: '@deepseek-ai/dsh-tasks-local' + +- id: tool-tasks + name: '@deepseek-ai/dsh-tool-tasks' + - id: tool-subagent-codex name: '@deepseek-ai/dsh-tool-subagent' disabled: true config: provider: codex toolName: subagent_codex - enableRunInBackground: false + backgroundMode: one-shot maxDepth: provider-managed ``` @@ -66,19 +72,19 @@ Codex 子任务会在一个全新的临时线程中,以单个轮次接收这 这与父请求缓存相互独立。能否复用只取决于 Codex 自身的提供方、模型、指令、工具和临时线程请求。 -### 父级工具结果(间接) +### 父级调度与结果(间接) #### 模型看到的内容 -通过 `dsh-tool-subagent`,父级模型只会看到选定的 Codex 最终答案,或者在结果未完成时看到消费方给出的原样错误。Codex 的过程说明、推理(reasoning)、工具活动、stderr、工作区差异和产品标识符均不会复制到父会话。 +通过 `dsh-tool-subagent`,前台调用会让父级模型看到选定的 Codex 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id;随后通用任务控制面会送达有界完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Codex 的过程说明、推理(reasoning)、工具活动、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。 #### 对 token 的影响 -父级输入只会增加工具结果中保留的最终答案或错误内容。本提供方自身不添加父级工具 schema。 +前台输入会增加工具结果中保留的最终答案或错误内容。后台输入还会包含启动确认、完成通知,以及 `task_output`、`task_kill` 或后续状态结果;子任务 token 仍不会进入父级上下文。本提供方自身不添加父级工具 schema。 #### 对 KV Cache 的影响 -仅追加:新的工具结果接在可复用的父请求前缀之后。 +仅追加:前台会在可复用的父请求前缀后增加一个结果,后台则会继续追加 Task 启动确认、通知以及后续控制或收集结果。后台调度可能增加一个由通知唤醒的轮次,但这些消息都不会改写更早的前缀。 ## 已知限制与后续工作 @@ -86,6 +92,6 @@ Codex 子任务会在一个全新的临时线程中,以单个轮次接收这 - **产品安装和账户状态由宿主管理**:`codex` 缺失或不兼容、配置错误或身份验证失败,都会呈现为启动错误或运行错误;本插件不提供安装程序、登录流程或运行时版本门禁。 - **兼容性由开发证据锁定**:若要从已验证的 0.147.0 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。 - **没有人工审批路径**:已知的无人值守审批请求会被拒绝,未知服务器请求会以默认拒绝方式使运行失败;部署方无法通过本包配置允许策略。 -- **仅返回最终文本**:推理、过程说明、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部。 +- **产品载荷仅包含最终文本**:推理、过程说明、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部;通用 Task id、通知与状态来自共享任务运行时。 - **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。 - **没有按实际经过时间触发的超时或副作用回滚**:长时间运行的工作由调用方取消,且取消前已更改的文件或外部系统不会恢复原状。 diff --git a/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts b/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts index 6c4019f8c8..7c765a73ac 100644 --- a/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts +++ b/packages/subagent/subagent-codex/tests/loader-composition.e2e.ts @@ -15,7 +15,7 @@ const configPath = join(fixtureDir, 'cordis.yml') const repoTsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url)) describe('Codex provider public Loader composition', () => { - it('loads the opt-in package and foreground tool without starting Codex', async () => { + it('loads the opt-in package, one-shot task tool, and task controls without starting Codex', async () => { const { stdout, stderr } = await runLoaderSmoke({ label: 'subagent-codex Loader composition', tempDirPrefix: 'dsh-subagent-codex-loader-', @@ -44,9 +44,10 @@ describe('Codex provider public Loader composition', () => { }, tool: { name: 'subagent_codex', - parameterNames: ['description', 'prompt'], + parameterNames: ['description', 'prompt', 'run_in_background'], required: ['description', 'prompt'], }, + taskTools: ['task_kill', 'task_list', 'task_output'], starts: 0, }) }, LOADER_SMOKE_TEST_TIMEOUT_MS) diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index 7c233c1924..4a36413c7d 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -85,7 +85,11 @@ describe('dsh-tool-subagent', () => { it('registers a `subagent` tool that delegates to the configured provider and returns its output', async () => { const ctx = await setup({ provider: 'mock' }, { reply: 'child says hi' }) - const result = await callSubagent(ctx, { description: 'do a thing', prompt: 'go research X' }) + const result = await callSubagent(ctx, { + description: 'do a thing', + prompt: 'go research X', + run_in_background: false, + }) expect(result.isError).toBe(false) if (result.isError) throw new Error('expected subagent success') expect(result.value).toEqual({ From 14e70b1bf4e33a566ed93f153798b9f9d4bfe22e Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Wed, 12 Aug 2026 02:18:11 -0700 Subject: [PATCH 003/160] test(web): cover plugin settings tab states --- .../ui-plugin-config/tests/apply.client.spec.ts | 14 +++++++++++++- .../ui-plugin-config/tests/section.client.spec.tsx | 4 ++++ .../ui-plugins/tests/components.client.spec.tsx | 9 +++++---- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/client/ui-plugin-config/tests/apply.client.spec.ts b/packages/client/ui-plugin-config/tests/apply.client.spec.ts index 8c5e768c45..b0d7053e83 100644 --- a/packages/client/ui-plugin-config/tests/apply.client.spec.ts +++ b/packages/client/ui-plugin-config/tests/apply.client.spec.ts @@ -83,9 +83,21 @@ describe('ui-plugin-config apply', () => { const section = slots.entries('settings.section')[0]! const sectionFace = (section.inject as unknown as () => PluginConfigSectionInjected)() - expect(sectionFace.hooks.tabs.getSnapshot()).toEqual([ + const initialTabs = sectionFace.hooks.tabs.getSnapshot() + expect(initialTabs).toEqual([ { id: 'configurable', order: 0, label: '插件配置' }, ]) + expect(sectionFace.hooks.tabs.getSnapshot()).toBe(initialTabs) + + const listener = vi.fn() + const unsubscribe = sectionFace.hooks.tabs.subscribe(listener) + slots.register({ name: 'settings.plugins.tab', id: 'plain' } as never, () => null) + expect(sectionFace.hooks.tabs.getSnapshot()).toEqual([ + { id: 'configurable', order: 0, label: '插件配置' }, + { id: 'plain', order: 0, label: '' }, + ]) + unsubscribe() + const tab = slots.entries('settings.plugins.tab')[0]! expect((tab.inject as unknown as () => ConfigurablePluginsTabInjected)()).toEqual({ cardCount: 3 }) for (const entry of slots.entries('settings.plugin.item')) { diff --git a/packages/client/ui-plugin-config/tests/section.client.spec.tsx b/packages/client/ui-plugin-config/tests/section.client.spec.tsx index 8e0986892f..e00fd7d448 100644 --- a/packages/client/ui-plugin-config/tests/section.client.spec.tsx +++ b/packages/client/ui-plugin-config/tests/section.client.spec.tsx @@ -105,6 +105,10 @@ describe('PluginConfigSection', () => { expect(all.getAttribute('aria-selected')).toBe('true') expect(screen.getByText('all')).toBeTruthy() expect(screen.getByText('configurable').closest('[role="tabpanel"]')).toHaveProperty('hidden', true) + + fireEvent.click(configurable) + expect(configurable.getAttribute('aria-selected')).toBe('true') + expect(screen.getByText('all').closest('[role="tabpanel"]')).toHaveProperty('hidden', true) }) it('leads with its own heading and intro', () => { diff --git a/packages/client/ui-plugins/tests/components.client.spec.tsx b/packages/client/ui-plugins/tests/components.client.spec.tsx index bf104abcdb..7989837065 100644 --- a/packages/client/ui-plugins/tests/components.client.spec.tsx +++ b/packages/client/ui-plugins/tests/components.client.spec.tsx @@ -27,6 +27,7 @@ const SNAPSHOT = { { entryId: 'loading', moduleName: '@fixture/loading-name', enabled: true, fiberPhase: 'loading' }, { entryId: 'failed', moduleName: '@fixture/failed-name', enabled: true, fiberPhase: 'failed' }, { entryId: 'unloading', moduleName: '@fixture/unloading-name', enabled: true, fiberPhase: 'unloading' }, + { entryId: 'unobserved', moduleName: '@fixture/unobserved-name', enabled: true, fiberPhase: null }, { entryId: 'disabled-entry', moduleName: '@deepseek-ai/dsh-host-directory-picker-native', enabled: false, fiberPhase: null }, ], } as unknown as Snapshot @@ -42,9 +43,9 @@ describe('PluginSettingsSection', () => { expect(list).toHaveBeenCalledOnce() expect(screen.getByRole('searchbox', { name: en.search })).toBeTruthy() expect(screen.getByRole('heading', { name: en.catalog })).toBeTruthy() - expect(view.container.querySelector('[data-plugin-count]')?.textContent).toBe('6') - expect(screen.getAllByRole('listitem')).toHaveLength(6) - expect(screen.getAllByText(en.enabledTag)).toHaveLength(5) + expect(view.container.querySelector('[data-plugin-count]')?.textContent).toBe('7') + expect(screen.getAllByRole('listitem')).toHaveLength(7) + expect(screen.getAllByText(en.enabledTag)).toHaveLength(6) expect(screen.getByText(en.disabledTag)).toBeTruthy() for (const value of [ 'Mounted', @@ -52,10 +53,10 @@ describe('PluginSettingsSection', () => { 'Loading', 'Mount failed', 'Unloading', + 'Not mounted', ]) { expect(screen.getByRole('img', { name: value })).toBeTruthy() } - expect(screen.queryByRole('img', { name: 'Not mounted' })).toBeNull() const active = screen.getByRole('button', { name: 'hmr, Mounted, Enabled' }) expect(active.getAttribute('aria-expanded')).toBe('false') fireEvent.click(active) From caf7d48f88fb9768c7c8a0a4b7f3ed2bc581b3bd Mon Sep 17 00:00:00 2001 From: pku-xht Date: Wed, 12 Aug 2026 17:38:06 +0800 Subject: [PATCH 004/160] fix(subagent): preserve startup cleanup failures --- ...8-10-fork-children-stay-one-shot.i18n.yaml | 4 +- .../2026-08-10-fork-children-stay-one-shot.md | 4 +- ...26-08-10-fork-children-stay-one-shot.zh.md | 4 +- .../subagent-claude-code/README.i18n.yaml | 4 +- .../subagent/subagent-claude-code/README.md | 2 +- .../subagent-claude-code/README.zh.md | 2 +- .../subagent/subagent-codex/README.i18n.yaml | 4 +- packages/subagent/subagent-codex/README.md | 2 +- packages/subagent/subagent-codex/README.zh.md | 2 +- packages/subagent/tool-subagent/src/index.ts | 4 +- .../tool-subagent/tests/tool-subagent.spec.ts | 42 +++++++++++++++++++ 11 files changed, 59 insertions(+), 15 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml index 282779ce62..ef424471d8 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md -2026-08-10-fork-children-stay-one-shot.md: f799f9f7affe7dfecbbe12d0988cf6f9b936d43e -2026-08-10-fork-children-stay-one-shot.zh.md: 5c55e96296ed6678ac82afc0e40150d443f74c96 +2026-08-10-fork-children-stay-one-shot.md: 030bed3c1b516a54afd5f97b00000ed7ceaf64d8 +2026-08-10-fork-children-stay-one-shot.zh.md: 4b033a70c3674315c491e85be67a1d357aeac51b diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md index f799f9f7af..030bed3c1b 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md @@ -12,7 +12,7 @@ The child-scoped `report` return channel is now the largest such addition, and s ## Decision -Every shipped composition binds the fork delegation tool to `backgroundMode: one-shot`: [the base bundle](../../../../packages/bundle/base/cordis.patch.yml), [the ACP example](../../../../examples/acp-agent/cordis.yml), and [the headless example](../../../../examples/headless-agent/cordis.yml). All three have access to the generic Task runtime: the base bundle leaves `run_in_background` available, while the two examples set `enableRunInBackground: false` as an explicit foreground-only composition choice. +Every shipped composition binds the fork delegation tool to `backgroundMode: one-shot`: [the base bundle](../../../../packages/bundle/base/cordis.patch.yml), [the ACP example](../../../../examples/acp-agent/cordis.yml), and [the headless example](../../../../examples/headless-agent/cordis.yml). The base bundle leaves `run_in_background` available, because it mounts a task service; the two examples set `enableRunInBackground: false`, because they mount none and a one-shot background start would otherwise fail at call time on a missing `tasks` service. One-shot children — foreground and background alike — are created through `SubagentService.start()`, which never enters the continuable activation-setup registry, so neither `report` nor its prompt section is installed. A forked one-shot child's system prompt and tool schemas therefore equal its parent's, apart from the `persona` and `toolFilter` deltas a deployment opts into per delegation tool. @@ -38,7 +38,7 @@ The reintroduction condition is recorded as a `TODO(fork-continuable-prefix-reus ## Consequences -- No shipped composition creates a continuable forked child. The base bundle may return a one-shot Task id for `subagent_fork`, while the ACP and headless examples return the result to the caller's turn; `send_message` addresses only spawned children. +- No shipped composition creates a continuable forked child; `subagent_fork` returns a result to its caller's turn, and `send_message` addresses only spawned children. - A forked child's request prefix stays byte-identical to its parent's unless the deployment configures `persona` or `toolFilter` on the fork delegation tool, so the token cost of seeding buys provider-side reuse again. - The fork provider's continuable path has no production caller and no assembled-composition coverage. It keeps its package-level tests, and the seam still accepts it, so a bundle or `--patch` overlay can reintroduce it with no code change and no warning. - `subagent_fork`'s model-visible schema changes: the continuable background wording is replaced by the one-shot task wording in the base bundle, and disappears entirely from the two examples. The affected keyless snapshot tool-schema sidecars are re-recorded in the same change. diff --git a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md index 5c55e96296..4b033a70c3 100644 --- a/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.zh.md @@ -12,7 +12,7 @@ fork 与 spawn 的唯一区别是 child 的 Session 会以 parent 已完成轮 ## 决策 -所有随附组合都把 fork 委派工具绑定为 `backgroundMode: one-shot`:[base 组合包](../../../../packages/bundle/base/cordis.patch.yml)、[ACP 示例](../../../../examples/acp-agent/cordis.yml)与[headless 示例](../../../../examples/headless-agent/cordis.yml)。三者都能使用通用 Task 运行时:base 组合包保留 `run_in_background`,两个示例则把 `enableRunInBackground: false` 作为显式的仅前台组装选择。 +所有随附组合都把 fork 委派工具绑定为 `backgroundMode: one-shot`:[base 组合包](../../../../packages/bundle/base/cordis.patch.yml)、[ACP 示例](../../../../examples/acp-agent/cordis.yml)与[headless 示例](../../../../examples/headless-agent/cordis.yml)。base 组合包保留 `run_in_background`,因为它挂载了 task 服务;两个示例设置 `enableRunInBackground: false`,因为它们都不挂载 task 服务,否则一次 one-shot 后台启动会在调用时因缺少 `tasks` 服务而失败。 one-shot child——前台与后台皆然——经由 `SubagentService.start()` 创建,该路径从不进入可继续的 activation setup 注册表,因此 `report` 与它的提示词 section 都不会被安装。于是一个 fork 出的 one-shot child 的系统提示词与工具 schema 与其 parent 相同,只差部署逐个委派工具主动选择的 `persona` 与 `toolFilter` 增量。 @@ -38,7 +38,7 @@ one-shot child——前台与后台皆然——经由 `SubagentService.start()` ## 后果 -- 没有任何随附组合会创建可继续的 fork child。base 组合包中的 `subagent_fork` 可以返回 one-shot Task id,而 ACP 与 headless 示例会把结果返回给调用方的轮次;`send_message` 只寻址 spawn 出的 child。 +- 没有任何随附组合会创建可继续的 fork child;`subagent_fork` 把结果返回给调用方的轮次,而 `send_message` 只寻址 spawn 出的 child。 - 除非部署在 fork 委派工具上配置了 `persona` 或 `toolFilter`,fork child 的请求前缀与其 parent 逐字节相同,因此初始内容的 token 成本重新换来了提供方侧的复用。 - fork 提供方的可继续路径没有生产调用方,也没有整体组装层面的覆盖。它保留自己的包内测试,seam 也仍然接受它,因此某个组合包或 `--patch` 覆盖层可以无需改动代码、也不会有任何警告地把它重新引入。 - `subagent_fork` 面向模型的 schema 发生变化:base 组合包中可继续的后台措辞被 one-shot 的 task 措辞取代,在两个示例中则完全消失。受影响的无密钥快照工具 schema 伴随文件在同一次改动中重新记录。 diff --git a/packages/subagent/subagent-claude-code/README.i18n.yaml b/packages/subagent/subagent-claude-code/README.i18n.yaml index 168a921e7c..02dff5851d 100644 --- a/packages/subagent/subagent-claude-code/README.i18n.yaml +++ b/packages/subagent/subagent-claude-code/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent-claude-code/README.md -README.md: 556cde7da4467659d02709c43325e60fa7de6a74 -README.zh.md: bda00d7be4467e739604eeba0020850cb1a99d50 +README.md: f0f6e39820920e0b266802585b74a953a068828a +README.zh.md: 52baee6c1158bb38505fd0cf454a820853e0cf89 diff --git a/packages/subagent/subagent-claude-code/README.md b/packages/subagent/subagent-claude-code/README.md index 556cde7da4..f0f6e39820 100644 --- a/packages/subagent/subagent-claude-code/README.md +++ b/packages/subagent/subagent-claude-code/README.md @@ -82,7 +82,7 @@ Independent of the parent request cache. Reuse depends only on Claude Code's own #### What the model sees -Through `dsh-tool-subagent`, a foreground call gives the parent the strict final Claude Code answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a bounded completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, and product ids are not copied into the parent Session. +Through `dsh-tool-subagent`, a foreground call gives the parent the strict final Claude Code answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, and product ids are not copied into the parent Session. #### Token effect diff --git a/packages/subagent/subagent-claude-code/README.zh.md b/packages/subagent/subagent-claude-code/README.zh.md index bda00d7be4..52baee6c11 100644 --- a/packages/subagent/subagent-claude-code/README.zh.md +++ b/packages/subagent/subagent-claude-code/README.zh.md @@ -82,7 +82,7 @@ Claude Code 子任务会在一个全新的 SDK query 中接收独立文本任务 #### 模型看到的内容 -通过 `dsh-tool-subagent`,前台调用会让父级模型看到符合严格成功条件的 Claude Code 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id;随后通用任务控制面会送达有界完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。 +通过 `dsh-tool-subagent`,前台调用会让父级模型看到符合严格成功条件的 Claude Code 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id;随后通用任务控制面会送达完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。 #### 对 token 的影响 diff --git a/packages/subagent/subagent-codex/README.i18n.yaml b/packages/subagent/subagent-codex/README.i18n.yaml index 9c9c7e8648..5556cc7fac 100644 --- a/packages/subagent/subagent-codex/README.i18n.yaml +++ b/packages/subagent/subagent-codex/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/subagent/subagent-codex/README.md -README.md: 6a882c128ebd372ee148a4315a237d4eacf6d42b -README.zh.md: beb5411f6a449efc7a45c171d217f98657805740 +README.md: 570a2346b121a3d100600285f225a0906c38aeec +README.zh.md: 9fded2ceb35b276f0c698c937720ea13e13c6acc diff --git a/packages/subagent/subagent-codex/README.md b/packages/subagent/subagent-codex/README.md index 6a882c128e..570a2346b1 100644 --- a/packages/subagent/subagent-codex/README.md +++ b/packages/subagent/subagent-codex/README.md @@ -76,7 +76,7 @@ Independent of the parent request cache. Reuse depends only on Codex's own provi #### What the model sees -Through `dsh-tool-subagent`, a foreground call gives the parent the selected final Codex answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a bounded completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Codex commentary, reasoning, tool activity, stderr, workspace diffs, usage, and product ids are not copied into the parent Session. +Through `dsh-tool-subagent`, a foreground call gives the parent the selected final Codex answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Codex commentary, reasoning, tool activity, stderr, workspace diffs, usage, and product ids are not copied into the parent Session. #### Token effect diff --git a/packages/subagent/subagent-codex/README.zh.md b/packages/subagent/subagent-codex/README.zh.md index beb5411f6a..9fded2ceb3 100644 --- a/packages/subagent/subagent-codex/README.zh.md +++ b/packages/subagent/subagent-codex/README.zh.md @@ -76,7 +76,7 @@ Codex 子任务会在一个全新的临时线程中,以单个轮次接收这 #### 模型看到的内容 -通过 `dsh-tool-subagent`,前台调用会让父级模型看到选定的 Codex 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id;随后通用任务控制面会送达有界完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Codex 的过程说明、推理(reasoning)、工具活动、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。 +通过 `dsh-tool-subagent`,前台调用会让父级模型看到选定的 Codex 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id;随后通用任务控制面会送达完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Codex 的过程说明、推理(reasoning)、工具活动、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。 #### 对 token 的影响 diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index 1e118908c0..c8918d4e7a 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -113,7 +113,9 @@ async function settleStart(start: Promise, signal: AbortSignal): Pr try { return await settleRun(await start) } catch (error: unknown) { - return signal.aborted + // Product providers aggregate startup and rollback failures. Cancellation + // must not turn a failed cleanup into a cleanly killed Task. + return signal.aborted && !(error instanceof AggregateError) ? { status: 'killed' } : { status: 'failed', detail: String(error) } } diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index 4a36413c7d..21dc48f853 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -939,6 +939,48 @@ describe('dsh-tool-subagent background mode', () => { expect(text(output)).toBe('(no new output)\n[status: killed]') }) + it('reports startup rollback failure after cancellation as a failed task', async () => { + const ctx = await backgroundSetup({ provider: 'mock' }) + const parent = ownerAgent(ctx, 'sess-parent') + ctx.subagents.registerProvider({ + name: 'broken-start-rollback', + capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + inheritsParentContext: false, + start: request => new Promise((_resolve, reject) => { + request.signal.addEventListener('abort', () => { + reject(new AggregateError( + [new Error('startup aborted'), new Error('cleanup failed')], + 'startup failed and cleanup also failed', + )) + }, { once: true }) + }), + }) + tool.apply(ctx, { provider: 'broken-start-rollback', toolName: 'subagent_broken_rollback' }) + + await ctx.tools.execute({ + signal: testToolSignal, + callId: CallId('broken-rollback-start'), + name: 'subagent_broken_rollback', + arguments: { description: 'broken rollback', prompt: 'p', run_in_background: true }, + agent: parent, + }) + await ctx.tools.execute({ + signal: testToolSignal, + callId: CallId('broken-rollback-kill'), + name: 'task_kill', + arguments: { task_id: 'subagent-1' }, + agent: parent, + }) + const output = await ctx.tools.execute({ + signal: testToolSignal, + callId: CallId('broken-rollback-output'), + name: 'task_output', + arguments: { task_id: 'subagent-1', wait: true }, + agent: parent, + }) + expect(text(output)).toContain('[status: failed, AggregateError: startup failed and cleanup also failed]') + }) + it('forwards task_kill reasons through the run signal (and defaults one when absent)', async () => { // Use a provider that remains live until its signal is aborted. const ctx = await backgroundSetup({ provider: 'mock', agentOptions: { model: 'child-model' } }) From de0d16d3d163241c22e0d073892b897d2eeec5a8 Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Wed, 12 Aug 2026 02:41:29 -0700 Subject: [PATCH 005/160] fix(web): complete plugin tab accessibility --- .../2026-08-11-plugin-settings-tabs.i18n.yaml | 4 +-- .../2026-08-11-plugin-settings-tabs.md | 2 +- .../2026-08-11-plugin-settings-tabs.zh.md | 2 +- .../src/client/PluginConfigSection.module.css | 4 ++- .../src/client/PluginConfigSection.tsx | 22 +++++++++++-- .../tests/section.client.spec.tsx | 32 +++++++++++++++++++ 6 files changed, 59 insertions(+), 7 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.i18n.yaml index cdfdc3afde..980999fc1a 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md -2026-08-11-plugin-settings-tabs.md: 987e9f49e750a026c38a0d73c255cd335c53a4fe -2026-08-11-plugin-settings-tabs.zh.md: 887ca20bc09871d4bac5650a5d5eb71d5fcb81ee +2026-08-11-plugin-settings-tabs.md: c46276de2d0ffee141377190ccacf0e7884ec994 +2026-08-11-plugin-settings-tabs.zh.md: 00feb44f7b612e191c5b4c7dd3675ef34bf6e5ac diff --git a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md index 987e9f49e7..c46276de2d 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md +++ b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.md @@ -12,7 +12,7 @@ Plugin configuration and the read-only Loader inventory each registered a top-le `@deepseek-ai/dsh-client-ui-plugin-config` owns the single `settings.section` contribution with id `plugins`. It renders the shared title and compact tab chrome, declares the root-scoped list slot `settings.plugins.tab`, and projects that ledger's id, order, and locale-following label into its tabs. The slot's canonical type lives in `ui-settings`, so a tab contributor depends on the Settings domain contract rather than on another feature plugin. -The section owner contributes a `configurable` tab that declares the existing nested `settings.plugin.item` list. Configuration cards keep their namespace bindings, draft state, validation, and writes unchanged. `@deepseek-ai/dsh-client-ui-plugins` contributes an `all` tab to `settings.plugins.tab`; its Host Loader observer, generated Remote namespace, DTO, search semantics, and read-only disclosure cards remain unchanged. +The section owner contributes a `configurable` tab that declares the existing nested `settings.plugin.item` list. Configuration cards keep their namespace bindings, draft state, validation, and writes unchanged. `@deepseek-ai/dsh-client-ui-plugins` contributes an `all` tab to `settings.plugins.tab`; its Host Loader observer, generated Remote namespace, DTO, and search semantics remain unchanged. Disabled inventory entries omit the redundant unmounted runtime state from summaries and details, while enabled entries continue to expose their Cordis phase. The first ordered tab is selected by default. A tab mounts only when first selected and then remains mounted but hidden while the Plugins section stays mounted. This delays the inventory RPC until the user opens **Plugin list** and preserves drafts, search text, disclosure state, and the fetched snapshot while switching tabs. Closing Settings unmounts the section, so reopening it obtains a fresh inventory snapshot when that tab is selected again. diff --git a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.zh.md b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.zh.md index 887ca20bc0..00feb44f7b 100644 --- a/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-11-plugin-settings-tabs.zh.md @@ -12,7 +12,7 @@ Status: implemented `@deepseek-ai/dsh-client-ui-plugin-config` 拥有唯一一个 id 为 `plugins` 的 `settings.section` 贡献。它渲染共享标题和紧凑标签栏,声明根级列表 slot `settings.plugins.tab`,并把该记录中的 id、order 与跟随语言的 label 投影成标签页。该 slot 的规范类型位于 `ui-settings`,因此标签页贡献方依赖设置领域约定,而不是依赖另一个功能插件。 -分区拥有方贡献 `configurable` 标签页,由它声明既有的嵌套 `settings.plugin.item` 列表。配置卡片原有的命名空间绑定、草稿状态、校验与写入均保持不变。`@deepseek-ai/dsh-client-ui-plugins` 向 `settings.plugins.tab` 贡献 `all` 标签页;它的 Host Loader 观察器、生成的 Remote 命名空间、DTO、搜索语义与只读折叠卡片均保持不变。 +分区拥有方贡献 `configurable` 标签页,由它声明既有的嵌套 `settings.plugin.item` 列表。配置卡片原有的命名空间绑定、草稿状态、校验与写入均保持不变。`@deepseek-ai/dsh-client-ui-plugins` 向 `settings.plugins.tab` 贡献 `all` 标签页;它的 Host Loader 观察器、生成的 Remote 命名空间、DTO 与搜索语义保持不变。已停用的清单条目会在摘要和详情中省略重复的“未挂载”运行状态,已启用条目仍显示其 Cordis 阶段。 默认选择顺序中的第一个标签页。某个标签页只有首次被选择时才挂载,之后在“插件”分区保持挂载期间只隐藏而不卸载。这样会把清单 RPC 延迟到用户打开**插件列表**时,并在切换标签页时保留草稿、搜索文本、折叠状态和已读取的快照。关闭 Settings 会卸载该分区,因此再次打开后,重新选择该标签页时会取得新的清单快照。 diff --git a/packages/client/ui-plugin-config/src/client/PluginConfigSection.module.css b/packages/client/ui-plugin-config/src/client/PluginConfigSection.module.css index f5a162eee8..5760403c4a 100644 --- a/packages/client/ui-plugin-config/src/client/PluginConfigSection.module.css +++ b/packages/client/ui-plugin-config/src/client/PluginConfigSection.module.css @@ -58,7 +58,9 @@ } .tab:focus-visible { - outline: none; + outline: 2px solid var(--dsw-alias-state-business-primary); + outline-offset: 2px; + border-radius: 2px; color: var(--dsw-alias-label-primary); } diff --git a/packages/client/ui-plugin-config/src/client/PluginConfigSection.tsx b/packages/client/ui-plugin-config/src/client/PluginConfigSection.tsx index a75816b92e..592e495488 100644 --- a/packages/client/ui-plugin-config/src/client/PluginConfigSection.tsx +++ b/packages/client/ui-plugin-config/src/client/PluginConfigSection.tsx @@ -1,6 +1,6 @@ /** Plugins settings section: localized tabs around feature-owned pages. */ -import { useEffect, useId, useState } from 'react' +import { useEffect, useId, useRef, useState } from 'react' import type { HostObservable, InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime, } from '@deepseek-ai/dsh-client-ui-slots' @@ -32,6 +32,7 @@ export type PluginConfigSectionProps = /** Render one Plugins page whose contents arrive from feature-owned tabs. */ export function PluginConfigSection({ t, renderSlot, useTabs }: PluginConfigSectionProps) { const tabsId = useId() + const tabRefs = useRef>([]) const rows = useTabs(value => value) const [activeId, setActiveId] = useState() const [visitedIds, setVisitedIds] = useState>(() => new Set()) @@ -55,11 +56,12 @@ export function PluginConfigSection({ t, renderSlot, useTabs }: PluginConfigSect {rows.length === 0 ?

    {t('empty')}

    : ( <>
    - {rows.map((row) => { + {rows.map((row, index) => { const selected = row.id === active return ( diff --git a/packages/client/ui-plugin-config/tests/section.client.spec.tsx b/packages/client/ui-plugin-config/tests/section.client.spec.tsx index e00fd7d448..1191fc3aea 100644 --- a/packages/client/ui-plugin-config/tests/section.client.spec.tsx +++ b/packages/client/ui-plugin-config/tests/section.client.spec.tsx @@ -117,6 +117,38 @@ describe('PluginConfigSection', () => { expect(screen.getByRole('heading', { name: en.title })).toBeTruthy() expect(screen.getByText(en.intro)).toBeTruthy() }) + + it('moves focus and selection with standard horizontal tab keys', () => { + renderSection([ + { id: 'configurable', order: 0, label: en.configurableTab }, + { id: 'all', order: 10, label: 'Plugin list' }, + { id: 'diagnostics', order: 20, label: 'Diagnostics' }, + ]) + + const configurable = screen.getByRole('tab', { name: en.configurableTab }) + const all = screen.getByRole('tab', { name: 'Plugin list' }) + const diagnostics = screen.getByRole('tab', { name: 'Diagnostics' }) + expect(configurable.getAttribute('tabindex')).toBe('0') + expect(all.getAttribute('tabindex')).toBe('-1') + + configurable.focus() + fireEvent.keyDown(configurable, { key: 'ArrowRight' }) + expect(document.activeElement).toBe(all) + expect(all.getAttribute('aria-selected')).toBe('true') + + fireEvent.keyDown(all, { key: 'End' }) + expect(document.activeElement).toBe(diagnostics) + fireEvent.keyDown(diagnostics, { key: 'ArrowRight' }) + expect(document.activeElement).toBe(configurable) + fireEvent.keyDown(configurable, { key: 'ArrowLeft' }) + expect(document.activeElement).toBe(diagnostics) + fireEvent.keyDown(diagnostics, { key: 'Home' }) + expect(document.activeElement).toBe(configurable) + + fireEvent.keyDown(configurable, { key: 'Escape' }) + expect(document.activeElement).toBe(configurable) + expect(configurable.getAttribute('aria-selected')).toBe('true') + }) }) describe('ConfigurablePluginsTab', () => { From ba1b0e15fc75c2a6dffa047817a6c0c2c328e1e0 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Wed, 12 Aug 2026 16:37:11 +0800 Subject: [PATCH 006/160] ci: exempt only push from concurrency cancellation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two self-hosted standby drills each run their complete unsharded aggregate with one gate worker, which takes longer than the interval between master merges, so unconditional cancel-in-progress supersedes a drill before it reaches a verdict and the lane yields no readiness evidence for the failover runbook to point a responder at. Exempt push and nothing else. This has to be decided at workflow level: cancellation applies to the whole superseded run, so a job-level concurrency group cannot exempt its job. The negated form is load-bearing — naming pull_request alone would also stop cancelling workflow_dispatch, and each runner benchmark fans out to twelve larger runners for up to fifteen minutes in this same group on master, so a re-dispatch would queue ahead of a drill instead of replacing a stale measurement. It does not promise every push run finishes: a newer pending run still displaces an older one, only that the lanes periodically reach a verdict. A master push carries only wine-apt-cache and the two drills; every other job is pull-request-gated, workflow_dispatch-gated, or if: false. The spec pins that set and classifies by exact condition, since a negated event test mentions the event it excludes. --- .../2026-07-26-ci-failover-runbook.i18n.yaml | 4 +- .../process/2026-07-26-ci-failover-runbook.md | 4 ++ .../2026-07-26-ci-failover-runbook.zh.md | 4 ++ .github/workflows/ci.yml | 14 +++- scripts/ci-workflow.spec.ts | 65 +++++++++++++++++++ 5 files changed, 88 insertions(+), 3 deletions(-) diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml index 8615d248fc..47fb2a0572 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md -2026-07-26-ci-failover-runbook.md: 47901844f4ec581dff8500cc429c54a076b7642b -2026-07-26-ci-failover-runbook.zh.md: 88a793e144a4d06718ffe323a94f8e81f8e62b82 +2026-07-26-ci-failover-runbook.md: 6f2b4bfba9255549b0887aab9061a6513cb3a68f +2026-07-26-ci-failover-runbook.zh.md: 7bb1a025a71eb690d2d90fcdbddbdd56355786fc diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md index 47901844f4..6f2b4bfba9 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md @@ -12,6 +12,10 @@ The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.ym Each of the three required Linux worker jobs, the independent native Windows job, and the `all checks passed` verdict job — which would otherwise stay queued on the failed pool even after every worker passed — resolves its runner pool through the `DSH_CI_FAILOVER` repository variable. Unset (normal), they run on the hosted enterprise pools. Set to `selfhosted` by any repository writer, all five retarget onto the in-house self-hosted pools: the Linux jobs and verdict onto the `vm-backup` pool, coverage and snapshot concurrency drop to shared-VM bounds, and the hosted-path pnpm cache restores are skipped; the native Windows job onto the `dsh-win-ci` pool. The switch is writer-manageable repository state, not a merge, so it works while every check is red. The in-house pools' readiness is continuously re-proven by the `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes, which run the complete unsharded aggregates on every master push. +`ci.yml` exempts exactly one event from `cancel-in-progress` (`${{ github.event_name != 'push' }}`), so a superseded pull-request run is still cancelled while a push run is left to run. Each drill runs its complete unsharded aggregate with one gate worker, which takes longer than the interval between master merges; under unconditional cancellation a drill is superseded before reaching a verdict and the lane yields no readiness evidence for a responder to check. This is not a guarantee that every push run finishes: GitHub keeps a single pending entry per group, so a newer pending run displaces an older one, and during busy periods intermediate push runs still end as `cancelled`. What the carve-out buys is that the lane periodically reaches a verdict at all, which is what makes it usable as evidence. + +The decision belongs at workflow level because cancellation applies to the whole superseded run: a job-level `concurrency` group does not exempt its job. The negated form is load-bearing rather than cosmetic: naming `pull_request` alone would also stop cancelling `workflow_dispatch`, and each runner benchmark fans out to twelve larger runners for up to fifteen minutes inside this same group on master, so a re-dispatch would queue ahead of a drill instead of replacing a stale measurement. What bounds the cost is that a master push carries only `wine-apt-cache` and these two drills; every other job is pull-request-gated, `workflow_dispatch`-gated, or `if: false`, and `scripts/ci-workflow.spec.ts` pins that set — classifying by exact condition, since a negated event test mentions the event it excludes — so a new push-reachable job cannot quietly start accumulating uncancelled runs. + ### What the in-house pool is `vm-backup`: one 64-core VM, six always-on systemd-managed runner instances. Its image must preinstall Playwright Chromium's Linux system packages; CI downloads the lockfile-selected browser but never runs `apt` on this persistent shared host. Check the latest `serial / linux (self-hosted standby)` run before switching: its aggregate includes browser replay, so a green standby verifies both ordinary capacity and this browser prerequisite. diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md index 88a793e144..7bb1a025a7 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md @@ -12,6 +12,10 @@ Status: implemented 三个必需的 Linux 工作作业、独立的原生 Windows 作业,以及 `all checks passed` 判定作业(若不随切换,即使全部工作作业通过,它仍会滞留在故障池的队列中)——各自通过仓库变量 `DSH_CI_FAILOVER` 解析运行器池。变量不存在(正常)时它们运行在托管企业池上;由任何具备写权限的协作者设为 `selfhosted` 时,五个作业全部切换到公司自有的自托管池:Linux 作业与判定作业切到 `vm-backup` 池,覆盖率与快照的并发降到共享虚拟机上限,并跳过托管路径的 pnpm 缓存恢复;原生 Windows 作业切到 `dsh-win-ci` 池。这个开关是写者可管理的仓库状态而非一次合并,因此在所有检查都是红色时仍然有效。自有池的就绪状态由 `serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)` 通道持续验证——每次 master 推送都在其上运行完整的未分片聚合流程。 +`ci.yml` 只豁免一个事件不做取消(`${{ github.event_name != 'push' }}`),因此被取代的拉取请求运行仍会取消,而推送运行则留待执行。每次演练以单门禁工作进程执行完整的未分片聚合流程,耗时长于 master 合并的间隔;在无条件取消下,演练会在得出结论前被后续运行取代,该通道无法产出供响应者查看的就绪证据。这并不保证每次推送运行都能跑完:GitHub 每个组只保留一个待运行条目,更新的待运行条目会顶掉更早的,繁忙时段中间的推送运行仍会以 `cancelled` 结束。这项豁免换来的是该通道**周期性**地得出结论,而这正是它能作为证据的前提。 + +这个决定必须放在工作流级:取消作用于被取代的整个运行,作业级 `concurrency` 组并不能豁免其所属作业。采用否定式写法而非仅指名 `pull_request`,是有实质作用的:后者会连 `workflow_dispatch` 一起停止取消,而每次运行器基准测试会在 master 上的同一并发组内同时占用 12 台大规格运行器、最长 15 分钟,届时重复派发会排在演练之前,而不是替换掉已过时的测量。成本之所以可控,是因为一次 master 推送只承载 `wine-apt-cache` 和这两条演练;其余作业都受拉取请求门控、`workflow_dispatch` 门控或 `if: false`,并且 `scripts/ci-workflow.spec.ts` 会锁定这个集合——按条件精确匹配,因为否定式事件判断会包含它所排除的事件名——使新的推送可达作业无法悄悄开始累积未取消的运行。 + ### 自有池是什么 `vm-backup`:一台 64 核虚拟机,6 个常驻 systemd 管理的运行器实例。其镜像必须预装 Playwright Chromium 的 Linux 系统软件包;CI 会下载锁文件选定的浏览器,但绝不在这台持久化共享主机上运行 `apt`。切换前先看 `serial / linux (self-hosted standby)` 最近一次运行:其聚合流程包含浏览器回放,因此绿色热备同时验证常规容量和这项浏览器先决条件。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd3bbb4544..c3549c343a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,9 +15,21 @@ on: - larger-runner-benchmark - consolidated-runner-benchmark +# Cancel a superseded run on every event EXCEPT push. A push run carries the two +# self-hosted standby drills, which take longer than the interval between master +# merges, so cancelling supersedes a drill before it reaches a verdict and the +# lane yields no readiness evidence. Must be decided here: cancellation applies +# to the whole superseded run, so a job-level group cannot exempt its job. +# Negated rather than `== 'pull_request'` so workflow_dispatch keeps cancelling: +# a re-dispatched runner benchmark holds up to 12 larger runners for 15 minutes +# and shares this group with the drills on master, so queueing it would delay +# them. This does not promise every push run finishes (a newer pending run still +# displaces an older one) — only that the lanes periodically reach a verdict. +# Rationale and the bound on this carve-out: +# .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name != 'push' }} permissions: contents: read diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index 2642f234c4..06fb9c2a27 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -84,6 +84,71 @@ describe('CI workflow', () => { expect(aggregate.needs).not.toContain('serial-windows') }) + it('leaves push runs uncancelled, so the self-hosted standby drills reach a verdict', () => { + const workflow = loadWorkflow('.github/workflows/ci.yml') + if (!isRecord(workflow.jobs) || !isRecord(workflow.concurrency)) { + throw new TypeError('CI workflow must define jobs and a workflow-level concurrency block') + } + + // Cancellation applies to the whole superseded RUN, so this has to be + // decided at workflow level and gated on the event: a job-level group + // cannot exempt its job from its run being cancelled. Only push is exempt — + // a drill takes longer than the interval between master merges. The negated + // form is load-bearing: `== 'pull_request'` would also stop cancelling + // workflow_dispatch, and a re-dispatched runner benchmark holds up to 12 + // larger runners for 15 minutes in this same group on master. + expect(workflow.concurrency['cancel-in-progress']).toBe("${{ github.event_name != 'push' }}") + + // Neither drill may re-introduce a job-level group: it would not help, and + // it would imply the run-scoped cancellation had been solved locally. + for (const name of ['serial-linux-selfhosted', 'serial-windows']) { + const job = workflow.jobs[name] + if (!isRecord(job)) throw new TypeError(`${name} must be defined`) + expect(job.concurrency).toBeUndefined() + // Both stay master-push-only; that is what makes the push carve-out safe. + expect(job.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'") + } + + // What bounds the cost of never cancelling a push run: a master push may + // only carry the cache seeder and the two drills. Any job reachable on push + // would start accumulating uncancelled runs, so the set is pinned here. + // + // Classification is an exact allowlist of the conditions in use, not a + // substring match: `github.event_name != 'pull_request'` mentions + // `pull_request` yet IS push-reachable, so matching on the event name alone + // would silently misclassify it as gated. + const NOT_PUSH_REACHABLE = new Set([ + "github.event_name == 'pull_request'", + "always() && github.event_name == 'pull_request'", + "github.event_name == 'workflow_dispatch' && inputs.suite == 'larger-runner-benchmark'", + "github.event_name == 'workflow_dispatch' && inputs.suite == 'consolidated-runner-benchmark'", + ]) + const pushReachable = Object.entries(workflow.jobs) + .filter(([, job]) => { + if (!isRecord(job)) return false + if (job.if === undefined) return true // unconditional: runs on every event + if (job.if === false) return false // `if: false` parses as a boolean + if (typeof job.if !== 'string') return true // unrecognized shape: surface it + return !NOT_PUSH_REACHABLE.has(job.if.trim()) + }) + .map(([name]) => name) + .sort() + expect(pushReachable).toEqual(['serial-linux-selfhosted', 'serial-windows', 'wine-apt-cache']) + + // Why workflow_dispatch must keep cancelling: each benchmark fans out to a + // dozen larger runners at once, in this same group on master. If it stopped + // cancelling, a re-dispatch would queue ahead of a drill instead of + // replacing the stale measurement. + for (const name of ['larger-runner-benchmark', 'consolidated-runner-benchmark']) { + const job = workflow.jobs[name] + if (!isRecord(job) || !isRecord(job.strategy)) { + throw new TypeError(`${name} must define a matrix strategy`) + } + expect(job.strategy['max-parallel']).toBe(12) + expect(job['timeout-minutes']).toBe(15) + } + }) + it('keeps supported LSP source under native Windows coverage', () => { const config = readFileSync(resolve(root, 'vitest.config.ts'), 'utf8') From 1caca08301a0b2a96bad3e6831238847f803edcc Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Wed, 12 Aug 2026 18:16:24 +0800 Subject: [PATCH 007/160] docs: narrow the push-exemption guarantee, unbreak two static gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cancel-in-progress is evaluated against the newly triggered run, so exempting push means one master merge does not cancel the drill still running from the previous one — not that a drill always finishes. A benchmark dispatched on master shares the group and does cancel a mid-flight drill. Record that bound in the runbook and drop the overstated wording from the workflow comment and the spec name. Also repair two gates that fail on master and block every pull request: the telemetry note referenced an SDK proposal deleted in 408721954a, and the ui-settings-general README pair carried stale recorded hashes after both sides were updated together in aa1ec02bc6. --- .../2026-08-10-telemetry-default-off.i18n.yaml | 4 ++-- .../feature/2026-08-10-telemetry-default-off.md | 2 +- .../2026-08-10-telemetry-default-off.zh.md | 2 +- .../2026-07-26-ci-failover-runbook.i18n.yaml | 4 ++-- .../process/2026-07-26-ci-failover-runbook.md | 4 +++- .../process/2026-07-26-ci-failover-runbook.zh.md | 4 +++- .github/workflows/ci.yml | 6 +++--- .../client/ui-settings-general/README.i18n.yaml | 4 ++-- scripts/ci-workflow.spec.ts | 16 +++++++++------- 9 files changed, 26 insertions(+), 20 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml index 7c4995a88d..5fbc0483e4 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md -2026-08-10-telemetry-default-off.md: 4bda346c2b05a94106eb5658c3ee558a4b32407f -2026-08-10-telemetry-default-off.zh.md: 706f2c18fbbf226e0357fa99bf3fd61c39fce08a +2026-08-10-telemetry-default-off.md: 3b9cd4bc3e9c98ee96ae036a0e981aa585a02403 +2026-08-10-telemetry-default-off.zh.md: 18f83869243b3d5c65278e654f4ac10e9bfe7d30 diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md index 4bda346c2b..3b9cd4bc3e 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md @@ -12,7 +12,7 @@ DeepSeek Harness has two outbound telemetry feeds. During internal testing, the Both feeds use `DSH_TELEMETRY_MODE` as their positive consent setting. Unset and empty values resolve to `DISABLED`. `@deepseek-ai/dsh-session-telemetry-otel` also resolves an omitted `mode` to `DISABLED`, which constructs no OTel provider, processor, or exporter and leaves feedback in the local session log. The shared dsh base keeps the backend row mounted so disabled feedback can still explain that nothing was shared. A deployment opts into Session Log sharing through `FULL` or `FEEDBACK_ONLY`; only `FULL` also permits dsh-sdk launcher reporting. Any non-empty `DSH_TELEMETRY_DISABLED` remains an authoritative pre-load hard opt-out. The [default-mount decision](2026-07-31-web-telemetry-default-mount.md) continues to own the endpoint, batching cadence, and exit-drain settings. -The dsh-sdk launcher reads the same variable without parsing `cordis.yml` or booting Cordis. `FULL` permits reporting; `FEEDBACK_ONLY`, `DISABLED`, unset, and empty values deny it. Consent is frozen from the launching environment before the command runs, because `dsh-sdk start` loads a project `.env` and project code can mutate `process.env`: resolving afterwards would let a project grant reporting of its own configuration, which the [configuration source ownership decision](../architecture/2026-08-04-configuration-source-ownership.md) denies for the whole `DSH_*` namespace. An unsupported mode denies rather than throwing at that boundary, since telemetry may never change a command's result. This rule supersedes only the default-on launcher consent in the [SDK follow-up proposal](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md); its other capabilities remain proposed. +The dsh-sdk launcher reads the same variable without parsing `cordis.yml` or booting Cordis. `FULL` permits reporting; `FEEDBACK_ONLY`, `DISABLED`, unset, and empty values deny it. Consent is frozen from the launching environment before the command runs, because `dsh-sdk start` loads a project `.env` and project code can mutate `process.env`: resolving afterwards would let a project grant reporting of its own configuration, which the [configuration source ownership decision](../architecture/2026-08-04-configuration-source-ownership.md) denies for the whole `DSH_*` namespace. An unsupported mode denies rather than throwing at that boundary, since telemetry may never change a command's result. The versioned Web welcome notice states that Session Log upload is off by default, names `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` and `DSH_TELEMETRY_MODE=FULL` as the two opt-in choices, and discloses that `FULL` also enables dsh-sdk command telemetry. Its version changes with that material privacy statement so every profile acknowledges the current copy. diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md index 706f2c18fb..18f8386924 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md @@ -12,7 +12,7 @@ DeepSeek Harness 有两路出站遥测数据流。在内测阶段,共享基础 两路数据流都使用 `DSH_TELEMETRY_MODE` 作为正向授权配置。未设置和空值都解析为 `DISABLED`。`@deepseek-ai/dsh-session-telemetry-otel` 也将省略的 `mode` 解析为 `DISABLED`;该模式不构造 OTel 提供方、处理器或导出器,并将反馈留在本地会话日志中。dsh 共享基础配置继续挂载后端配置行,使禁用模式仍可在记录反馈时说明没有共享任何内容。部署方通过 `FULL` 或 `FEEDBACK_ONLY` 显式启用 Session Log 共享;只有 `FULL` 还允许 dsh-sdk 启动器上报。任何非空 `DSH_TELEMETRY_DISABLED` 仍是具有最高优先级的加载前硬性退出开关。[默认挂载决策](2026-07-31-web-telemetry-default-mount.md)继续负责 endpoint、批处理节奏和退出排空设置。 -dsh-sdk 启动器读取同一变量,不解析 `cordis.yml`,也不启动 Cordis。`FULL` 允许上报;`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝。授权在命令执行前从启动环境冻结:`dsh-sdk start` 会加载项目 `.env`,项目代码也能修改 `process.env`,若在执行后解析,项目便能自行授权上报其自身配置,而[配置来源所有权决策](../architecture/2026-08-04-configuration-source-ownership.md)对整个 `DSH_*` 命名空间禁止这种行为。在该边界上,不受支持的模式按拒绝处理而非抛出,因为遥测不得改变命令结果。此规则仅取代 [SDK 后续功能提案](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md)中启动器默认允许上报的规则;其余能力仍处于提案状态。 +dsh-sdk 启动器读取同一变量,不解析 `cordis.yml`,也不启动 Cordis。`FULL` 允许上报;`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝。授权在命令执行前从启动环境冻结:`dsh-sdk start` 会加载项目 `.env`,项目代码也能修改 `process.env`,若在执行后解析,项目便能自行授权上报其自身配置,而[配置来源所有权决策](../architecture/2026-08-04-configuration-source-ownership.md)对整个 `DSH_*` 命名空间禁止这种行为。在该边界上,不受支持的模式按拒绝处理而非抛出,因为遥测不得改变命令结果。 带版本的 Web 欢迎通知说明会话日志上传默认关闭,将 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 和 `DSH_TELEMETRY_MODE=FULL` 列为两种显式启用选项,并披露 `FULL` 同时会启用 dsh-sdk 命令遥测。其版本随这项重要的隐私声明一同变更,使每个 profile 都确认当前文案。 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml index 47fb2a0572..ed819bc19e 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md -2026-07-26-ci-failover-runbook.md: 6f2b4bfba9255549b0887aab9061a6513cb3a68f -2026-07-26-ci-failover-runbook.zh.md: 7bb1a025a71eb690d2d90fcdbddbdd56355786fc +2026-07-26-ci-failover-runbook.md: 90ef2905bec86911697e552c0c1eac96ea0d3a18 +2026-07-26-ci-failover-runbook.zh.md: 5c8fd0e90ae31e3622ae6b49217591a54b84c351 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md index 6f2b4bfba9..90ef2905be 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md @@ -12,7 +12,9 @@ The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.ym Each of the three required Linux worker jobs, the independent native Windows job, and the `all checks passed` verdict job — which would otherwise stay queued on the failed pool even after every worker passed — resolves its runner pool through the `DSH_CI_FAILOVER` repository variable. Unset (normal), they run on the hosted enterprise pools. Set to `selfhosted` by any repository writer, all five retarget onto the in-house self-hosted pools: the Linux jobs and verdict onto the `vm-backup` pool, coverage and snapshot concurrency drop to shared-VM bounds, and the hosted-path pnpm cache restores are skipped; the native Windows job onto the `dsh-win-ci` pool. The switch is writer-manageable repository state, not a merge, so it works while every check is red. The in-house pools' readiness is continuously re-proven by the `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes, which run the complete unsharded aggregates on every master push. -`ci.yml` exempts exactly one event from `cancel-in-progress` (`${{ github.event_name != 'push' }}`), so a superseded pull-request run is still cancelled while a push run is left to run. Each drill runs its complete unsharded aggregate with one gate worker, which takes longer than the interval between master merges; under unconditional cancellation a drill is superseded before reaching a verdict and the lane yields no readiness evidence for a responder to check. This is not a guarantee that every push run finishes: GitHub keeps a single pending entry per group, so a newer pending run displaces an older one, and during busy periods intermediate push runs still end as `cancelled`. What the carve-out buys is that the lane periodically reaches a verdict at all, which is what makes it usable as evidence. +`ci.yml` exempts exactly one event from `cancel-in-progress` (`${{ github.event_name != 'push' }}`), so one master push does not cancel the drill still running from the previous one. Each drill runs its complete unsharded aggregate with one gate worker, which takes longer than the interval between master merges; under unconditional cancellation a drill is superseded before reaching a verdict and the lane yields no readiness evidence for a responder to check. + +The exemption is narrower than "a drill always finishes", in two ways. GitHub keeps a single pending entry per group, so a newer pending run displaces an older one and intermediate push runs still end as `cancelled` during busy periods. And the expression is evaluated against the *newly triggered* run, so a run whose own event is not `push` — a benchmark dispatched on master, sharing the group `CI-` — evaluates to `true` and does cancel a drill that is mid-flight. That is a rare manual action and the next master push restores the evidence, so it does not warrant further mechanism. What the carve-out buys is that the lane periodically reaches a verdict at all, which is what makes it usable as evidence. The decision belongs at workflow level because cancellation applies to the whole superseded run: a job-level `concurrency` group does not exempt its job. The negated form is load-bearing rather than cosmetic: naming `pull_request` alone would also stop cancelling `workflow_dispatch`, and each runner benchmark fans out to twelve larger runners for up to fifteen minutes inside this same group on master, so a re-dispatch would queue ahead of a drill instead of replacing a stale measurement. What bounds the cost is that a master push carries only `wine-apt-cache` and these two drills; every other job is pull-request-gated, `workflow_dispatch`-gated, or `if: false`, and `scripts/ci-workflow.spec.ts` pins that set — classifying by exact condition, since a negated event test mentions the event it excludes — so a new push-reachable job cannot quietly start accumulating uncancelled runs. diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md index 7bb1a025a7..5c8fd0e90a 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.zh.md @@ -12,7 +12,9 @@ Status: implemented 三个必需的 Linux 工作作业、独立的原生 Windows 作业,以及 `all checks passed` 判定作业(若不随切换,即使全部工作作业通过,它仍会滞留在故障池的队列中)——各自通过仓库变量 `DSH_CI_FAILOVER` 解析运行器池。变量不存在(正常)时它们运行在托管企业池上;由任何具备写权限的协作者设为 `selfhosted` 时,五个作业全部切换到公司自有的自托管池:Linux 作业与判定作业切到 `vm-backup` 池,覆盖率与快照的并发降到共享虚拟机上限,并跳过托管路径的 pnpm 缓存恢复;原生 Windows 作业切到 `dsh-win-ci` 池。这个开关是写者可管理的仓库状态而非一次合并,因此在所有检查都是红色时仍然有效。自有池的就绪状态由 `serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)` 通道持续验证——每次 master 推送都在其上运行完整的未分片聚合流程。 -`ci.yml` 只豁免一个事件不做取消(`${{ github.event_name != 'push' }}`),因此被取代的拉取请求运行仍会取消,而推送运行则留待执行。每次演练以单门禁工作进程执行完整的未分片聚合流程,耗时长于 master 合并的间隔;在无条件取消下,演练会在得出结论前被后续运行取代,该通道无法产出供响应者查看的就绪证据。这并不保证每次推送运行都能跑完:GitHub 每个组只保留一个待运行条目,更新的待运行条目会顶掉更早的,繁忙时段中间的推送运行仍会以 `cancelled` 结束。这项豁免换来的是该通道**周期性**地得出结论,而这正是它能作为证据的前提。 +`ci.yml` 只豁免一个事件不做取消(`${{ github.event_name != 'push' }}`),因此一次 master 推送不会取消上一次推送留下的、仍在运行的演练。每次演练以单门禁工作进程执行完整的未分片聚合流程,耗时长于 master 合并的间隔;在无条件取消下,演练会在得出结论前被后续运行取代,该通道无法产出供响应者查看的就绪证据。 + +这项豁免比「演练总能跑完」要窄,有两点限制。其一,GitHub 每个组只保留一个待运行条目,更新的待运行条目会顶掉更早的,繁忙时段中间的推送运行仍会以 `cancelled` 结束。其二,该表达式是针对**新触发的运行**求值的,因此自身事件不是 `push` 的运行——例如在 master 上派发的基准测试,与演练共用 `CI-` 组——求值为 `true`,会取消正在运行中的演练。这属于罕见的手动操作,且下一次 master 推送即可恢复证据,因此不值得为它再加机制。这项豁免换来的是该通道**周期性**地得出结论,而这正是它能作为证据的前提。 这个决定必须放在工作流级:取消作用于被取代的整个运行,作业级 `concurrency` 组并不能豁免其所属作业。采用否定式写法而非仅指名 `pull_request`,是有实质作用的:后者会连 `workflow_dispatch` 一起停止取消,而每次运行器基准测试会在 master 上的同一并发组内同时占用 12 台大规格运行器、最长 15 分钟,届时重复派发会排在演练之前,而不是替换掉已过时的测量。成本之所以可控,是因为一次 master 推送只承载 `wine-apt-cache` 和这两条演练;其余作业都受拉取请求门控、`workflow_dispatch` 门控或 `if: false`,并且 `scripts/ci-workflow.spec.ts` 会锁定这个集合——按条件精确匹配,因为否定式事件判断会包含它所排除的事件名——使新的推送可达作业无法悄悄开始累积未取消的运行。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7100da5b1..08dbcc44f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,9 +23,9 @@ on: # Negated rather than `== 'pull_request'` so workflow_dispatch keeps cancelling: # a re-dispatched runner benchmark holds up to 12 larger runners for 15 minutes # and shares this group with the drills on master, so queueing it would delay -# them. This does not promise every push run finishes (a newer pending run still -# displaces an older one) — only that the lanes periodically reach a verdict. -# Rationale and the bound on this carve-out: +# them. The guarantee is narrow — evaluated against the newly triggered run, so a +# dispatch on master still cancels a mid-flight drill, and a newer pending push +# displaces an older one. Bounds and rationale: # .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/packages/client/ui-settings-general/README.i18n.yaml b/packages/client/ui-settings-general/README.i18n.yaml index 961fb0de13..3a0fae8d41 100644 --- a/packages/client/ui-settings-general/README.i18n.yaml +++ b/packages/client/ui-settings-general/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-settings-general/README.md -README.md: d8578a7fbd451c1b7ec54dadeb3d391d597cc18e -README.zh.md: 246c04193e79f46f1e8035c6a40f55a20f1d0c26 +README.md: d02230d281482d03545a7dd9bb06fd5f1085d017 +README.zh.md: 9e2011902227c8d656f57813d4ecec92147d0f6f diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index 3749601e59..d21dbc4b49 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -84,7 +84,7 @@ describe('CI workflow', () => { expect(aggregate.needs).not.toContain('serial-windows') }) - it('leaves push runs uncancelled, so the self-hosted standby drills reach a verdict', () => { + it('exempts push from cancellation, so one master merge does not kill the running drill', () => { const workflow = loadWorkflow('.github/workflows/ci.yml') if (!isRecord(workflow.jobs) || !isRecord(workflow.concurrency)) { throw new TypeError('CI workflow must define jobs and a workflow-level concurrency block') @@ -96,11 +96,13 @@ describe('CI workflow', () => { // a drill takes longer than the interval between master merges. The negated // form is load-bearing: `== 'pull_request'` would also stop cancelling // workflow_dispatch, and a re-dispatched runner benchmark holds up to 12 - // larger runners for 15 minutes in this same group on master. + // larger runners for 15 minutes in this same group on master. The + // expression is evaluated against the NEWLY TRIGGERED run, so a dispatch on + // master still cancels a mid-flight drill; the runbook records that bound. expect(workflow.concurrency['cancel-in-progress']).toBe("${{ github.event_name != 'push' }}") - // Neither drill may re-introduce a job-level group: it would not help, and - // it would imply the run-scoped cancellation had been solved locally. + // Neither drill may carry a job-level group: it would not exempt the job + // from run-scoped cancellation. for (const name of ['serial-linux-selfhosted', 'serial-windows']) { const job = workflow.jobs[name] if (!isRecord(job)) throw new TypeError(`${name} must be defined`) @@ -109,9 +111,9 @@ describe('CI workflow', () => { expect(job.if).toBe("github.event_name == 'push' && github.ref == 'refs/heads/master'") } - // What bounds the cost of never cancelling a push run: a master push may - // only carry the cache seeder and the two drills. Any job reachable on push - // would start accumulating uncancelled runs, so the set is pinned here. + // What bounds the cost of exempting push: a master push may only carry the + // cache seeder and the two drills. Any job reachable on push would start + // accumulating uncancelled runs, so the set is pinned here. // // Classification is an exact allowlist of the conditions in use, not a // substring match: `github.event_name != 'pull_request'` mentions From fb0fae65a8be25d1e3dca2fac56e96bc76ea1fe0 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Wed, 12 Aug 2026 18:51:38 +0800 Subject: [PATCH 008/160] test(ci-workflow): name the cancellation assertion literally Replace 'kill' with 'cancel' in the test name to match the mechanism and the runbook wording. --- scripts/ci-workflow.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index d21dbc4b49..1073b35018 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -84,7 +84,7 @@ describe('CI workflow', () => { expect(aggregate.needs).not.toContain('serial-windows') }) - it('exempts push from cancellation, so one master merge does not kill the running drill', () => { + it('exempts push from cancellation, so one master merge does not cancel the running drill', () => { const workflow = loadWorkflow('.github/workflows/ci.yml') if (!isRecord(workflow.jobs) || !isRecord(workflow.concurrency)) { throw new TypeError('CI workflow must define jobs and a workflow-level concurrency block') From 5ed98d2da9526d231acf63f53b0b62e506f441ae Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Wed, 12 Aug 2026 03:54:02 -0700 Subject: [PATCH 009/160] fix(docs): restore static gate consistency --- .../feature/2026-08-10-telemetry-default-off.i18n.yaml | 4 ++-- .../implemented/feature/2026-08-10-telemetry-default-off.md | 2 +- .../feature/2026-08-10-telemetry-default-off.zh.md | 2 +- packages/client/ui-settings-general/README.i18n.yaml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml index 7c4995a88d..fabb554407 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md -2026-08-10-telemetry-default-off.md: 4bda346c2b05a94106eb5658c3ee558a4b32407f -2026-08-10-telemetry-default-off.zh.md: 706f2c18fbbf226e0357fa99bf3fd61c39fce08a +2026-08-10-telemetry-default-off.md: c3e5d9e0b65449f91044f16e7649f4a5ff2f5b61 +2026-08-10-telemetry-default-off.zh.md: 8e2544eb7dee8b9bd6b8a4c81a28a8a03d3404d4 diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md index 4bda346c2b..c3e5d9e0b6 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md @@ -12,7 +12,7 @@ DeepSeek Harness has two outbound telemetry feeds. During internal testing, the Both feeds use `DSH_TELEMETRY_MODE` as their positive consent setting. Unset and empty values resolve to `DISABLED`. `@deepseek-ai/dsh-session-telemetry-otel` also resolves an omitted `mode` to `DISABLED`, which constructs no OTel provider, processor, or exporter and leaves feedback in the local session log. The shared dsh base keeps the backend row mounted so disabled feedback can still explain that nothing was shared. A deployment opts into Session Log sharing through `FULL` or `FEEDBACK_ONLY`; only `FULL` also permits dsh-sdk launcher reporting. Any non-empty `DSH_TELEMETRY_DISABLED` remains an authoritative pre-load hard opt-out. The [default-mount decision](2026-07-31-web-telemetry-default-mount.md) continues to own the endpoint, batching cadence, and exit-drain settings. -The dsh-sdk launcher reads the same variable without parsing `cordis.yml` or booting Cordis. `FULL` permits reporting; `FEEDBACK_ONLY`, `DISABLED`, unset, and empty values deny it. Consent is frozen from the launching environment before the command runs, because `dsh-sdk start` loads a project `.env` and project code can mutate `process.env`: resolving afterwards would let a project grant reporting of its own configuration, which the [configuration source ownership decision](../architecture/2026-08-04-configuration-source-ownership.md) denies for the whole `DSH_*` namespace. An unsupported mode denies rather than throwing at that boundary, since telemetry may never change a command's result. This rule supersedes only the default-on launcher consent in the [SDK follow-up proposal](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md); its other capabilities remain proposed. +The dsh-sdk launcher reads the same variable without parsing `cordis.yml` or booting Cordis. `FULL` permits reporting; `FEEDBACK_ONLY`, `DISABLED`, unset, and empty values deny it. Consent is frozen from the launching environment before the command runs, because `dsh-sdk start` loads a project `.env` and project code can mutate `process.env`: resolving afterwards would let a project grant reporting of its own configuration, which the [configuration source ownership decision](../architecture/2026-08-04-configuration-source-ownership.md) denies for the whole `DSH_*` namespace. An unsupported mode denies rather than throwing at that boundary, since telemetry may never change a command's result. This rule superseded the default-on launcher consent before the launcher and its proposal were deleted by the [SDK project toolchain removal](../simplification/2026-08-11-remove-sdk-project-toolchain.md). The versioned Web welcome notice states that Session Log upload is off by default, names `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` and `DSH_TELEMETRY_MODE=FULL` as the two opt-in choices, and discloses that `FULL` also enables dsh-sdk command telemetry. Its version changes with that material privacy statement so every profile acknowledges the current copy. diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md index 706f2c18fb..8e2544eb7d 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md @@ -12,7 +12,7 @@ DeepSeek Harness 有两路出站遥测数据流。在内测阶段,共享基础 两路数据流都使用 `DSH_TELEMETRY_MODE` 作为正向授权配置。未设置和空值都解析为 `DISABLED`。`@deepseek-ai/dsh-session-telemetry-otel` 也将省略的 `mode` 解析为 `DISABLED`;该模式不构造 OTel 提供方、处理器或导出器,并将反馈留在本地会话日志中。dsh 共享基础配置继续挂载后端配置行,使禁用模式仍可在记录反馈时说明没有共享任何内容。部署方通过 `FULL` 或 `FEEDBACK_ONLY` 显式启用 Session Log 共享;只有 `FULL` 还允许 dsh-sdk 启动器上报。任何非空 `DSH_TELEMETRY_DISABLED` 仍是具有最高优先级的加载前硬性退出开关。[默认挂载决策](2026-07-31-web-telemetry-default-mount.md)继续负责 endpoint、批处理节奏和退出排空设置。 -dsh-sdk 启动器读取同一变量,不解析 `cordis.yml`,也不启动 Cordis。`FULL` 允许上报;`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝。授权在命令执行前从启动环境冻结:`dsh-sdk start` 会加载项目 `.env`,项目代码也能修改 `process.env`,若在执行后解析,项目便能自行授权上报其自身配置,而[配置来源所有权决策](../architecture/2026-08-04-configuration-source-ownership.md)对整个 `DSH_*` 命名空间禁止这种行为。在该边界上,不受支持的模式按拒绝处理而非抛出,因为遥测不得改变命令结果。此规则仅取代 [SDK 后续功能提案](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md)中启动器默认允许上报的规则;其余能力仍处于提案状态。 +dsh-sdk 启动器读取同一变量,不解析 `cordis.yml`,也不启动 Cordis。`FULL` 允许上报;`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝。授权在命令执行前从启动环境冻结:`dsh-sdk start` 会加载项目 `.env`,项目代码也能修改 `process.env`,若在执行后解析,项目便能自行授权上报其自身配置,而[配置来源所有权决策](../architecture/2026-08-04-configuration-source-ownership.md)对整个 `DSH_*` 命名空间禁止这种行为。在该边界上,不受支持的模式按拒绝处理而非抛出,因为遥测不得改变命令结果。此规则在启动器及其提案被[SDK 项目工具链移除决策](../simplification/2026-08-11-remove-sdk-project-toolchain.md)删除之前,仅取代了启动器默认允许上报的规则。 带版本的 Web 欢迎通知说明会话日志上传默认关闭,将 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 和 `DSH_TELEMETRY_MODE=FULL` 列为两种显式启用选项,并披露 `FULL` 同时会启用 dsh-sdk 命令遥测。其版本随这项重要的隐私声明一同变更,使每个 profile 都确认当前文案。 diff --git a/packages/client/ui-settings-general/README.i18n.yaml b/packages/client/ui-settings-general/README.i18n.yaml index 961fb0de13..3a0fae8d41 100644 --- a/packages/client/ui-settings-general/README.i18n.yaml +++ b/packages/client/ui-settings-general/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-settings-general/README.md -README.md: d8578a7fbd451c1b7ec54dadeb3d391d597cc18e -README.zh.md: 246c04193e79f46f1e8035c6a40f55a20f1d0c26 +README.md: d02230d281482d03545a7dd9bb06fd5f1085d017 +README.zh.md: 9e2011902227c8d656f57813d4ecec92147d0f6f From be693f3bc7143ec6338e20752cf1ccff3362e2c2 Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Wed, 12 Aug 2026 04:23:03 -0700 Subject: [PATCH 010/160] test(plugin-inventory): avoid assuming loader sibling order --- .../plugin-inventory/tests/inventory.spec.ts | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/host/plugin-inventory/tests/inventory.spec.ts b/packages/host/plugin-inventory/tests/inventory.spec.ts index e979d34306..6de2943197 100644 --- a/packages/host/plugin-inventory/tests/inventory.spec.ts +++ b/packages/host/plugin-inventory/tests/inventory.spec.ts @@ -52,28 +52,28 @@ describe('PluginInventoryService', () => { }) await ctx.loader.create({ name: 'cordis:active', group: true }) - expect(inventory.list()).toEqual({ - entries: [ - { - entryId: activeId, - moduleName: 'cordis:active', - enabled: true, - fiberPhase: 'active', - }, - { - entryId: pendingId, - moduleName: 'cordis:pending', - enabled: true, - fiberPhase: 'pending', - }, - { - entryId: disabledId, - moduleName: 'cordis:not-installed', - enabled: false, - fiberPhase: null, - }, - ], - }) + const snapshot = inventory.list() + expect(snapshot.entries).toHaveLength(3) + expect(snapshot.entries).toEqual(expect.arrayContaining([ + { + entryId: activeId, + moduleName: 'cordis:active', + enabled: true, + fiberPhase: 'active', + }, + { + entryId: pendingId, + moduleName: 'cordis:pending', + enabled: true, + fiberPhase: 'pending', + }, + { + entryId: disabledId, + moduleName: 'cordis:not-installed', + enabled: false, + fiberPhase: null, + }, + ])) await ctx.loader.update(activeId, { disabled: true }) expect(inventory.list().entries.find(entry => entry.entryId === activeId)).toEqual({ From dabe6207f55603cff4c7da2cee546d97b24357e0 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Wed, 12 Aug 2026 20:26:47 +0800 Subject: [PATCH 011/160] =?UTF-8?q?fix:=20=E5=88=86=E9=A1=B5=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...12-full-session-turn-step-counts.i18n.yaml | 6 + ...026-08-12-full-session-turn-step-counts.md | 38 +++ ...-08-12-full-session-turn-step-counts.zh.md | 38 +++ apps/web/tests/chat-scroll-contract.e2e.ts | 31 +- apps/web/tests/complex-history.perf.ts | 11 +- apps/web/tests/seeded-history.e2e.ts | 6 + .../live-interactions/error-auth.expected.md | 1 + .../stats-paged-history/ui.expected.md | 242 ++++++++++++++++ apps/web/tests/stats-paged-history.e2e.ts | 136 +++++++++ apps/web/tsconfig.json | 1 + docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 1 + docs/config-catalog.zh.md | 1 + packages/bundle/web-app/cordis.patch.yml | 5 + packages/bundle/web-app/package.json | 1 + .../client/connection/src/client/fixture.ts | 84 ++++++ .../connection/tests/fixture.client.spec.ts | 16 +- .../src/client/sessions/assistant-timing.ts | 23 +- .../client/ui-conversation/README.i18n.yaml | 4 +- packages/client/ui-conversation/README.md | 4 +- packages/client/ui-conversation/README.zh.md | 4 +- packages/client/ui-conversation/package.json | 2 + .../src/client/chat/StatsLine.tsx | 23 +- .../tests/chat-stats.client.spec.tsx | 68 ++++- .../tests/gate-branch-tails.client.spec.tsx | 8 +- packages/client/ui-conversation/tsconfig.json | 3 + packages/llm/llm/src/message.ts | 22 +- packages/session/README.i18n.yaml | 4 +- packages/session/README.md | 1 + packages/session/README.zh.md | 1 + .../session/session-stats/README.i18n.yaml | 6 + packages/session/session-stats/README.md | 39 +++ packages/session/session-stats/README.zh.md | 39 +++ packages/session/session-stats/package.json | 62 ++++ packages/session/session-stats/src/client.ts | 10 + packages/session/session-stats/src/index.ts | 29 ++ .../session/session-stats/src/invariant.ts | 35 +++ .../session/session-stats/src/projection.ts | 179 ++++++++++++ packages/session/session-stats/src/types.ts | 46 +++ .../tests/loader-composition.spec.ts | 86 ++++++ .../session-stats/tests/projection.spec.ts | 271 ++++++++++++++++++ packages/session/session-stats/tsconfig.json | 30 ++ pnpm-lock.yaml | 34 +++ .../verify-package-readme-model-experience.ts | 1 + tsconfig.base.json | 2 + tsconfig.host.json | 2 + 46 files changed, 1594 insertions(+), 66 deletions(-) create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.i18n.yaml create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.zh.md create mode 100644 apps/web/tests/snapshots/stats-paged-history/ui.expected.md create mode 100644 apps/web/tests/stats-paged-history.e2e.ts create mode 100644 packages/session/session-stats/README.i18n.yaml create mode 100644 packages/session/session-stats/README.md create mode 100644 packages/session/session-stats/README.zh.md create mode 100644 packages/session/session-stats/package.json create mode 100644 packages/session/session-stats/src/client.ts create mode 100644 packages/session/session-stats/src/index.ts create mode 100644 packages/session/session-stats/src/invariant.ts create mode 100644 packages/session/session-stats/src/projection.ts create mode 100644 packages/session/session-stats/src/types.ts create mode 100644 packages/session/session-stats/tests/loader-composition.spec.ts create mode 100644 packages/session/session-stats/tests/projection.spec.ts create mode 100644 packages/session/session-stats/tsconfig.json diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.i18n.yaml new file mode 100644 index 0000000000..f50df929a7 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.i18n.yaml @@ -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/bug-fix/2026-08-12-full-session-turn-step-counts.md +2026-08-12-full-session-turn-step-counts.md: 7cea57e429d3ffc2da49c4a1359ee3489663ccc5 +2026-08-12-full-session-turn-step-counts.zh.md: 93e46cfb7ffa4f397b85ee80b26cca9f664d246a diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md new file mode 100644 index 0000000000..7cea57e429 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md @@ -0,0 +1,38 @@ +# Agent Note: Full-session stats-strip figures through a sessionStats projection + +Status: implemented + +English | [中文](2026-08-12-full-session-turn-step-counts.zh.md) + +## Problem + +The web chat stats strip folded `StatsLine`'s loaded conversation window (`deriveStats` over `chat.legacy.nodes`) for every non-token figure: the "N turns · M steps" counter, the LLM and tool wall times, and the TTFT/throughput averages. History is paged 50 messages at a time, so each 加载更早 (Load earlier) click grew the window and every figure with it — 7 turns · 44 steps became 10 turns · 89 steps after one page, and the LLM duration climbed the same way. The product expectation is whole-session figures independent of how much history a client has loaded. Token accounting in the same strip already had the correct architecture: the durable `tokenUsage` projection. + +## Decision + +A new function plugin `@deepseek-ai/dsh-session-stats` registers a `sessionStats` projection unit on `ctx.sessionProjections`, mounted as a web-app bundle row. The value carries the strip's whole non-token figure set — `{ turns, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens }`, field names mirroring the window fold so the two swap wholesale. `steps` counts `step/end` events and `turns` counts distinct turns carrying at least one (turn numbers are monotonic, so one `lastTurn` slot suffices); `llmMs` sums `step/start` → `assistant/message`; TTFT records the first non-empty delta chunk per step (surviving in-step `llm/retry`, the window `resetForRetry` parity); decode spans first token → assembled message on usage-reporting steps; `toolMs` pairs `tool/call` → `tool/result` by callId with unresolved calls dropped at `turn/end`. The first-token predicate `isTokenDelta` moved to `@deepseek-ai/dsh-llm/message` (the `StreamChunk` vocabulary owner) so the host fold and the client timing index share one implementation; client-runtime re-exports it. Delivery is entirely the existing projection seam — history tail-page block, `session/projection` push frames, list rows — with zero changes to apiproxy, wire schemas, or the client runtime. `StatsLine` reads `useProjection('sessionStats')` and falls back to the window fold when the key is undefined (an assembly without the unit). The client connection fixture mirrors the fold as `sessionStatsOf` under its existing every-composed-key discipline. + +`step/end` — not `assistant/message` — is the counted event, for two correctness reasons found while reviewing the obvious message-counting design: + +1. A max-tokens step appends an empty-content `assistant/message` that exists only to host usage and never reaches the surface; counting messages would count a step the transcript does not show. +2. A cancelled step aborts before its message assembles (no `assistant/message` at all), yet the client synthesizes a visible interrupted assistant node; counting messages would silently drop common cancelled steps. + +`step/end` is appended exactly once per entered step, in the loop's `finally`, so completed, failed, cancelled, and max-tokens steps all land one — and the counter advances at step settlement, the same moment the window fold advanced, so live behavior does not shift. + +## Alternatives considered + +**Count `assistant/message` events.** Rejected for the two correctness defects above (overcounts usage-host messages, undercounts cancelled steps). + +**Count `step/start` events.** Equivalent coverage (it precedes every `step/end`), but the counter would advance when a step begins instead of when it settles — a visible live-behavior change with no benefit; `step/end`'s `finally` placement gives the same completeness. + +**Register the unit in `core/agent-loop` (the event producer).** The loop is the product spine; a UI read model there adds a session-projection dependency to every assembly, against "plugins, not loop changes" and "keep opt-ins out of shipped defaults". + +**Register the unit in `token-meter` (an existing fold over the same events).** Turn/step counting is not token measurement; every projection key lives in the package owning its domain. + +**Fold the full log client-side.** The client holds only the paged window by design; the projection RFC's no-client-folding rule exists exactly so figures survive paging, compaction, and cold reads. + +**Keep wall times, TTFT, and throughput window-scoped.** The first shipped cut did, reading them as "what is on screen"; the same paging complaint immediately applied to the LLM duration, and a strip mixing whole-log counts with window-scoped times reads as one inconsistent figure set. The projection now carries the whole set, with the window fold demoted to the no-unit fallback. + +## Consequences + +The strip shows whole-log figures from the first tail page; paging leaves every group fixed. Defined edge differences from the old window semantics are documented in the package README: a step that produced no visible output (failed before content) still counts, a step truncated by a crash between `step/start` and `step/end` does not, a cancelled step is counted but contributes no wall time (no message assembled), and a max-tokens usage-host message contributes model time the surface does not show. Every web tail page and list row carries one more small key, and the unit's internal state changes on step boundaries and first-token chunks, so the change feed emits a few value-identical frames per step; TUI and headless assemblies serve no `sessionStats` key and any consumer falls back to window folding. Two e2e probes that had parsed the strip as a loaded-window measure (`chat-scroll-contract`, `complex-history.perf`) now count mounted flow rows / turn-tail footers instead. The `stats-paged-history` web scenario seeds a 28-turn log cold and pins that the whole strip reads full totals on a partial tail page and does not move across Load earlier. diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.zh.md b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.zh.md new file mode 100644 index 0000000000..93e46cfb7f --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.zh.md @@ -0,0 +1,38 @@ +# Agent Note: 通过 sessionStats 投影提供全会话统计条数字 + +Status: implemented + +[English](2026-08-12-full-session-turn-step-counts.md) | 中文 + +## 问题 + +Web 聊天统计条的每个非 token 数字都折算自 `StatsLine` 已加载的会话窗口(`deriveStats` 遍历 `chat.legacy.nodes`):「N 轮 · M 步」计数、LLM 与工具墙钟时间、TTFT/吞吐平均值。历史按每页 50 条消息分页,因此每点一次「加载更早」窗口变大、所有数字随之增长——7 轮 · 44 步在翻一页后变成 10 轮 · 89 步,LLM 时长同样攀升。产品预期是与客户端加载了多少历史无关的全会话数字。同一统计条里的 token 账目早已采用正确架构:持久的 `tokenUsage` 投影。 + +## 决定 + +新的函数插件 `@deepseek-ai/dsh-session-stats` 在 `ctx.sessionProjections` 上注册 `sessionStats` 投影单元,作为 web-app bundle 行挂载。值携带统计条完整的非 token 数字集——`{ turns, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens }`,字段名与窗口折叠一一对应以便整体互换。`steps` 统计 `step/end` 事件,`turns` 统计含至少一条该事件的不同 turn(turn 号单调递增,一个 `lastTurn` 槽即可);`llmMs` 累加 `step/start` → `assistant/message`;TTFT 记录每步首个非空 delta chunk(在步内 `llm/retry` 后保留,与窗口 `resetForRetry` 对齐);解码时长覆盖首 token → 已组装消息、仅统计上报 usage 的步;`toolMs` 按 callId 配对 `tool/call` → `tool/result`,未解决的调用在 `turn/end` 时丢弃。首 token 谓词 `isTokenDelta` 移入 `@deepseek-ai/dsh-llm/message`(`StreamChunk` 词汇的属主),Host 折叠与客户端计时索引共用同一实现;client-runtime 转发导出。投递完全复用现有投影缝——history 尾页块、`session/projection` 推送帧、列表行——apiproxy、wire schema 与客户端运行时零改动。`StatsLine` 读取 `useProjection('sessionStats')`,键为 undefined(未组合该单元的装配)时整体回退到窗口折叠。客户端 connection fixture 按其「镜像每个已组合键」的既有纪律以 `sessionStatsOf` 平行实现该折叠。 + +计数事件选 `step/end` 而非 `assistant/message`,源于评审直觉方案(按消息计数)时发现的两个正确性问题: + +1. max-tokens 步会追加一条仅为承载 usage 而存在的空内容 `assistant/message`,它从不进入 surface;按消息计数会把 transcript 上看不到的步计进去。 +2. 被取消的步在消息组装前就中止(完全没有 `assistant/message`),但客户端会合成可见的 interrupted assistant 节点;按消息计数会悄悄丢掉常见的取消步。 + +`step/end` 对每个进入的步在循环的 `finally` 中恰好追加一次,因此完成、失败、取消、max-tokens 的步都恰好落一条——且计数在步结算时推进,与窗口折算推进的时机相同,直播期行为不发生变化。 + +## 备选方案 + +**统计 `assistant/message` 事件。** 因上述两个正确性缺陷否决(多计 usage 宿主消息、少计被取消的步)。 + +**统计 `step/start` 事件。** 覆盖等价(它先于每条 `step/end`),但计数会在步开始而非结算时推进——一个没有收益的可见直播期行为变化;`step/end` 的 `finally` 位置给出同等完整性。 + +**把单元注册进 `core/agent-loop`(事件生产方)。** 循环是产品主干;把 UI 读模型放进去会给每个装配加上 session-projection 依赖,违反「用插件而非改循环」与「默认组合不带可选项」。 + +**把单元注册进 `token-meter`(折叠同批事件的现有单元)。** 轮/步计数不是 token 度量;每个投影键都住在拥有其领域的包里。 + +**在客户端折叠全量日志。** 客户端按设计只持有分页窗口;投影 RFC 的「不在客户端折叠」规则正是为了让数字在分页、压缩与冷读之间存活。 + +**墙钟时间、TTFT 与吞吐保持窗口口径。** 首个交付版本如此,将其解读为「屏幕上有什么」;同样的分页问题立刻落在 LLM 时长上,且全量计数与窗口时间混在一条统计条里读起来是一套自相矛盾的数字。投影现在携带完整集合,窗口折叠降级为无单元时的回退。 + +## 后果 + +统计条从第一个尾页起就显示全日志数字;翻页不再改变任何分组。与旧窗口语义的已定义边缘差异记录在包 README 中:未产生可见输出的步(在内容之前失败)仍计入;崩溃恰好截断在 `step/start` 与 `step/end` 之间的步不计;被取消的步计数但不计时(没有组装出消息);max-tokens 的 usage 宿主消息贡献 surface 上看不到的模型时间。每个 web 尾页与列表行多携带一个小键,且单元内部状态在步边界与首 token chunk 处变化,变更流每步会多发几帧值相同的推送;TUI 与 headless 装配不提供 `sessionStats` 键,其消费者回退窗口折叠。两个曾把统计条当作已加载窗口探针解析的 e2e(`chat-scroll-contract`、`complex-history.perf`)改为统计已挂载的消息流行/turn-tail 页脚。`stats-paged-history` web 场景冷种一份 28 轮日志,钉住整条统计条在不完整尾页上即读出全量数字、且「加载更早」前后不变。 diff --git a/apps/web/tests/chat-scroll-contract.e2e.ts b/apps/web/tests/chat-scroll-contract.e2e.ts index 5274759f47..6ce469a9c0 100644 --- a/apps/web/tests/chat-scroll-contract.e2e.ts +++ b/apps/web/tests/chat-scroll-contract.e2e.ts @@ -250,13 +250,16 @@ function scrollGeometry(page: Page): Promise { })) } -async function conversationTurns(page: Page): Promise { - const stats = page.getByText(/\d+ turns · \d+ steps/, { exact: true }).last() - await stats.waitFor({ timeout: 15_000 }) - const value = await stats.textContent() - const match = value?.match(/^(\d+) turns · \d+ steps$/) - if (match?.[1] === undefined) throw new Error(`unexpected conversation stats ${JSON.stringify(value)}`) - return Number(match[1]) +/** + * Rendered transcript rows in the loaded window. The stats strip cannot serve + * as this probe: its turn/step counts ride the whole-log sessionStats + * projection and stay fixed across paging by design, while the row count is + * exactly what grows when an older page prepends or a live turn streams in. + * @param page - the scenario page. + * @returns the number of mounted chat flow rows. + */ +async function loadedFlowRows(page: Page): Promise { + return page.locator('[data-chat-flow-key]').count() } async function openSeed(page: Page, fixture: ChatScrollFixture, tailMarker?: string): Promise { @@ -425,9 +428,9 @@ async function loadEarlierWithAnchor(page: Page): Promise { const older = page.getByRole('button', { name: 'Load earlier', exact: true }) await older.waitFor({ timeout: 10_000 }) const anchor = await visibleFlowAnchor(page) - const before = await conversationTurns(page) + const before = await loadedFlowRows(page) await older.click() - await expect.poll(() => conversationTurns(page), { timeout: 30_000 }).toBeGreaterThan(before) + await expect.poll(() => loadedFlowRows(page), { timeout: 30_000 }).toBeGreaterThan(before) await nextPaint(page) await expectSameFlowTop(page, anchor) } @@ -498,7 +501,7 @@ describe('web e2e: long Chat scroll contract', () => { await world.page.getByRole('button', { name: 'Send message', exact: true }).click() await world.page.getByText(LIVE_TEXT_FIRST, { exact: false }).last().waitFor({ timeout: 15_000 }) await wheelToHistoryStart(world.page) - const beforeTurns = await conversationTurns(world.page) + const beforeRows = await loadedFlowRows(world.page) await world.page.getByRole('button', { name: 'Load earlier', exact: true }).click() await expect.poll(() => held, { timeout: 10_000 }).toBe(true) @@ -511,7 +514,7 @@ describe('web e2e: long Chat scroll contract', () => { ).toBeGreaterThan(chunksAfterAnchor + 5) releaseHistory() - await expect.poll(() => conversationTurns(world.page), { timeout: 30_000 }).toBeGreaterThan(beforeTurns) + await expect.poll(() => loadedFlowRows(world.page), { timeout: 30_000 }).toBeGreaterThan(beforeRows) await nextPaint(world.page) await expectSameFlowTop(world.page, readerAnchor) } finally { @@ -531,7 +534,11 @@ describe('web e2e: long Chat scroll contract', () => { additionalPages += 1 } expect(additionalPages).toBeGreaterThan(0) - expect(await conversationTurns(world.page)).toBe(HISTORY_FIXTURE.turns + 1) + // The whole log is loaded: turn 1's unique marker renders in the + // transcript (scoped: the sidebar search row also carries it) and no + // page remains. + expect(await world.page.locator('[data-conversation-scroll]') + .getByText(HISTORY_FIXTURE.markers.user(1), { exact: false }).count()).toBe(1) expect(await world.page.getByRole('button', { name: 'Load earlier', exact: true }).count()).toBe(0) assertClean(world) }) diff --git a/apps/web/tests/complex-history.perf.ts b/apps/web/tests/complex-history.perf.ts index d3c27f0ce0..eeb9931473 100644 --- a/apps/web/tests/complex-history.perf.ts +++ b/apps/web/tests/complex-history.perf.ts @@ -797,12 +797,11 @@ async function stableCount( } async function conversationTurns(page: Page): Promise { - const stats = page.getByText(/\d+ turns · \d+ steps/, { exact: true }).last() - await stats.waitFor({ timeout: 15_000 }) - const value = await stats.textContent() - const match = value?.match(/^(\d+) turns · \d+ steps$/) - if (match?.[1] === undefined) throw new Error(`unexpected conversation stats ${JSON.stringify(value)}`) - return Number(match[1]) + // Loaded-window turn count: one mounted turn-tail footer per settled turn in + // the window (context keys are `${kind.length}:${kind}${id}`). The stats + // strip cannot serve as this probe: its counts ride the whole-log + // sessionStats projection and stay fixed across paging by design. + return stableCount(page.locator('[data-chat-flow-key^="9:turn-tail"]'), count => count > 0) } function retainedDelta( diff --git a/apps/web/tests/seeded-history.e2e.ts b/apps/web/tests/seeded-history.e2e.ts index 9ec978fb97..a385059cf6 100644 --- a/apps/web/tests/seeded-history.e2e.ts +++ b/apps/web/tests/seeded-history.e2e.ts @@ -256,6 +256,12 @@ describe('web e2e: seeded history renders through cold resume', () => { // client's "omitted key = capability absent → clear the row" rule from // wiping preset-owned projections on cold reads. expect(projections?.values).toHaveProperty('todos', null) + // The session-stats unit is a shipped web-app bundle row: whole-log + // turn/step counts ride the same tail block (the stats strip's source). + const sessionStats = projections?.values.sessionStats as { turns: number; steps: number } | undefined + expect(sessionStats).toBeDefined() + expect(sessionStats?.turns).toBeGreaterThanOrEqual(1) + expect(sessionStats?.steps).toBeGreaterThanOrEqual(sessionStats?.turns ?? 0) }) it.skipIf(MODE === 'record')('lists the seeded session cold and renders its history from the log', async () => { diff --git a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md index 83be4dc961..d8ef99349f 100644 --- a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md +++ b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md @@ -24,3 +24,4 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] +- text: 1 turns · 1 steps Input 0 tok · Output 0 tok diff --git a/apps/web/tests/snapshots/stats-paged-history/ui.expected.md b/apps/web/tests/snapshots/stats-paged-history/ui.expected.md new file mode 100644 index 0000000000..35c74aadd7 --- /dev/null +++ b/apps/web/tests/snapshots/stats-paged-history/ui.expected.md @@ -0,0 +1,242 @@ +- banner: + - navigation "Session hierarchy": + - button "{{workspace}}" [disabled] + - tablist: + - tab "Chat" [selected] + - tab "Trajectory" +- text: m1 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r1 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m2 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r2 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m3 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r3 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m4 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r4 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m5 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r5 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m6 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r6 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m7 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r7 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m8 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r8 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m9 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r9 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m10 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r10 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m11 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r11 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m12 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r12 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m13 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r13 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m14 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r14 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m15 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r15 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m16 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r16 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m17 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r17 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m18 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r18 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m19 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r19 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m20 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r20 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m21 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r21 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m22 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r22 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m23 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r23 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m24 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r24 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m25 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r25 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m26 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r26 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m27 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r27 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} m28 7/25 {{clock}} +- button "Copy": + - img +- paragraph: r28 +- button "Copy": + - img +- button "Branch into a new conversation": + - img +- text: 7/25 {{clock}} Ran for {{duration}} +- button "Back to bottom": + - img +- textbox "Message the agent" +- button "Commands": + - img +- 'button "Access mode, current: Workspace Write"': Workspace Write +- button "Select model, current DeepSeek-V4-Flash": + - text: DeepSeek-V4-Flash + - img +- button "Send message" [disabled] +- text: 28 turns · 28 steps LLM {{duration}} Input 0 tok · Output 0 tok diff --git a/apps/web/tests/stats-paged-history.e2e.ts b/apps/web/tests/stats-paged-history.e2e.ts new file mode 100644 index 0000000000..e7ce2cdd06 --- /dev/null +++ b/apps/web/tests/stats-paged-history.e2e.ts @@ -0,0 +1,136 @@ +// Web e2e scenario: full-session stats over paged history. A deterministic +// 28-turn log (56 surface messages — more than one 50-message history page) +// seeded cold through the REAL persistence API must render whole-log turn/step +// counts from the sessionStats projection on first open, and loading the +// older page must NOT change them. This pins the bug the projection fixed: +// the pre-projection window fold recounted per loaded page, so 加载更早 grew +// the counter. Zero model calls; the seed is generated, not recorded, because +// no line of it is model output. +import { fileURLToPath } from 'node:url' +import type { Browser, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { + assertFixtureInventory, captureStableAria, compareOrRefreshGolden, + launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold, +} from './scaffold.ts' +import { newEnglishPage, saveFailureShot } from './support.ts' + +const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/stats-paged-history', import.meta.url)) +const UI_EXPECTED = fileURLToPath(new URL('./snapshots/stats-paged-history/ui.expected.md', import.meta.url)) +const MODE = webSnapshotMode() +const SEED_ID = 'stats-paged-history-web-e2e' + +/** Turn count: 2 surface messages per turn, so 28 turns overflow one 50-message page. */ +const TURNS = 28 +const FULL_COUNTS = `${TURNS} turns · ${TURNS} steps` + +/** + * Generate the seed: TURNS closed single-step turns of one short user prompt + * and one short assistant reply each. Times are fixed so the fixture is + * byte-deterministic; message ids are synthetic uuids (aria normalizes them). + * @param turns - closed turns to generate. + * @returns session.jsonl text for {@link seedSession}. + */ +function buildSeed(turns: number): string { + const lines = [JSON.stringify({ + type: 'session', version: 0, id: '{{sessionId}}', createdAt: 1784974100000, cwd: '{{cwd}}/workspace', + })] + let seq = 0 + let time = 1784974100000 + const at = (event: Record): void => { + lines.push(JSON.stringify({ ...event, seq: seq++, time: time++ })) + } + for (let turn = 1; turn <= turns; turn++) { + at({ type: 'turn/start', data: { turn } }) + at({ + type: 'user/message', + data: { content: [{ type: 'text', text: `m${turn}` }], source: { kind: 'user' } }, + surfaceOp: 'append', + }) + at({ type: 'step/start', data: { turn, step: 1 } }) + at({ + type: 'assistant/message', + data: { + turn, + step: 1, + message: { + id: `00000000-0000-4000-8000-${String(turn).padStart(12, '0')}`, + role: 'assistant', + content: [{ type: 'text', text: `r${turn}` }], + source: { kind: 'model', provider: 'snapshot', model: 'snapshot-replier' }, + }, + }, + sourceEventSeqs: [], + surfaceOp: 'append', + }) + at({ type: 'step/end', data: { turn, step: 1 } }) + at({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } }) + } + return `${lines.join('\n')}\n` +} + +describe('web e2e: whole-session stats survive history paging', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + if (MODE === 'record') throw new Error('stats-paged-history is a keyless assembled snapshot') + scaffold = await launchWebScaffold({}) + await seedSession(scaffold, buildSeed(TURNS), SEED_ID) + browser = await chromium.launch() + page = await newEnglishPage(browser) + tripwire = watchConsole(page) + await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + }, 120_000) + + afterAll(async () => { + await browser?.close() + await scaffold?.close() + }) + + it('renders full-session counts on the partial tail page and keeps them across load-older', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-stats-paged')) + const groupRow = page.locator('[role="treeitem"]').first() + await groupRow.waitFor({ timeout: 15_000 }) + await groupRow.click() + const sessionRow = page.locator('[role="treeitem"]').nth(1) + await sessionRow.waitFor({ timeout: 10_000 }) + await sessionRow.click() + // Settled barrier: the newest recorded reply renders from the tail page. + await expect.poll(() => page.getByText(`r${TURNS}`, { exact: true }).count(), { timeout: 15_000 }).toBe(1) + // The tail page is partial (56 messages > one 50-message page): the first + // turns are NOT loaded, yet the strip already reports the whole log — + // the sessionStats projection, not the window fold. + expect(await page.getByText('m1', { exact: true }).count()).toBe(0) + await expect.poll(() => page.getByText(FULL_COUNTS, { exact: false }).count(), { timeout: 10_000 }).toBe(1) + const strip = page.getByText(FULL_COUNTS, { exact: false }).locator('..') + const stripBeforePaging = await strip.textContent() + + // 加载更早: prepending the older page must not move ANY strip figure — + // counts, wall times, or token groups. + await page.getByRole('button', { name: 'Load earlier' }).click() + await expect.poll(() => page.getByText('m1', { exact: true }).count(), { timeout: 10_000 }).toBe(1) + expect(await strip.textContent()).toBe(stripBeforePaging) + // With the whole log loaded, the window mounts one turn-tail footer per + // settled turn — the loaded-window probe the scroll/perf lanes count now + // that the strip is whole-log-scoped. + expect(await page.locator('[data-chat-flow-key^="9:turn-tail"]').count()).toBe(TURNS) + }, 60_000) + + it('matches the paged-stats aria golden', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-stats-paged-aria')) + const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)) + .split(SEED_ID).join('{{seededId}}') + await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE) + }) + + it('issued zero model calls and stayed clean', async () => { + expect(tripwire.pageErrors).toEqual([]) + expect(tripwire.warnings).toEqual([]) + await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md']) + }) +}) diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index e656b099ea..e7772882b1 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -48,6 +48,7 @@ "tests/replay-round-trip.e2e.ts", "tests/hmr-live.e2e.ts", "tests/seeded-history.e2e.ts", + "tests/stats-paged-history.e2e.ts", "tests/sidebar-scrollbar.e2e.ts", "tests/conversation-column-overflow.e2e.ts", "tests/code-mode-round.e2e.ts", diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 7b380a0698..389707aed7 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: 2bb1f315e02c3f4bab379593227f1c381818a9fa -config-catalog.zh.md: 4be51942de65a7e8afdaf71a44573ba1cdd6230d +config-catalog.md: f4feae3a31d9aa1798e0b06c2b72f43278a01281 +config-catalog.zh.md: a0068a6c0372022d05eae1a49ec6cd89f2625261 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 2bb1f315e0..f4feae3a31 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2798,6 +2798,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-session` ([`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts)) - `@deepseek-ai/dsh-session-checkpoint-policy` — requires `llm` · `sessionPersistence` · `sessions` · `tools` ([`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts)) - `@deepseek-ai/dsh-session-projection` ([`packages/session/session-projection/src/index.ts`](../packages/session/session-projection/src/index.ts)) +- `@deepseek-ai/dsh-session-stats` — requires `sessionProjections` ([`packages/session/session-stats/src/index.ts`](../packages/session/session-stats/src/index.ts)) - `@deepseek-ai/dsh-skill-badge` — requires `skills` ([`packages/skill/skill-badge/src/index.ts`](../packages/skill/skill-badge/src/index.ts)) - `@deepseek-ai/dsh-storage` ([`packages/storage/storage/src/index.ts`](../packages/storage/storage/src/index.ts)) - `@deepseek-ai/dsh-subagent` ([`packages/subagent/subagent/src/index.ts`](../packages/subagent/subagent/src/index.ts)) diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 4be51942de..a0068a6c03 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -2799,6 +2799,7 @@ export interface Config { - `@deepseek-ai/dsh-session`([`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts)) - `@deepseek-ai/dsh-session-checkpoint-policy` — 需要 `llm` · `sessionPersistence` · `sessions` · `tools`([`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts)) - `@deepseek-ai/dsh-session-projection`([`packages/session/session-projection/src/index.ts`](../packages/session/session-projection/src/index.ts)) +- `@deepseek-ai/dsh-session-stats` — 需要 `sessionProjections`([`packages/session/session-stats/src/index.ts`](../packages/session/session-stats/src/index.ts)) - `@deepseek-ai/dsh-skill-badge` — 需要 `skills`([`packages/skill/skill-badge/src/index.ts`](../packages/skill/skill-badge/src/index.ts)) - `@deepseek-ai/dsh-storage`([`packages/storage/storage/src/index.ts`](../packages/storage/storage/src/index.ts)) - `@deepseek-ai/dsh-subagent`([`packages/subagent/subagent/src/index.ts`](../packages/subagent/subagent/src/index.ts)) diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index c696b41503..144d6ec299 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -74,6 +74,11 @@ writeEveryEvents: 200 writeIntervalMs: 5000 + # Whole-log turn/step counts for the chat stats strip (the sessionStats + # projection key); the projection registry itself is a base-layer row. + - id: session-stats + name: '@deepseek-ai/dsh-session-stats' + # Resolve bind host, SSH launch, and display once at boot, then mount the # matching dual-face directory picker. Mount -native or -browse directly in # an overlay to pin the interaction. diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index 3375fed71a..fece731d89 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -89,6 +89,7 @@ "@deepseek-ai/dsh-host-webserver": "workspace:^", "@deepseek-ai/dsh-message-feedback": "workspace:^", "@deepseek-ai/dsh-session-projection-cache": "workspace:^", + "@deepseek-ai/dsh-session-stats": "workspace:^", "@deepseek-ai/dsh-storage": "workspace:^", "@deepseek-ai/dsh-storage-domain": "workspace:^", "@deepseek-ai/dsh-storage-json": "workspace:^", diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index 8085c7d323..4103fc45f2 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -9,6 +9,7 @@ import { createAssistantMessage, createToolResultMessage, createUserMessage, + isTokenDelta, } from '@deepseek-ai/dsh-llm/message' import { CallId } from '@deepseek-ai/dsh-llm/brand' import type { @@ -861,6 +862,76 @@ function tokenUsageOf(log: readonly SessionEvent[]): FixtureTokenUsageProjection return totals } +/** Fixture parallel of session-stats' whole-log counting and wall-time fold. */ +function sessionStatsOf(log: readonly SessionEvent[]): { + turns: number + steps: number + llmMs: number + toolMs: number + ttftMs: number + ttftSteps: number + decodeMs: number + decodeTokens: number +} { + const value = { turns: 0, steps: 0, llmMs: 0, toolMs: 0, ttftMs: 0, ttftSteps: 0, decodeMs: 0, decodeTokens: 0 } + let lastTurn: number | null = null + let openStep: { turn: number; step: number; startTime: number; firstTokenTime: number | null } | null = null + const pendingCalls = new Map() + for (const event of log) { + switch (event.type) { + case 'step/start': + openStep = { turn: event.data.turn, step: event.data.step, startTime: event.time, firstTokenTime: null } + break + case 'assistant/chunk': + if (openStep !== null && openStep.turn === event.data.turn && openStep.step === event.data.step + && openStep.firstTokenTime === null && isTokenDelta(event.data.chunk)) { + openStep.firstTokenTime = event.time + } + break + case 'assistant/message': { + if (openStep === null || openStep.turn !== event.data.turn || openStep.step !== event.data.step) break + value.llmMs += Math.max(0, event.time - openStep.startTime) + if (openStep.firstTokenTime !== null) { + value.ttftMs += Math.max(0, openStep.firstTokenTime - openStep.startTime) + value.ttftSteps += 1 + const outputTokens = event.data.usage?.outputTokens + if (typeof outputTokens === 'number' && Number.isFinite(outputTokens) && outputTokens >= 0) { + value.decodeMs += Math.max(0, event.time - openStep.firstTokenTime) + value.decodeTokens += outputTokens + } + } + openStep = null + break + } + case 'tool/call': + pendingCalls.set(event.data.callId, event.time) + break + case 'tool/result': { + const callId = event.data.message.source.callId + const dispatched = pendingCalls.get(callId) + if (dispatched === undefined) break + pendingCalls.delete(callId) + value.toolMs += Math.max(0, event.time - dispatched) + break + } + case 'step/end': + if (event.data.turn !== lastTurn) { + value.turns += 1 + lastTurn = event.data.turn + } + value.steps += 1 + openStep = null + break + case 'turn/end': + pendingCalls.clear() + break + default: + break + } + } + return value +} + interface FixtureRequestContext { provider: string model: string @@ -979,6 +1050,8 @@ function projectionValuesOf(log: readonly SessionEvent[]): Record 0) return frames if (type === 'session/title') { const values = projectionValuesOf(log) diff --git a/packages/client/connection/tests/fixture.client.spec.ts b/packages/client/connection/tests/fixture.client.spec.ts index 109e7acd93..1f842383d8 100644 --- a/packages/client/connection/tests/fixture.client.spec.ts +++ b/packages/client/connection/tests/fixture.client.spec.ts @@ -164,6 +164,10 @@ describe('createFixtureApi', () => { toolsTokens: 0, messageTokens: 0, }, + // Session-stats unit composed: no figure accrues on the empty log. + sessionStats: { + turns: 0, steps: 0, llmMs: 0, toolMs: 0, ttftMs: 0, ttftSteps: 0, decodeMs: 0, decodeTokens: 0, + }, } }, }) }) @@ -353,7 +357,7 @@ describe('createFixtureApi', () => { const envelopes: RpcRequest[] = [] for await (const envelope of api.events.mux(req({}), abort.signal)) { envelopes.push(envelope) - if (envelopes.length >= 11) abort.abort() + if (envelopes.length >= 12) abort.abort() } return envelopes } @@ -374,10 +378,12 @@ describe('createFixtureApi', () => { value: { systemTokens: 0, toolsTokens: 0 }, }) expect((first[8]?.payload as { value: { messageTokens: number } }).value.messageTokens).toBeGreaterThan(0) - expect(first[9]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' }) - expect(second[9]?.rpcId).toBe(first[9]?.rpcId) // stable rpcId across replays (host replay semantics) - expect(first[10]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' }) - expect(second[10]?.rpcId).toBe(first[10]?.rpcId) + expect(first[9]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'sessionStats' }) + expect((first[9]?.payload as { value: { turns: number; steps: number } }).value.steps).toBeGreaterThan(0) + expect(first[10]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' }) + expect(second[10]?.rpcId).toBe(first[10]?.rpcId) // stable rpcId across replays (host replay semantics) + expect(first[11]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' }) + expect(second[11]?.rpcId).toBe(first[11]?.rpcId) }) it('steer with no replay in flight falls through to a fresh queued turn; non-text blocks stringify empty', async () => { diff --git a/packages/client/runtime/src/client/sessions/assistant-timing.ts b/packages/client/runtime/src/client/sessions/assistant-timing.ts index 3c54c9c36d..8f58f8daab 100644 --- a/packages/client/runtime/src/client/sessions/assistant-timing.ts +++ b/packages/client/runtime/src/client/sessions/assistant-timing.ts @@ -2,9 +2,14 @@ // history fold derive AssistantTiming from the same step/start -> first token // delta -> assistant/message sequence. +import { isTokenDelta } from '@deepseek-ai/dsh-llm/message' import type { SessionEvent } from '@deepseek-ai/dsh-session/types' import type { AssistantTiming } from './conversation.ts' +// The first-token predicate lives with the StreamChunk vocabulary in dsh-llm; +// re-exported here so Chat Definitions keep their client-runtime import. +export { isTokenDelta } from '@deepseek-ai/dsh-llm/message' + /** Pre-finalize timing boundaries for one assistant step (start + first token). */ export interface AssistantStepMetadata { stepStartTime: number | null @@ -21,24 +26,6 @@ export function assistantStepKey(turn: number, step: number): string { return `${turn}\u0000${step}` } -/** - * Whether a chunk carries visible model output (first-token boundary). Empty - * deltas (heartbeats, empty tool-call frames) do not count as a first token. - * @param chunk - the assistant/chunk payload. - * @returns true when the chunk contains a non-empty text/reasoning/tool delta. - */ -export function isTokenDelta(chunk: SessionEvent<'assistant/chunk'>['data']['chunk']): boolean { - switch (chunk.type) { - case 'text-delta': - case 'reasoning-delta': - return chunk.text !== '' - case 'tool-call-delta': - return chunk.argumentsDelta !== '' || chunk.name !== undefined - default: - return false - } -} - /** * Fold one event into the per-step timing index: step/start opens the entry, * the first non-empty token delta stamps first-token time once. Other event diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 896a2fe608..0201a50494 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md -README.md: ed8f888d35693ecaa2667ea432b462f4bb3369cf -README.zh.md: e6a2dd0b545b66ab01b213b5ebc937e22af8ac1a +README.md: 6d0f3d0a088ad580d55d524699a7bf8d2806b724 +README.zh.md: 70f828d29512eb5e25a873a8fce2b4ffad6380fc diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index ed8f888d35..6d0f3d0a08 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -38,7 +38,7 @@ Per-session UI state for selection and the active view lives in the declared cha The composer bar declares session-scoped single seats for `'conversation.input.plan'` (right of the local access-mode control) and `'conversation.input.model'` (immediately before the pending indicator and send/stop controls), plus list slots for overlay, dock, left, and right input extensions. Feature packages own each control and its state; ui-conversation supplies placement, the `locked` owner prop, and the standard slot shares. The leading plus button is a Command launcher, not an attachment surface: it asks the session's `SlashController` to open only the `/` trigger's `command` source over the current textarea selection, while ui-slash's existing `MenuView` remains the sole floating menu and pick path. No file row, file input, upload protocol, or second menu component is introduced. While the `plan` projection's effective target is plan mode, InputBar swaps its textarea placeholder to the plan-task wording, localized through the `conversation` locale namespace this package registers (the `placeholder.plan` / `hint.plan` keys) and shared verbatim with the claimed `/plan` command hint (a host-folded value read through the standard-kit `useProjection`; owner-supplied placeholders win). A pending composer takeover remains mounted when another conversation view is active so the blocked agent can still receive its answer; without a pending interaction, the active-session composer belongs to Chat. The composer-bar slot itself is `session-maybe`: with no current session the same bar keeps message actions inert (machine faces absent, `disabled` owner prop), while the whole dashed card opens the existing Workspace picker by pointer and the read-only textarea opens it through Enter or Space. Disabled controls release pointer events to the card, and the card contains `pointerdown` so the open picker's outside-close cannot race a reopen. The bar never swaps in a parallel tree, so the textarea DOM survives Workspace selection; strict-session control seats stay empty until a session exists. -The chat stats line takes its token accounting from the generic token-meter `tokenUsage` projection read through the standard-kit `useProjection`: billed input is uncached input plus cache reads and writes; cache hit divides cache reads by that total. Visible nodes supply only the turn and step counts plus the LLM and tool wall times, which are window-scoped facts about what is on screen rather than accounting; durable token and context groups remain visible when compaction leaves no assistant node in the loaded window. The same window fold averages each recorded step's TTFT and divides sampled output tokens by their summed decode spans into a latency/throughput group localized through the `conversation` locale namespace (`TTFT avg … · … tok/s` in English); a step missing a timing boundary or a usage sample drops out of those figures instead of skewing them. The turn-count, step-count, duration, cache, and token labels use the same namespace. Each settled turn additionally appends hover-revealed `TTFT {s}s · {tps} tok/s` labels to its assistant footer after the `Ran for` duration — the turn's first-step TTFT and its turn-aggregate decode throughput — gated on the turn's timing being in the loaded window (a contiguous log suffix, so an in-window turn carries every one of its steps) and omitting whichever figure is unrecorded. A deployment without token-meter drops the token groups; when the line overflows, it elides with an ellipsis and a delayed hover tooltip carries the full text only while actually clipped. Context occupancy renders as the composer's trailing ContextMeter: a 14px occupancy ring after the model seat, fed by `contextPressure` and rendered only once both a numerator and a route capacity are known, that click-opens a panel pairing the `percent used` header and `~used / capacity` figures with a color-segmented bar and `~`-prefixed heuristic composition rows (system prompt, tools, messages) from the `contextBreakdown` projection. The ring and header read `projectedTokens` — the provider sample carried forward over the surface's movement since — so a compaction registers immediately instead of after a further turn; the composition rows stay wholly heuristic and therefore still do not sum to the header ([rationale](../../llm/token-meter/README.md)). Occupancy is deliberately an approximation: numerator and capacity are independent last-wins projection fields, not one atomic request observation. +The chat stats line takes its token accounting from the generic token-meter `tokenUsage` projection read through the standard-kit `useProjection`: billed input is uncached input plus cache reads and writes; cache hit divides cache reads by that total. The turn and step counts, the LLM and tool wall times, and the latency/throughput group all ride the whole-log `sessionStats` projection (host-folded from step boundaries, first-token chunks, tool pairs, and assembled messages), so paging and compaction cannot change any strip figure; an assembly without that unit falls back to the window fold over visible nodes, whose fields mirror the projection's. The strip averages each recorded step's TTFT and divides sampled output tokens by their summed decode spans into a latency/throughput group localized through the `conversation` locale namespace (`TTFT avg … · … tok/s` in English); a step missing a timing boundary or a usage sample drops out of those figures instead of skewing them, and durable count, token, and context groups remain visible when compaction leaves no assistant node in the loaded window. The turn-count, step-count, duration, cache, and token labels use the same namespace. Each settled turn additionally appends hover-revealed `TTFT {s}s · {tps} tok/s` labels to its assistant footer after the `Ran for` duration — the turn's first-step TTFT and its turn-aggregate decode throughput — gated on the turn's timing being in the loaded window (a contiguous log suffix, so an in-window turn carries every one of its steps) and omitting whichever figure is unrecorded. A deployment without token-meter drops the token groups; when the line overflows, it elides with an ellipsis and a delayed hover tooltip carries the full text only while actually clipped. Context occupancy renders as the composer's trailing ContextMeter: a 14px occupancy ring after the model seat, fed by `contextPressure` and rendered only once both a numerator and a route capacity are known, that click-opens a panel pairing the `percent used` header and `~used / capacity` figures with a color-segmented bar and `~`-prefixed heuristic composition rows (system prompt, tools, messages) from the `contextBreakdown` projection. The ring and header read `projectedTokens` — the provider sample carried forward over the surface's movement since — so a compaction registers immediately instead of after a further turn; the composition rows stay wholly heuristic and therefore still do not sum to the header ([rationale](../../llm/token-meter/README.md)). Occupancy is deliberately an approximation: numerator and capacity are independent last-wins projection fields, not one atomic request observation. `src/client/` is organized by domain. `contract/` is the shared face for slot declarations, composed props, and cross-domain types; `skeleton/`, `chat/`, `input/`, `queue/`, and `settings/` keep their implementations internal, while `apply.ts` is their assembly point. The `/client` exports contain only loader entries, service classes, and contract types; components and store factories reach the page through slot registrations. @@ -54,7 +54,7 @@ None; this package neither assembles nor sends a provider request. ## Known Limitations and Deferred Work -- **Stats-line durations and speeds cover the in-window flow only** — LLM and tool wall times plus the TTFT and throughput averages fold the snapshot's assistant `timing` and tool call/result pairs, so nodes outside the loaded event window (older history) are not counted. +- **The stats-line fallback fold covers the in-window flow only** — without the `sessionStats` projection (an assembly that does not mount the unit), every figure folds the snapshot's assistant `timing` and tool call/result pairs, so nodes outside the loaded event window (older history) are not counted and the numbers grow per loaded page. - **The details panel has no entry point** — `ChatViewInjected.openDetails` is implemented but uncalled, so the raw selected-call display is unreachable in the assembled application. There is no Input/Output/Metadata switch, Prev/Next stepping, or trajectory deep link. - **Assistant per-message paging is a reserved slot** — drawn in the design, not implemented. The finalized content IconActions row (copy / clock / branch) ships under the last content-text assistant of each turn that has ended; mid-turn narration, Think-only nodes, and every node of a turn still producing steps stay chrome-free. Branch stays disabled unless that message is also the last transcript node of a completed turn; when enabled, it forks through that turn, increments the inherited title on the client, and opens the child. A fork or rename failure leaves the source selected ([decision](../../../.agents/notes/implemented/bug-fix/2026-08-02-message-fork-actions-require-completed-turn-tail.md)). - **Sent user messages cannot be edited** — user bubbles retain clock and copy; branch lives only under assistant answers ([decision](../../../.agents/notes/implemented/simplification/2026-08-06-user-bubbles-drop-the-branch-action.md)). Editing returns with the capability behind it: a client mutation over a settled user message, plus the host behavior for the turn that already consumed it ([decision](../../../.agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.md)). diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index e6a2dd0b54..70f828d295 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -38,7 +38,7 @@ Host 带 placement 的 `session/queue` 快照也会携带待处理 steering。Qu 输入栏为 `'conversation.input.plan'`(位于本地 access 模式控件右侧)和 `'conversation.input.model'`(渲染在 pending 指示器与发送/停止控件之前)声明会话作用域的单实例 seat,并为 overlay、dock、left 和 right 输入扩展声明列表 slot。各功能包拥有相应控件及其状态;ui-conversation 提供放置位置、`locked` owner prop 和标准 slot share。前置加号按钮是 Command launcher,而非附件入口:它要求当前会话的 `SlashController` 基于 textarea 当前 selection,只打开 `/` trigger 的 `command` source,同时 ui-slash 既有的 `MenuView` 仍是唯一的浮层菜单与 pick 路径。不引入 File 行、file input、上传协议或第二套菜单组件。当 `plan` 投影的有效目标为 plan mode 时,InputBar 将文本框 placeholder 切换为 plan 任务措辞,经本包注册的 `conversation` locale 命名空间(`placeholder.plan` / `hint.plan` 键)本地化,并与已认领 `/plan` 命令的提示逐字共用同一份文案(经标准套件 `useProjection` 读取的 host 折叠值;owner 提供的 placeholder 优先)。另一个会话视图活跃时,待处理的 composer 接管仍保持挂载,使被阻塞的 agent(智能体)仍能收到回答;没有待处理交互时,活跃会话的 composer 归 Chat 所有。composer bar slot 本身为 `session-maybe`:没有当前会话时,同一个 bar 会让消息操作保持不可交互(machine face 均缺席、`disabled` owner prop),整张虚线卡片可经指针打开现有 Workspace picker,只读 textarea 也可通过 Enter 或 Space 打开。禁用控件会把指针事件交给卡片,卡片也会拦下 `pointerdown`,避免已打开 picker 的外点关闭与重新打开发生竞态。它不会换入一棵平行树,因此选择 Workspace 时 textarea DOM 不会被销毁;严格会话作用域的控件 seat 在会话存在之前保持为空。 -聊天统计行的 token 账目来自经标准套件 `useProjection` 读取的通用 token-meter 投影 `tokenUsage`:计费输入为未缓存输入、缓存读取与缓存写入之和;缓存命中率以缓存读取除以该总量。可见节点只提供轮次与步骤计数,以及 LLM(大语言模型)和工具的墙钟时间:这些是关于「屏幕上有什么」的窗口作用域事实,而非账目;压缩(compaction)使已加载窗口不再包含 assistant 节点时,持久 token 与上下文分组仍保持可见。同一次窗口折算还会把每个有完整记录的步骤的 TTFT(首 token 延迟)取平均,并用采样到的输出 token 数除以其解码时长之和,得到经 `conversation` locale 命名空间本地化的延迟/吞吐分组(中文为 `首 token 平均 … · … tok/s`);缺少某个 timing 边界或 usage 采样的步骤会直接退出这些数字,而不是让它们失真。轮次计数、步骤计数、耗时、缓存与 token 各项的标签也使用同一命名空间。每个已结算轮次还会在其 assistant footer 的 `用时` 之后追加 hover 才显示的 `首 token {s}秒 · {tps} tok/s` 标签——即该轮次首个步骤的 TTFT 与轮次聚合的解码吞吐——仅当该轮次的 timing 位于已加载窗口内才显示(窗口是日志的连续后缀,因此窗口内的轮次必然带着它的全部步骤),未记录的数字会各自省略。未组合 token-meter 的部署会整组省略 token 分组;统计行过长时以省略号截断,仅在内容真的被裁切时由延迟 hover tooltip 承载完整文本。上下文占用率渲染为 composer 尾部的 ContextMeter:模型座位之后的一枚 14px 占用圆环,由 `contextPressure` 供数,仅当分子与路由容量都已知时才渲染;点击弹出的面板把「已用百分比」标题与 `~已用 / 容量` 数字,与来自 `contextBreakdown` 投影、带 `~` 前缀的启发式组成明细行(系统提示词、工具、对话消息)及分色分段进度条并列。圆环与标题读取 `projectedTokens`——把提供方样本沿此后表层的增减推进到当下——因此压缩会立刻反映出来,而不必再等一整轮;组成明细行仍是纯启发式,因此加起来依然不等于标题数字([原理](../../llm/token-meter/README.md))。占用率是刻意为之的近似值:分子与容量是两个相互独立的「后写覆盖」投影字段,并非同一次请求的原子观测。 +聊天统计行的 token 账目来自经标准套件 `useProjection` 读取的通用 token-meter 投影 `tokenUsage`:计费输入为未缓存输入、缓存读取与缓存写入之和;缓存命中率以缓存读取除以该总量。轮次与步骤计数、LLM(大语言模型)与工具墙钟时间、以及延迟/吞吐分组都来自全日志的 `sessionStats` 投影(Host 端从步边界、首 token chunk、工具配对与已组装消息折算),因此分页与压缩都无法改变统计条的任何数字;未组合该单元的装配回退为对可见节点做窗口折算,其字段与投影一一对应。统计条把每个有完整记录的步骤的 TTFT(首 token 延迟)取平均,并用采样到的输出 token 数除以其解码时长之和,得到经 `conversation` locale 命名空间本地化的延迟/吞吐分组(中文为 `首 token 平均 … · … tok/s`);缺少某个 timing 边界或 usage 采样的步骤会直接退出这些数字,而不是让它们失真;压缩(compaction)使已加载窗口不再包含 assistant 节点时,持久计数、token 与上下文分组仍保持可见。轮次计数、步骤计数、耗时、缓存与 token 各项的标签也使用同一命名空间。每个已结算轮次还会在其 assistant footer 的 `用时` 之后追加 hover 才显示的 `首 token {s}秒 · {tps} tok/s` 标签——即该轮次首个步骤的 TTFT 与轮次聚合的解码吞吐——仅当该轮次的 timing 位于已加载窗口内才显示(窗口是日志的连续后缀,因此窗口内的轮次必然带着它的全部步骤),未记录的数字会各自省略。未组合 token-meter 的部署会整组省略 token 分组;统计行过长时以省略号截断,仅在内容真的被裁切时由延迟 hover tooltip 承载完整文本。上下文占用率渲染为 composer 尾部的 ContextMeter:模型座位之后的一枚 14px 占用圆环,由 `contextPressure` 供数,仅当分子与路由容量都已知时才渲染;点击弹出的面板把「已用百分比」标题与 `~已用 / 容量` 数字,与来自 `contextBreakdown` 投影、带 `~` 前缀的启发式组成明细行(系统提示词、工具、对话消息)及分色分段进度条并列。圆环与标题读取 `projectedTokens`——把提供方样本沿此后表层的增减推进到当下——因此压缩会立刻反映出来,而不必再等一整轮;组成明细行仍是纯启发式,因此加起来依然不等于标题数字([原理](../../llm/token-meter/README.md))。占用率是刻意为之的近似值:分子与容量是两个相互独立的「后写覆盖」投影字段,并非同一次请求的原子观测。 `src/client/` 按领域组织。`contract/` 是 slot 声明、组合 props 与跨领域类型的共享表层;`skeleton/`、`chat/`、`input/`、`queue/` 和 `settings/` 保持内部实现,`apply.ts` 是它们的组装点。`/client` 导出表层只包含 loader entry、service class 和 contract 类型;组件与 store factory 经 slot 注册抵达页面。 @@ -54,7 +54,7 @@ Host 带 placement 的 `session/queue` 快照也会携带待处理 steering。Qu ## 已知限制与暂缓事项 -- **统计行的耗时与速率只覆盖窗口内消息流**:LLM 与工具墙钟时间以及 TTFT 与吞吐平均值由快照的 assistant `timing` 与工具 call/result 配对折算,落在已加载事件窗口之外的节点(更早的历史)不计入。 +- **统计行的回退折算只覆盖窗口内消息流**:未组合 `sessionStats` 投影单元的装配中,所有数字由快照的 assistant `timing` 与工具 call/result 配对折算,落在已加载事件窗口之外的节点(更早的历史)不计入,数字随加载页数增长。 - **详情面板没有入口**:`ChatViewInjected.openDetails` 虽已实现却无人调用,因此以原始形式显示已选择调用的那部分在组装后的应用中不可达。没有 Input/Output/Metadata 切换、Prev/Next 步进,也没有 trajectory 深链接。 - **assistant 逐消息分页是预留 slot**:设计中已有图稿,尚未实现。已定稿的内容 IconActions 行(复制/时钟/分支)只挂在每个已结束轮次中最后一条带 text 内容的 assistant 下;轮次中间的叙述、纯 Think 节点,以及仍在产出步骤的轮次里的所有节点都不带 chrome。除非该消息同时也是已完成轮次的最后一个 transcript 节点,否则分支保持禁用;启用后,它会 fork 到该轮次末尾,在 client 端递增继承标题并打开子会话。fork 或改名失败时源会话保持选中([决策](../../../.agents/notes/implemented/bug-fix/2026-08-02-message-fork-actions-require-completed-turn-tail.md))。 - **已发送的 user 消息无法编辑**:user 气泡保留时钟和复制;分支只存在于 assistant 回答之下([决策](../../../.agents/notes/implemented/simplification/2026-08-06-user-bubbles-drop-the-branch-action.md))。编辑功能要与其背后的能力一起回归:既需要针对已定稿 user 消息的 client 变更,也需要 host 侧对已经消费过它的轮次给出行为([决策](../../../.agents/notes/implemented/simplification/2026-07-31-drop-user-message-edit-stub.md))。 diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index a9996ce09a..3fe601174d 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -70,6 +70,7 @@ "@deepseek-ai/dsh-compact": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm-retry": "workspace:^", + "@deepseek-ai/dsh-session-stats": "workspace:^", "@deepseek-ai/dsh-token-meter": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", "react": "^18.2.0" @@ -98,6 +99,7 @@ "@deepseek-ai/dsh-permission": "workspace:^", "@deepseek-ai/dsh-plan-mode": "workspace:^", "@deepseek-ai/dsh-session-projection": "workspace:^", + "@deepseek-ai/dsh-session-stats": "workspace:^", "@deepseek-ai/dsh-token-meter": "workspace:^", "@deepseek-ai/dsh-tool-todo": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", diff --git a/packages/client/ui-conversation/src/client/chat/StatsLine.tsx b/packages/client/ui-conversation/src/client/chat/StatsLine.tsx index 8e43f3c0b4..177afdb05a 100644 --- a/packages/client/ui-conversation/src/client/chat/StatsLine.tsx +++ b/packages/client/ui-conversation/src/client/chat/StatsLine.tsx @@ -6,6 +6,8 @@ import { Fragment, memo, useLayoutEffect, useMemo, useRef, useState } from 'reac import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' import type { ConversationSnapshot, UseProjection } from '@deepseek-ai/dsh-client-runtime/client' import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots' +// Type-only: merges the sessionStats key into SessionProjectionMap for useProjection. +import type {} from '@deepseek-ai/dsh-session-stats/client' import type { ContextPressureProjection, TokenUsageProjection } from '@deepseek-ai/dsh-token-meter/client' import type { ComposerBarProps } from '../contract/slots.ts' import { formatTokensPerSecond } from './message-chrome.ts' @@ -30,14 +32,16 @@ interface WindowStats { } /** - * Fold assistant and tool-result nodes into the window-scoped display totals. + * Fold assistant and tool-result nodes into window-scoped display totals — + * the FALLBACK for assemblies without the `sessionStats` projection. * - * Counts and wall times describe the loaded window on purpose — they answer - * "what is on screen". Token accounting deliberately does NOT come from here: - * the window is paged and compaction rewrites it, so billing rides the durable - * `tokenUsage` projection instead. + * Every displayed figure rides that durable whole-log projection (and token + * accounting rides `tokenUsage`) because the window is paged and compaction + * rewrites it; this fold answers "what is on screen" only when no projection + * value is served. Its field names deliberately mirror the projection's so + * the two swap wholesale. * @param nodes - snapshot nodes. - * @returns visible counts and summed wall times. + * @returns fallback counts and summed wall times. */ export function deriveStats(nodes: ConversationSnapshot['nodes']): WindowStats { const turns = new Set() @@ -158,8 +162,12 @@ export interface StatsLineProps { export const StatsLine = memo(function StatsLine({ useSession, useProjection, t }: StatsLineProps) { const settledNodes = useSession(s => s.chat.legacy.nodes) - const stats = useMemo(() => deriveStats(settledNodes), [settledNodes]) + const windowStats = useMemo(() => deriveStats(settledNodes), [settledNodes]) const usage = useProjection('tokenUsage') + // Every figure rides the durable sessionStats projection, so paging and + // compaction cannot change any of them; an assembly without the unit falls + // back to the window-scoped fold wholesale (same field names). + const stats = useProjection('sessionStats') ?? windowStats // Pipe-separated groups (figma stats strip); a group with no data drops out whole. const groups: string[] = [] if (stats.steps > 0) { @@ -168,7 +176,6 @@ export const StatsLine = memo(function StatsLine({ useSession, useProjection, t if (stats.llmMs > 0) durations.push(t('stats.llm', { duration: formatDuration(stats.llmMs) })) if (stats.toolMs > 0) durations.push(t('stats.toolCall', { duration: formatDuration(stats.toolMs) })) if (durations.length > 0) groups.push(durations.join(' · ')) - // Window-scoped like the wall times above: averages describe loaded steps. const speeds: string[] = [] if (stats.ttftSteps > 0) { speeds.push(t('stats.ttftAverage', { duration: formatDuration(stats.ttftMs / stats.ttftSteps) })) diff --git a/packages/client/ui-conversation/tests/chat-stats.client.spec.tsx b/packages/client/ui-conversation/tests/chat-stats.client.spec.tsx index b53a1aa739..6abcf611d2 100644 --- a/packages/client/ui-conversation/tests/chat-stats.client.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-stats.client.spec.tsx @@ -98,9 +98,11 @@ describe('deriveStats', () => { ]) expect(stats.turns).toBe(2) expect(stats.steps).toBe(3) - // Window-scoped by design: the paged window is not an accounting source, so - // the fold exposes no billing fields (billing rides the projection); - // decodeTokens is a throughput input, not a billed total. + // The window fold's counts are only the fallback for assemblies without + // the sessionStats projection; the paged window is not an accounting + // source either, so the fold exposes no billing fields (billing rides the + // tokenUsage projection); decodeTokens is a throughput input, not a + // billed total. expect(Object.keys(stats).sort()).toEqual( ['decodeMs', 'decodeTokens', 'llmMs', 'steps', 'toolMs', 'ttftMs', 'ttftSteps', 'turns'], ) @@ -169,6 +171,14 @@ describe('formatters', () => { describe('StatsLine', () => { const USAGE = { uncachedInputTokens: 10, outputTokens: 5, cacheReadTokens: 90, cacheWriteTokens: 0 } + /** A whole-log sessionStats value: zeros plus overrides. */ + function sessionStats(overrides: Record): Record { + return { + turns: 0, steps: 0, llmMs: 0, toolMs: 0, ttftMs: 0, ttftSteps: 0, decodeMs: 0, decodeTokens: 0, + ...overrides, + } + } + /** Stub the projection seat: a key-addressed table of whole values. */ function projections(values: Record): StatsLineProps['useProjection'] { return (key: string) => values[key] @@ -281,6 +291,58 @@ describe('StatsLine', () => { expect(view.container.textContent).toBe('1 turns · 1 steps') }) + it('renders whole-session counts from the sessionStats projection over the paged window', () => { + // The bug's acceptance at unit level: one loaded page must not scope the + // counter — the durable projection's totals win over the window fold. + const { source } = makeSource({ nodes: [assistant(1, 1)] }) + const view = render() + expect(view.container.textContent) + .toBe('10 turns · 89 steps| Cache hit 90%| Input 100 tok · Output 5 tok') + }) + + it('treats a defined zero-count projection as empty, not as fallback', () => { + // A composed unit always serves the key; all-zero genuinely means no + // closed step in the whole log, so nothing renders on a brand-new session. + const empty = makeSource() + const view = render() + expect(view.container.textContent).toBe('') + }) + + it('keeps the counts group over an empty visible window when the projection carries totals', () => { + // Extends the durable-groups guarantee: full-session counts survive a + // window that compaction (or paging) left without assistant nodes. + const { source } = makeSource() + const view = render() + expect(view.container.textContent) + .toBe('7 turns · 44 steps| Cache hit 90%| Input 100 tok · Output 5 tok') + }) + + it('renders whole-log wall times and speeds from the projection, not the loaded window', () => { + // The 加载更早 hazard beyond counts: LLM/tool durations and the TTFT and + // throughput figures must not grow per loaded page either. An untimed + // 1-node window renders the projection's whole-log figures verbatim. + const { source } = makeSource({ nodes: [assistant(1, 1)] }) + const view = render() + expect(view.container.textContent).toBe( + '200 turns · 200 steps| LLM 1m40s · Tool call 1m2s| TTFT avg 0.8s · 20 tok/s| Cache hit 90%| Input 100 tok · Output 5 tok', + ) + }) + it('omits cache hit when nothing was billed on the input side', () => { const { source } = makeSource({ nodes: [assistant(1, 1)] }) const view = render( { expect(view.container.querySelector('[data-state="ok"]')).not.toBeNull() }) - it('StatsLine counts window nodes but drops every token group without a projection', () => { - // Node `usage` is deliberately ignored: billing rides the durable - // tokenUsage projection, so an absent projection leaves counts only. + it('StatsLine falls back to window-node counts and drops every token group without projections', () => { + // No sessionStats key → the window fold supplies the counts (the + // assembly-without-the-unit fallback). Node `usage` is deliberately + // ignored: billing rides the durable tokenUsage projection, so an absent + // projection leaves counts only. const nodes = [ { kind: 'assistant', seq: 1, time: 1, turn: 1, step: 1, blocks: [] }, { kind: 'assistant', seq: 2, time: 2, turn: 1, step: 2, blocks: [], usage: { inputTokens: 4, outputTokens: 6 } }, diff --git a/packages/client/ui-conversation/tsconfig.json b/packages/client/ui-conversation/tsconfig.json index 0ef0bd8a19..b9ac320842 100644 --- a/packages/client/ui-conversation/tsconfig.json +++ b/packages/client/ui-conversation/tsconfig.json @@ -44,6 +44,9 @@ { "path": "../../session/session-projection" }, + { + "path": "../../session/session-stats" + }, { "path": "../../llm/token-meter" }, diff --git a/packages/llm/llm/src/message.ts b/packages/llm/llm/src/message.ts index 7863e66d58..45c0315a2a 100644 --- a/packages/llm/llm/src/message.ts +++ b/packages/llm/llm/src/message.ts @@ -2,7 +2,7 @@ import { MessageId, type CallId } from './brand.ts' import { deepFreeze } from './call-config.ts' -import type { ContentBlock, ToolResultBlock } from './types.ts' +import type { ContentBlock, StreamChunk, ToolResultBlock } from './types.ts' /** Provider/model identity and adapter-private replay data for an assistant message. */ export interface AssistantProvenance { @@ -239,3 +239,23 @@ export function createToolResultMessage(input: ToolResultMessageInput): ToolResu }], }) } + +/** + * Whether a stream chunk carries visible model output (the first-token + * boundary shared by client step timing and the whole-log sessionStats + * projection). Empty deltas (heartbeats, empty tool-call frames) do not count + * as a first token. + * @param chunk - the stream chunk to test. + * @returns true when the chunk contains a non-empty text/reasoning/tool delta. + */ +export function isTokenDelta(chunk: StreamChunk): boolean { + switch (chunk.type) { + case 'text-delta': + case 'reasoning-delta': + return chunk.text !== '' + case 'tool-call-delta': + return chunk.argumentsDelta !== '' || chunk.name !== undefined + default: + return false + } +} diff --git a/packages/session/README.i18n.yaml b/packages/session/README.i18n.yaml index 3829d946f0..e9312ecd0e 100644 --- a/packages/session/README.i18n.yaml +++ b/packages/session/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/README.md -README.md: 586d1be0286a0de935b0b08313e6965452b85376 -README.zh.md: 60e58e6d471a48d3ced03a518e96aeeae79110e8 +README.md: 64aa8e4fdf1e85d74dfa0a77803a04e881b547c4 +README.zh.md: 520e969cbf623ac4a0a3dc82b6fb54f12ba8ffa3 diff --git a/packages/session/README.md b/packages/session/README.md index 586d1be028..64aa8e4fdf 100644 --- a/packages/session/README.md +++ b/packages/session/README.md @@ -25,6 +25,7 @@ Serves current, log-derived per-session state to client carriers. |---|---|---| | [`session-projection/`](session-projection/README.md) | Defines and drives session projection units | `ctx.sessionProjections` | | [`session-projection-cache/`](session-projection-cache/README.md) | Persists and restores projection checkpoints | `ctx.sessionProjectionCache` | +| [`session-stats/`](session-stats/README.md) | Serves whole-log conversation counts and wall times (`sessionStats` unit) | registers on `ctx.sessionProjections` | ## Titles diff --git a/packages/session/README.zh.md b/packages/session/README.zh.md index 60e58e6d47..520e969cbf 100644 --- a/packages/session/README.zh.md +++ b/packages/session/README.zh.md @@ -25,6 +25,7 @@ |---|---|---| | [`session-projection/`](session-projection/README.md) | 定义并驱动会话投影单元 | `ctx.sessionProjections` | | [`session-projection-cache/`](session-projection-cache/README.md) | 持久化并恢复投影检查点 | `ctx.sessionProjectionCache` | +| [`session-stats/`](session-stats/README.md) | 提供全日志会话计数与墙钟时间(`sessionStats` 单元) | 注册到 `ctx.sessionProjections` | ## 标题 diff --git a/packages/session/session-stats/README.i18n.yaml b/packages/session/session-stats/README.i18n.yaml new file mode 100644 index 0000000000..8896173827 --- /dev/null +++ b/packages/session/session-stats/README.i18n.yaml @@ -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 packages/session/session-stats/README.md +README.md: a42d5b1b6efe8ff522a43431f1ccb68b71607876 +README.zh.md: 5929280a8a619fb37a6f6468fb839962702a68c3 diff --git a/packages/session/session-stats/README.md b/packages/session/session-stats/README.md new file mode 100644 index 0000000000..a42d5b1b6e --- /dev/null +++ b/packages/session/session-stats/README.md @@ -0,0 +1,39 @@ +# @deepseek-ai/dsh-session-stats + +English | [中文](README.zh.md) + +Function plugin registering the `sessionStats` projection unit: whole-log conversation figures — turn/step counts and the LLM, tool, first-token, and decode wall times — folded from step boundaries, stream chunks, tool pairs, and assembled assistant messages, and served through the session-projection seam (registry snapshot, change feed, and every projection carrier: history tail page, `session/projection` push frames, session list rows). Clients render full-session figures that paging and compaction cannot change; the reference consumer is the web chat stats strip, whose window fold mirrors these field names as its no-unit fallback. + +## Fold semantics + +- `steps` counts `step/end` events. The agent loop appends exactly one per entered step, in a `finally`, so completed, failed, cancelled, and max-tokens steps all count. Counting assembled assistant messages instead would overcount max-tokens usage-host messages (empty content, excluded from the surface) and undercount cancelled steps (aborted before the message assembles). +- `turns` counts distinct turns carrying at least one closed step; rejected or empty turns (closed with no step) are uncounted. Turn numbers are host-assigned and monotonic per session, so the fold keeps only the last counted turn. +- `llmMs` sums `step/start` → `assistant/message` per step that assembled a message (retry waits inside the step are model time, as in the window fold). +- `ttftMs`/`ttftSteps` sum and count `step/start` → first non-empty delta chunk; the first attempt's boundary survives an in-step `llm/retry` (window `resetForRetry` parity). +- `decodeMs`/`decodeTokens` sum first token → assembled message and the provider-reported output tokens, only over steps carrying both. +- `toolMs` sums `tool/call` → `tool/result` pairs matched by callId; unresolved calls are dropped at `turn/end` (results land within their turn). +- Every field is 0 until its first contributing event. A composed registry always serves the key, so clients read the value, never key presence. + +## Composition + +```yaml +- id: session-stats + name: '@deepseek-ai/dsh-session-stats' +``` + +Injects `sessionProjections` — the plugin's whole purpose; in assemblies without the registry the fiber stays pending and nothing registers. + +## Model Experience + +None, as the plugin only computes a client-facing read model of already-logged session events and touches no prompt, message, schema, stream, or tool result. + +#### KV Cache effect + +None; the plugin never assembles or sends provider requests. + +## Known Limitations and Deferred Work + +- **Steps count work attempted, not visible output** — a step that failed before producing any visible content still closed with `step/end` and counts; a step truncated by a crash between `step/start` and `step/end` does not. +- **A cancelled step is counted but untimed** — no assistant message assembles, so its partial stream time enters no wall-time figure, matching the window fold's untimed interrupted node; a max-tokens usage-host message conversely contributes model time the surface does not show. +- **Counts are log-scoped, not surface-scoped** — steps whose messages were later compacted away stay counted; the figures describe the whole session, not the current model-visible surface. +- **Mounted only in the web-app bundle** — other assemblies serve no `sessionStats` key, and their consumers fall back to window-scoped counting (the web stats strip's fallback path). diff --git a/packages/session/session-stats/README.zh.md b/packages/session/session-stats/README.zh.md new file mode 100644 index 0000000000..5929280a8a --- /dev/null +++ b/packages/session/session-stats/README.zh.md @@ -0,0 +1,39 @@ +# @deepseek-ai/dsh-session-stats + +[English](README.md) | 中文 + +注册 `sessionStats` projection 单元的函数插件:从步边界、流式 chunk、工具配对与已组装的 assistant 消息折叠出全日志会话数字——轮/步计数以及 LLM、工具、首 token、解码墙钟时间——经 session-projection 缝对外提供(registry 快照、变更流,以及每一个 projection 载体:history 尾页、`session/projection` 推送帧、会话列表行)。客户端由此渲染分页与压缩都无法改变的全会话数字;参考消费者是 Web 聊天统计条,其窗口折叠以相同字段名充当无单元时的回退。 + +## 折叠语义 + +- `steps` 统计 `step/end` 事件。agent loop 对每个进入的步在 `finally` 中恰好追加一条,因此完成、失败、取消、max-tokens 的步全部计入。若改按已组装的 assistant 消息计数,则会多算 max-tokens 的 usage 宿主消息(空内容、被排除在 surface 之外),并少算被取消的步(在消息组装前已中止)。 +- `turns` 统计含至少一个已关闭步的不同 turn;被拒绝或空轮(未进入任何步即关闭)不计。turn 号由宿主分配、按会话单调递增,因此折叠只需保留最近计入的 turn。 +- `llmMs` 按步累加 `step/start` → `assistant/message`(组装出消息的步;步内重试的等待与窗口折叠一样计入模型时间)。 +- `ttftMs`/`ttftSteps` 累加并统计 `step/start` → 首个非空 delta chunk;首次尝试的边界在步内 `llm/retry` 后保留(与窗口 `resetForRetry` 对齐)。 +- `decodeMs`/`decodeTokens` 累加首 token → 已组装消息的时长与提供方上报的输出 token,仅统计两者兼备的步。 +- `toolMs` 按 callId 配对累加 `tool/call` → `tool/result`;未解决的调用在 `turn/end` 时丢弃(结果总在其轮内落地)。 +- 每个字段在首个贡献事件之前均为 0。已装配的 registry 恒提供该键,客户端读取值本身,而非键的存在性。 + +## 组合 + +```yaml +- id: session-stats + name: '@deepseek-ai/dsh-session-stats' +``` + +注入 `sessionProjections`——这是插件的全部用途;在没有 registry 的装配中 fiber 保持挂起,不注册任何内容。 + +## 模型体验 + +无,因为插件只计算面向客户端的、由已写入日志的会话事件派生的读模型,不触碰任何提示词、消息、schema、流或工具结果。 + +#### KV Cache 影响 + +无;插件从不组装或发送提供方请求。 + +## 已知局限与延后工作 + +- **步数统计的是已发生的工作,而非可见输出**——在产生任何可见内容前就失败的步仍以 `step/end` 关闭并计入;进程崩溃恰好截断在 `step/start` 与 `step/end` 之间的步不计。 +- **被取消的步计数但不计时**——没有组装出 assistant 消息,其部分流式时间不进入任何墙钟数字,与窗口折叠的无计时 interrupted 节点一致;反之 max-tokens 的 usage 宿主消息贡献 surface 上看不到的模型时间。 +- **计数是日志口径,不是 surface 口径**——消息后来被压缩掉的步仍然计入;数字描述整个会话,而非当前模型可见 surface。 +- **仅挂载于 web-app bundle**——其他装配不提供 `sessionStats` 键,其消费者回退到窗口口径计数(Web 统计条的回退路径)。 diff --git a/packages/session/session-stats/package.json b/packages/session/session-stats/package.json new file mode 100644 index 0000000000..f029ce83b9 --- /dev/null +++ b/packages/session/session-stats/package.json @@ -0,0 +1,62 @@ +{ + "name": "@deepseek-ai/dsh-session-stats", + "description": "Whole-log conversation counts and wall times projection (sessionStats) for the DeepSeek Harness", + "version": "0.0.1-rc.2", + "publishConfig": { + "access": "restricted" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/session/session-stats" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./invariant": { + "types": "./lib/types/invariant.d.ts", + "default": "./lib/invariant.js" + }, + "./types": { + "types": "./lib/types/types.d.ts", + "default": "./lib/types/types.js" + }, + "./client": { + "types": "./lib/types/client.d.ts", + "default": "./lib/types/client.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/invariant.js", + "lib/types/**/*.js", + "lib/types/**/*.d.ts" + ], + "license": "BSD-3-Clause", + "peerDependencies": { + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-session-projection": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + }, + "dependencies": { + "zod": "^4.4.3" + }, + "devDependencies": { + "@deepseek-ai/cordis-plugin-include": "workspace:^", + "@deepseek-ai/cordis-plugin-loader": "workspace:^", + "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-llm": "workspace:^", + "@deepseek-ai/dsh-session": "workspace:^", + "@deepseek-ai/dsh-session-projection": "workspace:^", + "@deepseek-ai/cordis": "workspace:^" + } +} diff --git a/packages/session/session-stats/src/client.ts b/packages/session/session-stats/src/client.ts new file mode 100644 index 0000000000..9f6adc5356 --- /dev/null +++ b/packages/session/session-stats/src/client.ts @@ -0,0 +1,10 @@ +/** + * Client-namespace projection of the session-stats domain: a pure re-export + * of the package's types outlet. Client code imports ONLY the client + * namespace (repo discipline), so `./client` projects the same single-source + * content `./types` serves to host consumers — zero duplication. + * + * @module @deepseek-ai/dsh-session-stats/client + */ + +export type * from './types.ts' diff --git a/packages/session/session-stats/src/index.ts b/packages/session/session-stats/src/index.ts new file mode 100644 index 0000000000..54679b6e78 --- /dev/null +++ b/packages/session/session-stats/src/index.ts @@ -0,0 +1,29 @@ +/** + * Function plugin registering the `sessionStats` projection unit: whole-log + * turn/step counts and LLM/tool/first-token/decode wall times served through + * the session-projection seam (registry snapshot, change feed, and every + * projection carrier), so clients render full-session figures that paging and + * compaction cannot change. The plugin owns only the fold; delivery is the + * seam's. + * + * @module @deepseek-ai/dsh-session-stats + */ + +import type { Context } from '@deepseek-ai/cordis' +import { sessionStatsProjectionDefinition } from './projection.ts' + +export type * from './types.ts' + +/** Cordis plugin name. */ +export const name = 'session-stats' +/** The projection registry is the plugin's whole purpose; without it the fiber stays pending. */ +export const inject = ['sessionProjections'] + +/** + * Register the `sessionStats` unit; the registration is an effect on this + * plugin's fiber, so unloading removes the key. + * @param ctx - registrant context carrying the projection registry. + */ +export function apply(ctx: Context): void { + ctx.sessionProjections.register(sessionStatsProjectionDefinition) +} diff --git a/packages/session/session-stats/src/invariant.ts b/packages/session/session-stats/src/invariant.ts new file mode 100644 index 0000000000..582e5dcdb4 --- /dev/null +++ b/packages/session/session-stats/src/invariant.ts @@ -0,0 +1,35 @@ +/** + * Package-owned invariant companion for `@deepseek-ai/dsh-session-stats`. + * @module @deepseek-ai/dsh-session-stats/invariant + */ + +/* jscpd:ignore-start */ +import type { Context } from '@deepseek-ai/cordis' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' + +const PACKAGE_NAME = '@deepseek-ai/dsh-session-stats' + +/** Cordis companion plugin name. */ +export const name = 'session-stats-invariant' +/** Service required before the companion can reserve package ownership. */ +export const inject = ['invariants'] + +/** + * No runtime invariant: the package owns a single pure projection fold whose + * wire payload is schema-validated by the projection registry at every + * snapshot and change-feed emission, and the event relations the fold relies + * on (`step/end` exactly once per entered step, monotonic host-assigned turn + * numbers, chunk and tool events carrying their step coordinates and call + * ids) are owned and runtime-checked by dsh-agent-loop and the session + * surface, not here. + */ +const install: InvariantInstaller = () => {} + +/** + * Register this package's invariant companion. + * @param ctx - Cordis context carrying the invariant service. + * @returns the installed registration's disposer after setup succeeds. + */ +export const apply = (ctx: Context): Promise<() => void> => + Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) +/* jscpd:ignore-end */ diff --git a/packages/session/session-stats/src/projection.ts b/packages/session/session-stats/src/projection.ts new file mode 100644 index 0000000000..e155622920 --- /dev/null +++ b/packages/session/session-stats/src/projection.ts @@ -0,0 +1,179 @@ +/** + * The `sessionStats` projection unit: a pure fold of step boundaries, stream + * chunks, tool pairs, and assembled assistant messages into whole-log counts + * and wall times. + * + * `step/end` — not `assistant/message` — is the counted step event because it + * is the step lifecycle authority: the loop appends exactly one per entered + * step, in a `finally`, so completed, failed, cancelled, and max-tokens steps + * all land one. Counting assembled assistant messages instead would overcount + * max-tokens usage-host messages (empty content, excluded from the surface) + * and undercount cancelled steps (aborted before the message assembles). + * + * The wall-time folds mirror the client window fold field by field + * (`deriveStats` in dsh-client-ui-conversation, that fold's whole-window + * fallback role): model time is `step/start` → `assistant/message`, first + * token is the first non-empty delta chunk and survives an in-step + * `llm/retry`, decode spans first token → assembled message on steps that + * also report output tokens, and tool time pairs `tool/call` → `tool/result` + * by callId. A cancelled step assembles no message, so its partial stream + * time stays uncounted in every time figure — matching the window, which + * renders it as an untimed interrupted node. + * + * @module @deepseek-ai/dsh-session-stats/projection + */ + +import { z } from 'zod' +import { isTokenDelta } from '@deepseek-ai/dsh-llm/message' +import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' + +/** Accumulated whole-log figures (the view is exactly these totals). */ +interface SessionStatsTotals { + /** Distinct turns with at least one closed step so far. */ + turns: number + /** Closed steps so far. */ + steps: number + /** Summed model wall time over message-assembling steps, ms. */ + llmMs: number + /** Summed matched tool call→result wall time, ms. */ + toolMs: number + /** Summed first-token latency over `ttftSteps`, ms. */ + ttftMs: number + /** Steps carrying a recorded first token. */ + ttftSteps: number + /** Summed decode wall time over usage-reporting steps, ms. */ + decodeMs: number + /** Summed provider output tokens over the same steps. */ + decodeTokens: number +} + +/** + * Fold state: the totals plus the in-flight boundaries they accrue from. + * Turn numbers are host-assigned and monotonic per session, so a single + * `lastTurn` slot decides "first closed step of a new turn"; the state is + * plain JSON per the unit contract (persisted-cache precondition). + */ +interface SessionStatsState extends SessionStatsTotals { + /** Turn of the last counted `step/end`; null before the first. */ + lastTurn: number | null + /** The open step's boundary facts; null outside a step or after its message assembled. */ + openStep: { turn: number; step: number; startTime: number; firstTokenTime: number | null } | null + /** Dispatch times of tool calls whose result has not landed, by callId. */ + pendingCalls: Record +} + +const sessionStatsSchema = z.object({ + turns: z.number().int().nonnegative(), + steps: z.number().int().nonnegative(), + llmMs: z.number().nonnegative(), + toolMs: z.number().nonnegative(), + ttftMs: z.number().nonnegative(), + ttftSteps: z.number().int().nonnegative(), + decodeMs: z.number().nonnegative(), + decodeTokens: z.number().nonnegative(), +}).strict() + +/** + * Provider-reported completion tokens, guarded the way the window fold guards + * node usage. + * @param usage - the assistant/message event's optional usage record. + * @returns the output-token count, or null when unreported or invalid. + */ +function usageOutputTokens(usage: unknown): number | null { + if (typeof usage !== 'object' || usage === null) return null + const value = (usage as { outputTokens?: unknown }).outputTokens + return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null +} + +/** The `sessionStats` unit registered on `ctx.sessionProjections` (exported for the unit spec). */ +export const sessionStatsProjectionDefinition: ProjectionDefinition<'sessionStats', SessionStatsState> = { + key: 'sessionStats', + schema: sessionStatsSchema, + init: () => ({ + turns: 0, + steps: 0, + llmMs: 0, + toolMs: 0, + ttftMs: 0, + ttftSteps: 0, + decodeMs: 0, + decodeTokens: 0, + lastTurn: null, + openStep: null, + pendingCalls: {}, + }), + apply: (state, event) => { + // Every uninteresting event returns the same reference (Object.is gates the change feed). + switch (event.type) { + case 'step/start': + return { + ...state, + openStep: { turn: event.data.turn, step: event.data.step, startTime: event.time, firstTokenTime: null }, + } + case 'assistant/chunk': { + const open = state.openStep + if (open === null || open.turn !== event.data.turn || open.step !== event.data.step) return state + if (open.firstTokenTime !== null || !isTokenDelta(event.data.chunk)) return state + return { ...state, openStep: { ...open, firstTokenTime: event.time } } + } + case 'assistant/message': { + const open = state.openStep + if (open === null || open.turn !== event.data.turn || open.step !== event.data.step) return state + // One assembled message per step: closing the boundary means a + // defensive duplicate cannot accrue twice. + const next: SessionStatsState = { + ...state, + llmMs: state.llmMs + Math.max(0, event.time - open.startTime), + openStep: null, + } + if (open.firstTokenTime !== null) { + next.ttftMs += Math.max(0, open.firstTokenTime - open.startTime) + next.ttftSteps += 1 + const outputTokens = usageOutputTokens(event.data.usage) + if (outputTokens !== null) { + next.decodeMs += Math.max(0, event.time - open.firstTokenTime) + next.decodeTokens += outputTokens + } + } + return next + } + case 'tool/call': + return { ...state, pendingCalls: { ...state.pendingCalls, [event.data.callId]: event.time } } + case 'tool/result': { + const callId = event.data.message.source.callId + const dispatched = state.pendingCalls[callId] + if (dispatched === undefined) return state + const pendingCalls = Object.fromEntries( + Object.entries(state.pendingCalls).filter(([id]) => id !== callId), + ) + return { ...state, toolMs: state.toolMs + Math.max(0, event.time - dispatched), pendingCalls } + } + case 'step/end': + return { + ...state, + turns: state.lastTurn === event.data.turn ? state.turns : state.turns + 1, + steps: state.steps + 1, + lastTurn: event.data.turn, + openStep: null, + } + case 'turn/end': + // A call whose result never landed belongs to a cancelled or failed + // turn; results always land within their turn, so drop the leftovers + // instead of growing persisted state forever. + return Object.keys(state.pendingCalls).length === 0 ? state : { ...state, pendingCalls: {} } + default: + return state + } + }, + view: state => ({ + turns: state.turns, + steps: state.steps, + llmMs: state.llmMs, + toolMs: state.toolMs, + ttftMs: state.ttftMs, + ttftSteps: state.ttftSteps, + decodeMs: state.decodeMs, + decodeTokens: state.decodeTokens, + }), + stateVersion: 2, +} diff --git a/packages/session/session-stats/src/types.ts b/packages/session/session-stats/src/types.ts new file mode 100644 index 0000000000..e11a300b19 --- /dev/null +++ b/packages/session/session-stats/src/types.ts @@ -0,0 +1,46 @@ +/** + * Pure types of the session-stats domain: the ONE home of the `sessionStats` + * projection-key declaration, free of this package's host-side value imports + * (cordis context, zod, the llm chunk predicate). Two namespace projections + * serve it — `./types` for host consumers, `./client` for client aggregates — + * with zero content duplication. + * + * @module @deepseek-ai/dsh-session-stats/types + */ + +// Marks this file a module so the declaration below AUGMENTS the projection +// table instead of declaring an ambient module. +export {} + +/** + * Whole-log conversation figures, independent of how much history a client + * has paged in. Counts and wall times all fold from the complete durable log; + * every field is 0 until its first contributing event lands. Field names + * mirror the client window fold so an assembly without this unit can fall + * back to it wholesale. + */ +export interface SessionStatsProjection { + /** Distinct turns carrying at least one closed step (`step/end`); rejected or empty turns are uncounted. */ + turns: number + /** Closed steps (`step/end` events) — completed, failed, and cancelled steps alike. */ + steps: number + /** Summed model wall time (`step/start` → `assistant/message`) over steps that assembled a message. */ + llmMs: number + /** Summed tool wall time over `tool/call` → `tool/result` pairs matched by callId. */ + toolMs: number + /** Summed first-token latency (`step/start` → first non-empty delta chunk) over `ttftSteps`. */ + ttftMs: number + /** Steps carrying a recorded first token. */ + ttftSteps: number + /** Summed decode wall time (first token → `assistant/message`) over steps that also report output tokens. */ + decodeMs: number + /** Summed provider output tokens over the same decode-timed steps. */ + decodeTokens: number +} + +declare module '@deepseek-ai/dsh-session-projection/types' { + interface SessionProjectionMap { + /** Whole-log turn/step counts and wall times; see {@link SessionStatsProjection}. */ + sessionStats: SessionStatsProjection + } +} diff --git a/packages/session/session-stats/tests/loader-composition.spec.ts b/packages/session/session-stats/tests/loader-composition.spec.ts new file mode 100644 index 0000000000..e3889eaeb4 --- /dev/null +++ b/packages/session/session-stats/tests/loader-composition.spec.ts @@ -0,0 +1,86 @@ +/** + * REAL-composition proof: the shipped YAML shape (session + projection + * registry + session-stats) boots through the vendored Loader, the function + * plugin's namespace survives (no default export), and a full logged turn + * serves `{turns: 1, steps: 1}` through the composed registry. + */ + +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import { afterEach, describe, expect, it } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import Loader from '@deepseek-ai/cordis-plugin-loader' +import Include from '@deepseek-ai/cordis-plugin-include' +import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' +import * as SessionStatsPlugin from '@deepseek-ai/dsh-session-stats' + +let root: string | undefined +let context: Context | undefined + +afterEach(async () => { + await context?.fiber.dispose() + context = undefined + if (root !== undefined) await rm(root, { recursive: true, force: true }) + root = undefined +}) + +async function loadYaml(lines: readonly string[]): Promise { + root = await mkdtemp(join(tmpdir(), 'dsh-session-stats-loader-')) + const configPath = join(root, 'cordis.yml') + await writeFile(configPath, [...lines, ''].join('\n')) + + context = new Context() + context.baseUrl = pathToFileURL(root).href + '/' + await context.plugin(Loader) + context.loader.builtins.include = Include + const modules = new Map([ + ['@deepseek-ai/dsh-session', SessionStore], + ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry], + ['@deepseek-ai/dsh-session-stats', SessionStatsPlugin], + ]) + context.loader.internal = { + version: 'v2', + async import(specifier: string) { + if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`) + return modules.get(specifier) + }, + } as unknown as NonNullable + await context.loader.create({ + name: 'cordis:include', + config: { path: pathToFileURL(configPath).href }, + }) + await context.loader.await() + return context +} + +describe('real Loader composition', () => { + it('loads the shipped session-stats YAML shape and serves whole-log counts', async () => { + const loaded = await loadYaml([ + "- name: '@deepseek-ai/dsh-session'", + "- name: '@deepseek-ai/dsh-session-projection'", + "- name: '@deepseek-ai/dsh-session-stats'", + ]) + + const unloaded = [...loaded.loader.entries()] + .filter(entry => entry.fiber === undefined && !entry.disabled) + .map(entry => entry.options.name) + expect(unloaded).toEqual([]) + + const session = loaded.sessions.create(SessionId('composed')) + session.append('turn/start', { turn: 1 }) + session.append('step/start', { turn: 1, step: 1 }) + session.append('step/end', { turn: 1, step: 1 }) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + expect(loaded.sessionProjections.snapshot(session).values.sessionStats) + .toMatchObject({ turns: 1, steps: 1 }) + }) + + it('keeps the function-plugin namespace free of a default export', () => { + // A default export beside the named form makes the Loader discard the + // namespace (postmortem 0001) — pin its absence. + expect('default' in SessionStatsPlugin).toBe(false) + }) +}) diff --git a/packages/session/session-stats/tests/projection.spec.ts b/packages/session/session-stats/tests/projection.spec.ts new file mode 100644 index 0000000000..fce09b4e9e --- /dev/null +++ b/packages/session/session-stats/tests/projection.spec.ts @@ -0,0 +1,271 @@ +/** + * The `sessionStats` projection unit: mounting the plugin beside the + * projection registry serves whole-log counts and wall times folded from step + * boundaries, chunks, tool pairs, and assembled messages; compositions + * without the registry are unaffected; unmounting the plugin removes the key + * (HMR safety). The two counting regressions pinned here are the reasons the + * fold counts step boundaries instead of assistant messages: a cancelled step + * never assembles a message but still counts, and a max-tokens usage-host + * message (empty content) adds no extra step. Wall-time math runs against the + * exported definition directly, where event times are controlled. + */ + +import { describe, expect, it } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import { createMessage } from '@deepseek-ai/dsh-llm' +import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' +import type { Session, SessionEvent } from '@deepseek-ai/dsh-session' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' +import * as SessionStatsPlugin from '@deepseek-ai/dsh-session-stats' +import { sessionStatsProjectionDefinition } from '@deepseek-ai/dsh-session-stats/src/projection.ts' +import type { SessionStatsProjection } from '@deepseek-ai/dsh-session-stats/types' + +async function harness(withStatsPlugin: boolean): Promise<{ ctx: Context; session: Session }> { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + if (withStatsPlugin) await ctx.plugin(SessionStatsPlugin) + return { ctx, session: ctx.sessions.create(SessionId('counted')) } +} + +/** Close one step; returns the counted `step/end` seq. */ +function closeStep(session: Session, turn: number, step: number): number { + session.append('step/start', { turn, step }) + return session.append('step/end', { turn, step }).seq +} + +/** Append the max-tokens usage-host shape: an assistant/message with empty content. */ +function appendEmptyAssistantMessage(session: Session, turn: number, step: number): void { + session.append('assistant/message', { + turn, + step, + message: createMessage({ + role: 'assistant', + content: [], + source: { kind: 'model', provider: 'mock', model: 'mock' }, + }), + }, { surfaceOp: 'append', sourceEventSeqs: [] }) +} + +/** The all-zero projection value plus overrides, for exact fold expectations. */ +function totals(overrides: Partial = {}): SessionStatsProjection { + return { + turns: 0, steps: 0, llmMs: 0, toolMs: 0, ttftMs: 0, ttftSteps: 0, decodeMs: 0, decodeTokens: 0, + ...overrides, + } +} + +describe('sessionStats projection unit (registry drive)', () => { + it('serves zero figures on the empty log', async () => { + const { ctx, session } = await harness(true) + expect(ctx.sessionProjections.snapshot(session).values.sessionStats).toEqual(totals()) + }) + + it('counts distinct turns and closed steps and notifies the change feed with the causing seq', async () => { + const { ctx, session } = await harness(true) + const changes: { key: string; value: unknown; seq: number }[] = [] + ctx.sessionProjections.onChanged((_session, key, value, seq) => { + changes.push({ key, value, seq }) + }) + session.append('turn/start', { turn: 1 }) + const firstSeq = closeStep(session, 1, 1) + const secondSeq = closeStep(session, 1, 2) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + session.append('turn/start', { turn: 2 }) + const thirdSeq = closeStep(session, 2, 1) + session.append('turn/end', { turn: 2, reason: { kind: 'completed' } }) + // Boundary events that carry no figure change (turn/start, empty-prune + // turn/end, user input) fold to the same reference and stay silent; + // step/start opens a boundary (internal state) and step/end commits the + // counts, so each closed step notifies twice with the step/end value last. + const counted = changes.filter(change => (change.value as SessionStatsProjection).steps > 0 + || change.seq === firstSeq) + expect(changes.every(change => change.key === 'sessionStats')).toBe(true) + expect(counted.map(change => ({ seq: change.seq, value: change.value }))).toContainEqual( + { seq: firstSeq, value: totals({ turns: 1, steps: 1 }) }, + ) + expect(changes.at(-1)).toEqual({ key: 'sessionStats', value: totals({ turns: 2, steps: 3 }), seq: thirdSeq }) + const snapshot = ctx.sessionProjections.snapshot(session) + expect(snapshot.values.sessionStats).toEqual(totals({ turns: 2, steps: 3 })) + expect(snapshot.asOfSeq).toBe(session.seq - 1) + expect(changes.map(change => change.seq)).toContain(secondSeq) + }) + + it('does not count a rejected or empty turn that closes with no step', async () => { + const { ctx, session } = await harness(true) + session.append('turn/start', { turn: 1 }) + session.append('turn/end', { turn: 1, reason: { kind: 'blocked' } }) + expect(ctx.sessionProjections.snapshot(session).values.sessionStats).toEqual(totals()) + }) + + it('counts a cancelled step that closed without an assistant message', async () => { + // Regression: an aborted stream never assembles assistant/message, but the + // loop's finally still appends step/end — the step happened and counts. + const { ctx, session } = await harness(true) + session.append('turn/start', { turn: 1 }) + closeStep(session, 1, 1) + session.append('turn/end', { turn: 1, reason: { kind: 'aborted', reason: { kind: 'legacy' } } }) + expect(ctx.sessionProjections.snapshot(session).values.sessionStats) + .toMatchObject({ turns: 1, steps: 1 }) + }) + + it('adds no extra step for a max-tokens usage-host assistant message', async () => { + // Regression: the empty-content assistant/message exists only to host + // usage and is excluded from the surface; the step counts once, from its + // step/end, while the message contributes only its model wall time. + const { ctx, session } = await harness(true) + session.append('turn/start', { turn: 1 }) + session.append('step/start', { turn: 1, step: 1 }) + appendEmptyAssistantMessage(session, 1, 1) + session.append('step/end', { turn: 1, step: 1 }) + session.append('turn/end', { turn: 1, reason: { kind: 'max-tokens' } }) + expect(ctx.sessionProjections.snapshot(session).values.sessionStats) + .toMatchObject({ turns: 1, steps: 1, ttftSteps: 0, decodeTokens: 0 }) + }) + + it('folds steps already in the log when the plugin mounts late (lazy cell build)', async () => { + const { ctx, session } = await harness(false) + session.append('turn/start', { turn: 1 }) + closeStep(session, 1, 1) + closeStep(session, 1, 2) + session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) + await ctx.plugin(SessionStatsPlugin) + expect(ctx.sessionProjections.snapshot(session).values.sessionStats) + .toMatchObject({ turns: 1, steps: 2 }) + }) + + it('has no sessionStats key without the plugin, and drops it when the plugin unloads (HMR safety)', async () => { + const { ctx, session } = await harness(false) + expect('sessionStats' in ctx.sessionProjections.snapshot(session).values).toBe(false) + const fiber = await ctx.plugin(SessionStatsPlugin) + session.append('turn/start', { turn: 1 }) + closeStep(session, 1, 1) + expect(ctx.sessionProjections.snapshot(session).values.sessionStats) + .toMatchObject({ turns: 1, steps: 1 }) + await fiber.dispose() + expect('sessionStats' in ctx.sessionProjections.snapshot(session).values).toBe(false) + }) +}) + +/** Build one synthetic committed event with a controlled timestamp. */ +function at(time: number, type: string, data: unknown): SessionEvent { + return { type, seq: time, time, data } as unknown as SessionEvent +} + +/** Fold a synthetic event list through the definition and view the result. */ +function fold(events: readonly SessionEvent[]): SessionStatsProjection { + const state = events.reduce( + (folded, event) => sessionStatsProjectionDefinition.apply(folded, event), + sessionStatsProjectionDefinition.init(), + ) + return sessionStatsProjectionDefinition.view(state) +} + +describe('sessionStats wall-time fold (controlled timestamps)', () => { + const message = createMessage({ + role: 'assistant', + content: [{ type: 'text', text: 'answer' }], + source: { kind: 'model', provider: 'mock', model: 'mock' }, + }) + + it('accrues model, first-token, and decode time from one fully recorded step', () => { + expect(fold([ + at(1_000, 'step/start', { turn: 1, step: 1 }), + at(1_800, 'assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'a' } }), + at(4_800, 'assistant/message', { turn: 1, step: 1, message, usage: { inputTokens: 10, outputTokens: 60 } }), + at(4_900, 'step/end', { turn: 1, step: 1 }), + ])).toEqual(totals({ + turns: 1, steps: 1, llmMs: 3_800, ttftMs: 800, ttftSteps: 1, decodeMs: 3_000, decodeTokens: 60, + })) + }) + + it('keeps the first attempt token boundary across an in-step retry (window resetForRetry parity)', () => { + expect(fold([ + at(1_000, 'step/start', { turn: 1, step: 1 }), + at(1_200, 'assistant/chunk', { turn: 1, step: 1, chunk: { type: 'reasoning-delta', index: 0, text: 'x' } }), + at(2_000, 'llm/retry', { turn: 1, step: 1 }), + at(3_000, 'assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'y' } }), + at(5_000, 'assistant/message', { turn: 1, step: 1, message }), + at(5_100, 'step/end', { turn: 1, step: 1 }), + ])).toEqual(totals({ turns: 1, steps: 1, llmMs: 4_000, ttftMs: 200, ttftSteps: 1 })) + }) + + it('ignores empty deltas, non-token chunks, and chunks outside the open step', () => { + expect(fold([ + // Chunk before any step/start: no open boundary. + at(500, 'assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'stray' } }), + at(1_000, 'step/start', { turn: 1, step: 1 }), + at(1_100, 'assistant/chunk', { turn: 1, step: 1, chunk: { type: 'block-start', index: 0, blockType: 'text' } }), + at(1_200, 'assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: '' } }), + at(1_300, 'assistant/chunk', { turn: 2, step: 9, chunk: { type: 'text-delta', index: 0, text: 'other' } }), + at(1_400, 'assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'first' } }), + at(2_000, 'assistant/message', { turn: 1, step: 1, message }), + at(2_100, 'step/end', { turn: 1, step: 1 }), + ])).toEqual(totals({ turns: 1, steps: 1, llmMs: 1_000, ttftMs: 400, ttftSteps: 1 })) + }) + + it('leaves a cancelled step untimed: counted by step/end, no assembled message to accrue from', () => { + expect(fold([ + at(1_000, 'step/start', { turn: 1, step: 1 }), + at(1_500, 'assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'partial' } }), + at(2_000, 'step/end', { turn: 1, step: 1 }), + ])).toEqual(totals({ turns: 1, steps: 1 })) + }) + + it('pairs tool wall time by callId, ignores orphan results, and prunes leftovers at turn/end', () => { + const result = (callId: string): unknown => + ({ turn: 1, step: 1, message: { source: { kind: 'tool', callId } } }) + const paired = fold([ + at(1_000, 'step/start', { turn: 1, step: 1 }), + at(1_100, 'tool/call', { turn: 1, step: 1, callId: 'a', name: 'read', arguments: '{}' }), + at(1_200, 'tool/call', { turn: 1, step: 1, callId: 'b', name: 'read', arguments: '{}' }), + // Out-of-order settlement pairs by id, not adjacency. + at(4_200, 'tool/result', result('b')), + at(1_600, 'tool/result', result('a')), + at(5_000, 'tool/result', result('ghost')), + at(5_100, 'step/end', { turn: 1, step: 1 }), + ]) + expect(paired).toEqual(totals({ turns: 1, steps: 1, toolMs: 3_500 })) + // An unresolved call is dropped at turn/end; a later result cannot pair. + const pruned = fold([ + at(1_000, 'step/start', { turn: 1, step: 1 }), + at(1_100, 'tool/call', { turn: 1, step: 1, callId: 'orphan', name: 'read', arguments: '{}' }), + at(2_000, 'step/end', { turn: 1, step: 1 }), + at(2_100, 'turn/end', { turn: 1, reason: { kind: 'aborted', reason: { kind: 'legacy' } } }), + at(9_000, 'tool/result', result('orphan')), + ]) + expect(pruned).toEqual(totals({ turns: 1, steps: 1 })) + }) + + it('skips decode for an invalid usage report and ignores a duplicate assembled message', () => { + const events = [ + at(1_000, 'step/start', { turn: 1, step: 1 }), + at(1_400, 'assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'a' } }), + // A malformed provider report: guarded like the window fold guards node usage. + at(2_000, 'assistant/message', { turn: 1, step: 1, message, usage: { inputTokens: 1, outputTokens: -5 } }), + ] + expect(fold([...events, at(2_100, 'step/end', { turn: 1, step: 1 })])) + .toEqual(totals({ turns: 1, steps: 1, llmMs: 1_000, ttftMs: 400, ttftSteps: 1 })) + // The first message closed the step boundary; a defensive duplicate finds + // no open step and folds to the same reference. + const state = events.reduce( + (folded, event) => sessionStatsProjectionDefinition.apply(folded, event), + sessionStatsProjectionDefinition.init(), + ) + expect(sessionStatsProjectionDefinition.apply( + state, + at(2_050, 'assistant/message', { turn: 1, step: 1, message }), + )).toBe(state) + }) + + it('accrues nothing for unrelated events and clamps negative clock skew to zero', () => { + const state = sessionStatsProjectionDefinition.init() + const untouched = sessionStatsProjectionDefinition.apply(state, at(1, 'user/message', { content: [] })) + expect(untouched).toBe(state) + expect(fold([ + at(2_000, 'step/start', { turn: 1, step: 1 }), + at(1_000, 'assistant/message', { turn: 1, step: 1, message }), + at(2_100, 'step/end', { turn: 1, step: 1 }), + ])).toEqual(totals({ turns: 1, steps: 1 })) + }) +}) diff --git a/packages/session/session-stats/tsconfig.json b/packages/session/session-stats/tsconfig.json new file mode 100644 index 0000000000..a6ed34022e --- /dev/null +++ b/packages/session/session-stats/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../support/invariants" + }, + { + "path": "../../llm/llm" + }, + { + "path": "../../core/session" + }, + { + "path": "../session-projection" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e71c12536..fa1a181ac9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1665,6 +1665,9 @@ importers: '@deepseek-ai/dsh-session-projection-cache': specifier: workspace:^ version: link:../../session/session-projection-cache + '@deepseek-ai/dsh-session-stats': + specifier: workspace:^ + version: link:../../session/session-stats '@deepseek-ai/dsh-storage': specifier: workspace:^ version: link:../../storage/storage @@ -2149,6 +2152,9 @@ importers: '@deepseek-ai/dsh-session-projection': specifier: workspace:^ version: link:../../session/session-projection + '@deepseek-ai/dsh-session-stats': + specifier: workspace:^ + version: link:../../session/session-stats '@deepseek-ai/dsh-token-meter': specifier: workspace:^ version: link:../../llm/token-meter @@ -6195,6 +6201,34 @@ importers: specifier: workspace:^ version: link:../../storage/storage-domain + packages/session/session-stats: + dependencies: + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:^ + version: link:../../../vendor/cordis + '@deepseek-ai/cordis-plugin-include': + specifier: workspace:^ + version: link:../../../vendor/include + '@deepseek-ai/cordis-plugin-loader': + specifier: workspace:^ + version: link:../../../vendor/loader + '@deepseek-ai/dsh-invariants': + specifier: workspace:^ + version: link:../../support/invariants + '@deepseek-ai/dsh-llm': + specifier: workspace:^ + version: link:../../llm/llm + '@deepseek-ai/dsh-session': + specifier: workspace:^ + version: link:../../core/session + '@deepseek-ai/dsh-session-projection': + specifier: workspace:^ + version: link:../session-projection + packages/session/session-telemetry: devDependencies: '@deepseek-ai/cordis': diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index b11a8845e8..3a9ada1fb9 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -120,6 +120,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/sdk/protocol': { kind: 'none', reason: 'Client-facing wire library; the runtime plugins behind the serving entry own model-facing behavior.' }, 'packages/session/session-projection': { kind: 'none', reason: 'The projection registry serves client-facing read models of already-logged session state and registers nothing model-facing.' }, 'packages/session/session-projection-cache': { kind: 'none', reason: 'The persisted cache accelerates host-side cold reads of projection state and registers nothing model-facing.' }, + 'packages/session/session-stats': { kind: 'none', reason: 'The sessionStats unit folds already-logged step boundaries into a client-facing read model and registers nothing model-facing.' }, 'packages/session-query/session-query': { kind: 'none', reason: 'The trusted query service exposes cloned records only to callers and registers nothing model-facing.' }, 'packages/session-query/session-query-sqlite': { kind: 'none', reason: 'The search backend returns hits only to callers and registers nothing model-facing.' }, 'packages/settings/settings': { kind: 'indirect', reason: 'The seam stores and resolves user settings; consumer plugins own any model-facing content fed by a value.' }, diff --git a/tsconfig.base.json b/tsconfig.base.json index ff8e58e361..605abb00d9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -58,6 +58,8 @@ "@deepseek-ai/dsh-tool-todo/client": ["./packages/todo/tool-todo/src/client.ts"], "@deepseek-ai/dsh-session-title/types": ["./packages/session/session-title/src/types.ts"], "@deepseek-ai/dsh-session-title/client": ["./packages/session/session-title/src/client.ts"], + "@deepseek-ai/dsh-session-stats/types": ["./packages/session/session-stats/src/types.ts"], + "@deepseek-ai/dsh-session-stats/client": ["./packages/session/session-stats/src/client.ts"], "@deepseek-ai/dsh-plan-mode/types": ["./packages/plan/plan-mode/src/types.ts"], "@deepseek-ai/dsh-plan-mode/client": ["./packages/plan/plan-mode/src/client.ts"], "@deepseek-ai/dsh-agent-presets/types": ["./packages/preset/agent-presets/src/types.ts"], diff --git a/tsconfig.host.json b/tsconfig.host.json index 118078eb0d..51b46420fa 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -35,6 +35,7 @@ "apps/web/tests/replay-round-trip.e2e.ts", "apps/web/tests/hmr-live.e2e.ts", "apps/web/tests/seeded-history.e2e.ts", + "apps/web/tests/stats-paged-history.e2e.ts", "apps/web/tests/sidebar-scrollbar.e2e.ts", "apps/web/tests/conversation-column-overflow.e2e.ts", "apps/web/tests/code-mode-round.e2e.ts", @@ -137,6 +138,7 @@ { "path": "./packages/session/session-persistence-sqlite" }, { "path": "./packages/session/session-projection" }, { "path": "./packages/session/session-projection-cache" }, + { "path": "./packages/session/session-stats" }, { "path": "./packages/session-query/session-query" }, { "path": "./packages/session-query/session-query-sqlite" }, { "path": "./packages/settings/settings" }, From ca5e67490e0ae650823abc6e2994d651eb6b63c4 Mon Sep 17 00:00:00 2001 From: Turtle Date: Wed, 12 Aug 2026 21:23:06 +0800 Subject: [PATCH 012/160] docs: recommend agent-assisted architecture exploration --- docs/architecture.i18n.yaml | 4 ++-- docs/architecture.md | 2 +- docs/architecture.zh.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index d348645119..5b69ce4564 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/architecture.md -architecture.md: e0909c7924fcb9a8b29b3a3426c847e9ca7f4dfb -architecture.zh.md: b35088aa2a00623eacb2d53b19c86d9c192cd1d0 +architecture.md: bd14a0645c13f9831f57e1966b53b82bd691dbb7 +architecture.zh.md: fd449382d7989c6874e8e8cc04096976fe920418 diff --git a/docs/architecture.md b/docs/architecture.md index e0909c7924..bd14a0645c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,7 +4,7 @@ English | [中文](architecture.zh.md) Read this before changing anything under `packages/`. It assumes you know Cordis; if you do not, start with the [primer](cordis-primer.md) or the [tutorial](cordis-tutorial/index.md). -The repository is large; use an agent to explore it. +We recommend using an agent to explore the codebase and understand its architecture. ## Cordis diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index b35088aa2a..fd449382d7 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -4,7 +4,7 @@ 改动 `packages/` 下的任何内容之前,请先阅读本文。本文假定你已了解 Cordis;如果尚未了解,请先阅读[入门](cordis-primer.md)或[教程](cordis-tutorial/index.md)。 -仓库很大;请借助 agent(智能体)来探索它。 +建议使用 agent(智能体)探索代码库并理解其架构。 ## Cordis From bf9eb7b5381a64b605d33bbd466545eeac9a8b09 Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Wed, 12 Aug 2026 06:37:56 -0700 Subject: [PATCH 013/160] docs: repair merged translation records --- .../feature/2026-08-10-telemetry-default-off.i18n.yaml | 2 -- .../feature/2026-08-10-telemetry-default-off.zh.md | 1 - packages/client/README.i18n.yaml | 4 +--- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml index 751b4454f8..fabb554407 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml @@ -4,5 +4,3 @@ # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md 2026-08-10-telemetry-default-off.md: c3e5d9e0b65449f91044f16e7649f4a5ff2f5b61 2026-08-10-telemetry-default-off.zh.md: 8e2544eb7dee8b9bd6b8a4c81a28a8a03d3404d4 -2026-08-10-telemetry-default-off.md: 8163079eb5f8d6170e329c164141364681030793 -2026-08-10-telemetry-default-off.zh.md: c8e16f84248bde5bdc2c1d4bdb814f0d80fcf77e diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md index 4671e1f6a1..8e2544eb7d 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md @@ -13,7 +13,6 @@ DeepSeek Harness 有两路出站遥测数据流。在内测阶段,共享基础 两路数据流都使用 `DSH_TELEMETRY_MODE` 作为正向授权配置。未设置和空值都解析为 `DISABLED`。`@deepseek-ai/dsh-session-telemetry-otel` 也将省略的 `mode` 解析为 `DISABLED`;该模式不构造 OTel 提供方、处理器或导出器,并将反馈留在本地会话日志中。dsh 共享基础配置继续挂载后端配置行,使禁用模式仍可在记录反馈时说明没有共享任何内容。部署方通过 `FULL` 或 `FEEDBACK_ONLY` 显式启用 Session Log 共享;只有 `FULL` 还允许 dsh-sdk 启动器上报。任何非空 `DSH_TELEMETRY_DISABLED` 仍是具有最高优先级的加载前硬性退出开关。[默认挂载决策](2026-07-31-web-telemetry-default-mount.md)继续负责 endpoint、批处理节奏和退出排空设置。 dsh-sdk 启动器读取同一变量,不解析 `cordis.yml`,也不启动 Cordis。`FULL` 允许上报;`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝。授权在命令执行前从启动环境冻结:`dsh-sdk start` 会加载项目 `.env`,项目代码也能修改 `process.env`,若在执行后解析,项目便能自行授权上报其自身配置,而[配置来源所有权决策](../architecture/2026-08-04-configuration-source-ownership.md)对整个 `DSH_*` 命名空间禁止这种行为。在该边界上,不受支持的模式按拒绝处理而非抛出,因为遥测不得改变命令结果。此规则在启动器及其提案被[SDK 项目工具链移除决策](../simplification/2026-08-11-remove-sdk-project-toolchain.md)删除之前,仅取代了启动器默认允许上报的规则。 -dsh-sdk 启动器读取同一变量,不解析 `cordis.yml`,也不启动 Cordis。`FULL` 允许上报;`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝。授权在命令执行前从启动环境冻结:`dsh-sdk start` 会加载项目 `.env`,项目代码也能修改 `process.env`,若在执行后解析,项目便能自行授权上报其自身配置,而[配置来源所有权决策](../architecture/2026-08-04-configuration-source-ownership.md)对整个 `DSH_*` 命名空间禁止这种行为。在该边界上,不受支持的模式按拒绝处理而非抛出,因为遥测不得改变命令结果。遥测授权由本说明持有;SDK 项目配置或工具链不得代替启动环境显式启用遥测。 带版本的 Web 欢迎通知说明会话日志上传默认关闭,将 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 和 `DSH_TELEMETRY_MODE=FULL` 列为两种显式启用选项,并披露 `FULL` 同时会启用 dsh-sdk 命令遥测。其版本随这项重要的隐私声明一同变更,使每个 profile 都确认当前文案。 diff --git a/packages/client/README.i18n.yaml b/packages/client/README.i18n.yaml index cb78dff700..fe9c9e40a6 100644 --- a/packages/client/README.i18n.yaml +++ b/packages/client/README.i18n.yaml @@ -3,6 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/README.md README.md: b9452d1f763be5be6953cb7973da8a2c909ed979 -README.zh.md: 1225101cff05a3f5655c4660d3d6d86be47d9a30 -README.md: 236531281c17ef982982e97caad99491584bd0b5 -README.zh.md: 73bc3e31c90a4f12c4c5f11e9fd0552601dcc7a6 +README.zh.md: 0dfb6e6b6d619f111e36d5bdf57d127ac1ca9ef5 From 62a437c082b5fd1740247e25eab783dc5abf4e10 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Wed, 12 Aug 2026 21:43:35 +0800 Subject: [PATCH 014/160] review: address ds-review-bot findings on session-stats - stateVersion starts at 1: the package is new, no persisted rows predate it - pendingCalls pairs by own key so a provider callId naming a prototype property cannot fold toolMs to NaN on an unmatched crash-recovery result - StatsLine folds the window fallback only when no sessionStats value is served, and gates the token group on actual token activity instead of steps, so failed-only sessions drop the zero-token group - correct the crash-step counting semantics in the README and Agent Note: recovery closes interrupted steps with a synthetic step/end on reload - reword the window-scoped alternative as a plain rejected option and name isTokenDelta's home beside the StreamChunk type --- ...12-full-session-turn-step-counts.i18n.yaml | 4 ++-- ...026-08-12-full-session-turn-step-counts.md | 6 +++--- ...-08-12-full-session-turn-step-counts.zh.md | 6 +++--- apps/web/tests/math-rendering.e2e.ts | 2 +- .../live-interactions/cancel.expected.md | 2 +- .../live-interactions/error-auth.expected.md | 2 +- .../markdown-cjk-strong/ui.expected.md | 2 +- .../snapshots/markdown-images/ui.expected.md | 2 +- .../markdown-inline-code-links/ui.expected.md | 2 +- .../snapshots/math-rendering/ui.expected.md | 2 +- .../queue-actions/preserved.expected.md | 2 +- .../stats-paged-history/ui.expected.md | 2 +- .../src/client/sessions/assistant-timing.ts | 2 +- .../src/client/chat/StatsLine.tsx | 13 +++++++----- .../tests/chat-stats.client.spec.tsx | 11 ++++++++++ .../session/session-stats/README.i18n.yaml | 4 ++-- packages/session/session-stats/README.md | 2 +- packages/session/session-stats/README.zh.md | 2 +- .../session/session-stats/src/projection.ts | 8 +++++-- .../session-stats/tests/projection.spec.ts | 21 +++++++++++++++++++ 20 files changed, 68 insertions(+), 29 deletions(-) diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.i18n.yaml index f50df929a7..18ed791f68 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md -2026-08-12-full-session-turn-step-counts.md: 7cea57e429d3ffc2da49c4a1359ee3489663ccc5 -2026-08-12-full-session-turn-step-counts.zh.md: 93e46cfb7ffa4f397b85ee80b26cca9f664d246a +2026-08-12-full-session-turn-step-counts.md: ecfa00dc3e24101953a9d5a724dba17682d839bd +2026-08-12-full-session-turn-step-counts.zh.md: 85e2a26b5af51296e20f29af49e909c6e182ea05 diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md index 7cea57e429..ecfa00dc3e 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md +++ b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.md @@ -10,7 +10,7 @@ The web chat stats strip folded `StatsLine`'s loaded conversation window (`deriv ## Decision -A new function plugin `@deepseek-ai/dsh-session-stats` registers a `sessionStats` projection unit on `ctx.sessionProjections`, mounted as a web-app bundle row. The value carries the strip's whole non-token figure set — `{ turns, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens }`, field names mirroring the window fold so the two swap wholesale. `steps` counts `step/end` events and `turns` counts distinct turns carrying at least one (turn numbers are monotonic, so one `lastTurn` slot suffices); `llmMs` sums `step/start` → `assistant/message`; TTFT records the first non-empty delta chunk per step (surviving in-step `llm/retry`, the window `resetForRetry` parity); decode spans first token → assembled message on usage-reporting steps; `toolMs` pairs `tool/call` → `tool/result` by callId with unresolved calls dropped at `turn/end`. The first-token predicate `isTokenDelta` moved to `@deepseek-ai/dsh-llm/message` (the `StreamChunk` vocabulary owner) so the host fold and the client timing index share one implementation; client-runtime re-exports it. Delivery is entirely the existing projection seam — history tail-page block, `session/projection` push frames, list rows — with zero changes to apiproxy, wire schemas, or the client runtime. `StatsLine` reads `useProjection('sessionStats')` and falls back to the window fold when the key is undefined (an assembly without the unit). The client connection fixture mirrors the fold as `sessionStatsOf` under its existing every-composed-key discipline. +A new function plugin `@deepseek-ai/dsh-session-stats` registers a `sessionStats` projection unit on `ctx.sessionProjections`, mounted as a web-app bundle row. The value carries the strip's whole non-token figure set — `{ turns, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens }`, field names mirroring the window fold so the two swap wholesale. `steps` counts `step/end` events and `turns` counts distinct turns carrying at least one (turn numbers are monotonic, so one `lastTurn` slot suffices); `llmMs` sums `step/start` → `assistant/message`; TTFT records the first non-empty delta chunk per step (surviving in-step `llm/retry`, the window `resetForRetry` parity); decode spans first token → assembled message on usage-reporting steps; `toolMs` pairs `tool/call` → `tool/result` by callId with unresolved calls dropped at `turn/end`. The first-token predicate `isTokenDelta` moved to `@deepseek-ai/dsh-llm/message` (beside the `StreamChunk` type it discriminates) so the host fold and the client timing index share one implementation; client-runtime re-exports it. Delivery is entirely the existing projection seam — history tail-page block, `session/projection` push frames, list rows — with zero changes to apiproxy, wire schemas, or the client runtime. `StatsLine` reads `useProjection('sessionStats')` and falls back to the window fold when the key is undefined (an assembly without the unit). The client connection fixture mirrors the fold as `sessionStatsOf` under its existing every-composed-key discipline. `step/end` — not `assistant/message` — is the counted event, for two correctness reasons found while reviewing the obvious message-counting design: @@ -31,8 +31,8 @@ A new function plugin `@deepseek-ai/dsh-session-stats` registers a `sessionStats **Fold the full log client-side.** The client holds only the paged window by design; the projection RFC's no-client-folding rule exists exactly so figures survive paging, compaction, and cold reads. -**Keep wall times, TTFT, and throughput window-scoped.** The first shipped cut did, reading them as "what is on screen"; the same paging complaint immediately applied to the LLM duration, and a strip mixing whole-log counts with window-scoped times reads as one inconsistent figure set. The projection now carries the whole set, with the window fold demoted to the no-unit fallback. +**Keep wall times, TTFT, and throughput window-scoped, reading them as "what is on screen".** Rejected: the same paging complaint applies to the LLM duration, and a strip mixing whole-log counts with window-scoped times reads as one inconsistent figure set. The projection carries the whole set, with the window fold demoted to the no-unit fallback. ## Consequences -The strip shows whole-log figures from the first tail page; paging leaves every group fixed. Defined edge differences from the old window semantics are documented in the package README: a step that produced no visible output (failed before content) still counts, a step truncated by a crash between `step/start` and `step/end` does not, a cancelled step is counted but contributes no wall time (no message assembled), and a max-tokens usage-host message contributes model time the surface does not show. Every web tail page and list row carries one more small key, and the unit's internal state changes on step boundaries and first-token chunks, so the change feed emits a few value-identical frames per step; TUI and headless assemblies serve no `sessionStats` key and any consumer falls back to window folding. Two e2e probes that had parsed the strip as a loaded-window measure (`chat-scroll-contract`, `complex-history.perf`) now count mounted flow rows / turn-tail footers instead. The `stats-paged-history` web scenario seeds a 28-turn log cold and pins that the whole strip reads full totals on a partial tail page and does not move across Load earlier. +The strip shows whole-log figures from the first tail page; paging leaves every group fixed. Defined edge differences from the old window semantics are documented in the package README: a step that produced no visible output (failed before content) still counts, a step interrupted by a crash counts once recovery closes it with a synthetic `step/end` on reload (`interruptedTurnClosers`), a cancelled step is counted but contributes no wall time (no message assembled), and a max-tokens usage-host message contributes model time the surface does not show. Every web tail page and list row carries one more small key, and the unit's internal state changes on step boundaries and first-token chunks, so the change feed emits a few value-identical frames per step; TUI and headless assemblies serve no `sessionStats` key and any consumer falls back to window folding. Two e2e probes that had parsed the strip as a loaded-window measure (`chat-scroll-contract`, `complex-history.perf`) now count mounted flow rows / turn-tail footers instead. The `stats-paged-history` web scenario seeds a 28-turn log cold and pins that the whole strip reads full totals on a partial tail page and does not move across Load earlier. diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.zh.md b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.zh.md index 93e46cfb7f..85e2a26b5a 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-12-full-session-turn-step-counts.zh.md @@ -10,7 +10,7 @@ Web 聊天统计条的每个非 token 数字都折算自 `StatsLine` 已加载 ## 决定 -新的函数插件 `@deepseek-ai/dsh-session-stats` 在 `ctx.sessionProjections` 上注册 `sessionStats` 投影单元,作为 web-app bundle 行挂载。值携带统计条完整的非 token 数字集——`{ turns, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens }`,字段名与窗口折叠一一对应以便整体互换。`steps` 统计 `step/end` 事件,`turns` 统计含至少一条该事件的不同 turn(turn 号单调递增,一个 `lastTurn` 槽即可);`llmMs` 累加 `step/start` → `assistant/message`;TTFT 记录每步首个非空 delta chunk(在步内 `llm/retry` 后保留,与窗口 `resetForRetry` 对齐);解码时长覆盖首 token → 已组装消息、仅统计上报 usage 的步;`toolMs` 按 callId 配对 `tool/call` → `tool/result`,未解决的调用在 `turn/end` 时丢弃。首 token 谓词 `isTokenDelta` 移入 `@deepseek-ai/dsh-llm/message`(`StreamChunk` 词汇的属主),Host 折叠与客户端计时索引共用同一实现;client-runtime 转发导出。投递完全复用现有投影缝——history 尾页块、`session/projection` 推送帧、列表行——apiproxy、wire schema 与客户端运行时零改动。`StatsLine` 读取 `useProjection('sessionStats')`,键为 undefined(未组合该单元的装配)时整体回退到窗口折叠。客户端 connection fixture 按其「镜像每个已组合键」的既有纪律以 `sessionStatsOf` 平行实现该折叠。 +新的函数插件 `@deepseek-ai/dsh-session-stats` 在 `ctx.sessionProjections` 上注册 `sessionStats` 投影单元,作为 web-app bundle 行挂载。值携带统计条完整的非 token 数字集——`{ turns, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens }`,字段名与窗口折叠一一对应以便整体互换。`steps` 统计 `step/end` 事件,`turns` 统计含至少一条该事件的不同 turn(turn 号单调递增,一个 `lastTurn` 槽即可);`llmMs` 累加 `step/start` → `assistant/message`;TTFT 记录每步首个非空 delta chunk(在步内 `llm/retry` 后保留,与窗口 `resetForRetry` 对齐);解码时长覆盖首 token → 已组装消息、仅统计上报 usage 的步;`toolMs` 按 callId 配对 `tool/call` → `tool/result`,未解决的调用在 `turn/end` 时丢弃。首 token 谓词 `isTokenDelta` 移入 `@deepseek-ai/dsh-llm/message`(与其判别的 `StreamChunk` 类型同处),Host 折叠与客户端计时索引共用同一实现;client-runtime 转发导出。投递完全复用现有投影缝——history 尾页块、`session/projection` 推送帧、列表行——apiproxy、wire schema 与客户端运行时零改动。`StatsLine` 读取 `useProjection('sessionStats')`,键为 undefined(未组合该单元的装配)时整体回退到窗口折叠。客户端 connection fixture 按其「镜像每个已组合键」的既有纪律以 `sessionStatsOf` 平行实现该折叠。 计数事件选 `step/end` 而非 `assistant/message`,源于评审直觉方案(按消息计数)时发现的两个正确性问题: @@ -31,8 +31,8 @@ Web 聊天统计条的每个非 token 数字都折算自 `StatsLine` 已加载 **在客户端折叠全量日志。** 客户端按设计只持有分页窗口;投影 RFC 的「不在客户端折叠」规则正是为了让数字在分页、压缩与冷读之间存活。 -**墙钟时间、TTFT 与吞吐保持窗口口径。** 首个交付版本如此,将其解读为「屏幕上有什么」;同样的分页问题立刻落在 LLM 时长上,且全量计数与窗口时间混在一条统计条里读起来是一套自相矛盾的数字。投影现在携带完整集合,窗口折叠降级为无单元时的回退。 +**墙钟时间、TTFT 与吞吐保持窗口口径,解读为「屏幕上有什么」。** 否决:同样的分页问题一样落在 LLM 时长上,且全量计数与窗口时间混在一条统计条里读起来是一套自相矛盾的数字。投影携带完整集合,窗口折叠降级为无单元时的回退。 ## 后果 -统计条从第一个尾页起就显示全日志数字;翻页不再改变任何分组。与旧窗口语义的已定义边缘差异记录在包 README 中:未产生可见输出的步(在内容之前失败)仍计入;崩溃恰好截断在 `step/start` 与 `step/end` 之间的步不计;被取消的步计数但不计时(没有组装出消息);max-tokens 的 usage 宿主消息贡献 surface 上看不到的模型时间。每个 web 尾页与列表行多携带一个小键,且单元内部状态在步边界与首 token chunk 处变化,变更流每步会多发几帧值相同的推送;TUI 与 headless 装配不提供 `sessionStats` 键,其消费者回退窗口折叠。两个曾把统计条当作已加载窗口探针解析的 e2e(`chat-scroll-contract`、`complex-history.perf`)改为统计已挂载的消息流行/turn-tail 页脚。`stats-paged-history` web 场景冷种一份 28 轮日志,钉住整条统计条在不完整尾页上即读出全量数字、且「加载更早」前后不变。 +统计条从第一个尾页起就显示全日志数字;翻页不再改变任何分组。与旧窗口语义的已定义边缘差异记录在包 README 中:未产生可见输出的步(在内容之前失败)仍计入;被崩溃打断的步在重新加载、恢复为其补写合成 `step/end` 后计入(`interruptedTurnClosers`);被取消的步计数但不计时(没有组装出消息);max-tokens 的 usage 宿主消息贡献 surface 上看不到的模型时间。每个 web 尾页与列表行多携带一个小键,且单元内部状态在步边界与首 token chunk 处变化,变更流每步会多发几帧值相同的推送;TUI 与 headless 装配不提供 `sessionStats` 键,其消费者回退窗口折叠。两个曾把统计条当作已加载窗口探针解析的 e2e(`chat-scroll-contract`、`complex-history.perf`)改为统计已挂载的消息流行/turn-tail 页脚。`stats-paged-history` web 场景冷种一份 28 轮日志,钉住整条统计条在不完整尾页上即读出全量数字、且「加载更早」前后不变。 diff --git a/apps/web/tests/math-rendering.e2e.ts b/apps/web/tests/math-rendering.e2e.ts index de24c1ca76..67af32373c 100644 --- a/apps/web/tests/math-rendering.e2e.ts +++ b/apps/web/tests/math-rendering.e2e.ts @@ -120,7 +120,7 @@ describe('web e2e: settled Markdown math rendering', () => { await expect.poll(() => page.locator('.katex-display').count(), { timeout: 10_000 }).toBe(2) expect(await page.locator('.katex-error').count()).toBe(0) await expect.poll( - () => page.getByText('Input 0 tok · Output 0 tok', { exact: false }).count(), + () => page.getByText('1 turns · 1 steps', { exact: false }).count(), { timeout: 10_000 }, ).toBe(1) diff --git a/apps/web/tests/snapshots/live-interactions/cancel.expected.md b/apps/web/tests/snapshots/live-interactions/cancel.expected.md index 4ea903679c..bafa739a58 100644 --- a/apps/web/tests/snapshots/live-interactions/cancel.expected.md +++ b/apps/web/tests/snapshots/live-interactions/cancel.expected.md @@ -31,4 +31,4 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps Input 0 tok · Output 0 tok +- text: 1 turns · 1 steps diff --git a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md index 975ebe3a0d..341ddf22db 100644 --- a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md +++ b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md @@ -27,4 +27,4 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps Input 0 tok · Output 0 tok +- text: 1 turns · 1 steps diff --git a/apps/web/tests/snapshots/markdown-cjk-strong/ui.expected.md b/apps/web/tests/snapshots/markdown-cjk-strong/ui.expected.md index dbaca89b8a..adae2f723e 100644 --- a/apps/web/tests/snapshots/markdown-cjk-strong/ui.expected.md +++ b/apps/web/tests/snapshots/markdown-cjk-strong/ui.expected.md @@ -53,4 +53,4 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} Input 0 tok · Output 0 tok +- text: 1 turns · 1 steps LLM {{duration}} diff --git a/apps/web/tests/snapshots/markdown-images/ui.expected.md b/apps/web/tests/snapshots/markdown-images/ui.expected.md index f90d10f616..85c537bb18 100644 --- a/apps/web/tests/snapshots/markdown-images/ui.expected.md +++ b/apps/web/tests/snapshots/markdown-images/ui.expected.md @@ -32,4 +32,4 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} Input 0 tok · Output 0 tok +- text: 1 turns · 1 steps LLM {{duration}} diff --git a/apps/web/tests/snapshots/markdown-inline-code-links/ui.expected.md b/apps/web/tests/snapshots/markdown-inline-code-links/ui.expected.md index f345357f1b..d940beabc9 100644 --- a/apps/web/tests/snapshots/markdown-inline-code-links/ui.expected.md +++ b/apps/web/tests/snapshots/markdown-inline-code-links/ui.expected.md @@ -44,4 +44,4 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} Input 0 tok · Output 0 tok +- text: 1 turns · 1 steps LLM {{duration}} diff --git a/apps/web/tests/snapshots/math-rendering/ui.expected.md b/apps/web/tests/snapshots/math-rendering/ui.expected.md index 0bb9a9b19b..5561c3574e 100644 --- a/apps/web/tests/snapshots/math-rendering/ui.expected.md +++ b/apps/web/tests/snapshots/math-rendering/ui.expected.md @@ -48,4 +48,4 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps LLM {{duration}} Input 0 tok · Output 0 tok +- text: 1 turns · 1 steps LLM {{duration}} diff --git a/apps/web/tests/snapshots/queue-actions/preserved.expected.md b/apps/web/tests/snapshots/queue-actions/preserved.expected.md index 7951af37e4..43c9665ac1 100644 --- a/apps/web/tests/snapshots/queue-actions/preserved.expected.md +++ b/apps/web/tests/snapshots/queue-actions/preserved.expected.md @@ -50,4 +50,4 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 1 turns · 1 steps Input 0 tok · Output 0 tok +- text: 1 turns · 1 steps diff --git a/apps/web/tests/snapshots/stats-paged-history/ui.expected.md b/apps/web/tests/snapshots/stats-paged-history/ui.expected.md index 8179078b8b..78d175af5d 100644 --- a/apps/web/tests/snapshots/stats-paged-history/ui.expected.md +++ b/apps/web/tests/snapshots/stats-paged-history/ui.expected.md @@ -354,4 +354,4 @@ - text: DeepSeek-V4-Flash - img - button "Send message" [disabled] -- text: 28 turns · 28 steps LLM {{duration}} Input 0 tok · Output 0 tok +- text: 28 turns · 28 steps LLM {{duration}} diff --git a/packages/client/runtime/src/client/sessions/assistant-timing.ts b/packages/client/runtime/src/client/sessions/assistant-timing.ts index 8f58f8daab..179f76281d 100644 --- a/packages/client/runtime/src/client/sessions/assistant-timing.ts +++ b/packages/client/runtime/src/client/sessions/assistant-timing.ts @@ -6,7 +6,7 @@ import { isTokenDelta } from '@deepseek-ai/dsh-llm/message' import type { SessionEvent } from '@deepseek-ai/dsh-session/types' import type { AssistantTiming } from './conversation.ts' -// The first-token predicate lives with the StreamChunk vocabulary in dsh-llm; +// The first-token predicate lives beside the StreamChunk type in dsh-llm; // re-exported here so Chat Definitions keep their client-runtime import. export { isTokenDelta } from '@deepseek-ai/dsh-llm/message' diff --git a/packages/client/ui-conversation/src/client/chat/StatsLine.tsx b/packages/client/ui-conversation/src/client/chat/StatsLine.tsx index 177afdb05a..147d2b7c6c 100644 --- a/packages/client/ui-conversation/src/client/chat/StatsLine.tsx +++ b/packages/client/ui-conversation/src/client/chat/StatsLine.tsx @@ -162,12 +162,13 @@ export interface StatsLineProps { export const StatsLine = memo(function StatsLine({ useSession, useProjection, t }: StatsLineProps) { const settledNodes = useSession(s => s.chat.legacy.nodes) - const windowStats = useMemo(() => deriveStats(settledNodes), [settledNodes]) const usage = useProjection('tokenUsage') // Every figure rides the durable sessionStats projection, so paging and // compaction cannot change any of them; an assembly without the unit falls - // back to the window-scoped fold wholesale (same field names). - const stats = useProjection('sessionStats') ?? windowStats + // back to the window-scoped fold wholesale (same field names), paid only + // while no projection value is served. + const projected = useProjection('sessionStats') + const stats = useMemo(() => projected ?? deriveStats(settledNodes), [projected, settledNodes]) // Pipe-separated groups (figma stats strip); a group with no data drops out whole. const groups: string[] = [] if (stats.steps > 0) { @@ -190,9 +191,11 @@ export const StatsLine = memo(function StatsLine({ useSession, useProjection, t // Context occupancy deliberately lives on the composer's ContextMeter ring, // not here — one home per fact. // Billing rides the durable projection, so these survive paging and - // compaction. Suppress the empty projection on a brand-new session. + // compaction. Gated on actual token activity: a session whose steps all + // settled without billing (e.g. every request failed) shows its counts + // without a zero-token group. if (usage !== undefined - && (stats.steps > 0 || billedInputTokens(usage) > 0 || usage.outputTokens > 0)) { + && (billedInputTokens(usage) > 0 || usage.outputTokens > 0)) { const cacheHit = cacheHitPercent(usage) if (cacheHit !== null) groups.push(t('stats.cacheHit', { percent: cacheHit })) groups.push(t('stats.tokens', { diff --git a/packages/client/ui-conversation/tests/chat-stats.client.spec.tsx b/packages/client/ui-conversation/tests/chat-stats.client.spec.tsx index 6abcf611d2..4ace851a66 100644 --- a/packages/client/ui-conversation/tests/chat-stats.client.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-stats.client.spec.tsx @@ -314,6 +314,17 @@ describe('StatsLine', () => { expect(view.container.textContent).toBe('') }) + it('hides the zero-token group when steps closed without any billed activity', () => { + // A session whose only turn failed before billing (e.g. an auth error): + // the counts group renders alone, not an uninformative zero-token group. + const { source } = makeSource() + const view = render() + expect(view.container.textContent).toBe('1 turns · 1 steps') + }) + it('keeps the counts group over an empty visible window when the projection carries totals', () => { // Extends the durable-groups guarantee: full-session counts survive a // window that compaction (or paging) left without assistant nodes. diff --git a/packages/session/session-stats/README.i18n.yaml b/packages/session/session-stats/README.i18n.yaml index 8896173827..d12db01c69 100644 --- a/packages/session/session-stats/README.i18n.yaml +++ b/packages/session/session-stats/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-stats/README.md -README.md: a42d5b1b6efe8ff522a43431f1ccb68b71607876 -README.zh.md: 5929280a8a619fb37a6f6468fb839962702a68c3 +README.md: 81b0de17e335b67936afd6fb11f15411beee3b76 +README.zh.md: 606628ea09bc34203a5374e49db62c1646293846 diff --git a/packages/session/session-stats/README.md b/packages/session/session-stats/README.md index a42d5b1b6e..81b0de17e3 100644 --- a/packages/session/session-stats/README.md +++ b/packages/session/session-stats/README.md @@ -33,7 +33,7 @@ None; the plugin never assembles or sends provider requests. ## Known Limitations and Deferred Work -- **Steps count work attempted, not visible output** — a step that failed before producing any visible content still closed with `step/end` and counts; a step truncated by a crash between `step/start` and `step/end` does not. +- **Steps count work attempted, not visible output** — a step that failed before producing any visible content still closed with `step/end` and counts; a step interrupted by a crash counts after the session reloads, when crash recovery appends its synthetic `step/end` (`interruptedTurnClosers` in dsh-session). - **A cancelled step is counted but untimed** — no assistant message assembles, so its partial stream time enters no wall-time figure, matching the window fold's untimed interrupted node; a max-tokens usage-host message conversely contributes model time the surface does not show. - **Counts are log-scoped, not surface-scoped** — steps whose messages were later compacted away stay counted; the figures describe the whole session, not the current model-visible surface. - **Mounted only in the web-app bundle** — other assemblies serve no `sessionStats` key, and their consumers fall back to window-scoped counting (the web stats strip's fallback path). diff --git a/packages/session/session-stats/README.zh.md b/packages/session/session-stats/README.zh.md index 5929280a8a..606628ea09 100644 --- a/packages/session/session-stats/README.zh.md +++ b/packages/session/session-stats/README.zh.md @@ -33,7 +33,7 @@ ## 已知局限与延后工作 -- **步数统计的是已发生的工作,而非可见输出**——在产生任何可见内容前就失败的步仍以 `step/end` 关闭并计入;进程崩溃恰好截断在 `step/start` 与 `step/end` 之间的步不计。 +- **步数统计的是已发生的工作,而非可见输出**——在产生任何可见内容前就失败的步仍以 `step/end` 关闭并计入;被崩溃打断的步在会话重新加载后计入,届时崩溃恢复为其补写合成的 `step/end`(dsh-session 的 `interruptedTurnClosers`)。 - **被取消的步计数但不计时**——没有组装出 assistant 消息,其部分流式时间不进入任何墙钟数字,与窗口折叠的无计时 interrupted 节点一致;反之 max-tokens 的 usage 宿主消息贡献 surface 上看不到的模型时间。 - **计数是日志口径,不是 surface 口径**——消息后来被压缩掉的步仍然计入;数字描述整个会话,而非当前模型可见 surface。 - **仅挂载于 web-app bundle**——其他装配不提供 `sessionStats` 键,其消费者回退到窗口口径计数(Web 统计条的回退路径)。 diff --git a/packages/session/session-stats/src/projection.ts b/packages/session/session-stats/src/projection.ts index e155622920..a000300873 100644 --- a/packages/session/session-stats/src/projection.ts +++ b/packages/session/session-stats/src/projection.ts @@ -140,8 +140,12 @@ export const sessionStatsProjectionDefinition: ProjectionDefinition<'sessionStat case 'tool/call': return { ...state, pendingCalls: { ...state.pendingCalls, [event.data.callId]: event.time } } case 'tool/result': { + // Own-key check: callId is provider-minted (model/tool JSON boundary), + // so a prototype property name ('constructor', 'toString') on a result + // with no recorded call must read as unmatched, not as an inherited + // function that would poison toolMs with NaN. const callId = event.data.message.source.callId - const dispatched = state.pendingCalls[callId] + const dispatched = Object.hasOwn(state.pendingCalls, callId) ? state.pendingCalls[callId] : undefined if (dispatched === undefined) return state const pendingCalls = Object.fromEntries( Object.entries(state.pendingCalls).filter(([id]) => id !== callId), @@ -175,5 +179,5 @@ export const sessionStatsProjectionDefinition: ProjectionDefinition<'sessionStat decodeMs: state.decodeMs, decodeTokens: state.decodeTokens, }), - stateVersion: 2, + stateVersion: 1, } diff --git a/packages/session/session-stats/tests/projection.spec.ts b/packages/session/session-stats/tests/projection.spec.ts index fce09b4e9e..ebe728181b 100644 --- a/packages/session/session-stats/tests/projection.spec.ts +++ b/packages/session/session-stats/tests/projection.spec.ts @@ -237,6 +237,27 @@ describe('sessionStats wall-time fold (controlled timestamps)', () => { expect(pruned).toEqual(totals({ turns: 1, steps: 1 })) }) + it('pairs only own pendingCalls keys: a prototype-name callId without a recorded call stays unmatched', () => { + const result = (callId: string): unknown => + ({ turn: 1, step: 1, message: { source: { kind: 'tool', callId } } }) + // Crash recovery (TOOL_NOT_STARTED) emits results with no preceding + // tool/call; a provider-minted callId colliding with an Object prototype + // property must read as absent, not as an inherited function that would + // fold toolMs to NaN and fail the value schema. + expect(fold([ + at(1_000, 'step/start', { turn: 1, step: 1 }), + at(1_500, 'tool/result', result('toString')), + at(2_000, 'step/end', { turn: 1, step: 1 }), + ])).toEqual(totals({ turns: 1, steps: 1 })) + // The same name pairs normally once its call is recorded. + expect(fold([ + at(1_000, 'step/start', { turn: 1, step: 1 }), + at(1_100, 'tool/call', { turn: 1, step: 1, callId: 'constructor', name: 'read', arguments: '{}' }), + at(1_600, 'tool/result', result('constructor')), + at(2_000, 'step/end', { turn: 1, step: 1 }), + ])).toEqual(totals({ turns: 1, steps: 1, toolMs: 500 })) + }) + it('skips decode for an invalid usage report and ignores a duplicate assembled message', () => { const events = [ at(1_000, 'step/start', { turn: 1, step: 1 }), From 47e3e6f0ed40dfe8f424456899a25ad3e084ab75 Mon Sep 17 00:00:00 2001 From: fz Date: Wed, 12 Aug 2026 21:47:28 +0800 Subject: [PATCH 015/160] feat(web): rename code preset to PTC mode --- apps/cli/config/agent-presets/code/preset.yml | 2 +- .../snapshots/agent-preset-authoring/created.expected.md | 8 ++++---- .../snapshots/agent-preset-authoring/damaged.expected.md | 8 ++++---- .../snapshots/agent-preset-authoring/section.expected.md | 8 ++++---- packages/client/ui-agent-preset/src/client/locales.ts | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/cli/config/agent-presets/code/preset.yml b/apps/cli/config/agent-presets/code/preset.yml index 17eaccb871..fc0836f479 100644 --- a/apps/cli/config/agent-presets/code/preset.yml +++ b/apps/cli/config/agent-presets/code/preset.yml @@ -1,3 +1,3 @@ -name: 代码模式 +name: PTC 模式 description: 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。 order: 2 diff --git a/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md index 40b9497831..ed674dfd57 100644 --- a/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md +++ b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md @@ -35,13 +35,13 @@ - img - text: 复制 - listitem: - - 'button "设为默认: 代码模式"': - - text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。 + - 'button "设为默认: PTC 模式"': + - text: PTC 模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。 - code: code - - 'button "查看: 代码模式"': + - 'button "查看: PTC 模式"': - img - text: 查看 - - 'button "复制: 代码模式"': + - 'button "复制: PTC 模式"': - img - text: 复制 - listitem: diff --git a/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md index c3e9035098..688fb65822 100644 --- a/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md +++ b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md @@ -35,13 +35,13 @@ - img - text: 复制 - listitem: - - 'button "设为默认: 代码模式"': - - text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。 + - 'button "设为默认: PTC 模式"': + - text: PTC 模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。 - code: code - - 'button "查看: 代码模式"': + - 'button "查看: PTC 模式"': - img - text: 查看 - - 'button "复制: 代码模式"': + - 'button "复制: PTC 模式"': - img - text: 复制 - listitem: diff --git a/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md index e411e8ea28..b4bd5f44e6 100644 --- a/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md +++ b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md @@ -35,13 +35,13 @@ - img - text: 复制 - listitem: - - 'button "设为默认: 代码模式"': - - text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。 + - 'button "设为默认: PTC 模式"': + - text: PTC 模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。 - code: code - - 'button "查看: 代码模式"': + - 'button "查看: PTC 模式"': - img - text: 查看 - - 'button "复制: 代码模式"': + - 'button "复制: PTC 模式"': - img - text: 复制 - listitem: diff --git a/packages/client/ui-agent-preset/src/client/locales.ts b/packages/client/ui-agent-preset/src/client/locales.ts index 2a3dee06f3..54acc298a1 100644 --- a/packages/client/ui-agent-preset/src/client/locales.ts +++ b/packages/client/ui-agent-preset/src/client/locales.ts @@ -100,7 +100,7 @@ export const zh: Record = { view: '查看', presetStandardName: '标准模式', presetStandardDescription: '功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。', - presetCodeName: '代码模式', + presetCodeName: 'PTC 模式', presetCodeDescription: '具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。', presetMinimalName: '极简模式', presetMinimalDescription: '仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。', From cb31f5e5b58443e78bdeefb6099c4455dd34e4a3 Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Wed, 12 Aug 2026 21:48:18 +0800 Subject: [PATCH 016/160] docs: regenerate the module graph for dsh-session-stats --- docs/module-graph.i18n.yaml | 4 ++-- docs/module-graph.md | 9 ++++++++- docs/module-graph.zh.md | 9 ++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index 980d198c32..b528a4356d 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: 371699d8a4aab83eb8603ce1623373fe56871dd3 -module-graph.zh.md: 381a387ff77ef36dda31655bb9c0e4d5b931e544 +module-graph.md: 5197a184f2be283e3bb57de91d4a4d3cb22e51b2 +module-graph.zh.md: 3b312b731d32ed71674d800c35a5868e0b5d94ee diff --git a/docs/module-graph.md b/docs/module-graph.md index 371699d8a4..5197a184f2 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -274,6 +274,7 @@ flowchart TD pkg_session_persistence_sqlite["session-persistence-sqlite"] pkg_session_projection["session-projection"] pkg_session_projection_cache["session-projection-cache"] + pkg_session_stats["session-stats"] pkg_session_telemetry["session-telemetry"] pkg_session_telemetry_otel["session-telemetry-otel"] pkg_session_title["session-title"] @@ -568,6 +569,10 @@ flowchart TD pkg_session_projection_cache --> pkg_session_persistence pkg_session_projection_cache --> pkg_session_projection pkg_session_projection_cache --> pkg_storage_domain + pkg_session_stats --> pkg_invariants + pkg_session_stats --> pkg_llm + pkg_session_stats --> pkg_session + pkg_session_stats --> pkg_session_projection pkg_session_telemetry --> pkg_agent pkg_session_telemetry --> pkg_invariants pkg_session_telemetry --> pkg_session @@ -1212,6 +1217,7 @@ flowchart TD pkg_client_ui_conversation --> pkg_compact pkg_client_ui_conversation --> pkg_invariants pkg_client_ui_conversation --> pkg_llm_retry + pkg_client_ui_conversation --> pkg_session_stats pkg_client_ui_conversation --> pkg_token_meter pkg_client_ui_conversation --> pkg_tools pkg_client_ui_directory_picker --> pkg_client_locale @@ -1461,6 +1467,7 @@ flowchart TD | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | | [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | | [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) | +| [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | | [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`session-title`](../packages/session/session-title) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | | [`tasks`](../packages/tasks/tasks) | `tasks` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | @@ -1565,7 +1572,7 @@ flowchart TD | [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | | [`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) | | [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/support/invariants), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | -| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-attachment`](../packages/client/ui-attachment), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | +| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-attachment`](../packages/client/ui-attachment), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`session-stats`](../packages/session/session-stats), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | | [`client-ui-directory-picker`](../packages/client/ui-directory-picker) | `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), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | | [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | | [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 381a387ff7..3b312b731d 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -276,6 +276,7 @@ flowchart TD pkg_session_persistence_sqlite["session-persistence-sqlite"] pkg_session_projection["session-projection"] pkg_session_projection_cache["session-projection-cache"] + pkg_session_stats["session-stats"] pkg_session_telemetry["session-telemetry"] pkg_session_telemetry_otel["session-telemetry-otel"] pkg_session_title["session-title"] @@ -570,6 +571,10 @@ flowchart TD pkg_session_projection_cache --> pkg_session_persistence pkg_session_projection_cache --> pkg_session_projection pkg_session_projection_cache --> pkg_storage_domain + pkg_session_stats --> pkg_invariants + pkg_session_stats --> pkg_llm + pkg_session_stats --> pkg_session + pkg_session_stats --> pkg_session_projection pkg_session_telemetry --> pkg_agent pkg_session_telemetry --> pkg_invariants pkg_session_telemetry --> pkg_session @@ -1214,6 +1219,7 @@ flowchart TD pkg_client_ui_conversation --> pkg_compact pkg_client_ui_conversation --> pkg_invariants pkg_client_ui_conversation --> pkg_llm_retry + pkg_client_ui_conversation --> pkg_session_stats pkg_client_ui_conversation --> pkg_token_meter pkg_client_ui_conversation --> pkg_tools pkg_client_ui_directory_picker --> pkg_client_locale @@ -1463,6 +1469,7 @@ flowchart TD | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | | [`session-persistence-sqlite`](../packages/session/session-persistence-sqlite) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | | [`session-projection-cache`](../packages/session/session-projection-cache) | `session` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain) | +| [`session-stats`](../packages/session/session-stats) | `session` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | | [`session-telemetry`](../packages/session/session-telemetry) | `session` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`session-title`](../packages/session/session-title) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | | [`tasks`](../packages/tasks/tasks) | `tasks` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | @@ -1567,7 +1574,7 @@ flowchart TD | [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | | [`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) | | [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/support/invariants), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | -| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-attachment`](../packages/client/ui-attachment), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | +| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-attachment`](../packages/client/ui-attachment), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`session-stats`](../packages/session/session-stats), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | | [`client-ui-directory-picker`](../packages/client/ui-directory-picker) | `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), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | | [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | | [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) | From 1ca6f68b173d8a4c2625ab060270ad08737aed48 Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Wed, 12 Aug 2026 05:07:47 -0700 Subject: [PATCH 017/160] fix(web): guide clickable file references --- ...6-08-07-web-inline-file-mentions.i18n.yaml | 4 +-- .../2026-08-07-web-inline-file-mentions.md | 10 ++++-- .../2026-08-07-web-inline-file-mentions.zh.md | 10 ++++-- apps/web/tests/shipped-composition.e2e.ts | 18 ++++++++--- .../file-reference-prompt.expected.md | 1 + docs/config-catalog.md | 2 +- .../client/ui-deliverables/README.i18n.yaml | 4 +-- packages/client/ui-deliverables/README.md | 17 ++++++++-- packages/client/ui-deliverables/README.zh.md | 17 ++++++++-- packages/client/ui-deliverables/package.json | 4 ++- packages/client/ui-deliverables/src/index.ts | 31 +++++++++++++++---- .../client/ui-deliverables/src/invariant.ts | 7 ++--- .../tests/produced-files.client.spec.tsx | 5 +-- .../tests/prompt.client.spec.ts | 30 ++++++++++++++++++ packages/client/ui-deliverables/tsconfig.json | 3 ++ pnpm-lock.yaml | 3 ++ .../verify-package-readme-model-experience.ts | 1 - 17 files changed, 130 insertions(+), 37 deletions(-) create mode 100644 apps/web/tests/snapshots/web-runtime-context/file-reference-prompt.expected.md create mode 100644 packages/client/ui-deliverables/tests/prompt.client.spec.ts diff --git a/.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.i18n.yaml b/.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.i18n.yaml index b86faaa501..b97dae4740 100644 --- a/.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.md -2026-08-07-web-inline-file-mentions.md: cb51e6cfa5eebe0341b50c7638d51ac2d8641ba7 -2026-08-07-web-inline-file-mentions.zh.md: 160d3838fc8d02e57aa4371cb9d8a9ec7ccf6b5b +2026-08-07-web-inline-file-mentions.md: 0c7341e9aec688a0661fedb18731a1c172215f45 +2026-08-07-web-inline-file-mentions.zh.md: a9ef53933647feb27f9dde2d454166cf17de4fbe diff --git a/.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.md b/.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.md index cb51e6cfa5..0c7341e9ae 100644 --- a/.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.md +++ b/.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.md @@ -4,11 +4,11 @@ Status: implemented English | [中文](2026-08-07-web-inline-file-mentions.zh.md) -> Scope: linking inline-code tokens in the closing message's prose to the files the turn produced. Not in scope: recognizing paths in plain prose, linking files the turn did not produce, and mentions in streaming or mid-turn messages. +> Scope: guiding final responses to name primary output files as inline code, then linking those tokens to files the turn changed. Not in scope: recognizing paths in plain prose, linking files absent from successful mutation locations, and mentions in streaming or mid-turn messages. ## Problem -The produced-files row lists a turn's output, but the closing message usually also *names* the file in prose — as inline code, like `` `deepseek-homepage.html` `` — and that mention was inert text. The reader's eye lands on the sentence first; the affordance sat one row below it. +The produced-files row lists a turn's output, but the closing message usually also *names* the file in prose — as inline code, like `` `deepseek-homepage.html` `` — and that mention was inert text. The reader's eye lands on the sentence first; the affordance sat one row below it. The model was not told that this exact inline-code spelling activates the Web file opener, so producing the useful reference depended on habit. ## Decision @@ -16,13 +16,17 @@ The produced-files row lists a turn's output, but the closing message usually al **The renderer owns no vocabulary, and the provider is the deliverables plugin.** `MarkdownText` takes an optional `MarkdownFileMentions` resolver and consults it for inline-code tokens — after URL promotion, which wins, and never inside an anchor, where a button cannot nest. What names a file is decided behind the optional `chatFileMentions` service ui-conversation reaches via `ctx.get`: ui-deliverables provides it beside its turn-tail chain entry, so one cordis.yml line composes the row and the prose links in or out together, and ui-primitives gains no session concepts. Mentions apply to settled renders only — the streaming cache must not bake in handlers that could go stale, and the vocabulary is not final until the turn closes. The consumer memoizes the resolver on the closing seq rather than the growing transcript, so a settled message's cached parse survives stream appends. +**The provider also owns the model guidance for its accepted syntax.** The ui-deliverables Node half registers a static `ui:deliverable-file-references` section that asks the model to mention primary files from successful creation or modification calls in its final response and to write those and any other changed-file references as Markdown inline code, using the exact file-tool path or a basename only when it is unique within the Turn. The guidance deliberately says nothing about unrelated local-path formats. The shipped Web patch is the only composition that loads ui-deliverables, so the guidance exists exactly where the renderer exists; removing the package removes both. The row remains the correctness path when the model omits a mention, and the matching vocabulary still rejects anything not recorded by a successful mutation. + ## Alternatives considered - **Path-shaped regex over all prose** — links `package.json` mentioned abstractly and examples that were never written; every false positive is a click that opens nothing or the wrong file. The vocabulary approach cannot produce a dead link. - **Linking suffix matches (`out/index.html` mentioned as `index.html` in a subdirectory listing)** — deferred; exact path and unique basename cover the observed closing-message shapes, and a wider matcher can loosen later without breaking the seam. - **Resolving in ui-primitives against a passed path list** — puts matching policy in the generic renderer, where other consumers would inherit it unasked. The resolver contract keeps policy with the owner. - **Threading the vocabulary through the turn-tail chain** — the chain is a render dispatch below the message; mentions decorate markdown inside it, which only data reaching MarkdownText can do. The optional service is that data path, and its absence is the off state. +- **Registering the guidance in dsh-web-app** — makes the app bundle describe a feature-specific rendering syntax and allows the renderer and its prompt to drift or be composed independently. The feature package's existing Node half gives one cordis.yml row joint ownership. +- **Adding a post-turn model step to identify the output** — adds latency and another generation even though the final response already has the necessary file-tool history. One static prompt paragraph stays in the reusable prefix and asks the existing final generation to emit the accepted spelling. ## Consequences -The mention and the row are two affordances for one fact (full path as `title` on both); the mention itself wears the markdown sheet's anchor language — link-blue at rest, hover underline — because an at-rest underline collides with monospace descenders inside the code chip. `apps/web/tests/produced-file-mentions.e2e.ts` pins the assembled behavior with a built write-turn seed: unique basename links, ambiguous and unknown tokens stay inert; it does not click, for the produced-files restraint (the opener launches a real application). Mentions in mid-turn narration stay inert even for files the turn later produces, because the vocabulary attaches to the closing message only. The window-prepend edge — a window that starts mid-turn later gaining earlier same-turn writes — leaves a mention unlinked until remount, never wrongly linked. +The mention and the row are two affordances for one fact (full path as `title` on both); the mention itself wears the markdown sheet's anchor language — link-blue at rest, hover underline — because an at-rest underline collides with monospace descenders inside the code chip. The prompt section is constant for the package mount and therefore remains cacheable across Turns. The keyless shipped-Web composition snapshot pins the exact model-visible paragraph, while `apps/web/tests/produced-file-mentions.e2e.ts` pins the assembled rendering with a built write-turn seed: unique basename links, ambiguous and unknown tokens stay inert. Files created indirectly by terminal commands remain outside the vocabulary, even when the model names them. Mentions in mid-turn narration stay inert even for files the turn later produces, because the vocabulary attaches to the closing message only. The window-prepend edge — a window that starts mid-turn later gaining earlier same-turn writes — leaves a mention unlinked until remount, never wrongly linked. diff --git a/.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.zh.md b/.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.zh.md index 160d3838fc..a9ef539336 100644 --- a/.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.zh.md +++ b/.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.zh.md @@ -4,11 +4,11 @@ Status: implemented [English](2026-08-07-web-inline-file-mentions.md) | 中文 -> 范围:把收尾消息正文中的行内代码 token 链接到本轮产出的文件。不在范围内:识别普通正文中的路径、链接本轮未产出的文件,以及流式或轮次中途消息里的提及。 +> 范围:引导最终回复以行内代码点名主要输出文件,再把这些 token 链接到本轮变更的文件。不在范围内:识别普通正文中的路径、链接成功修改位置中不存在的文件,以及流式或轮次中途消息里的提及。 ## 问题 -产物行列出了一轮的输出,但收尾消息通常也会在正文里*点名*文件——以行内代码形式,如 `` `deepseek-homepage.html` ``——而这个提及是死文本。读者的视线先落在句子上;可点击的交互却在下面一行。 +产物行列出了一轮的输出,但收尾消息通常也会在正文里*点名*文件——以行内代码形式,如 `` `deepseek-homepage.html` ``——而这个提及是死文本。读者的视线先落在句子上;可点击的交互却在下面一行。模型并不知道这种精确的行内代码写法会启用 Web 文件打开器,因此能否产生有用引用取决于模型习惯。 ## 决定 @@ -16,13 +16,17 @@ Status: implemented **渲染器不持有词表,提供方是 deliverables 插件。**`MarkdownText` 接受可选的 `MarkdownFileMentions` 解析器,对行内代码 token 询问它——URL 提升优先于解析器,且绝不在锚点内部(按钮不能嵌套在链接里)。什么算文件名的决定藏在 ui-conversation 经 `ctx.get` 触达的可选 `chatFileMentions` 服务背后:ui-deliverables 在其 turn-tail chain 注册项旁提供该服务,因此 cordis.yml 中的一行同时把产物行和正文链接组合进来或去掉,ui-primitives 不引入任何会话概念。提及只作用于已定稿的渲染——流式缓存不能固化可能过期的 handler,而且词表在轮次收尾前并不最终。消费方按收尾 seq 而非不断增长的 transcript(文本记录)记忆化解析器,因此已定稿消息的缓存解析在流式追加中得以保留。 +**提供方也拥有其所接受语法的模型指引。**ui-deliverables 的 Node 侧注册静态段落 `ui:deliverable-file-references`,要求模型在最终回复中点名来自成功创建或修改调用的主要文件,并将这些文件以及正文中提到的其他本轮变更文件写成 Markdown 行内代码:使用文件工具采用的精确路径,或仅在 basename 能唯一指代本轮文件时使用 basename。该指引刻意不涉及无关的本地路径格式。正式提供的组合中只有 Web patch 加载 ui-deliverables,因此提示词只存在于渲染器存在的地方;移除该包会同时移除两者。模型遗漏提及时,文件行仍承担正确性兜底;匹配词表仍拒绝成功修改记录之外的所有文件。 + ## 考虑过的替代方案 - **对全部正文跑路径形状的正则**——会把随口提到的 `package.json` 和从未写过的示例都链接上;每个误报都会带来一次点击,结果要么什么也没打开,要么打开了错误文件。词表方案不可能产生死链。 - **链接后缀匹配(子目录列表里把 `out/index.html` 写作 `index.html`)**——暂缓;精确路径加唯一 basename 已覆盖观察到的收尾消息形态,之后放宽匹配器不会破坏这道 seam。 - **在 ui-primitives 里对传入的路径列表做解析**——把匹配策略放进通用渲染器,其他消费方会被动继承。解析器约定让策略留在持有者手里。 - **经由 turn-tail chain 传递词表**——chain 是消息下方的渲染派发;提及要装饰的是消息内部的 markdown,只有抵达 MarkdownText 的数据才做得到。可选服务就是那条数据通路,它的缺席即关闭态。 +- **在 dsh-web-app 中注册指引**——会让应用组合包描述某个功能特有的渲染语法,也允许渲染器与提示词分别组合或产生漂移。该功能包现有的 Node 侧让 cordis.yml 中的一项可以共同持有两者。 +- **增加一次轮次结束后的模型调用来识别输出**——即使最终回复已经拥有所需的文件工具历史,仍会增加延迟和一次生成。一个静态提示词段落可以留在可复用前缀中,并要求现有的最终生成输出渲染器接受的写法。 ## 后果 -提及与产物行是同一事实的两个交互面(两者都以完整路径作 `title`);提及本身采用 markdown 样式表的锚点语言——静止为链接蓝、悬停出下划线——因为静止下划线在 code 胶囊里会压住等宽字的下伸部。`apps/web/tests/produced-file-mentions.e2e.ts` 用构造的写入轮次 seed 钉住组装后的行为:唯一 basename 成链,歧义与未知 token 保持死文本;它不执行点击,沿用产物行测试不启动外部应用的限制(opener 会启动真实应用)。轮次中途叙述里的提及即使命名了本轮后来产出的文件也保持死文本,因为词表只挂在收尾消息上。窗口前插的边界——从轮次中途开始的窗口后来补入了同轮更早的写入——只会让提及在重挂载前暂不成链,绝不会错链。 +提及与产物行是同一事实的两个交互面(两者都以完整路径作 `title`);提及本身采用 markdown 样式表的锚点语言——静止为链接蓝、悬停出下划线——因为静止下划线在 code 胶囊里会压住等宽字的下伸部。提示词段落在本包加载期间保持不变,因此可跨 Turn 复用缓存。无密钥的正式 Web 组合快照钉住模型可见段落的精确文本,`apps/web/tests/produced-file-mentions.e2e.ts` 则用构造的写入轮次 seed 钉住组装后的渲染行为:唯一 basename 成链,歧义与未知 token 保持死文本。终端命令间接创建的文件即使被模型点名,仍不在词表中。轮次中途叙述里的提及即使命名了本轮后来产出的文件也保持死文本,因为词表只挂在收尾消息上。窗口前插的边界——从轮次中途开始的窗口后来补入了同轮更早的写入——只会让提及在重挂载前暂不成链,绝不会错链。 diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts index ee00353ef8..aa43d350fb 100644 --- a/apps/web/tests/shipped-composition.e2e.ts +++ b/apps/web/tests/shipped-composition.e2e.ts @@ -1,9 +1,11 @@ // Boots the shipped Web composition over the built dist this lane already uses // and asserts what that composition produces: the model-visible tool catalog -// and the sandbox/approval knobs it ships with. No browser and no model call — -// these are composition facts, and the browser scenarios in this lane cover the -// surface itself. +// and file-reference guidance plus the sandbox/approval knobs it ships with. +// No browser and no model call — these are composition facts, and the browser +// scenarios in this lane cover the surface itself. +import { readFileSync } from 'node:fs' import { tmpdir } from 'node:os' +import { fileURLToPath } from 'node:url' import { afterEach, expect, it } from 'vitest' import { CallId } from '@deepseek-ai/dsh-llm' import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox' @@ -15,8 +17,13 @@ import type {} from '@deepseek-ai/dsh-user-approval' import type {} from '@deepseek-ai/dsh-permission' import type {} from '@deepseek-ai/dsh-agent-presets' import type {} from '@deepseek-ai/dsh-commands' +import type {} from '@deepseek-ai/dsh-system-prompt' import { launchWebScaffold, type WebScaffold } from './scaffold.ts' +const FILE_REFERENCE_PROMPT = fileURLToPath(new URL( + './snapshots/web-runtime-context/file-reference-prompt.expected.md', import.meta.url, +)) + /** * The catalog the shipped Web composition puts in front of the model, minus the * ripgrep-dependent pair below. The absences are deliberate, not incidental @@ -66,7 +73,7 @@ afterEach(async () => { scaffold = undefined }) -it('assembles the shipped Web catalog with the confined access default', async () => { +it('assembles the shipped Web catalog, file-reference guidance, and confined access default', async () => { scaffold = await launchWebScaffold() const ctx = scaffold.ctx // The catalog belongs to an AGENT, not to the process: every model-facing row @@ -85,6 +92,9 @@ it('assembles the shipped Web catalog with the confined access default', async ( // The packaged ripgrep binary ships with the dependency, so the pair is a // fixed roster member on every host. expect(names.filter(name => RIPGREP_TOOLS.includes(name))).toEqual(RIPGREP_TOOLS) + const fileReferenceSection = (await ctx.systemPrompt.assemble({ scope: handle.agent })).sections + .find(section => section.name === 'ui:deliverable-file-references') + expect(fileReferenceSection?.text).toBe(readFileSync(FILE_REFERENCE_PROMPT, 'utf8').trimEnd()) } finally { await handle.dispose() } diff --git a/apps/web/tests/snapshots/web-runtime-context/file-reference-prompt.expected.md b/apps/web/tests/snapshots/web-runtime-context/file-reference-prompt.expected.md new file mode 100644 index 0000000000..6561157aec --- /dev/null +++ b/apps/web/tests/snapshots/web-runtime-context/file-reference-prompt.expected.md @@ -0,0 +1 @@ +When you successfully create or modify files, mention the primary outputs in your final response. To make those and any other changed-file references clickable in Web, format them as Markdown inline code using the exact file-tool path, or a basename when unique among the files changed in that turn. diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 3eb59072b7..a10089b686 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2806,7 +2806,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co - `@deepseek-ai/dsh-client-ui-agent-preset` ([`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts)) - `@deepseek-ai/dsh-client-ui-command` ([`packages/client/ui-command/src/index.ts`](../packages/client/ui-command/src/index.ts)) - `@deepseek-ai/dsh-client-ui-conversation` ([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts)) -- `@deepseek-ai/dsh-client-ui-deliverables` ([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-deliverables` — requires `systemPrompt` ([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts)) - `@deepseek-ai/dsh-client-ui-directory-picker` ([`packages/client/ui-directory-picker/src/index.ts`](../packages/client/ui-directory-picker/src/index.ts)) - `@deepseek-ai/dsh-client-ui-directory-picker-native` ([`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts)) - `@deepseek-ai/dsh-client-ui-feedback` ([`packages/client/ui-feedback/src/index.ts`](../packages/client/ui-feedback/src/index.ts)) diff --git a/packages/client/ui-deliverables/README.i18n.yaml b/packages/client/ui-deliverables/README.i18n.yaml index cfa85256ec..70b3f889bf 100644 --- a/packages/client/ui-deliverables/README.i18n.yaml +++ b/packages/client/ui-deliverables/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-deliverables/README.md -README.md: 9cfcde9510c5d6814d5b42fd58a0520ea37b8679 -README.zh.md: 4e09524e269df31cdabce0d5f6e6900d23305265 +README.md: e7118eac75f31b3ffc3f2434371dba06f029d241 +README.zh.md: 0c41170ef38331166f6ad87df0c3e5976200eaf9 diff --git a/packages/client/ui-deliverables/README.md b/packages/client/ui-deliverables/README.md index 9cfcde9510..e7118eac75 100644 --- a/packages/client/ui-deliverables/README.md +++ b/packages/client/ui-deliverables/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Produced-files feature owner: registers the deliverables row a finished turn ends with into the chat view's `conversation.chat.turnTail` hole. All policy lives here; removing this plugin's line from cordis.yml removes the surface entirely, and the owning view renders an empty hole at zero cost. +Produced-files and clickable-reference feature owner. The Node half registers final-response guidance with the system-prompt registry; the browser half registers the deliverables row a finished turn ends with into the chat view's `conversation.chat.turnTail` hole and links matching inline-code references in the closing prose. The shipped Web patch is the only composition that loads this package. Removing its one cordis.yml entry removes the guidance, row, and prose links together. `deliverablesDefinition` folds each Turn's successful mutation calls into engine-published `DeliverablesTurnData`; `producedForClosing` reads that data with the closing Assistant seq. The vocabulary is the mutation tools' own follow-along `locations`, never the closing prose: a produced file is listed whether or not the model remembered to name it. A mutation is recognized by render intent, not tool name — a diff card, or a generic card whose `kind` is `edit` (the shape `str_replace_editor`'s insert presents) — so a new mutation tool joins by declaring what it does. Reads, deletes, and failed calls contribute nothing; a path appears once per Turn in first-seen order. The Conversation Location index owns Turn membership, so a Turn that mutates and then ends without content text cannot spill into the next Turn's row. @@ -10,15 +10,26 @@ Produced-files feature owner: registers the deliverables row a finished turn end The closing prose carries the same vocabulary. This plugin provides the `chatFileMentions` service the chat view consults per closing message: `producedFileMentions` resolves an inline-code token by exact path, or by being exactly the basename of exactly one produced path — a basename two paths share stays inert rather than guessing, so a mention link can never open the wrong file or 404. A resolved mention keeps its code chip and takes the markdown sheet's link language — link-blue at rest, underlined on hover, exactly like URL-promoted inline code — with the full path as its `title`; mentions never render inside anchors or streaming text. Decision record: the [inline file mentions Agent Note](../../../.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.md). +The Node half registers the static `ui:deliverable-file-references` system-prompt section. It asks the model to mention the primary files it successfully created or modified and to write those and any other changed-file references as Markdown inline code, using the exact file-tool path or a basename only when unique within the Turn. The guidance makes the renderer's accepted syntax explicit; it does not govern unrelated path discussions or widen the renderer's successful-mutation vocabulary. + ## Model Experience -None, as the row is a pure client derivation over already-logged tool metadata and nothing here reaches a model request. +### Clickable file-reference guidance + +#### What the model sees + +One fixed paragraph instructs the model to name primary files from successful creation or modification calls in its final response and to format those and any other changed-file references as exact-path or unique-basename Markdown inline code, such as `out/report.html`. + +#### Token effect + +One fixed prompt paragraph whenever this package is loaded; no tool schema, tool result, or per-Turn context is added. #### KV Cache effect -None; this package neither assembles nor sends provider requests. +The section is static at order 190 for the lifetime of the package mount, so it remains in the reusable prompt prefix and does not change across Turns. ## Known Limitations and Deferred Work - **Mention matching is exact path or unique basename only.** A suffix mention (`out/index.html` written as `index.html` resolves; `deep/out/index.html` written as `out/index.html` does not) stays inert; widening the matcher is deferred until a real closing-message shape needs it. +- **Files created indirectly by terminal commands remain outside the matching vocabulary.** Naming such a file in inline code does not make it clickable unless a successful mutation location also records that path. - **Native folder handoff targets the Host desktop.** A browser reached through a non-loopback authority omits the action, as does a deployment reporting no native opener. SSH forwarding that makes a remote Host look loopback-local must set the gateway's `nativeOpen: false`; so must a headless macOS/Windows Host, a WSL deployment without working Windows interop, or any Linux desktop whose display/opener probe is a false positive. Identifying the operator-visible desktop remains deployment policy. diff --git a/packages/client/ui-deliverables/README.zh.md b/packages/client/ui-deliverables/README.zh.md index 4e09524e26..0c41170ef3 100644 --- a/packages/client/ui-deliverables/README.zh.md +++ b/packages/client/ui-deliverables/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -产出文件功能的属主:把已完成轮次末尾的产出文件行注册到 chat 视图的 `conversation.chat.turnTail` slot 中。全部策略都在本包内;从 cordis.yml 中删去本插件那一行即可整体移除该界面,属主视图无需额外开销即可渲染空 slot。 +产出文件与可点击文件引用功能的属主。Node 侧向系统提示词 registry 注册最终回复指引;浏览器侧把已完成轮次末尾的产出文件行注册到 chat 视图的 `conversation.chat.turnTail` slot,并将收尾正文中匹配的行内代码引用转换为链接。正式提供的组合中只有 Web patch 加载本包;从 cordis.yml 中删去这一项会同时移除提示词、文件行与正文链接。 `deliverablesDefinition` 把每个轮次中成功的修改调用折叠进引擎发布的 `DeliverablesTurnData`;`producedForClosing` 结合收尾 Assistant 的 seq 读取这份数据。依据的是修改工具自身附带的 `locations`,而不是收尾正文:无论模型是否记得点名,产出文件都会被列出。修改操作按渲染意图而非工具名识别:diff 卡片,或 `kind` 为 `edit` 的通用卡片(即 `str_replace_editor` 的 insert 操作所呈现的形态);因此新的修改工具只需声明自身行为即可加入。读取、删除和失败的调用不贡献任何条目;同一路径在一个轮次内按首见顺序只出现一次。Conversation Location 索引负责维护轮次归属关系,因此一个轮次即使先修改文件、随后没有正文内容就结束,也不会溢进下一个轮次的行里。 @@ -10,15 +10,26 @@ 收尾正文承载同一份词表。本插件提供供 chat 视图按收尾消息查询的 `chatFileMentions` 服务:`producedFileMentions` 按精确路径解析行内代码 token,或当 token 恰好等于某条产出路径的 basename,且这样的路径仅有一条时解析——两条路径共享同一 basename 时,文本保持不可点击而不作猜测,因此提及链接永远不会打开错误的文件,也不会导致 404。解析成功的提及保留代码标签,并采用 Markdown 样式表的链接样式:静止时为链接蓝色,悬停时显示下划线,与 URL 提升的行内代码完全一致——完整路径作为其 `title`;提及绝不会渲染在链接内部或流式文本中。决策记录:[行内文件提及 Agent Note](../../../.agents/notes/implemented/feature/2026-08-07-web-inline-file-mentions.md)。 +Node 侧注册静态系统提示词段落 `ui:deliverable-file-references`。它要求模型点名成功创建或修改的主要文件,并将这些文件以及正文中提到的其他本轮变更文件写成 Markdown 行内代码:使用文件工具采用的精确路径,或仅在 basename 能唯一指代本轮文件时使用 basename。该提示词只向模型说明渲染器接受的语法;它不约束无关的路径讨论,也不会扩大渲染器的成功修改词表。 + ## 模型体验 -无。该行是对已记录工具元数据的纯客户端派生,这里没有任何内容进入模型请求。 +### 可点击文件引用指引 + +#### 模型看到的内容 + +一段固定提示词要求模型在最终回复中点名成功创建或修改的主要文件,并将这些文件以及正文中提到的其他本轮变更文件写成采用精确路径或唯一 basename 的 Markdown 行内代码,例如 `out/report.html`。 + +#### Token 影响 + +加载本包时增加一段固定提示词;不增加工具 schema、工具结果或按 Turn 变化的上下文。 #### KV Cache 影响 -无;该包既不组装也不发送提供方请求。 +该段落在本包加载期间始终以顺序 190 保持静态,因此留在可复用的提示词前缀中,不会随 Turn 改变。 ## 已知限制与暂缓事项 - **提及匹配只认精确路径或唯一 basename。**后缀式提及(`out/index.html` 写作 `index.html` 可解析;`deep/out/index.html` 写作 `out/index.html` 则不行)保持不可点击;等真实的收尾消息形态产生需求后再放宽匹配规则。 +- **终端命令间接创建的文件仍不在匹配词表内。**除非某个成功修改位置也记录了该路径,否则在行内代码中点名这类文件不会使其可点击。 - **原生文件夹交接以 Host 桌面为目标。**经非 loopback 权威访问的浏览器会省略该操作,报告没有原生打开器的部署也一样。若 SSH 转发让远端 Host 看似处于本机 loopback,部署必须为网关设置 `nativeOpen: false`;无界面的 macOS/Windows Host、Windows interop 不可用的 WSL,或 display/opener 探测误报的 Linux 桌面也必须这样配置。识别操作者实际可见的桌面仍属于部署策略。 diff --git a/packages/client/ui-deliverables/package.json b/packages/client/ui-deliverables/package.json index 4d7ea0d314..2a0559fc7b 100644 --- a/packages/client/ui-deliverables/package.json +++ b/packages/client/ui-deliverables/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-client-ui-deliverables", - "description": "Produced-files turn tail: the deliverables row a finished turn ends with", + "description": "Produced-files turn tail and clickable final-response file references for Web", "version": "0.0.1-rc.2", "publishConfig": { "access": "restricted" @@ -55,6 +55,7 @@ "@deepseek-ai/dsh-client-ui-conversation": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-system-prompt": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, "devDependencies": { @@ -65,6 +66,7 @@ "@deepseek-ai/dsh-client-ui-conversation": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-system-prompt": "workspace:^", "@types/react": "~18.3.1", "@deepseek-ai/cordis": "workspace:^" }, diff --git a/packages/client/ui-deliverables/src/index.ts b/packages/client/ui-deliverables/src/index.ts index c6ea80fa21..6e6c387803 100644 --- a/packages/client/ui-deliverables/src/index.ts +++ b/packages/client/ui-deliverables/src/index.ts @@ -1,9 +1,28 @@ /** - * Deliverables plugin, node half. Pure UI plugin: the empty apply exists so - * the plugin appears in the host cordis.yml / Loader; the browser half ships - * via exports["./client"], discovered through the package.json dsh.client - * declaration. + * Deliverables plugin, node half. Registers the response-format guidance that + * lets the browser half recognize final-response file references. The browser + * half ships via exports["./client"], discovered through the package.json + * dsh.client declaration. */ -/** Host plugin body — no host-side behavior for this surface plugin. */ -export function apply(): void {} +import type { Context } from '@deepseek-ai/cordis' +import type {} from '@deepseek-ai/dsh-system-prompt' + +/** Services required for the model guidance paired with the browser renderer. */ +export const inject = ['systemPrompt'] + +/** Stable final-response guidance owned by the matching renderer. */ +const FILE_REFERENCE_PROMPT = 'When you successfully create or modify files, mention the primary outputs in your final response. ' + + 'To make those and any other changed-file references clickable in Web, format them as Markdown inline code using the exact file-tool path, or a basename when unique among the files changed in that turn.' + +/** + * Register model guidance for the file-reference renderer shipped by this package. + * @param ctx - host context carrying the system-prompt registry. + */ +export function apply(ctx: Context): void { + ctx.systemPrompt.section({ + name: 'ui:deliverable-file-references', + order: 190, + text: FILE_REFERENCE_PROMPT, + }) +} diff --git a/packages/client/ui-deliverables/src/invariant.ts b/packages/client/ui-deliverables/src/invariant.ts index d14e474ca3..7fa5eaa216 100644 --- a/packages/client/ui-deliverables/src/invariant.ts +++ b/packages/client/ui-deliverables/src/invariant.ts @@ -15,10 +15,9 @@ export const name = 'client-ui-deliverables-invariant' export const inject = ['invariants'] /** - * No runtime invariant: one slot registration and one dictionary - * registration, both effect-owned with disposal proven by the HMR-safety - * spec — the plugin emits no cordis events and owns no cross-plugin mutable - * state. + * No runtime invariant: the prompt section, slot, dictionary, event + * definition, and optional service registrations are effect-owned with + * disposal proven by their plugin specs; this package owns no mutable state. */ const install: InvariantInstaller = () => {} diff --git a/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx b/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx index 468dd242fa..89cbfb8a58 100644 --- a/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx +++ b/packages/client/ui-deliverables/tests/produced-files.client.spec.tsx @@ -27,7 +27,6 @@ import { type DeliverablesTurnData, } from '../src/client/turn-deliverables.ts' import { apply, inject } from '../src/client/index.ts' -import { apply as applyNode } from '../src/index.ts' import { apply as applyInvariant } from '../src/invariant.ts' import { en, zh } from '../src/client/locales.ts' @@ -438,9 +437,7 @@ describe('producedFileMentions resolver', () => { }) describe('package shells', () => { - it('the node half mounts inert and the invariant companion registers ownership', async () => { - // The node half is deliberately inert; mounting it must simply not throw. - applyNode() + it('the invariant companion registers ownership', async () => { const registered: string[] = [] const ctx = new Context() ctx.provide('invariants') diff --git a/packages/client/ui-deliverables/tests/prompt.client.spec.ts b/packages/client/ui-deliverables/tests/prompt.client.spec.ts new file mode 100644 index 0000000000..2ae6f0f1e3 --- /dev/null +++ b/packages/client/ui-deliverables/tests/prompt.client.spec.ts @@ -0,0 +1,30 @@ +/** Node-half coverage for the model guidance paired with Web file references. */ + +import { Context } from '@deepseek-ai/cordis' +import { afterEach, describe, expect, it } from 'vitest' +import SystemPrompt from '@deepseek-ai/dsh-system-prompt' +import { apply, inject } from '../src/index.ts' + +let ctx: Context | undefined + +afterEach(async () => { + await ctx?.fiber.dispose() + ctx = undefined +}) + +describe('ui-deliverables node plugin', () => { + it('registers final-response file-reference guidance only while mounted', async () => { + ctx = new Context() + await ctx.plugin(SystemPrompt, { persona: '' }) + const mounted = ctx.plugin({ apply, inject }) + await mounted.await() + + const section = (await ctx.systemPrompt.assemble()).sections + .find(entry => entry.name === 'ui:deliverable-file-references') + expect(section?.text).toMatchInlineSnapshot('"When you successfully create or modify files, mention the primary outputs in your final response. To make those and any other changed-file references clickable in Web, format them as Markdown inline code using the exact file-tool path, or a basename when unique among the files changed in that turn."') + + await mounted.dispose() + expect((await ctx.systemPrompt.assemble()).sections + .some(entry => entry.name === 'ui:deliverable-file-references')).toBe(false) + }) +}) diff --git a/packages/client/ui-deliverables/tsconfig.json b/packages/client/ui-deliverables/tsconfig.json index 3fa938986f..5a403871cd 100644 --- a/packages/client/ui-deliverables/tsconfig.json +++ b/packages/client/ui-deliverables/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "../../support/invariants" + }, + { + "path": "../../core/system-prompt" } ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 364a560e38..8ca3fabc56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2213,6 +2213,9 @@ importers: '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../support/invariants + '@deepseek-ai/dsh-system-prompt': + specifier: workspace:^ + version: link:../../core/system-prompt '@types/react': specifier: ~18.3.1 version: 18.3.31 diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index 813175b307..a4a8234fc2 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -73,7 +73,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' }, 'packages/client/ui-feedback': { kind: 'none', reason: 'Browser-side controls over the message-feedback sidecar; ratings and notes never enter the Session log, model context, or telemetry.' }, 'packages/client/ui-tool': { kind: 'none', reason: 'Browser-side Tool presentation layer; renders logged calls without changing model context.' }, - 'packages/client/ui-deliverables': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' }, 'packages/client/ui-task': { kind: 'none', reason: 'Browser-side read-only projection of ctx.tasks records; dsh-tool-tasks owns the model-facing behavior.' }, 'packages/client/ui-workflow-run': { kind: 'none', reason: 'Browser-side UI plugin layer; renders durable workflow records without changing model context.' }, 'packages/client/ui-slash': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' }, From 7c4c118456a3113456e87c666c16a874310258fd Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 12 Aug 2026 22:54:27 +0800 Subject: [PATCH 018/160] fix(web): reclaim overlay scrollbar gutter --- ...-composer-tab-gutter-reservation.i18n.yaml | 4 +- ...6-08-04-composer-tab-gutter-reservation.md | 2 + ...8-04-composer-tab-gutter-reservation.zh.md | 2 + ...-overlay-seat-width-compensation.i18n.yaml | 6 ++ ...omposer-overlay-seat-width-compensation.md | 36 +++++++++ ...oser-overlay-seat-width-compensation.zh.md | 36 +++++++++ apps/web/tests/composer-tab-geometry.e2e.ts | 76 ++++++++++--------- .../geometry.expected.md | 14 ++-- .../skeleton/ConversationRoot.module.css | 21 +++-- .../client/ui-theme/src/styles/scrollbar.css | 5 ++ 10 files changed, 153 insertions(+), 49 deletions(-) create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.i18n.yaml create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.zh.md diff --git a/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.i18n.yaml index 8a9b6236ae..0082a6f991 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md -2026-08-04-composer-tab-gutter-reservation.md: 3b28c35c1f11676e41cabde76d1b0d16c688f034 -2026-08-04-composer-tab-gutter-reservation.zh.md: c357dd06c52a834c18d2e8a25246d4ad003db548 +2026-08-04-composer-tab-gutter-reservation.md: 2c2813371af073eee4bd80ce7813e77af3c0c319 +2026-08-04-composer-tab-gutter-reservation.zh.md: e450def2482218028875a48fce8842c88b5ba82e diff --git a/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md b/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md index 3b28c35c1f..2c2813371a 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md +++ b/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md @@ -2,6 +2,8 @@ Status: implemented +> Superseded for overlay views by [the seat-width compensation](2026-08-12-composer-overlay-seat-width-compensation.md): the overlay branch no longer reserves a gutter, and the seat compensates for the bar instead. Chat keeps the reservation described here. + English | [中文](2026-08-04-composer-tab-gutter-reservation.zh.md) ## Problem diff --git a/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.zh.md b/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.zh.md index c357dd06c5..e450def248 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.zh.md @@ -2,6 +2,8 @@ Status: implemented +> 对覆盖视图已被 [座位宽度补偿](2026-08-12-composer-overlay-seat-width-compensation.md) 取代:覆盖分支不再预留滚动条槽,座位改为补偿滚动条宽度。Chat 仍保留本文所述的预留。 + [English](2026-08-04-composer-tab-gutter-reservation.md) | 中文 ## 问题 diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.i18n.yaml new file mode 100644 index 0000000000..fd88ecbc93 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.i18n.yaml @@ -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/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md +2026-08-12-composer-overlay-seat-width-compensation.md: 85281ed4301046672ff4c02320b196a397e6d163 +2026-08-12-composer-overlay-seat-width-compensation.zh.md: 7bcf060e651a828068ef207caaebd01fa2ffd7ae diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md b/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md new file mode 100644 index 0000000000..85281ed430 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md @@ -0,0 +1,36 @@ +# Agent Note: The overlay composer seat compensates for the bar instead of reserving a gutter + +Status: implemented + +English | [中文](2026-08-12-composer-overlay-seat-width-compensation.zh.md) + +## Problem + +The [composer-tab gutter reservation](2026-08-04-composer-tab-gutter-reservation.md) made the column's scroller reserve a scrollbar gutter unconditionally, so the composer seat measured the same width in Chat and in a view with a composer overlay. The cost was paid by every overlay view: the view's content column ended 8px short of the column's right edge, because the scroller reserved a gutter for a bar it never draws — the trajectory ledger owns its own scrollers and the outer box never scrolls. + +The trajectory table made that cost visible: its full-width row divider lines stopped 8px short of the pane edge, leaving a strip of whitespace at the right of every line and of the whole content column. + +## Decision + +The reservation now belongs to Chat alone. The overlay branch declares `scrollbar-gutter: auto`, so the view's content spans the full column; the overlay composer seat (absolutely positioned against the padding box) gives back the bar's width with `right: var(--dsh-scrollbar-width)`, so the input card still measures the same width as Chat's seat and does not move between tabs. + +The compensation value is not a literal: ui-theme's scrollbar.css defines `--dsh-scrollbar-width` (8px on the WebKit path) beside the `::-webkit-scrollbar` rule it mirrors, and the seat reads that variable. A change to the sheet's bar width reaches the compensation in the same reviewable diff as the bar itself. + +## Alternatives considered + +**Keep the unconditional reservation and shrink every overlay view.** The pre-fix behavior. It keeps one declaration for both tabs but taxes every overlay view with an 8px content column, which the trajectory ledger surfaced as visible whitespace. Rejected because the overlay views own their scrolling; they should not pay for Chat's bar. + +**Reserve on the overlay branch too and let the view bleed into the gutter.** More moving parts for the same result: the gutter would still exist on a box that never scrolls, and the view would have to break out of the content box to reclaim its width. + +**Accept the 4px card shift.** Dropping the reservation without compensating the seat would move the input card on every tab switch, which is exactly the symptom the earlier note fixed. Rejected: the card position is a deliberate cross-tab invariant. + +## Consequences + +- Chat keeps its reserved gutter and its stable card position; nothing changes on that tab. +- Overlay views (trajectory) span the full column; the trajectory ledger's divider lines reach the pane edge. +- The input card still holds one horizontal position across the Chat and Trajectory tabs, now by two mechanisms instead of one: Chat reserves, the overlay seat compensates. +- `--dsh-scrollbar-width` becomes a public ui-theme variable read outside ui-theme; the scrollbar-styles spec's indirection checks only scan `--dsh-scrollbar-thumb{,-hover}` rebinds, so the width variable is not covered by the pair gate. + +## Testing + +`apps/web/tests/composer-tab-geometry.e2e.ts` still asserts the card holds its position across tabs and now also asserts the split: Chat's scroller keeps `scrollbar-gutter: stable` and a nonzero band, while the overlay branch resolves `auto` with a zero band. The control cascade changed with the mechanism: it now drops the seat's `right` compensation (instead of dropping a gutter Chat never had on that branch) and measures the same 4px shift, proving the equal rectangles are not a tab switch that never reached layout. The committed golden records both states. diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.zh.md b/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.zh.md new file mode 100644 index 0000000000..7bcf060e65 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.zh.md @@ -0,0 +1,36 @@ +# Agent Note: 覆盖视图的 composer 座位改为补偿滚动条宽度,不再预留滚动条槽 + +Status: implemented + +[English](2026-08-12-composer-overlay-seat-width-compensation.md) | 中文 + +## 问题 + +[composer 标签页滚动条槽预留](2026-08-04-composer-tab-gutter-reservation.md) 让会话列滚动容器无条件预留一条滚动条槽,使 composer 座位在 Chat 与带 composer 覆盖的视图中测得相同宽度。代价由每个覆盖视图承担:视图内容列比列右边缘窄 8px,因为滚动容器为一条它从不绘制的滚动条预留了槽——trajectory 台账由视图内部自己的滚动容器滚动,外层盒子从不滚动。 + +trajectory 表格让这个代价显形:整行分隔线在面板右边缘前 8px 处停止,每条线右侧以及整个内容列右侧都留下一条空白带。 + +## 决策 + +预留现在只属于 Chat。覆盖分支声明 `scrollbar-gutter: auto`,视图内容占满整列;覆盖分支的 composer 座位(相对 padding box 绝对定位)用 `right: var(--dsh-scrollbar-width)` 让出滚动条宽度,使输入卡仍与 Chat 座位测得相同宽度,切换标签页时不移动。 + +补偿值不是字面量:ui-theme 的 scrollbar.css 在它镜像的 `::-webkit-scrollbar` 规则旁定义 `--dsh-scrollbar-width`(WebKit 路径 8px),座位读取该变量。样式表滚动条宽度一变,补偿就会与滚动条本身出现在同一次可审阅的 diff 中。 + +## 备选方案 + +**保留无条件预留,压缩每个覆盖视图。** 修复前行为。两个标签页一条声明,但每个覆盖视图都要付出 8px 内容列,trajectory 台账将其显现为可见空白。已拒绝:覆盖视图自己滚动,不应为 Chat 的滚动条买单。 + +**覆盖分支也预留,并让视图渗入滚动条槽。** 同样结果下更多活动部件:从不滚动的盒子上仍存在滚动条槽,视图还得突破内容盒才能取回宽度。 + +**接受 4px 卡片位移。** 去掉预留却不补偿座位,会在每次切换标签页时移动输入卡——正是前一份 note 修复的症状。已拒绝:卡片位置是刻意保持的跨标签页不变量。 + +## 后果 + +- Chat 保留滚动条槽与稳定的卡片位置;该标签页无任何变化。 +- 覆盖视图(trajectory)占满整列;trajectory 台账的分隔线到达面板右边缘。 +- 输入卡在 Chat 与 Trajectory 标签页间仍保持同一水平位置,现在由两种机制而非一种达成:Chat 预留,覆盖座位补偿。 +- `--dsh-scrollbar-width` 成为 ui-theme 对外、且被 ui-theme 之外读取的变量;scrollbar-styles 规格的间接层检查只扫描 `--dsh-scrollbar-thumb{,-hover}` 重绑,宽度变量不受成对门禁覆盖。 + +## 测试 + +`apps/web/tests/composer-tab-geometry.e2e.ts` 仍断言输入卡在标签页间保持位置,并新增断言拆分:Chat 滚动容器保持 `scrollbar-gutter: stable` 与非零槽宽,覆盖分支解析为 `auto` 且槽宽为零。控制级联随机制改变:现在移除座位的 `right` 补偿(而非移除该分支上 Chat 从未有过的槽),测得同样的 4px 位移,证明相等的矩形并非从未到达布局的标签页切换。提交的 golden 记录两种状态。 diff --git a/apps/web/tests/composer-tab-geometry.e2e.ts b/apps/web/tests/composer-tab-geometry.e2e.ts index bb43e20406..b491b1257c 100644 --- a/apps/web/tests/composer-tab-geometry.e2e.ts +++ b/apps/web/tests/composer-tab-geometry.e2e.ts @@ -11,12 +11,13 @@ // gets an absolutely positioned seat instead, laid out against the padding box, // which the scrollbar never reduces. // -// Without a shared reservation the two tabs disagree by exactly the bar's -// width for as long as the transcript overflows: the card jumps sideways on -// every tab switch, and inside Chat alone at the moment a growing transcript -// starts to scroll. The column reserves the gutter unconditionally -// (`scrollbar-gutter: stable`) and states the overlay branch as a scroll -// container on the same axes, so both edges are the same edge. +// The column handles the two edges without reserving the gutter on both: Chat +// keeps `scrollbar-gutter: stable` so its seat's content box never jumps as the +// transcript starts to scroll; the overlay branch does NOT reserve (the view +// owns its own scrollers, so a reserved gutter would only narrow the view's +// content by the bar's width), and the overlay seat instead gives back the +// bar's width (`right: var(--dsh-scrollbar-width)`) so both seats measure the +// same width and the card does not move. // // Only a real engine can show this. The seat's geometry is layout: jsdom gives // every element a zero-sized box and reports no scrollbar at all, so a unit spec @@ -33,12 +34,11 @@ // configuration: ui-theme's scrollbar.css gives `::-webkit-scrollbar` a width, // and a bar that occupies layout space is what the product actually draws. // -// The scenario runs that unreserved cascade in the page — `scrollbar-gutter: auto` -// on the scroller, `overflow: hidden` on the overlay branch — and measures the -// same two tabs through it, which is what keeps the equal rectangles above from -// being explained by a tab switch that never reached the layout. It is the -// reported symptom as a number: the card moves 4px, half the 8px band, on each -// edge. +// The scenario runs that unreserved cascade in the page — the overlay seat's +// `right` compensation dropped to 0 — and measures the same two tabs through +// it, which is what keeps the equal rectangles above from being explained by a +// tab switch that never reached the layout. It is the reported symptom as a +// number: the card moves 4px, half the 8px band, on each edge. // // Zero model calls: a seeded cold session renders from its log, and switching // tabs asks the host for nothing. A stray stream would fail loud with NO_ADAPTER. @@ -62,9 +62,9 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/composer-tab-geometry', * Absolute coordinates are deliberately absent: they depend on the sidebar's * laid-out width and on font metrics, so committing them would produce a fixture * that has to be re-recorded per platform. What is recorded is the distance - * between the two tabs' rectangles, which is zero when the reservation holds and + * between the two tabs' rectangles, which is zero when the compensation holds and * the bar's width when it does not — including under the control, so the golden - * carries the shift the unreserved cascade produces rather than only its absence. + * carries the shift the uncompensated cascade produces rather than only its absence. */ const GEOMETRY_EXPECTED = join(SNAPSHOT_DIR, 'geometry.expected.md') const MODE = webSnapshotMode() @@ -114,15 +114,15 @@ async function setMeasuredViewport( } /** - * The unreserved cascade, injected into the page: the reservation dropped and - * the overlay branch forced to a hidden box. `!important` beats the module + * The uncompensated cascade, injected into the page: the overlay seat's `right` + * compensation dropped to 0, so it measures the full padding box while Chat's + * seat still rides the reserved content box. `!important` beats the module * rules without a rebuild, and the id lets the control be lifted again in the * same session. */ const CONTROL_STYLE_ID = 'composer-tab-geometry-control' const CONTROL_CSS = ` -[data-conversation-scroll] { scrollbar-gutter: auto !important; } -[data-conversation-scroll]:has([data-conversation-composer-overlay]) { overflow: hidden !important; } +[data-conversation-scroll]:has([data-conversation-composer-overlay]) > [data-composer-seat] { right: 0 !important; } ` /** The column scroller and the input card as the browser lays them out, in one tab. */ @@ -221,9 +221,11 @@ async function compareTabs(page: Page): Promise { } /** - * Run the unreserved cascade in the page for one measurement, then lift it. + * Run the uncompensated cascade in the page for one measurement, then lift it: + * the overlay seat's `right` compensation dropped to 0, so it measures the + * full padding box while Chat's seat still rides the reserved content box. * @param page - the page under test. - * @returns the comparison as the column lays out without the reservation. + * @returns the comparison as the column lays out without the compensation. */ async function compareTabsWithoutReservation(page: Page): Promise { await page.evaluate(({ id, css }) => { @@ -268,7 +270,7 @@ async function openSeededSession(page: Page): Promise { * Render the golden body. * @param wide - comparison at the viewport where the card sits at its width cap. * @param narrow - comparison at the viewport where the card shrinks with the column. - * @param control - comparison at the wide viewport with the reservation removed. + * @param control - comparison at the wide viewport with the compensation removed. * @returns the golden body, without a trailing newline. */ function renderGeometry(wide: TabComparison, narrow: TabComparison, control: TabComparison): string { @@ -291,7 +293,7 @@ function renderGeometry(wide: TabComparison, narrow: TabComparison, control: Tab '', ...section(`Wide viewport (${String(WIDE_VIEWPORT.width)}px, card at its cap)`, wide), ...section(`Narrow viewport (${String(NARROW_VIEWPORT.width)}px, card shrinking with the column)`, narrow), - ...section('Wide viewport, reservation removed in the page (control)', control), + ...section('Wide viewport, seat compensation removed in the page (control)', control), ].join('\n').trimEnd() } @@ -322,22 +324,25 @@ describe('web e2e: input card position across view tabs', () => { await scaffold?.close() }) - it('reserves the same gutter in both tabs while the transcript scrolls', async () => { + it('reserves the gutter in Chat and lets Trajectory own its width', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-composer-tab-geometry-band')) await setMeasuredViewport(page, WIDE_VIEWPORT, false) // Vacuity guard, in two parts. A transcript that does not overflow gives // Chat no scrollbar, and a hidden or overlaid bar gives it no width; either - // would make the tabs agree without the reservation doing anything. + // would make the tabs agree without the compensation doing anything. await expect.poll(async () => (await measureTab(page)).scrolls, { timeout: 10_000 }).toBe(true) const comparison = await compareTabs(page) expect(comparison.chat.band).toBeGreaterThan(0) - // The reservation reaches both states, which is the whole point: the same - // band, on a box that scrolls and on one that only holds a view. + // Chat keeps the unconditional reservation so its seat's content box never + // jumps as the transcript starts to scroll. expect(comparison.chat.gutter).toBe('stable') - expect(comparison.trajectory.gutter).toBe('stable') - expect(comparison.trajectory.band).toBe(comparison.chat.band) + // The overlay branch does NOT reserve: the view owns its own scrollers, so + // a reserved gutter would only narrow the view's content by the bar's + // width. The seat compensates instead, which the next test asserts. + expect(comparison.trajectory.gutter).toBe('auto') + expect(comparison.trajectory.band).toBe(0) // Declared as a scroll container on both axes rather than left to compute: - // `overflow: hidden` would drop the reservation in WebKit, and a `visible` + // `overflow: hidden` would drop any reservation in WebKit, and a `visible` // horizontal axis computes to `auto` beside a scrolling one. expect(comparison.trajectory.overflowY).toBe('auto') expect(comparison.trajectory.overflowX).toBe('hidden') @@ -378,20 +383,21 @@ describe('web e2e: input card position across view tabs', () => { expect(tripwire.pageErrors).toEqual([]) }, 60_000) - it('moves the card again once the reservation is removed in the page', async () => { + it('moves the card again once the seat compensation is removed in the page', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-composer-tab-geometry-control')) await setMeasuredViewport(page, WIDE_VIEWPORT, false) // The control: without it, equal rectangles could also mean the tab switch - // never reached the layout. Under the unreserved cascade the Chat scroller - // keeps its bar and the Trajectory branch becomes a hidden box with none, - // and the card moves by half the band on each edge. + // never reached the layout. Under the unreserved cascade the overlay seat + // loses its `right` compensation and measures the full padding box, so the + // card moves by half the band on each edge. Chat's own reservation is + // untouched — that is the side that must not change. const comparison = await compareTabsWithoutReservation(page) - expect(comparison.chat.gutter).toBe('auto') + expect(comparison.chat.gutter).toBe('stable') expect(comparison.chat.band).toBeGreaterThan(0) expect(comparison.trajectory.band).toBe(0) expect(comparison.leftShift).toBe(comparison.chat.band / 2) expect(comparison.rightShift).toBe(comparison.chat.band / 2) - // Restoring the sheet restores the reservation, so the control cannot leak + // Restoring the sheet restores the compensation, so the control cannot leak // into the remaining measurements. const restored = await compareTabs(page) expect(restored.leftShift).toBe(0) diff --git a/apps/web/tests/snapshots/composer-tab-geometry/geometry.expected.md b/apps/web/tests/snapshots/composer-tab-geometry/geometry.expected.md index 9735019508..e95adaaa6f 100644 --- a/apps/web/tests/snapshots/composer-tab-geometry/geometry.expected.md +++ b/apps/web/tests/snapshots/composer-tab-geometry/geometry.expected.md @@ -5,9 +5,9 @@ - Chat: scrollbar-gutter stable, overflow hidden/auto - Chat scroller scrolls: true - Chat reserved band: 8px -- Trajectory: scrollbar-gutter stable, overflow hidden/auto +- Trajectory: scrollbar-gutter auto, overflow hidden/auto - Trajectory scroller scrolls: false -- Trajectory reserved band: 8px +- Trajectory reserved band: 0px - input card left edge moves between tabs: 0px - input card right edge moves between tabs: 0px - input card width changes between tabs: 0px @@ -17,19 +17,19 @@ - Chat: scrollbar-gutter stable, overflow hidden/auto - Chat scroller scrolls: true - Chat reserved band: 8px -- Trajectory: scrollbar-gutter stable, overflow hidden/auto +- Trajectory: scrollbar-gutter auto, overflow hidden/auto - Trajectory scroller scrolls: false -- Trajectory reserved band: 8px +- Trajectory reserved band: 0px - input card left edge moves between tabs: 0px - input card right edge moves between tabs: 0px - input card width changes between tabs: 0px -## Wide viewport, reservation removed in the page (control) +## Wide viewport, seat compensation removed in the page (control) -- Chat: scrollbar-gutter auto, overflow hidden/auto +- Chat: scrollbar-gutter stable, overflow hidden/auto - Chat scroller scrolls: true - Chat reserved band: 8px -- Trajectory: scrollbar-gutter auto, overflow hidden/hidden +- Trajectory: scrollbar-gutter auto, overflow hidden/auto - Trajectory scroller scrolls: false - Trajectory reserved band: 0px - input card left edge moves between tabs: 4px diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css index 1b72470feb..c6618fc624 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css @@ -272,12 +272,17 @@ .scrollBody:has([data-conversation-composer-overlay]) { position: relative; /* A clipping box nothing scrolls out of, stated as a scroll container on both - axes rather than `overflow: hidden`: WebKit honours the reservation above - only in the `overflow-y: auto` form, and a single-axis scroller computes - the other axis to `auto` - ([decision](../../../../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). */ + axes rather than `overflow: hidden`: a single-axis scroller computes the + other axis to `auto`, and the seat's absolute positioning below needs the + box to stay a containing block in every engine. */ overflow-x: hidden; overflow-y: auto; + /* The view owns its own scrollers (the trajectory ledger scrolls inside the + view), so this box never scrolls here; reserving a gutter would only + narrow the view's content by the bar's width. Chat keeps the reservation + on the same element, so the seat below compensates with the bar's width + ([decision](../../../../../../.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md)). */ + scrollbar-gutter: auto; } .scrollBody:has([data-conversation-composer-overlay]) > .viewArea { @@ -288,7 +293,13 @@ .scrollBody:has([data-conversation-composer-overlay]) > .composerSeat { position: absolute; - right: 0; + /* Chat's seat rides this box's content box, which its own scrolling shortens + by the bar's width; the overlay seat is against the padding box, so it + must give back the same width to keep the input card in place across + tabs. Reads ui-theme's --dsh-scrollbar-width, which mirrors the WebKit + bar's layout width; Firefox's thin bar resolves to the same 8px on the + platforms this app targets, so the compensation holds there too. */ + right: var(--dsh-scrollbar-width); bottom: 0; left: 0; } diff --git a/packages/client/ui-theme/src/styles/scrollbar.css b/packages/client/ui-theme/src/styles/scrollbar.css index 31b5a8aabe..71f67d4f57 100644 --- a/packages/client/ui-theme/src/styles/scrollbar.css +++ b/packages/client/ui-theme/src/styles/scrollbar.css @@ -17,6 +17,11 @@ body { --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l1); --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l1); + /* The WebKit bar's layout width, mirrored by the ::-webkit-scrollbar rule + below. A surface that must align itself beside a space-consuming bar + (ConversationRoot's overlay composer seat) reads this instead of + hardcoding the number. */ + --dsh-scrollbar-width: 8px; } /* The two paths are mutually exclusive, and the gate is load-bearing rather From de97ea0ba8d40b1c81b81e15a1f6f8a691edb8b3 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 12 Aug 2026 22:54:38 +0800 Subject: [PATCH 019/160] fix(sidebar): synchronize collapsed rail motion --- ...psed-sidebar-shared-entry-motion.i18n.yaml | 6 ++++ ...2-collapsed-sidebar-shared-entry-motion.md | 33 +++++++++++++++++ ...ollapsed-sidebar-shared-entry-motion.zh.md | 33 +++++++++++++++++ .agents/notes/archived/manifest.json | 3 ++ packages/client/ui-sidebar/README.i18n.yaml | 4 +-- packages/client/ui-sidebar/README.md | 2 ++ packages/client/ui-sidebar/README.zh.md | 2 ++ .../src/client/SidebarRoot.module.css | 35 +++++++++++++------ .../ui-sidebar/src/client/SidebarRoot.tsx | 13 +++---- .../tests/sidebar-styles.client.spec.ts | 28 +++++++++++++-- packages/client/ui-workspace/README.i18n.yaml | 4 +-- packages/client/ui-workspace/README.md | 2 +- packages/client/ui-workspace/README.zh.md | 2 +- .../src/client/WorkspaceBrowser.module.css | 4 ++- .../src/client/WorkspaceBrowser.tsx | 10 +++--- .../tests/browser-styles.client.spec.ts | 6 ++++ 16 files changed, 157 insertions(+), 30 deletions(-) create mode 100644 .agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.i18n.yaml create mode 100644 .agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.md create mode 100644 .agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md diff --git a/.agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.i18n.yaml b/.agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.i18n.yaml new file mode 100644 index 0000000000..971bc98a0c --- /dev/null +++ b/.agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.i18n.yaml @@ -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/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.md +2026-08-12-collapsed-sidebar-shared-entry-motion.md: c5bc18973db693cf9ba60800fbcf7720593dbad5 +2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md: b87ef6322d548061cc2e29c0e8f9896098c09041 diff --git a/.agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.md b/.agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.md new file mode 100644 index 0000000000..c5bc18973d --- /dev/null +++ b/.agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.md @@ -0,0 +1,33 @@ +# Agent Note: Collapsed sidebar upper controls share one entry motion + +Status: implemented +Archived: 2026-08-12 + +English | [中文](2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md) + +## Problem + +The collapsed sidebar rail renders four upper controls owned by two packages: the shell owns the toggle and New Session, while the workspace region owns add and search. Their opacity timing matched, but their geometry did not. Right-aligned controls moved with the narrowing column while left-aligned controls stayed fixed, so add appeared slower than search even under the same fade. + +The bottom settings control has a different role. It is pinned to the rail foot and must not join the upper controls' horizontal entry. + +## Decision + +At the rail settle point, the four upper 36px controls start from one left-anchored layout and share one `150ms` animation from `translateX(49px)` to their final 10px inset. The shell applies the translation to its toggle and New Session seats and once to the workspace region, so add and search inherit the same path without nested transforms. Opacity uses the same animation timeline. + +The settings seat uses a separate opacity-only keyframe with the same duration and easing. A page that starts collapsed renders the rail without an entry animation, and reduced-motion mode disables both keyframes. + +## Alternatives considered + +**Keep every rail control fixed at its final inset.** This removes the mismatch, but it also removes the requested horizontal entry from the four upper controls. + +**Animate each workspace button independently.** This would duplicate shell timing inside `ui-workspace` and could apply both a region and child transform. Translating the registered region once keeps animation ownership in the sidebar shell. + +**Translate the settings control with the upper controls.** Rejected because settings is a bottom-pinned foot action, not part of the upper control sequence. + +## Consequences + +- Toggle, New Session, add, and search follow the same horizontal coordinates throughout collapse. +- Settings fades at its final horizontal coordinate. +- Static collapsed renders retain their final geometry without startup motion. +- Style tests pin the shared animation assignments, translation distance, base anchors, and settings exception. diff --git a/.agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md b/.agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md new file mode 100644 index 0000000000..b87ef6322d --- /dev/null +++ b/.agents/notes/archived/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md @@ -0,0 +1,33 @@ +# Agent Note: 收起侧栏的上方控件共用同一进入动画 + +Status: implemented +Archived: 2026-08-12 + +[English](2026-08-12-collapsed-sidebar-shared-entry-motion.md) | 中文 + +## Problem + +收起侧栏轨道的四个上方控件由两个包渲染:外壳持有侧栏切换与新建会话,Workspace 区域持有添加和搜索。它们的透明度时序相同,但几何行为不同。右对齐控件会随栏变窄而移动,左对齐控件则保持不动,因此添加即使使用相同淡入,视觉上仍比搜索慢。 + +底部设置控件承担不同角色。它固定在轨道页脚,不能参与上方控件的横向进入。 + +## Decision + +轨道落位时,四个 36px 上方控件从同一个左对齐布局开始,共用一段 `150ms` 动画,从 `translateX(49px)` 移动到最终 10px 内边距。外壳把位移分别应用于侧栏切换、新建会话,并只对 Workspace 区域应用一次,因此添加与搜索会继承同一路径,不产生嵌套变换。透明度使用同一条动画时间线。 + +设置控件使用时长与缓动相同、但只改变透明度的独立关键帧。页面初始即为收起状态时不会播放进入动画;减少动态效果模式会禁用两段关键帧。 + +## Alternatives considered + +**把每个轨道控件固定在最终内边距。** 这能消除不一致,但也会移除四个上方控件所需的横向进入效果。 + +**分别为每个 Workspace 按钮添加动画。** 这会在 `ui-workspace` 中重复外壳时序,还可能同时应用区域与子控件变换。只移动一次已注册区域,可以让动画继续由侧栏外壳持有。 + +**让设置控件随上方控件一起移动。** 不予采纳,因为设置是固定在底部的页脚操作,不属于上方控件序列。 + +## Consequences + +- 侧栏切换、新建会话、添加与搜索在整个收起过程中使用相同横坐标。 +- 设置在最终横坐标上淡入。 +- 静态收起渲染保持最终几何,不播放启动动画。 +- 样式测试固定共用动画分配、位移距离、基础锚点与设置例外。 diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json index a8377c346f..f8786470c7 100644 --- a/.agents/notes/archived/manifest.json +++ b/.agents/notes/archived/manifest.json @@ -97,6 +97,9 @@ "bug-fix/2026-08-10-web-favicon-dark-mode.i18n.yaml": "sha256:859c4399f9a017a68ba89552fdafa05e73c0599d94cee9551c84ea5b749a14f3", "bug-fix/2026-08-10-web-favicon-dark-mode.md": "sha256:4d17e247abd76ae3aed5fb4e075fd66a2838292f89f7021c82a79fe37ed905e6", "bug-fix/2026-08-10-web-favicon-dark-mode.zh.md": "sha256:7bbff8a3b7061c127afcc75cd2a8043b02a999b78c0180edd8f7e4807fcfe71d", + "bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.i18n.yaml": "sha256:3ce4f6e39e173fc304bf64deca9c95bcddc1dbb492e065ca8c267a7a40788588", + "bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.md": "sha256:7b169aa4543edfc965de5a8b7b9e60aa9d9d5218693cd0b57908e2d482280723", + "bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md": "sha256:88db36c698800bf55c3c7531d6f92665576d978c29c15ff7d74215fb93376cb1", "feature/2026-06-14-acp-agent-client-protocol.i18n.yaml": "sha256:006795baa43ae962a8d125cc0f1e9f134bc2ee9fb758b6e7669e3fa0126e1918", "feature/2026-06-14-acp-agent-client-protocol.md": "sha256:6828c0af74bb3fb96206ca6b21c0e56a000b50e4744aad4bc2c05092f3a5a31b", "feature/2026-06-14-acp-agent-client-protocol.zh.md": "sha256:ba104e841a1fb84edbd3b6c8119d50445b7785255a7a8d13bb9ac8a2cb4d2e69", diff --git a/packages/client/ui-sidebar/README.i18n.yaml b/packages/client/ui-sidebar/README.i18n.yaml index 562396d88f..ac3531b205 100644 --- a/packages/client/ui-sidebar/README.i18n.yaml +++ b/packages/client/ui-sidebar/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-sidebar/README.md -README.md: 9974118f69901de985e012e1b62f95a0bcee64c2 -README.zh.md: 11b0aa142cf62626ab6105e2c405d506e35349b0 +README.md: 7cc4fe0a722fe5f8cf0e983a0e3fdb30cd31bb64 +README.zh.md: 96b6f60f871758c126e6c387c6ce3f0993039af0 diff --git a/packages/client/ui-sidebar/README.md b/packages/client/ui-sidebar/README.md index 9974118f69..7cc4fe0a72 100644 --- a/packages/client/ui-sidebar/README.md +++ b/packages/client/ui-sidebar/README.md @@ -8,6 +8,8 @@ New Session starts the runtime's page-local frontend Session Intent. The runtime `SidebarRootComponentProps` composes the layout owner share, the global `useSessions` and `useWorkspaces` hooks, the declared `sidebar.workspaces` and `sidebar.settings` child slots, and injected `startSession` plus sidebar-toggle callbacks. There is no plugin store. +During a live collapse, the shell holds the expanded content at its current width while it fades out for 150ms. The four upper controls—the shell toggle and New Session plus add and search rendered through `sidebar.workspaces`—then share one 150ms fade and 49px leftward translation into the 56px rail, ending with the layout's 300ms column slide; every 36px control box follows the same path to the rail's 10px left inset. The bottom-pinned `sidebar.settings` control shares the fade timing but has no horizontal translation. A page that starts collapsed renders the rail statically, and reduced-motion mode disables both transitions. + Scrollbars in the column are a pointer affordance: the shell rebinds ui-theme's [scrollbar indirection](../ui-theme/README.md) to `transparent` whenever the pointer is outside it, and keeps the thumb drawn for 2s after the pointer leaves, so a list nobody is pointing at carries no bar. The reservation that keeps rows from moving belongs to the scrolling region ([ui-workspace](../ui-workspace/README.md)), so revealing a thumb never reflows. The foot is the `sidebar.settings` seat: the sidebar renders only the bottom-pinned layout slot and shares its column state (`wide`); ui-settings registers the trigger row and settings panel there. diff --git a/packages/client/ui-sidebar/README.zh.md b/packages/client/ui-sidebar/README.zh.md index 11b0aa142c..96b6f60f87 100644 --- a/packages/client/ui-sidebar/README.zh.md +++ b/packages/client/ui-sidebar/README.zh.md @@ -8,6 +8,8 @@ New Session 会启动运行时的页面局部前端 Session Intent。运行时 `SidebarRootComponentProps` 组合布局 owner share、全局 `useSessions` 和 `useWorkspaces` 钩子、已声明的 `sidebar.workspaces` 与 `sidebar.settings` 子 slot,以及注入的 `startSession` 与侧边栏切换回调。这里没有插件 store。 +实时收起时,外壳会把展开内容固定在当前宽度,并用 150ms 将其淡出。随后,上方四个控件——外壳的侧栏切换与新建会话,以及通过 `sidebar.workspaces` 渲染的添加和搜索——共用一次 150ms 的淡入和 49px 左移,在布局的 300ms 栏滑动结束时一起进入 56px 轨道;每个 36px 控件盒都会沿同一条路径到达轨道左侧 10px 的内边距。固定在底部的 `sidebar.settings` 控件只共用淡入时序,不发生横向位移。页面初始即为收起状态时会静态渲染轨道;减少动态效果模式会禁用两段过渡。 + 栏内的滚动条是一种指针可供性:只要指针不在栏内,外壳就把 ui-theme 的[滚动条间接层](../ui-theme/README.md)重新绑定为 `transparent`;指针离开后滑块再保留 2 秒,因此没人指向的列表不会带着滚动条。避免行位移的空间预留属于滚动区域本身([ui-workspace](../ui-workspace/README.md)),所以显示滑块不会引起重排。 页脚承载 `sidebar.settings`:侧边栏只渲染固定在底部的布局 slot,并共享其栏状态(`wide`);ui-settings 在此注册触发行和设置面板。 diff --git a/packages/client/ui-sidebar/src/client/SidebarRoot.module.css b/packages/client/ui-sidebar/src/client/SidebarRoot.module.css index 17333b5ccc..a0740d50fa 100644 --- a/packages/client/ui-sidebar/src/client/SidebarRoot.module.css +++ b/packages/client/ui-sidebar/src/client/SidebarRoot.module.css @@ -58,24 +58,35 @@ from { opacity: 0; } } -/* Rail controls hold hidden while the column slides shut, then fade in over - the slide's tail: .railIn applies at settle (150ms into the 0.3s AppFrame - track transition), so a 100ms delay + 150ms fade starts just before the - slide ends (250ms) and finishes at 400ms; `backwards` keeps them at - opacity 0 through the delay. Only a live collapse gets .railIn — a - refresh straight into the collapsed state renders statically. */ +/* At the 150ms rail settle, the four upper controls enter from the former + rail right edge over the remaining 150ms of the AppFrame track transition. + The bottom-pinned settings seat shares their opacity timeline but stays + horizontally fixed. Only a live collapse gets .railIn; a cold collapsed + render stays static. */ .railIn .iconButton, .railIn .newSession, +.railIn .regionArea { + animation: rail-in 150ms var(--ds-ease-in-out) backwards; +} + .railIn .footArea { - animation: rail-in 150ms var(--ds-ease-in-out) 100ms backwards; + animation: rail-fade-in 150ms var(--ds-ease-in-out) backwards; } @keyframes rail-in { + from { + opacity: 0; + transform: translateX(49px); + } +} + +@keyframes rail-fade-in { from { opacity: 0; } } -/* Logo row (figma pad (4,8,4,8)): brand left, panel toggle right-anchored — - the toggle is the rail's expand control and slides in with the right edge. */ +/* Logo row (figma pad (4,8,4,8)): expanded keeps the panel toggle at the + right edge; the rail gives every control the same base left anchor before + the shared entry translation is applied. */ .logoRow { flex: none; display: flex; @@ -93,6 +104,7 @@ height: 36px; padding: 0; margin-bottom: 12px; + justify-content: flex-start; } /* Brand group (figma I133:7632): the full wordmark rides the text ink @@ -185,6 +197,8 @@ } .collapsed .newSession { + align-self: flex-start; + width: 36px; height: 36px; padding: 0; margin: 0 0 12px; @@ -239,7 +253,8 @@ .fading > *, .railIn .iconButton, .railIn .newSession, - .railIn .footArea { + .railIn .footArea, + .railIn .regionArea { transition: none; animation: none; } diff --git a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx index f464066f56..5f392f93b3 100644 --- a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx +++ b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx @@ -2,12 +2,13 @@ * Sidebar shell: column geometry only. Collapse is a slide plus crossfade: * content freezes at its expanded width (inline style) and fades out in place * while the sliding column (AppFrame grid tracks) clips it — nothing reflows - * mid-slide. At settle the wide-only content unmounts and the control rows - * snap to the 56px rail (one icon each, same top-down order) fading in as the - * slide ends. The workspace/session browsing region between the New Session - * button and the foot is the `sidebar.workspaces` registrant's, and the foot - * is the `sidebar.settings` registrant's; the shell hands them the wide flag - * (plus an expand request callback for the browser). + * mid-slide. At settle the wide-only content unmounts and the four upper + * controls enter the 56px rail from the same horizontal offset (one icon each, + * same top-down order) on one fade that ends with the slide. The bottom-pinned + * settings control only fades. The workspace/session browsing region between + * the New Session button and the foot is the `sidebar.workspaces` registrant's, + * and the foot is the `sidebar.settings` registrant's; the shell hands them the + * wide flag (plus an expand request callback for the browser). * * The column also owns whether the scroll regions nested in it draw a * scrollbar at all: the shell tracks the pointer and rebinds ui-theme's diff --git a/packages/client/ui-sidebar/tests/sidebar-styles.client.spec.ts b/packages/client/ui-sidebar/tests/sidebar-styles.client.spec.ts index c4abce1911..19e7a54380 100644 --- a/packages/client/ui-sidebar/tests/sidebar-styles.client.spec.ts +++ b/packages/client/ui-sidebar/tests/sidebar-styles.client.spec.ts @@ -1,4 +1,4 @@ -/** Sidebar shell inset contract shared with the nested workspace browser. */ +/** Sidebar shell style contracts shared with its slot-owned controls. */ import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { describe, expect, it } from 'vitest' @@ -25,7 +25,7 @@ function declarations(selector: string): Map | undefined { return undefined } -describe('SidebarRoot.module.css inset', () => { +describe('SidebarRoot.module.css', () => { it('shares and cancels the wide shell trailing padding structurally', () => { const root = declarations('.root') expect(root?.get('--dsh-sidebar-inline-padding')).toBe('12px') @@ -39,4 +39,28 @@ describe('SidebarRoot.module.css inset', () => { expect(declarations('.collapsed .regionArea')?.get('padding-left')).toBe('0') expect(declarations('.collapsed .regionArea')?.get('margin-right')).toBe('0') }) + + it('moves the four upper controls while the settings seat only fades', () => { + const animation = 'rail-in 150ms var(--ds-ease-in-out) backwards' + for (const selector of [ + '.railIn .iconButton', + '.railIn .newSession', + '.railIn .regionArea', + ]) { + expect(declarations(selector)?.get('animation')).toBe(animation) + } + expect(declarations('.railIn .footArea')?.get('animation')).toBe( + 'rail-fade-in 150ms var(--ds-ease-in-out) backwards', + ) + expect(css).toMatch( + /@keyframes rail-in\s*\{\s*from\s*\{\s*opacity: 0;\s*transform: translateX\(49px\);\s*}\s*}/, + ) + expect(css).toMatch(/@keyframes rail-fade-in\s*\{\s*from\s*\{\s*opacity: 0;\s*}\s*}/) + }) + + it('gives shell rail controls the same base anchor for their shared translation', () => { + expect(declarations('.collapsed .logoRow')?.get('justify-content')).toBe('flex-start') + expect(declarations('.collapsed .newSession')?.get('align-self')).toBe('flex-start') + expect(declarations('.collapsed .newSession')?.get('width')).toBe('36px') + }) }) diff --git a/packages/client/ui-workspace/README.i18n.yaml b/packages/client/ui-workspace/README.i18n.yaml index 7a50937c41..6d5eff023b 100644 --- a/packages/client/ui-workspace/README.i18n.yaml +++ b/packages/client/ui-workspace/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-workspace/README.md -README.md: 9d7d4d77cc064146f1fdaed615509215c64308fc -README.zh.md: ca35d7cd2e7ff176f4ea40d1e9a3a6d1a7457462 +README.md: 8878aa49dcccd60ddcde5f0a9563bbfbd969c9c0 +README.zh.md: c83f1c53d5471dd8d52b933163a573eb98a70c2e diff --git a/packages/client/ui-workspace/README.md b/packages/client/ui-workspace/README.md index 9d7d4d77cc..8878aa49dc 100644 --- a/packages/client/ui-workspace/README.md +++ b/packages/client/ui-workspace/README.md @@ -6,7 +6,7 @@ Shared Workspace browser and picker plugin. `WorkspaceBrowser` fills the sidebar The browser renders grouped or flat Session rows from the global runtime hooks and owns Workspace add/rename/reorder plus Session reorder. A Workspace remembers whether it is closed or showing Sessions; an open Workspace shows five Sessions by default, offers a transient **Show more** control for the remainder, and returns to five after the whole Workspace is closed and reopened. Creating a Session from a Workspace row first opens that group so the new row remains visible when the Session state arrives. Once the Workspace list baseline is ready, browser-persisted expansion and Session-order records retain only current Workspace ids plus Ungrouped and the flat-list account. View options combine grouping with one browser-persisted Session order per account: real Workspaces initialize from `WorkspaceView.sessionIds`, while Ungrouped and the cross-Workspace flat list initialize from recency. **Manual** and **Last updated** apply in either presentation. Entering Last updated performs a complete recency sort and later user prompts or steers promote their Session once, while entering Manual preserves every current position and disables later promotion. Dragging edits the current order in either mode; Manual-mode drags for real Workspaces also update the Host Session account, while Ungrouped and flat-list orders remain browser-local because neither has one Workspace account. Flat rows omit the empty leading status slot because they have no parent hierarchy, but retain it when a Session status is visible. Workspace drag order is Host-durable in either Session order mode. -Collapsed search is one header action beside the view and add actions. Activating it expands the field across the header; an outside click collapses only a query that is empty after trimming, while the clear control always resets and collapses it. A non-blank search query replaces either browsing mode with one flat result list: case-insensitive title and Workspace substring matches appear immediately, while a 250 ms debounced Host request adds ranked current-conversation content matches and snippets. The English search input and its defensive request path remove NUL, cap the query at the wire schema's 500 UTF-16 code units without splitting a surrogate pair, and preserve the existing debounce and cancellation behavior. Each new query aborts the preceding request; a failed content search leaves metadata matches visible with a warning. The list is capped at 20, asks the user to narrow broader queries, and opens the selected Session without clearing the query or jumping to a specific event. +Collapsed search is one header action beside the view and add actions. In the rail, add and search render as 36px controls on the shell's shared horizontal entry path. Activating search expands the field across the header; an outside click collapses only a query that is empty after trimming, while the clear control always resets and collapses it. A non-blank search query replaces either browsing mode with one flat result list: case-insensitive title and Workspace substring matches appear immediately, while a 250 ms debounced Host request adds ranked current-conversation content matches and snippets. The English search input and its defensive request path remove NUL, cap the query at the wire schema's 500 UTF-16 code units without splitting a surrogate pair, and preserve the existing debounce and cancellation behavior. Each new query aborts the preceding request; a failed content search leaves metadata matches visible with a warning. The list is capped at 20, asks the user to narrow broader queries, and opens the selected Session without clearing the query or jumping to a specific event. The picker lists real Host Workspace entities through the global `useWorkspaces` hook. Selecting a Workspace invokes the slot owner's `onPick` callback to retarget the frontend Session object. Distinct canonical paths remain separate id-keyed Workspaces when their basenames and display titles match; the sidebar hover detail exposes the full path. Each registration declares a **directory-flow child hole** (`single` kind: `conversation.hero.workspace.directoryFlow` / `sidebar.workspaces.directoryFlow`) that the composed picker package's client half fills with its picking interaction — the [`-native`](../../host/directory-picker-native/README.md) backend's renderless OS-chooser driver today, an in-app browsing dialog under a `-browse` composition. The flat **Add workspace...** action renders only while the surface's hole is occupied (occupancy read per menu render; an empty hole means the composition has no picking affordance — the seam's documented no-flow default, under which the sidebar header drops its add button rather than offering a dead one). This package owns the trigger and the adoption: the occupant reports one picked path per open through the hole's owner conversation (`open`/`busy`/`onPicked`/`onCancel`/`onError`), and the owner adopts it through the object layer, selecting the committed Workspace only after its list projection has refreshed; cancellation is silent, and errors land in the retryable folder dialog whose **Choose again** reopens the flow. Adding has exactly one route: the occupant's own create-folder affordance already covers a brand-new directory, so no separate create-by-name dialog exists. A menu only appears where there is something to choose between — with no Workspace listed, the anchor gesture raises the flow directly instead of a one-row popover, and it waits for the list baseline before treating an empty list as final. The runtime Session and Workspace services own materialization. The Workspace row's Delete action opens a confirmation that states the retention boundary, blocks duplicate submission, and keeps failures open; success removes the group while its Sessions remain under Ungrouped. The Session row's Rename action opens the same browser-owned dialog pattern prefilled with the row's display title: no client-side conflict rule exists (the host normalizes and may reject with `title-invalid`, rendered in the dialog alert), and confirming an unchanged title is deliberately allowed — it pins the current automatic title against regeneration. The Session row's Archive action commits without a confirmation dialog (non-destructive: the log and the workspace accounting slot remain) through `ctx.workspaces.archiveSession`; the row disappears from every grouping surface — workspace groups, Ungrouped, content search, and the flat list — when the archive-set echo lands, and failures are console diagnostics that leave the tree unchanged. A blank New Session row is a pure placeholder: it renders no row menu and no time label (nothing has happened in it yet), so rename, fork, and archive first apply once the first prompt lands. diff --git a/packages/client/ui-workspace/README.zh.md b/packages/client/ui-workspace/README.zh.md index ca35d7cd2e..c83f1c53d5 100644 --- a/packages/client/ui-workspace/README.zh.md +++ b/packages/client/ui-workspace/README.zh.md @@ -6,7 +6,7 @@ 该浏览器通过全局运行时钩子将 Session 行渲染为分组或扁平形式,并负责 Workspace 添加/重命名/重排序以及 Session 重排序。每个 Workspace 会记住自身是关闭还是显示 Session;打开后默认显示五条 Session,其余条目通过临时的**展开其余**控件显示,而关闭并重新打开整个 Workspace 后会恢复为五条。从 Workspace 行创建 Session 时会先打开该分组,使 Session 状态到达后新行保持可见。Workspace 列表基线就绪后,浏览器持久化的展开状态与 Session 顺序记录只保留当前 Workspace id、Ungrouped 和单列表记账。视图选项把分组方式和每个记账各自的一份浏览器持久化 Session 顺序放在一起:真实 Workspace 从 `WorkspaceView.sessionIds` 初始化,Ungrouped 和跨 Workspace 的单列表则从最近更新时间顺序初始化。**手动排序**和**最近更新**在两种呈现方式下都可用。进入最近更新时会执行一次完整的时间排序,后续 user prompt 或 steer 会将对应 Session 置顶一次;进入手动排序则保留所有当前位置并停用后续置顶。两种模式下的拖拽都会编辑当前顺序;真实 Workspace 在手动模式下的拖拽还会更新 Host Session 记账,而 Ungrouped 和单列表因没有单一 Workspace 记账,其顺序始终只保存在浏览器本地。单列表没有父级层次,因此不显示空的左侧状态槽;Session 存在可见状态时仍保留该槽。无论采用哪种 Session 顺序,Workspace 拖拽顺序都由 Host 持久化。 -折叠搜索是视图和添加操作旁的一枚区头按钮。激活后,输入框会扩展并占据区头;点击外部只会收起经清除首尾空白后为空的查询,而清除控件总会重置并收起搜索。非空白查询会以单一扁平结果列表替代任一浏览模式:不区分大小写的标题和 Workspace 子串匹配项会立即显示,经 250 ms 防抖的 Host 请求则会加入经过排序的当前对话内容匹配项及其摘要片段。英文搜索输入框及其防御性请求路径会移除 NUL,将查询限制在传输 schema 规定的 500 个 UTF-16 代码单元内且不会拆分代理项对,并保留现有的防抖与取消行为。每次新查询都会中止前一个请求;内容搜索失败时,元数据匹配项仍会显示,同时给出警告。列表最多显示 20 条结果,并会在查询过宽时提示用户缩小范围;打开所选 Session 时既不会清除查询,也不会跳转至特定事件。 +折叠搜索是视图和添加操作旁的一枚区头按钮。在轨道中,添加和搜索会渲染为沿外壳共用横向进入路径移动的 36px 控件。激活搜索后,输入框会扩展并占据区头;点击外部只会收起经清除首尾空白后为空的查询,而清除控件总会重置并收起搜索。非空白查询会以单一扁平结果列表替代任一浏览模式:不区分大小写的标题和 Workspace 子串匹配项会立即显示,经 250 ms 防抖的 Host 请求则会加入经过排序的当前对话内容匹配项及其摘要片段。英文搜索输入框及其防御性请求路径会移除 NUL,将查询限制在传输 schema 规定的 500 个 UTF-16 代码单元内且不会拆分代理项对,并保留现有的防抖与取消行为。每次新查询都会中止前一个请求;内容搜索失败时,元数据匹配项仍会显示,同时给出警告。列表最多显示 20 条结果,并会在查询过宽时提示用户缩小范围;打开所选 Session 时既不会清除查询,也不会跳转至特定事件。 该选择器通过全局 `useWorkspaces` hook 列出真实的 Host Workspace 实体。选择 Workspace 会调用 slot owner 的 `onPick` 回调,重新定位前端 Session 对象。不同的规范化路径即使 basename 和显示标题相同,仍会作为由 id 区分的独立 Workspace;侧边栏的悬停详情会显示完整路径。每个注册各自声明一个**目录流子 slot**(`single` kind:`conversation.hero.workspace.directoryFlow`/`sidebar.workspaces.directoryFlow`),由组合的选择器包 client half 填入其选取交互——今天是 [`-native`](../../host/directory-picker-native/README.md) 后端的无渲染 OS 选择器驱动,`-browse` 组合下则是应用内浏览对话框。平铺显示的 **添加工作区…** 操作仅在当前界面的 slot 被占用时渲染(每次菜单渲染读取占用状态;slot 为空意味着该组合没有目录选择能力——seam 文档化的无流程默认行为,此时侧边栏区头直接不渲染添加按钮,而非留下一个点了没反应的按钮)。本包持有触发与接纳:占用方通过 slot 的属主交互约定(`open`/`busy`/`onPicked`/`onCancel`/`onError`)每次打开上报一个所选路径,owner 通过对象层接纳它,并等待 Workspace 列表投影刷新后才选中已提交的 Workspace;取消操作不会显示提示,错误落入可重试的文件夹对话框,其 **重新选择** 会重新打开流程。添加只有一条路径:占用者自带的新建文件夹能力已经覆盖了全新目录,因此不再单设按名称创建的对话框。菜单只在确有多个目标可选时出现——没有 Workspace 可列时,锚点手势直接拉起流程,而不是弹出只有一行的浮层;在列表基线落地前,空列表不算最终结果。运行时 Session 与 Workspace 服务负责物化。Workspace 行内的 Delete 操作会打开确认框,说明保留边界、阻止重复提交,并在失败时保持打开;成功后,该分组会被移除,其 Session 则留在 Ungrouped 下。Session 行内的 Rename 操作打开同款浏览器持有的对话框,并以该行的显示标题预填:客户端不设名称冲突规则(host 负责规范化,可能以 `title-invalid` 拒绝,错误渲染在对话框告警区);确认未修改的标题是有意允许的——这正是把当前自动标题钉住、不再被重新生成覆盖的手势。Session 行内的 Archive 操作不经确认对话框直接提交(非破坏性:日志和 workspace 记账席位保持不变),通过 `ctx.workspaces.archiveSession` 归档;归档集合回声落地后,该行从所有分组视图——workspace 分组、Ungrouped、内容搜索和平铺列表——中消失,失败只作为控制台诊断输出,树保持不变。空白的「新会话」行只是占位符:不渲染行菜单和时间标签(其中还没有发生任何事),重命名、fork 和归档都从首条提示词落地后才可用。 diff --git a/packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css b/packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css index a160f2ffd7..4d19163322 100644 --- a/packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css +++ b/packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css @@ -240,11 +240,13 @@ /* Rail variant (own .rail class from the wide owner prop — the region never reads the shell's class names): the two icon controls stack as 36x36 - circles matching the shell's rail rhythm. */ + circles matching the shell's rail rhythm. Both use the rail's shared base + left anchor so the outer shell can translate the whole region uniformly. */ .rail .sectionHeader { gap: 0; padding-left: 0; margin-bottom: 12px; + justify-content: flex-start; } .rail .headerActions { diff --git a/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx b/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx index e9d6ed192f..ebd0e11a33 100644 --- a/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx +++ b/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx @@ -3,11 +3,11 @@ * `sidebar.workspaces` hole: section header (title + view options + add * workspace), search, the grouped tree or flat list, and the workspace * dialogs. Wide state renders the full browser; rail state renders the two - * region icons (search / add workspace), each requesting shell expansion - * through the owner share. Adding is the header button's one action, so it - * raises the directory flow with no menu in between; the flow and its error - * dialog live in WorkspacePicker (same package — direct composition, no slot - * between them). + * region icons (search / add workspace) as 36px controls on the shell's shared + * rail entry path, each requesting expansion through the owner share. Adding is the header + * button's one action, so it raises the directory flow with no menu in + * between; the flow and its error dialog live in WorkspacePicker (same + * package — direct composition, no slot between them). */ import { useEffect, useMemo, useRef, useState } from 'react' import clsx from 'clsx' diff --git a/packages/client/ui-workspace/tests/browser-styles.client.spec.ts b/packages/client/ui-workspace/tests/browser-styles.client.spec.ts index d66baef917..5554c977bb 100644 --- a/packages/client/ui-workspace/tests/browser-styles.client.spec.ts +++ b/packages/client/ui-workspace/tests/browser-styles.client.spec.ts @@ -107,4 +107,10 @@ describe('WorkspaceBrowser.module.css list', () => { expect(rowDeclarations('.sessionRow.selected')?.get('background')) .toBe('var(--dsw-alias-interactive-bg-hover)') }) + + it('pins both rail controls to the shared left anchor during the column slide', () => { + expect(declarations('.rail .sectionHeader')?.get('justify-content')).toBe('flex-start') + expect(declarations('.rail .iconButton')?.get('width')).toBe('36px') + expect(declarations('.rail .search')?.get('width')).toBe('36px') + }) }) From ece86450804ef4343a9db400445b82a7ccc68290 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 12 Aug 2026 23:28:15 +0800 Subject: [PATCH 020/160] fix: suppress runtime context in minimal profiles --- ...nimal-preset-owns-rl-composition.i18n.yaml | 4 +- ...8-10-minimal-preset-owns-rl-composition.md | 4 +- ...0-minimal-preset-owns-rl-composition.zh.md | 4 +- ...0-current-sandbox-policy-context.i18n.yaml | 4 +- ...26-07-30-current-sandbox-policy-context.md | 2 +- ...07-30-current-sandbox-policy-context.zh.md | 2 +- ...l-profiles-bare-two-tool-runtime.i18n.yaml | 4 +- ...-minimal-profiles-bare-two-tool-runtime.md | 8 +- ...nimal-profiles-bare-two-tool-runtime.zh.md | 8 +- .../agent-presets/minimal/agent.cordis.yml | 7 +- apps/web/tests/minimal-preset.snapshot.ts | 3 + docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 15 +- docs/config-catalog.zh.md | 15 +- docs/subsystems/system-prompt.i18n.yaml | 4 +- docs/subsystems/system-prompt.md | 10 +- docs/subsystems/system-prompt.zh.md | 10 +- examples/jsonrpc-agent/README.i18n.yaml | 4 +- examples/jsonrpc-agent/README.md | 2 +- examples/jsonrpc-agent/README.zh.md | 2 +- examples/jsonrpc-agent/minimal.cordis.yml | 4 +- examples/jsonrpc-agent/tests/sdk.snapshot.ts | 33 ++-- .../notifications.expected.jsonl | 143 +++++++++--------- .../snapshots/persistent-tools/session.jsonl | 143 +++++++++--------- packages/core/system-prompt/README.i18n.yaml | 4 +- packages/core/system-prompt/README.md | 7 +- packages/core/system-prompt/README.zh.md | 7 +- packages/core/system-prompt/src/index.ts | 49 ++++-- .../core/system-prompt/tests/scoped.spec.ts | 17 +++ .../system-prompt/tests/system-prompt.spec.ts | 19 +++ .../agent-spine-demo/README.i18n.yaml | 4 +- packages/examples/agent-spine-demo/README.md | 4 +- .../examples/agent-spine-demo/README.zh.md | 4 +- .../examples/agent-spine-demo/src/index.ts | 11 +- .../agent-spine-demo/tests/agent-core.spec.ts | 9 ++ packages/preset/persona/README.i18n.yaml | 4 +- packages/preset/persona/README.md | 5 +- packages/preset/persona/README.zh.md | 5 +- packages/preset/persona/src/index.ts | 4 + packages/preset/persona/tests/persona.spec.ts | 37 +++++ .../tool-cordis/src/api-catalog.ts | 4 + 41 files changed, 400 insertions(+), 233 deletions(-) diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml index cca3debb14..0b27ea5d9f 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md -2026-08-10-minimal-preset-owns-rl-composition.md: d83083de74f1d3203822d82e4e04560819412b48 -2026-08-10-minimal-preset-owns-rl-composition.zh.md: 36aeea0d8af6e371eddd6f09e36bb48a0348959e +2026-08-10-minimal-preset-owns-rl-composition.md: 65d24f9a03eedffac34f0c0141a8e2f643a48b7b +2026-08-10-minimal-preset-owns-rl-composition.zh.md: 72eaa0e5bba46743abd336e64316c886fa58631a diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md index d83083de74..65d24f9a03 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md +++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md @@ -14,13 +14,13 @@ The split also hid other drift. The preset mounted one-shot Bash rather than the The shipped Web `minimal` preset is the sole Web owner of the RL agent composition. It declares an entry-local PTY registry and local backend, persistent `bash` with the RL environment description and 300-second timeout, and `str_replace_editor`. Tool presentation remains a deployment choice. The later [bare two-tool runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) supersedes this note's original compaction and filesystem-provider choices: the current preset mounts an entry-local `fs-local` provider and no compaction backend. The editor accepts no `requireAbsolutePath` setting because absolute paths are its unconditional contract. -The preset persona is exactly `You are a helpful software engineer assistant.` and sets `complete: true`. A complete `PromptSection` participates in ordinary assembly so tools, contexts, variables, and cooperative listeners still resolve; after the `system-prompt/assemble` waterfall, the prompt registry restores a detached copy of that section as the sole system-prompt section. Multiple effective complete sections reject assembly. This final registry constraint prevents harness identity, Web orientation, tool guidance, or an assembly listener from appending prompt text. +The preset persona is exactly `You are a helpful software engineer assistant.`, sets `complete: true`, and suppresses runtime context for its agent scope. A complete `PromptSection` participates in ordinary assembly so tools, variables, and cooperative listeners still resolve; after the `system-prompt/assemble` waterfall, the prompt registry restores a detached copy of that section as the sole system-prompt section and discards every dynamic context contribution. Multiple effective complete sections reject assembly. These final registry constraints prevent harness identity, Web orientation, tool guidance, an assembly listener, sandbox policy, approval policy, delegation, or another dynamic context provider from adding model input. The process-wide `core-web.cordis.yml` patch is absent. Browser UI, workspace attachment, persistence, subprocess, sandbox, permission, model routing, and other cross-session services remain host-owned. Selecting `minimal` changes one agent's model-facing composition and shadows the host filesystem provider only for that agent, without changing other sessions in the Web process. ## Verification -System-prompt and persona package tests prove final complete-section enforcement, including waterfall mutation and duplicate rejection. The shipped-preset composition test asserts the exact prompt, Bash description, absolute editor schema, and two-tool catalog under the default native presentation. The keyless Web replay sends a real request through a `minimal` agent while global identity, Web-orientation text, and a test section are registered, asserts the entry-local filesystem is bare and compaction is absent, then executes two persistent Bash calls to prove environment and cwd state survive and executes the editor through an absolute path. +System-prompt and persona package tests prove final complete-section and runtime-context suppression, including waterfall mutation and duplicate rejection. The shipped-preset composition test asserts the exact prompt, Bash description, absolute editor schema, and two-tool catalog under the default native presentation. The keyless Web replay sends a real request through a `minimal` agent while global identity, Web-orientation text, dynamic policy contexts, and a test section are registered, asserts that no runtime-context snapshot exists, the entry-local filesystem is bare, and compaction is absent, then executes two persistent Bash calls to prove environment and cwd state survive and executes the editor through an absolute path. The standalone [`minimal.cordis.yml`](../../../../examples/jsonrpc-agent/minimal.cordis.yml) is the complete two-tool composition for the bundled JSON-RPC runtime. The [bare two-tool runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) owns its launch-specific environment configuration, bare filesystem, and absence of compaction. Its keyless SDK replay asserts the assembled system prompt and two-tool catalog, executes persistent Bash across calls, and exercises the editor; the Python SDK tutorial provides the runnable entry point. diff --git a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md index 36aeea0d8a..72eaa0e5bb 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.zh.md @@ -14,13 +14,13 @@ Status: implemented 随附的 Web `minimal` preset 是 RL agent 组合在 Web 中的唯一所有者。它声明 entry 本地的 PTY 注册表与本地后端、带 RL 环境描述且超时为 300 秒的持久 `bash`,以及 `str_replace_editor`。工具呈现仍由部署选择。后续的[裸双工具运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md)取代了本记录最初的压缩与文件系统提供方选择:当前 preset 挂载 entry 本地的 `fs-local` 提供方,不挂载压缩后端。编辑器不接受 `requireAbsolutePath` 设置,因为要求绝对路径是它的无条件约定。 -preset persona 恰好是 `You are a helpful software engineer assistant.`,并设置 `complete: true`。complete `PromptSection` 参与常规组装,因此工具、上下文、变量和协作式监听器仍会解析;`system-prompt/assemble` waterfall(瀑布式事件)结束后,提示词注册表会将该段落的独立副本恢复为唯一的系统提示词段落。存在多个有效 complete 段时,组装会被拒绝。这项最终注册表约束可防止 harness 身份、Web 定位、工具引导或组装监听器追加提示词文本。 +preset persona 恰好是 `You are a helpful software engineer assistant.`,它设置 `complete: true`,并为其 agent 作用域抑制 runtime context。complete `PromptSection` 参与常规组装,因此工具、变量和协作式监听器仍会解析;`system-prompt/assemble` waterfall(瀑布式事件)结束后,提示词注册表会将该段落的独立副本恢复为唯一的系统提示词段落,并丢弃每个动态上下文贡献。存在多个有效 complete 段时,组装会被拒绝。这些最终注册表约束可防止 harness 身份、Web 定位、工具引导、组装监听器、沙箱策略、批准策略、委派或其他动态上下文提供方添加模型输入。 进程级 `core-web.cordis.yml` patch 不再存在。浏览器 UI、workspace 附加、持久化、子进程、沙箱、权限、模型路由及其他跨会话服务仍由宿主持有。选择 `minimal` 会改变一个 agent 面向模型的组合,并且仅为该 agent 遮蔽宿主文件系统提供方,不会改变 Web 进程中的其他会话。 ## 验证 -系统提示词与 persona 包测试证明了 complete 段的最终约束,包括 waterfall 修改与重复项拒绝。交付 preset 组合测试在默认原生呈现下断言精确的提示词、Bash 描述、要求绝对路径的编辑器 schema 和双工具目录。无密钥 Web 回放通过 `minimal` agent 发送一个真实请求,同时注册全局身份、Web 定位文本和一个测试段落;它断言 entry 本地文件系统是裸后端且压缩不存在,随后执行两次持久 Bash 调用,证明环境与 cwd 状态能够保留,并通过绝对路径执行编辑器。 +系统提示词与 persona 包测试证明了 complete 段最终约束与 runtime-context 抑制,包括 waterfall 修改与重复项拒绝。交付 preset 组合测试在默认原生呈现下断言精确的提示词、Bash 描述、要求绝对路径的编辑器 schema 和双工具目录。无密钥 Web 回放通过 `minimal` agent 发送一个真实请求,同时注册全局身份、Web 定位文本、动态策略上下文和一个测试段落;它断言不存在 runtime-context 快照、entry 本地文件系统是裸后端且压缩不存在,随后执行两次持久 Bash 调用,证明环境与 cwd 状态能够保留,并通过绝对路径执行编辑器。 独立的 [`minimal.cordis.yml`](../../../../examples/jsonrpc-agent/minimal.cordis.yml) 是内置 JSON-RPC 运行时的完整双工具组合。[裸双工具运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md)说明其启动方式专属的环境配置、裸文件系统和无压缩选择。其无密钥 SDK 回放会断言组装后的系统提示词与双工具目录,跨调用执行持久 Bash,并使用编辑器;Python SDK 教程提供可运行的入口。 diff --git a/.agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.i18n.yaml b/.agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.i18n.yaml index 5455415389..695a161749 100644 --- a/.agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.md -2026-07-30-current-sandbox-policy-context.md: ab231d19aec4852369c0f1a13fc50c69ab8128f1 -2026-07-30-current-sandbox-policy-context.zh.md: 4ac493dcfff5c286d569a46c34686dc4b8746618 +2026-07-30-current-sandbox-policy-context.md: b4c657ba52ff26e2e295e3635201424a155738d0 +2026-07-30-current-sandbox-policy-context.zh.md: bd32b021ef8622dc6843a163b21714a263bc8bed diff --git a/.agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.md b/.agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.md index ab231d19ae..b4c657ba52 100644 --- a/.agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.md +++ b/.agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.md @@ -12,7 +12,7 @@ The sandbox policy already enforced and logged each session's file-effect mode, `dsh-sandbox-policy`, the owner of mode and workspace-root resolution, registers one `sandbox:policy` cache-safe context contribution. Every agent request resolves the active session directly through `ctx.sandboxPolicy.resolve({ session })`; there is no denial-history scan or process-local “last told” state. -The policy contribution is capability-neutral and present for every agent session. It does not maintain a second inventory of mounted backends or tools; model-visible schemas remain the authority for available operations, while the context conditions its claims on any available operation that the DSH file sandbox enforces. The [capability-neutral policy context decision](../simplification/2026-07-31-capability-neutral-sandbox-policy-context.md) supersedes the earlier family-registration mechanism while retaining this note's cache-safe delivery and durable snapshot design. +The policy contribution is capability-neutral and available to every agent session by default. A composition may suppress the complete runtime-context channel when its model interface intentionally excludes dynamic context; this does not disable policy enforcement. The contribution does not maintain a second inventory of mounted backends or tools; model-visible schemas remain the authority for available operations, while the context conditions its claims on any available operation that the DSH file sandbox enforces. The [capability-neutral policy context decision](../simplification/2026-07-31-capability-neutral-sandbox-policy-context.md) supersedes the earlier family-registration mechanism while retaining this note's cache-safe delivery and durable snapshot design. The contribution states only facts shared by every enforcement dialect. `read-only` says an available sandbox-enforced operation cannot modify files in the standing mode and directs the model to try an available tool normally, then follow any denial and escalation guidance that tool returns. `workspace-write` states the canonical session workspace with non-exclusive wording and summarizes, without enumerating, that some platform temporary areas may also be writable. `danger-full-access` says the DSH file sandbox does not restrict file modifications by available operations. Backend-selected temporary paths, `/dev/null`, runner readiness, exact tool availability, and other policy domains are absent because `resolve()` cannot establish them at request assembly. diff --git a/.agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.zh.md b/.agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.zh.md index 4ac493dcff..bd32b021ef 100644 --- a/.agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.zh.md +++ b/.agents/notes/implemented/feature/2026-07-30-current-sandbox-policy-context.zh.md @@ -12,7 +12,7 @@ Status: implemented `dsh-sandbox-policy` 负责解析模式与工作区根目录,并注册一项缓存安全的 `sandbox:policy` 上下文贡献。每次 agent(智能体)请求都通过 `ctx.sandboxPolicy.resolve({ session })` 直接解析当前会话;不存在拒绝历史扫描或进程本地的「上次告知」状态。 -该策略贡献不依赖具体能力,并存在于每个 agent 会话中。它不会另行维护一份已挂载后端或工具清单;模型可见的 schema 仍是可用操作的权威来源,而上下文会将其声明限定在 DSH 文件沙箱所强制执行的任何可用操作上。[不依赖具体能力的策略上下文决策](../simplification/2026-07-31-capability-neutral-sandbox-policy-context.md)取代了较早的家族注册机制,同时保留本 Agent Note 的缓存安全交付与持久快照设计。 +该策略贡献不依赖具体能力,并默认可用于每个 agent 会话。当模型接口有意排除动态上下文时,组合可以抑制完整的 runtime-context 通道;这不会禁用策略强制机制。该贡献不会另行维护一份已挂载后端或工具清单;模型可见的 schema 仍是可用操作的权威来源,而上下文会将其声明限定在 DSH 文件沙箱所强制执行的任何可用操作上。[不依赖具体能力的策略上下文决策](../simplification/2026-07-31-capability-neutral-sandbox-policy-context.md)取代了较早的家族注册机制,同时保留本 Agent Note 的缓存安全交付与持久快照设计。 该贡献只说明所有强制执行方言所共有的事实。`read-only` 表明受沙箱强制执行的可用操作在常驻模式下无法修改文件,并指示模型正常尝试可用工具,随后遵循该工具返回的任何拒绝与升权引导。`workspace-write` 用非排他措辞说明规范化的会话工作区,并概述某些平台临时区域可能也可写,而不逐一列举。`danger-full-access` 表明 DSH 文件沙箱不会限制可用操作修改文件。后端选择的临时路径、`/dev/null`、runner 就绪状态、确切的工具可用情况和其他策略领域都不会出现,因为 `resolve()` 无法在请求组装时确定它们。 diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml index aae790e624..f9814a5866 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md -2026-08-11-minimal-profiles-bare-two-tool-runtime.md: cf48c71214420443411bf91253b5e3a1c72a2c6c -2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: 5cfe819e1cd18f9c9a3c8d63ab9bf8e34b888a24 +2026-08-11-minimal-profiles-bare-two-tool-runtime.md: df61189c3072aff9c55154fa99b43ca50df50c3d +2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md: 54548b493e3d7eb5c34c455f658138a1107ca5fe diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md index cf48c71214..df61189c30 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md @@ -12,7 +12,7 @@ The two launch paths also have different configuration owners. Web mounts a per- ## Decision -Both shipped minimal profiles expose exactly persistent `bash` and `str_replace_editor`, mount no context-compaction provider, and run the editor against `@deepseek-ai/dsh-fs-local`. The Web preset isolates `ctx.fs` inside the agent entry and mounts `fs-local` beside the editor, so other Web agents retain the host filesystem provider. Its persona remains the fixed complete prompt owned by the earlier [minimal-preset composition decision](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md). +Both shipped minimal profiles expose exactly persistent `bash` and `str_replace_editor`, mount no context-compaction provider, suppress every `dsh-system-prompt` runtime-context contribution for fresh sessions, and run the editor against `@deepseek-ai/dsh-fs-local`. The Web preset isolates `ctx.fs` inside the agent entry and mounts `fs-local` beside the editor, so other Web agents retain the host filesystem provider. Its persona remains the fixed complete prompt owned by the earlier [minimal-preset composition decision](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md) and applies runtime-context suppression only to that agent scope. The standalone spine forwards the same setting to its process-owned system-prompt service. Sandbox and approval services remain mounted and enforce their policies; only their model-facing dynamic context is absent. The standalone [`minimal.cordis.yml`](../../../../examples/jsonrpc-agent/minimal.cordis.yml) remains a complete JSON-RPC process composition. It mounts `dsh-jsonrpc`, the local PTY and subprocess services required by persistent Bash, `fs-local`, the two tool consumers, and uncompressed JSONL persistence. It does not mount `token-meter`, `compact-basic`, `fs-sandbox`, or `fs-policy`. Persistent Bash still consumes the deployment's danger-full-access sandbox policy; the editor is not confined by that policy. @@ -20,9 +20,9 @@ The standalone [`minimal.cordis.yml`](../../../../examples/jsonrpc-agent/minimal ## Verification -The Web replay boots the complete Web host, creates the agent through the preset service, and asserts that the scoped filesystem is bare, no scoped compaction service exists, and the assembled request contains exactly the fixed prompt and two tools. It then executes persistent Bash and the editor against the real scoped services. +The Web replay boots the complete Web host, creates the agent through the preset service, and asserts that the scoped filesystem is bare, no scoped compaction service exists, no system-prompt-owned runtime-context message was appended, and the assembled request contains exactly the fixed prompt and two tools. It then executes persistent Bash and the editor against the real scoped services. -The SDK replay boots the real JSON-RPC agent process through the SDK client, injects an environment-selected prompt, asserts the assembled prompt and exact two-tool catalog, and executes both tools. Python SDK bundled-runtime coverage initializes the standalone configuration through each available packaged carrier with environment-selected model, model capacity, and prompt values. Cordis validation checks that both configurations resolve their declared plugins and configuration fields. +The SDK replay boots the real JSON-RPC agent process through the SDK client, injects an environment-selected prompt, asserts the assembled prompt, exact two-tool catalog, and absence of every system-prompt-owned runtime-context message, and executes both tools. Python SDK bundled-runtime coverage initializes the standalone configuration through each available packaged carrier with environment-selected model, model capacity, and prompt values. Cordis validation checks that both configurations resolve their declared plugins and configuration fields. ## Alternatives considered @@ -36,4 +36,4 @@ The SDK replay boots the real JSON-RPC agent process through the SDK client, inj ## Consequences -Minimal sessions never summarize or replace earlier history; callers must keep turns within the selected model's context capacity. The editor can address any absolute path visible to the runtime process, independently of the persistent shell's sandbox policy. The two launch paths share their model-facing tool and no-compaction guarantees while retaining different prompt and model configuration appropriate to their owners. The Python SDK path continues to communicate only through the bundled stdio JSON-RPC runtime. +Minimal sessions never summarize or replace earlier history and never add a runtime-context snapshot; callers must keep turns within the selected model's context capacity and must not rely on model-visible narration of standing sandbox or approval policy. The editor can address any absolute path visible to the runtime process, independently of the persistent shell's sandbox policy. The two launch paths share their model-facing tool, no-context, and no-compaction guarantees while retaining different prompt and model configuration appropriate to their owners. The Python SDK path continues to communicate only through the bundled stdio JSON-RPC runtime. diff --git a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md index 5cfe819e1c..54548b493e 100644 --- a/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md +++ b/.agents/notes/implemented/feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.zh.md @@ -12,7 +12,7 @@ Web `minimal` preset 与独立 JSON-RPC minimal 组合对外提供持久 `bash` ## 决策 -两种随附 minimal profile 都只对外提供持久 `bash` 与 `str_replace_editor`,不挂载上下文压缩提供方,并让编辑器使用 `@deepseek-ai/dsh-fs-local`。Web preset 在 agent entry 内隔离 `ctx.fs`,将 `fs-local` 与编辑器一起挂载,因此其他 Web agent 仍使用宿主文件系统提供方。其 persona 继续采用较早的 [minimal preset 组合决策](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md)所拥有的固定 complete 提示词。 +两种随附 minimal profile 都只对外提供持久 `bash` 与 `str_replace_editor`,不挂载上下文压缩提供方,为新建会话抑制每个 `dsh-system-prompt` runtime-context 贡献,并让编辑器使用 `@deepseek-ai/dsh-fs-local`。Web preset 在 agent entry 内隔离 `ctx.fs`,将 `fs-local` 与编辑器一起挂载,因此其他 Web agent 仍使用宿主文件系统提供方。其 persona 继续采用较早的 [minimal preset 组合决策](../bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md)所拥有的固定 complete 提示词,并仅为该 agent 作用域实施 runtime-context 抑制。独立 spine 将同一设置转发给其进程拥有的 system-prompt 服务。沙箱与批准服务仍保持挂载并强制其策略;只有它们面向模型的动态上下文缺席。 独立的 [`minimal.cordis.yml`](../../../../examples/jsonrpc-agent/minimal.cordis.yml) 仍是完整的 JSON-RPC 进程组合。它挂载 `dsh-jsonrpc`、持久 Bash 所需的本地 PTY 和子进程服务、`fs-local`、两个工具消费方,以及未压缩的 JSONL 持久化。它不挂载 `token-meter`、`compact-basic`、`fs-sandbox` 或 `fs-policy`。持久 Bash 仍消费部署的 danger-full-access 沙箱策略;编辑器不受该策略限制。 @@ -20,9 +20,9 @@ Web `minimal` preset 与独立 JSON-RPC minimal 组合对外提供持久 `bash` ## 验证 -Web 回放会启动完整 Web 宿主,通过 preset 服务创建 agent,并断言作用域文件系统为裸后端、不存在作用域压缩服务,而且组装请求只包含固定提示词与两个工具。随后,它通过真实作用域服务执行持久 Bash 和编辑器。 +Web 回放会启动完整 Web 宿主,通过 preset 服务创建 agent,并断言作用域文件系统为裸后端、不存在作用域压缩服务、没有追加 system-prompt 拥有的 runtime-context 消息,而且组装请求只包含固定提示词与两个工具。随后,它通过真实作用域服务执行持久 Bash 和编辑器。 -SDK 回放通过 SDK 客户端启动真实 JSON-RPC agent 进程,注入由环境选择的提示词,断言组装提示词与精确双工具目录,并执行两个工具。Python SDK 内置运行时覆盖会通过每种可用的打包载体,使用环境选择的模型、模型容量和提示词值初始化独立配置。Cordis 校验会检查两份配置能否解析声明的插件和配置字段。 +SDK 回放通过 SDK 客户端启动真实 JSON-RPC agent 进程,注入由环境选择的提示词,断言组装提示词与精确双工具目录,另外断言不存在任何 system-prompt 拥有的 runtime-context 消息,并执行两个工具。Python SDK 内置运行时覆盖会通过每种可用的打包载体,使用环境选择的模型、模型容量和提示词值初始化独立配置。Cordis 校验会检查两份配置能否解析声明的插件和配置字段。 ## 考虑过的替代方案 @@ -36,4 +36,4 @@ SDK 回放通过 SDK 客户端启动真实 JSON-RPC agent 进程,注入由环 ## 后果 -Minimal 会话不会摘要或替换较早历史;调用方必须让会话轮次保持在所选模型的上下文容量内。编辑器可以访问运行时进程可见的任何绝对路径,且不受持久 shell 沙箱策略影响。两条启动路径共享面向模型的工具与无压缩保证,同时保留适合各自所有者的不同提示词和模型配置。Python SDK 路径继续仅通过内置 stdio JSON-RPC 运行时通信。 +Minimal 会话不会摘要或替换较早历史,也不会添加 runtime-context 快照;调用方必须让会话轮次保持在所选模型的上下文容量内,且不得依赖模型可见的常驻沙箱或批准策略说明。编辑器可以访问运行时进程可见的任何绝对路径,且不受持久 shell 沙箱策略影响。两条启动路径共享面向模型的工具、无上下文与无压缩保证,同时保留适合各自所有者的不同提示词和模型配置。Python SDK 路径继续仅通过内置 stdio JSON-RPC 运行时通信。 diff --git a/apps/cli/config/agent-presets/minimal/agent.cordis.yml b/apps/cli/config/agent-presets/minimal/agent.cordis.yml index 80c1cecb83..07e8aa173d 100644 --- a/apps/cli/config/agent-presets/minimal/agent.cordis.yml +++ b/apps/cli/config/agent-presets/minimal/agent.cordis.yml @@ -1,15 +1,16 @@ # The `minimal` agent preset: a fixed-prompt, two-tool coding-agent composition. # # The persona is the complete system prompt, so global identity, Web orientation, -# tool guidance, and later assembly listeners cannot add prompt text. The model -# composes only the persistent `bash` and `str_replace_editor` tools. Context -# compaction is deliberately absent. +# tool guidance, and later assembly listeners cannot add prompt text. Runtime +# context snapshots are suppressed for this preset, and the model composes only +# persistent `bash` and `str_replace_editor`. Context compaction is absent. - id: persona name: '@deepseek-ai/dsh-persona' config: text: You are a helpful software engineer assistant. complete: true + includeRuntimeContext: false # The PTY registry is an agent-owned service, so it lives in an entry-local # realm. The backend still consumes the host sandbox policy and subprocess diff --git a/apps/web/tests/minimal-preset.snapshot.ts b/apps/web/tests/minimal-preset.snapshot.ts index 51d6e074dc..0f66b0458e 100644 --- a/apps/web/tests/minimal-preset.snapshot.ts +++ b/apps/web/tests/minimal-preset.snapshot.ts @@ -55,6 +55,9 @@ describe('minimal agent preset', () => { const requestHeader = agentHandle.agent.session.requestHeader() if (requestHeader === undefined) throw new Error('the minimal agent issued no model request') + expect(agentHandle.agent.session.events.some(event => event.type === 'user/message' + && event.data.source.kind === 'plugin' + && event.data.source.plugin === '@deepseek-ai/dsh-system-prompt')).toBe(false) const presetFileSystem = scaffold.ctx.agentPresets.serviceFor(agentHandle.agent, 'fs') expect(presetFileSystem).toBeDefined() expect(presetFileSystem?.sandboxMode).toBeUndefined() diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 33fae42ac2..5cc1fd5f76 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: f075a5c22049df1696a1849cd88bd3b783adde21 -config-catalog.zh.md: 952a77df3ba279155b4364a083917f816efd2d63 +config-catalog.md: cdfff44e2de274247e6a30074459349a446932f3 +config-catalog.zh.md: 2b90c6862d26b7d362606db528e298a5b760f061 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index f075a5c220..cdfff44e2d 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -168,9 +168,10 @@ Source: [`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/ag /** * Bundle config: each field forwarded verbatim to the child that owns it — * `agents` to the agent loop (an app that pre-creates no agents, like the ACP - * bridge, simply omits it), `includeHarnessIdentity`, `persona`, and `toolOrder` - * to the system-prompt plugin (the fixed opener, deployment persona, and explicit - * model-facing tool order), the `tools` object to the tool registry (its presentation `mode`), + * bridge, simply omits it), `includeHarnessIdentity`, `includeRuntimeContext`, + * `persona`, and `toolOrder` to the system-prompt plugin (the fixed opener, + * dynamic-context policy, deployment persona, and explicit model-facing tool + * order), the `tools` object to the tool registry (its presentation `mode`), * `dshHome` to bash environment and local skill discovery, `sessionTitle` to * the fallback title service, `skills` to the * skill registry/local provider/tool consumer, `workspaceContext` to the @@ -194,6 +195,8 @@ export interface Config { maxParallelToolCalls?: AgentLoopConfig['maxParallelToolCalls'] /** Whether the system prompt includes the fixed Harness identity (default true). */ includeHarnessIdentity?: SystemPromptConfig['includeHarnessIdentity'] + /** Whether model history includes dynamic runtime-context snapshots (default true). */ + includeRuntimeContext?: SystemPromptConfig['includeRuntimeContext'] /** The deployment persona (see dsh-system-prompt's `Config`). */ persona?: SystemPromptConfig['persona'] /** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */ @@ -247,7 +250,7 @@ export interface GoalConfig { Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillLocal`](../packages/skill/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`TasksConfig`](#deepseek-aidsh-tasks-local) · [`toolBash`](../packages/bash/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`toolTasks`](../packages/tasks/tool-tasks/src/index.ts) · [`workspaceContext`](../packages/context/workspace-context/src/index.ts) -Source: [`packages/examples/agent-spine-demo/src/index.ts:91`](../packages/examples/agent-spine-demo/src/index.ts) +Source: [`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts) ## `@deepseek-ai/dsh-agent-tool-mode` @@ -1237,6 +1240,8 @@ export interface Config { text: string /** Make this persona the complete system prompt, suppressing every other section. */ complete?: boolean + /** Suppress dynamic runtime-context snapshots for this persona's agent scope. */ + includeRuntimeContext?: boolean } ``` @@ -2049,6 +2054,8 @@ Source: [`packages/e2b/subprocess-e2b/src/index.ts:25`](../packages/e2b/subproce export interface Config { /** Include the fixed DeepSeek Harness identity before the deployment persona (default true). */ includeHarnessIdentity?: boolean + /** Include dynamic runtime-context snapshots in model history (default true). */ + includeRuntimeContext?: boolean /** * Deployment-wide order-0 persona template. A scoped section named * `deployment:persona` shadows it; `{{variable}}` references are strict. diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 952a77df3b..2b90c6862d 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -170,9 +170,10 @@ export type PresetTrust = 'system' | 'user' /** * Bundle config: each field forwarded verbatim to the child that owns it — * `agents` to the agent loop (an app that pre-creates no agents, like the ACP - * bridge, simply omits it), `includeHarnessIdentity`, `persona`, and `toolOrder` - * to the system-prompt plugin (the fixed opener, deployment persona, and explicit - * model-facing tool order), the `tools` object to the tool registry (its presentation `mode`), + * bridge, simply omits it), `includeHarnessIdentity`, `includeRuntimeContext`, + * `persona`, and `toolOrder` to the system-prompt plugin (the fixed opener, + * dynamic-context policy, deployment persona, and explicit model-facing tool + * order), the `tools` object to the tool registry (its presentation `mode`), * `dshHome` to bash environment and local skill discovery, `sessionTitle` to * the fallback title service, `skills` to the * skill registry/local provider/tool consumer, `workspaceContext` to the @@ -196,6 +197,8 @@ export interface Config { maxParallelToolCalls?: AgentLoopConfig['maxParallelToolCalls'] /** Whether the system prompt includes the fixed Harness identity (default true). */ includeHarnessIdentity?: SystemPromptConfig['includeHarnessIdentity'] + /** Whether model history includes dynamic runtime-context snapshots (default true). */ + includeRuntimeContext?: SystemPromptConfig['includeRuntimeContext'] /** The deployment persona (see dsh-system-prompt's `Config`). */ persona?: SystemPromptConfig['persona'] /** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */ @@ -249,7 +252,7 @@ export interface GoalConfig { 依赖:[`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfig`](#deepseek-aidsh-goal) · [`InvariantConfig`](#deepseek-aidsh-invariants) · [`SessionTitleConfig`](#deepseek-aidsh-session-title) · [`SkillLocal`](../packages/skill/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`TasksConfig`](#deepseek-aidsh-tasks-local) · [`toolBash`](../packages/bash/tool-bash/src/index.ts) · [`toolGoal`](../packages/goal/tool-goal/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`toolTasks`](../packages/tasks/tool-tasks/src/index.ts) · [`workspaceContext`](../packages/context/workspace-context/src/index.ts) -来源:[`packages/examples/agent-spine-demo/src/index.ts:91`](../packages/examples/agent-spine-demo/src/index.ts) +来源:[`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts) ## `@deepseek-ai/dsh-agent-tool-mode` @@ -1239,6 +1242,8 @@ export interface Config { text: string /** Make this persona the complete system prompt, suppressing every other section. */ complete?: boolean + /** Suppress dynamic runtime-context snapshots for this persona's agent scope. */ + includeRuntimeContext?: boolean } ``` @@ -2051,6 +2056,8 @@ export interface Config { export interface Config { /** Include the fixed DeepSeek Harness identity before the deployment persona (default true). */ includeHarnessIdentity?: boolean + /** Include dynamic runtime-context snapshots in model history (default true). */ + includeRuntimeContext?: boolean /** * Deployment-wide order-0 persona template. A scoped section named * `deployment:persona` shadows it; `{{variable}}` references are strict. diff --git a/docs/subsystems/system-prompt.i18n.yaml b/docs/subsystems/system-prompt.i18n.yaml index 3ac169b4e0..0b69a5ca8c 100644 --- a/docs/subsystems/system-prompt.i18n.yaml +++ b/docs/subsystems/system-prompt.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/system-prompt.md -system-prompt.md: f03d11866bcdcf3bcb5c5fe96931b059cb8d0336 -system-prompt.zh.md: 23b3e0b20cc4772baf22727d2e57866aa7cbbeb0 +system-prompt.md: 8a6006d0b96552b52d0b050c304a11a95add8d9e +system-prompt.zh.md: 02be9eed795d3a6c8e43403048f135d493b6509d diff --git a/docs/subsystems/system-prompt.md b/docs/subsystems/system-prompt.md index f03d11866b..8a6006d0b9 100644 --- a/docs/subsystems/system-prompt.md +++ b/docs/subsystems/system-prompt.md @@ -117,6 +117,14 @@ section(section: PromptSection): () => void */ context(context: PromptContext): () => void +/** + * Suppress every dynamic runtime-context contribution in the calling + * context's scope without changing the services that own or enforce those + * facts. Multiple suppressors remain independently disposable. + * @returns the exact Cordis effect disposer. + */ +suppressRuntimeContext(): () => void + /** * Register a tool-schema provider in the calling context's scope. Global and * matching scoped providers both contribute; returning the reserved @@ -148,7 +156,7 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine async assemble(context: AssembleContext = {}): Promise ``` -Source: [`packages/core/system-prompt/src/index.ts:334`](../../packages/core/system-prompt/src/index.ts) +Source: [`packages/core/system-prompt/src/index.ts:338`](../../packages/core/system-prompt/src/index.ts) diff --git a/docs/subsystems/system-prompt.zh.md b/docs/subsystems/system-prompt.zh.md index 23b3e0b20c..02be9eed79 100644 --- a/docs/subsystems/system-prompt.zh.md +++ b/docs/subsystems/system-prompt.zh.md @@ -117,6 +117,14 @@ section(section: PromptSection): () => void */ context(context: PromptContext): () => void +/** + * Suppress every dynamic runtime-context contribution in the calling + * context's scope without changing the services that own or enforce those + * facts. Multiple suppressors remain independently disposable. + * @returns the exact Cordis effect disposer. + */ +suppressRuntimeContext(): () => void + /** * Register a tool-schema provider in the calling context's scope. Global and * matching scoped providers both contribute; returning the reserved @@ -148,7 +156,7 @@ variable(name: string, provider: (context: AssembleContext) => string | undefine async assemble(context: AssembleContext = {}): Promise ``` -Source: [`packages/core/system-prompt/src/index.ts:334`](../../packages/core/system-prompt/src/index.ts) +Source: [`packages/core/system-prompt/src/index.ts:338`](../../packages/core/system-prompt/src/index.ts) diff --git a/examples/jsonrpc-agent/README.i18n.yaml b/examples/jsonrpc-agent/README.i18n.yaml index d5c070643d..fb045ee0a1 100644 --- a/examples/jsonrpc-agent/README.i18n.yaml +++ b/examples/jsonrpc-agent/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write examples/jsonrpc-agent/README.md -README.md: 967f3f499962bf1fd1873fc16ac8fd8075b0df3b -README.zh.md: f84ab95132e820cf0fcf45bff4ae30d9bccb55c1 +README.md: 249a9cb8a3c6256ee92cca0dabeb7723cbca91d0 +README.zh.md: 64a5e607bd37e97b60cb0a4f6ee89a7c766d7464 diff --git a/examples/jsonrpc-agent/README.md b/examples/jsonrpc-agent/README.md index 967f3f4999..249a9cb8a3 100644 --- a/examples/jsonrpc-agent/README.md +++ b/examples/jsonrpc-agent/README.md @@ -30,7 +30,7 @@ Pass the config path through the Python SDK's `cordis` option or `DSH_CORDIS_CON ## Minimal variant -[`minimal.cordis.yml`](minimal.cordis.yml) is the complete standalone counterpart of the Web `minimal` preset. `DSH_SYSTEM_PROMPT` selects its system prompt, with `You are a helpful software engineer assistant.` as the fallback, and no context-compaction plugin is mounted. Its model-facing tools are exactly: +[`minimal.cordis.yml`](minimal.cordis.yml) is the complete standalone counterpart of the Web `minimal` preset. `DSH_SYSTEM_PROMPT` selects its system prompt, with `You are a helpful software engineer assistant.` as the fallback. It suppresses every system-prompt runtime-context contribution for fresh sessions and mounts no context-compaction plugin. Its model-facing tools are exactly: - owner-scoped persistent `bash` - `str_replace_editor` with `view`, `create`, `str_replace`, and `insert` diff --git a/examples/jsonrpc-agent/README.zh.md b/examples/jsonrpc-agent/README.zh.md index f84ab95132..64a5e607bd 100644 --- a/examples/jsonrpc-agent/README.zh.md +++ b/examples/jsonrpc-agent/README.zh.md @@ -30,7 +30,7 @@ ## 极简变体 -[`minimal.cordis.yml`](minimal.cordis.yml) 是 Web `minimal` preset 的完整独立版本。`DSH_SYSTEM_PROMPT` 选择它的系统提示词,未设置时使用 `You are a helpful software engineer assistant.`,且不挂载上下文压缩插件。面向模型的工具严格只有: +[`minimal.cordis.yml`](minimal.cordis.yml) 是 Web `minimal` preset 的完整独立版本。`DSH_SYSTEM_PROMPT` 选择它的系统提示词,未设置时使用 `You are a helpful software engineer assistant.`。它为新建会话抑制每个 system-prompt runtime-context 贡献,且不挂载上下文压缩插件。面向模型的工具严格只有: - 所有者作用域内持久化的 `bash` - 提供 `view`、`create`、`str_replace` 与 `insert` 的 `str_replace_editor` diff --git a/examples/jsonrpc-agent/minimal.cordis.yml b/examples/jsonrpc-agent/minimal.cordis.yml index bd9e513c43..69b019fbff 100644 --- a/examples/jsonrpc-agent/minimal.cordis.yml +++ b/examples/jsonrpc-agent/minimal.cordis.yml @@ -1,6 +1,7 @@ # Complete unattended minimal-agent composition for the Python SDK. The model # sees one deployment-selected system prompt and only the owner-scoped -# persistent Bash and string-replace editor tools. Context compaction is absent. +# persistent Bash and string-replace editor tools. Runtime-context injection and +# context compaction are absent. - id: jsonrpc name: '@deepseek-ai/dsh-jsonrpc' @@ -47,6 +48,7 @@ name: '@deepseek-ai/dsh-agent-spine-demo' config: includeHarnessIdentity: false + includeRuntimeContext: false persona: !!js process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.' workspaceContext: false skills: diff --git a/examples/jsonrpc-agent/tests/sdk.snapshot.ts b/examples/jsonrpc-agent/tests/sdk.snapshot.ts index 68de880773..2e5fae4cfb 100644 --- a/examples/jsonrpc-agent/tests/sdk.snapshot.ts +++ b/examples/jsonrpc-agent/tests/sdk.snapshot.ts @@ -77,8 +77,8 @@ interface SdkScenario { expectedSystem?: string /** Exact model-facing descriptions for selected tools. */ expectedToolDescriptions?: Readonly> - /** Stable policy-context clauses the real assembled request must include or omit. */ - policyContext?: { includes: readonly string[]; excludes: readonly string[] } + /** Expected runtime-context state in the real assembled request. */ + runtimeContext?: false | { includes: readonly string[]; excludes: readonly string[] } } const SCENARIOS: SdkScenario[] = [ @@ -111,10 +111,7 @@ const SCENARIOS: SdkScenario[] = [ expectedTools: { bash: ['command'], str_replace_editor: ['command', 'path'] }, expectedSystem: MINIMAL_SYSTEM_PROMPT, expectedToolDescriptions: { bash: MINIMAL_BASH_DESCRIPTION }, - policyContext: { - includes: ['Current DSH file policy: danger-full-access.', 'file modifications by available operations'], - excludes: ['write and edit tools', 'terminal sessions', 'one-shot bash commands'], - }, + runtimeContext: false, }, ] @@ -182,8 +179,8 @@ function assembledSystem(log: PersistedLog): string { return system } -function assembledPolicyContext(log: PersistedLog): string { - const contexts = log.content.trimEnd().split('\n').flatMap((line) => { +function assembledRuntimeContexts(log: PersistedLog): string[] { + return log.content.trimEnd().split('\n').flatMap((line) => { const event = JSON.parse(line) as { type?: string data?: { source?: { kind?: string; plugin?: string }; content?: Array<{ type?: string; text?: unknown }> } @@ -193,8 +190,6 @@ function assembledPolicyContext(log: PersistedLog): string { || event.data.source.plugin !== '@deepseek-ai/dsh-system-prompt') return [] return event.data.content?.flatMap(block => block.type === 'text' && typeof block.text === 'string' ? [block.text] : []) ?? [] }) - if (contexts.length !== 1) throw new Error(`session log has ${String(contexts.length)} runtime-context snapshots; expected one`) - return contexts[0] as string } function contextOf(logs: readonly { content: string; header: Record }[], cwd: string): NormalizeContext { @@ -447,14 +442,20 @@ describe('TypeScript SDK snapshots over the jsonrpc runtime', () => { if (parent === undefined) throw new Error(`${scenario.name} has no parent session log`) expect(assembledToolDescriptions(parent)).toMatchObject(scenario.expectedToolDescriptions) } - if (scenario.policyContext !== undefined) { + if (scenario.runtimeContext !== undefined) { const parent = ordered[0] if (parent === undefined) throw new Error(`${scenario.name} has no parent session log`) - const context = assembledPolicyContext(parent) - for (const clause of scenario.policyContext.includes) expect(context).toContain(clause) - for (const clause of scenario.policyContext.excludes) expect(context).not.toContain(clause) - const system = assembledSystem(parent) - for (const clause of scenario.policyContext.includes) expect(system).not.toContain(clause) + const contexts = assembledRuntimeContexts(parent) + if (scenario.runtimeContext === false) { + expect(contexts).toEqual([]) + } else { + expect(contexts).toHaveLength(1) + const context = contexts[0] as string + for (const clause of scenario.runtimeContext.includes) expect(context).toContain(clause) + for (const clause of scenario.runtimeContext.excludes) expect(context).not.toContain(clause) + const system = assembledSystem(parent) + for (const clause of scenario.runtimeContext.includes) expect(system).not.toContain(clause) + } } if (scenario.children > 0) { expect(notifications.some(n => n.method === 'subagent.started')).toBe(true) diff --git a/examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl index ea8f220308..550d3495f9 100644 --- a/examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl +++ b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl @@ -4,76 +4,75 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":2,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":3,"time":0,"data":{"turn":1,"step":1}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":4,"time":0,"data":{"content":[{"type":"text","text":"Prove that bash state persists. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":5,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":6,"time":0,"data":{"title":"Prove that bash state persists.","messageSeqs":[4],"source":{"kind":"fallback"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":7,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":8,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-1","name":"bash","argumentsDelta":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":16,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":17,"time":0,"data":{"turn":1,"step":1}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":18,"time":0,"data":{"turn":1,"step":2}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-2","name":"bash","argumentsDelta":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":23,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":24,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":25,"time":0,"data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":26,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[25],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":27,"time":0,"data":{"turn":1,"step":2}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":28,"time":0,"data":{"turn":1,"step":3}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":29,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":30,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-create","name":"str_replace_editor","argumentsDelta":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":33,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":34,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":35,"time":0,"data":{"turn":1,"step":3,"callId":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":36,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"editor-create"},"content":[{"type":"tool-result","toolCallId":"editor-create","content":[{"type":"text","text":"New file created successfully at: {{cwd}}/note.txt"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[35],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":37,"time":0,"data":{"turn":1,"step":3}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":38,"time":0,"data":{"turn":1,"step":4}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":39,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":40,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-view","name":"str_replace_editor","argumentsDelta":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":41,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":42,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":43,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":44,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":45,"time":0,"data":{"turn":1,"step":4,"callId":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":46,"time":0,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"editor-view"},"content":[{"type":"tool-result","toolCallId":"editor-view","content":[{"type":"text","text":"Here's the content of {{cwd}}/note.txt with line numbers (which has a total of 3 lines):\n 1 target:\n 2 \told\n 3 \n"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[45],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":47,"time":0,"data":{"turn":1,"step":4}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":48,"time":0,"data":{"turn":1,"step":5}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":49,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":50,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-replace","name":"str_replace_editor","argumentsDelta":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":51,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":52,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":53,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":54,"time":0,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":55,"time":0,"data":{"turn":1,"step":5,"callId":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":56,"time":0,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"editor-replace"},"content":[{"type":"tool-result","toolCallId":"editor-replace","content":[{"type":"text","text":"The file {{cwd}}/note.txt has been edited successfully."}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[55],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":57,"time":0,"data":{"turn":1,"step":5}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":58,"time":0,"data":{"turn":1,"step":6}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":59,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":60,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-exit","name":"bash","argumentsDelta":"{\"command\":\"exit 9\"}"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":61,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":62,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":63,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":64,"time":0,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[59,60,61,62,63],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":65,"time":0,"data":{"turn":1,"step":6,"callId":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":66,"time":0,"data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"bash-exit"},"content":[{"type":"tool-result","toolCallId":"bash-exit","content":[{"type":"text","text":"exit\n[shell exited: code 9]\nThe persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment."}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[65],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":67,"time":0,"data":{"turn":1,"step":6}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":68,"time":0,"data":{"turn":1,"step":7}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":69,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":70,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"text-delta","index":0,"text":"PERSISTENT_TOOLS_OK"}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":71,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PERSISTENT_TOOLS_OK"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":72,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":73,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":74,"time":0,"data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"PERSISTENT_TOOLS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[69,70,71,72,73],"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":75,"time":0,"data":{"turn":1,"step":7}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":76,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":5,"time":0,"data":{"title":"Prove that bash state persists.","messageSeqs":[4],"source":{"kind":"fallback"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":6,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":7,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-1","name":"bash","argumentsDelta":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":14,"time":0,"data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":16,"time":0,"data":{"turn":1,"step":1}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":17,"time":0,"data":{"turn":1,"step":2}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":18,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":19,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-2","name":"bash","argumentsDelta":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":20,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":21,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":23,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":24,"time":0,"data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":25,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[24],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":26,"time":0,"data":{"turn":1,"step":2}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":27,"time":0,"data":{"turn":1,"step":3}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":28,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":29,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-create","name":"str_replace_editor","argumentsDelta":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":30,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":31,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":32,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":33,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[28,29,30,31,32],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":34,"time":0,"data":{"turn":1,"step":3,"callId":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":35,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"editor-create"},"content":[{"type":"tool-result","toolCallId":"editor-create","content":[{"type":"text","text":"New file created successfully at: {{cwd}}/note.txt"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[34],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":36,"time":0,"data":{"turn":1,"step":3}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":37,"time":0,"data":{"turn":1,"step":4}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":38,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":39,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-view","name":"str_replace_editor","argumentsDelta":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":40,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":41,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":42,"time":0,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":43,"time":0,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[38,39,40,41,42],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":44,"time":0,"data":{"turn":1,"step":4,"callId":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":45,"time":0,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"editor-view"},"content":[{"type":"tool-result","toolCallId":"editor-view","content":[{"type":"text","text":"Here's the content of {{cwd}}/note.txt with line numbers (which has a total of 3 lines):\n 1 target:\n 2 \told\n 3 \n"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[44],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":46,"time":0,"data":{"turn":1,"step":4}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":47,"time":0,"data":{"turn":1,"step":5}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":48,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":49,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-replace","name":"str_replace_editor","argumentsDelta":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":50,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":51,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":52,"time":0,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":53,"time":0,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[48,49,50,51,52],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":54,"time":0,"data":{"turn":1,"step":5,"callId":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":55,"time":0,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"editor-replace"},"content":[{"type":"tool-result","toolCallId":"editor-replace","content":[{"type":"text","text":"The file {{cwd}}/note.txt has been edited successfully."}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[54],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":56,"time":0,"data":{"turn":1,"step":5}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":57,"time":0,"data":{"turn":1,"step":6}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":58,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":59,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-exit","name":"bash","argumentsDelta":"{\"command\":\"exit 9\"}"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":60,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":61,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":62,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":63,"time":0,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[58,59,60,61,62],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":64,"time":0,"data":{"turn":1,"step":6,"callId":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":65,"time":0,"data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"bash-exit"},"content":[{"type":"tool-result","toolCallId":"bash-exit","content":[{"type":"text","text":"exit\n[shell exited: code 9]\nThe persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment."}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[64],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":66,"time":0,"data":{"turn":1,"step":6}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":67,"time":0,"data":{"turn":1,"step":7}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":68,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":69,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"text-delta","index":0,"text":"PERSISTENT_TOOLS_OK"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":70,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PERSISTENT_TOOLS_OK"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":71,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":72,"time":0,"data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":73,"time":0,"data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"PERSISTENT_TOOLS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[68,69,70,71,72],"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":74,"time":0,"data":{"turn":1,"step":7}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":75,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}} {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}} diff --git a/examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl index be559eeb9a..77fe6f03a6 100644 --- a/examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl +++ b/examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl @@ -4,75 +4,74 @@ {"type":"agent/inbox/spliced","seq":2,"time":1785821461907,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1785331618312,"data":{"turn":1,"step":1}} {"type":"user/message","seq":4,"time":1785498592368,"data":{"content":[{"type":"text","text":"Prove that bash state persists. Then create {{cwd}}/note.txt with a tab-indented line, view it, replace that literal tab-indented line, and make the persistent shell exit with code 9."}],"source":{"kind":"user"},"role":"user","id":"9a08e199-69d7-4b85-bfa4-27b41a92672a"},"surfaceOp":"append"} -{"type":"user/message","seq":5,"time":1785730508088,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."}]},"role":"user","id":"bb38bdc2-276e-46ec-87a1-089732acbc8d"},"surfaceOp":"append"} -{"type":"session/title","seq":6,"time":1785730508088,"data":{"title":"Prove that bash state persists.","messageSeqs":[4],"source":{"kind":"fallback"}}} -{"type":"request/header","seq":7,"time":1785498592370,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} -{"type":"request/context","seq":8,"time":1785730508089,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"assistant/chunk","seq":9,"time":1785331618326,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","seq":10,"time":1785331618326,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-1","name":"bash","argumentsDelta":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}} -{"type":"assistant/chunk","seq":11,"time":1785331618326,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} -{"type":"assistant/chunk","seq":12,"time":1785498592372,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","seq":13,"time":1785730508090,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":14,"time":1785730508090,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"0d064526-8eff-482d-8525-ac478e1d1791"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[9,10,11,12,13],"surfaceOp":"append"} -{"type":"tool/call","seq":15,"time":1785730508090,"data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}} -{"type":"tool/result","seq":16,"time":1785730508425,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"2c01f81a-01ea-47e2-bf92-f7825b7cc69f"}},"sourceEventSeqs":[15],"surfaceOp":"append"} -{"type":"step/end","seq":17,"time":1785730508425,"data":{"turn":1,"step":1}} -{"type":"step/start","seq":18,"time":1785730508425,"data":{"turn":1,"step":2}} -{"type":"assistant/chunk","seq":19,"time":1785331618652,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","seq":20,"time":1785331618652,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-2","name":"bash","argumentsDelta":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}} -{"type":"assistant/chunk","seq":21,"time":1785331618652,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} -{"type":"assistant/chunk","seq":22,"time":1785498592702,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","seq":23,"time":1785730508426,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":24,"time":1785730508426,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"ba4078ce-0e18-419a-b720-339918aecf26"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[19,20,21,22,23],"surfaceOp":"append"} -{"type":"tool/call","seq":25,"time":1785730508426,"data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}} -{"type":"tool/result","seq":26,"time":1785730508537,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"6e3ad5e1-1149-44d5-bd20-d9cc0139c747"}},"sourceEventSeqs":[25],"surfaceOp":"append"} -{"type":"step/end","seq":27,"time":1785730508537,"data":{"turn":1,"step":2}} -{"type":"step/start","seq":28,"time":1785730508537,"data":{"turn":1,"step":3}} -{"type":"assistant/chunk","seq":29,"time":1785331618762,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","seq":30,"time":1785331618762,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-create","name":"str_replace_editor","argumentsDelta":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}}} -{"type":"assistant/chunk","seq":31,"time":1785331618762,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}}}} -{"type":"assistant/chunk","seq":32,"time":1785498592812,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","seq":33,"time":1785730508538,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":34,"time":1785730508538,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"e5769b2d-ea91-42fe-a78f-2f7f408f545e"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[29,30,31,32,33],"surfaceOp":"append"} -{"type":"tool/call","seq":35,"time":1785730508538,"data":{"turn":1,"step":3,"callId":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}} -{"type":"tool/result","seq":36,"time":1785730508551,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"editor-create"},"content":[{"type":"tool-result","toolCallId":"editor-create","content":[{"type":"text","text":"New file created successfully at: {{cwd}}/note.txt"}],"isError":false}],"role":"user","id":"af41060c-7007-4ada-89d6-8b15a0e8be7c"}},"sourceEventSeqs":[35],"surfaceOp":"append"} -{"type":"step/end","seq":37,"time":1785730508551,"data":{"turn":1,"step":3}} -{"type":"step/start","seq":38,"time":1785730508552,"data":{"turn":1,"step":4}} -{"type":"assistant/chunk","seq":39,"time":1785331618784,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","seq":40,"time":1785331618784,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-view","name":"str_replace_editor","argumentsDelta":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}} -{"type":"assistant/chunk","seq":41,"time":1785331618784,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}}} -{"type":"assistant/chunk","seq":42,"time":1785498592825,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","seq":43,"time":1785730508552,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":44,"time":1785730508552,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"0e9afabb-10a6-444c-ae22-fcdbb5e14695"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[39,40,41,42,43],"surfaceOp":"append"} -{"type":"tool/call","seq":45,"time":1785730508553,"data":{"turn":1,"step":4,"callId":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}} -{"type":"tool/result","seq":46,"time":1785730508554,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"editor-view"},"content":[{"type":"tool-result","toolCallId":"editor-view","content":[{"type":"text","text":"Here's the content of {{cwd}}/note.txt with line numbers (which has a total of 3 lines):\n 1 target:\n 2 \told\n 3 \n"}],"isError":false}],"role":"user","id":"a4472b37-6311-4880-bce2-cc369f9bc34b"}},"sourceEventSeqs":[45],"surfaceOp":"append"} -{"type":"step/end","seq":47,"time":1785730508554,"data":{"turn":1,"step":4}} -{"type":"step/start","seq":48,"time":1785730508554,"data":{"turn":1,"step":5}} -{"type":"assistant/chunk","seq":49,"time":1785331618801,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","seq":50,"time":1785331618801,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-replace","name":"str_replace_editor","argumentsDelta":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}}} -{"type":"assistant/chunk","seq":51,"time":1785331618801,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}}}} -{"type":"assistant/chunk","seq":52,"time":1785498592826,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","seq":53,"time":1785730508555,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":54,"time":1785730508555,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"ba189070-d46e-461e-969b-9bca032bb154"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[49,50,51,52,53],"surfaceOp":"append"} -{"type":"tool/call","seq":55,"time":1785730508555,"data":{"turn":1,"step":5,"callId":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}} -{"type":"tool/result","seq":56,"time":1785730508564,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"editor-replace"},"content":[{"type":"tool-result","toolCallId":"editor-replace","content":[{"type":"text","text":"The file {{cwd}}/note.txt has been edited successfully."}],"isError":false}],"role":"user","id":"17331db9-174b-4699-9c9e-3140921956c4"}},"sourceEventSeqs":[55],"surfaceOp":"append"} -{"type":"step/end","seq":57,"time":1785730508564,"data":{"turn":1,"step":5}} -{"type":"step/start","seq":58,"time":1785730508565,"data":{"turn":1,"step":6}} -{"type":"assistant/chunk","seq":59,"time":1785331618804,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} -{"type":"assistant/chunk","seq":60,"time":1785331618804,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-exit","name":"bash","argumentsDelta":"{\"command\":\"exit 9\"}"}}} -{"type":"assistant/chunk","seq":61,"time":1785331618804,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}} -{"type":"assistant/chunk","seq":62,"time":1785498592838,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","seq":63,"time":1785730508565,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":64,"time":1785730508565,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"7fa07d0f-e70a-460d-b685-bf8a63b6a8a0"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[59,60,61,62,63],"surfaceOp":"append"} -{"type":"tool/call","seq":65,"time":1785730508565,"data":{"turn":1,"step":6,"callId":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}} -{"type":"tool/result","seq":66,"time":1785730508641,"data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"bash-exit"},"content":[{"type":"tool-result","toolCallId":"bash-exit","content":[{"type":"text","text":"exit\n[shell exited: code 9]\nThe persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment."}],"isError":false}],"role":"user","id":"ccb91a28-4034-49bf-967d-450f68f7f9b8"}},"sourceEventSeqs":[65],"surfaceOp":"append"} -{"type":"step/end","seq":67,"time":1785730508641,"data":{"turn":1,"step":6}} -{"type":"step/start","seq":68,"time":1785730508642,"data":{"turn":1,"step":7}} -{"type":"assistant/chunk","seq":69,"time":1785331618807,"data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} -{"type":"assistant/chunk","seq":70,"time":1785331618807,"data":{"turn":1,"step":7,"chunk":{"type":"text-delta","index":0,"text":"PERSISTENT_TOOLS_OK"}}} -{"type":"assistant/chunk","seq":71,"time":1785331618807,"data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PERSISTENT_TOOLS_OK"}}}} -{"type":"assistant/chunk","seq":72,"time":1785498592934,"data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} -{"type":"assistant/chunk","seq":73,"time":1785730508642,"data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":74,"time":1785730508642,"data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"PERSISTENT_TOOLS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"43efc58a-46a1-4813-995f-1dc489438942"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[69,70,71,72,73],"surfaceOp":"append"} -{"type":"step/end","seq":75,"time":1785730508642,"data":{"turn":1,"step":7}} -{"type":"turn/end","seq":76,"time":1785730508642,"data":{"turn":1,"reason":{"kind":"completed"}}} +{"type":"session/title","seq":5,"time":1785498592368,"data":{"title":"Prove that bash state persists.","messageSeqs":[4],"source":{"kind":"fallback"}}} +{"type":"request/header","seq":6,"time":1786547087174,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","seq":7,"time":1786547087175,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} +{"type":"assistant/chunk","seq":8,"time":1786547087175,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":9,"time":1786547087175,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-1","name":"bash","argumentsDelta":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}} +{"type":"assistant/chunk","seq":10,"time":1785331618326,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} +{"type":"assistant/chunk","seq":11,"time":1785331618326,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":12,"time":1785331618326,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":13,"time":1786547087175,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"0d064526-8eff-482d-8525-ac478e1d1791"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"} +{"type":"tool/call","seq":14,"time":1786547087176,"data":{"turn":1,"step":1,"callId":"bash-1","name":"bash","arguments":"{\"command\":\"cd /tmp && export DSH_EXAMPLE_COUNT=1 && printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}} +{"type":"tool/result","seq":15,"time":1786547094340,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"bash-1"},"content":[{"type":"tool-result","toolCallId":"bash-1","content":[{"type":"text","text":"COUNT=1 CWD=/tmp"}],"isError":false}],"role":"user","id":"2c01f81a-01ea-47e2-bf92-f7825b7cc69f"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"step/end","seq":16,"time":1786547094340,"data":{"turn":1,"step":1}} +{"type":"step/start","seq":17,"time":1786547094341,"data":{"turn":1,"step":2}} +{"type":"assistant/chunk","seq":18,"time":1786547094341,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":19,"time":1786547094341,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-2","name":"bash","argumentsDelta":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}} +{"type":"assistant/chunk","seq":20,"time":1785331618652,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}}}} +{"type":"assistant/chunk","seq":21,"time":1785331618652,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":22,"time":1785331618652,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":23,"time":1786547094341,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"ba4078ce-0e18-419a-b720-339918aecf26"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"} +{"type":"tool/call","seq":24,"time":1786547094341,"data":{"turn":1,"step":2,"callId":"bash-2","name":"bash","arguments":"{\"command\":\"DSH_EXAMPLE_COUNT=$((DSH_EXAMPLE_COUNT + 1)); printf \\\"COUNT=%s CWD=%s\\\\n\\\" \\\"$DSH_EXAMPLE_COUNT\\\" \\\"$PWD\\\"\"}"}} +{"type":"tool/result","seq":25,"time":1786547097878,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"bash-2"},"content":[{"type":"tool-result","toolCallId":"bash-2","content":[{"type":"text","text":"COUNT=2 CWD=/tmp"}],"isError":false}],"role":"user","id":"6e3ad5e1-1149-44d5-bd20-d9cc0139c747"}},"sourceEventSeqs":[24],"surfaceOp":"append"} +{"type":"step/end","seq":26,"time":1786547097878,"data":{"turn":1,"step":2}} +{"type":"step/start","seq":27,"time":1786547097878,"data":{"turn":1,"step":3}} +{"type":"assistant/chunk","seq":28,"time":1786547097878,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":29,"time":1786547097878,"data":{"turn":1,"step":3,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-create","name":"str_replace_editor","argumentsDelta":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}}} +{"type":"assistant/chunk","seq":30,"time":1785331618762,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}}}} +{"type":"assistant/chunk","seq":31,"time":1785331618762,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":32,"time":1785331618762,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":33,"time":1786547097878,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"e5769b2d-ea91-42fe-a78f-2f7f408f545e"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[28,29,30,31,32],"surfaceOp":"append"} +{"type":"tool/call","seq":34,"time":1786547097879,"data":{"turn":1,"step":3,"callId":"editor-create","name":"str_replace_editor","arguments":"{\"command\":\"create\",\"path\":\"{{cwd}}/note.txt\",\"file_text\":\"target:\\n\\told\\n\"}"}} +{"type":"tool/result","seq":35,"time":1786547097885,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"editor-create"},"content":[{"type":"tool-result","toolCallId":"editor-create","content":[{"type":"text","text":"New file created successfully at: {{cwd}}/note.txt"}],"isError":false}],"role":"user","id":"af41060c-7007-4ada-89d6-8b15a0e8be7c"}},"sourceEventSeqs":[34],"surfaceOp":"append"} +{"type":"step/end","seq":36,"time":1786547097885,"data":{"turn":1,"step":3}} +{"type":"step/start","seq":37,"time":1786547097885,"data":{"turn":1,"step":4}} +{"type":"assistant/chunk","seq":38,"time":1786547097886,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":39,"time":1786547097886,"data":{"turn":1,"step":4,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-view","name":"str_replace_editor","argumentsDelta":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}} +{"type":"assistant/chunk","seq":40,"time":1785331618784,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}}}} +{"type":"assistant/chunk","seq":41,"time":1785331618784,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":42,"time":1785331618784,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":43,"time":1786547097886,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"0e9afabb-10a6-444c-ae22-fcdbb5e14695"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[38,39,40,41,42],"surfaceOp":"append"} +{"type":"tool/call","seq":44,"time":1786547097886,"data":{"turn":1,"step":4,"callId":"editor-view","name":"str_replace_editor","arguments":"{\"command\":\"view\",\"path\":\"{{cwd}}/note.txt\"}"}} +{"type":"tool/result","seq":45,"time":1786547097887,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"editor-view"},"content":[{"type":"tool-result","toolCallId":"editor-view","content":[{"type":"text","text":"Here's the content of {{cwd}}/note.txt with line numbers (which has a total of 3 lines):\n 1 target:\n 2 \told\n 3 \n"}],"isError":false}],"role":"user","id":"a4472b37-6311-4880-bce2-cc369f9bc34b"}},"sourceEventSeqs":[44],"surfaceOp":"append"} +{"type":"step/end","seq":46,"time":1786547097887,"data":{"turn":1,"step":4}} +{"type":"step/start","seq":47,"time":1786547097887,"data":{"turn":1,"step":5}} +{"type":"assistant/chunk","seq":48,"time":1786547097887,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":49,"time":1786547097887,"data":{"turn":1,"step":5,"chunk":{"type":"tool-call-delta","index":0,"id":"editor-replace","name":"str_replace_editor","argumentsDelta":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}}} +{"type":"assistant/chunk","seq":50,"time":1785331618801,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}}}} +{"type":"assistant/chunk","seq":51,"time":1785331618801,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":52,"time":1785331618801,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":53,"time":1786547097887,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"ba189070-d46e-461e-969b-9bca032bb154"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[48,49,50,51,52],"surfaceOp":"append"} +{"type":"tool/call","seq":54,"time":1786547097887,"data":{"turn":1,"step":5,"callId":"editor-replace","name":"str_replace_editor","arguments":"{\"command\":\"str_replace\",\"path\":\"{{cwd}}/note.txt\",\"old_str\":\"\\told\",\"new_str\":\"\\tnew\"}"}} +{"type":"tool/result","seq":55,"time":1786547097892,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"editor-replace"},"content":[{"type":"tool-result","toolCallId":"editor-replace","content":[{"type":"text","text":"The file {{cwd}}/note.txt has been edited successfully."}],"isError":false}],"role":"user","id":"17331db9-174b-4699-9c9e-3140921956c4"}},"sourceEventSeqs":[54],"surfaceOp":"append"} +{"type":"step/end","seq":56,"time":1786547097892,"data":{"turn":1,"step":5}} +{"type":"step/start","seq":57,"time":1786547097892,"data":{"turn":1,"step":6}} +{"type":"assistant/chunk","seq":58,"time":1786547097893,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} +{"type":"assistant/chunk","seq":59,"time":1786547097893,"data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"bash-exit","name":"bash","argumentsDelta":"{\"command\":\"exit 9\"}"}}} +{"type":"assistant/chunk","seq":60,"time":1785331618804,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}}}} +{"type":"assistant/chunk","seq":61,"time":1785331618804,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":62,"time":1785331618804,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} +{"type":"assistant/message","seq":63,"time":1786547097893,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"7fa07d0f-e70a-460d-b685-bf8a63b6a8a0"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[58,59,60,61,62],"surfaceOp":"append"} +{"type":"tool/call","seq":64,"time":1786547097893,"data":{"turn":1,"step":6,"callId":"bash-exit","name":"bash","arguments":"{\"command\":\"exit 9\"}"}} +{"type":"tool/result","seq":65,"time":1786547098003,"data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"bash-exit"},"content":[{"type":"tool-result","toolCallId":"bash-exit","content":[{"type":"text","text":"exit\n[shell exited: code 9]\nThe persistent bash shell was reset; the next bash call starts from the workspace with a fresh current directory and environment."}],"isError":false}],"role":"user","id":"ccb91a28-4034-49bf-967d-450f68f7f9b8"}},"sourceEventSeqs":[64],"surfaceOp":"append"} +{"type":"step/end","seq":66,"time":1786547098003,"data":{"turn":1,"step":6}} +{"type":"step/start","seq":67,"time":1786547098003,"data":{"turn":1,"step":7}} +{"type":"assistant/chunk","seq":68,"time":1786547098003,"data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} +{"type":"assistant/chunk","seq":69,"time":1786547098003,"data":{"turn":1,"step":7,"chunk":{"type":"text-delta","index":0,"text":"PERSISTENT_TOOLS_OK"}}} +{"type":"assistant/chunk","seq":70,"time":1785331618807,"data":{"turn":1,"step":7,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PERSISTENT_TOOLS_OK"}}}} +{"type":"assistant/chunk","seq":71,"time":1785331618807,"data":{"turn":1,"step":7,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} +{"type":"assistant/chunk","seq":72,"time":1785331618807,"data":{"turn":1,"step":7,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} +{"type":"assistant/message","seq":73,"time":1786547098003,"data":{"turn":1,"step":7,"message":{"role":"assistant","content":[{"type":"text","text":"PERSISTENT_TOOLS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"43efc58a-46a1-4813-995f-1dc489438942"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[68,69,70,71,72],"surfaceOp":"append"} +{"type":"step/end","seq":74,"time":1786547098003,"data":{"turn":1,"step":7}} +{"type":"turn/end","seq":75,"time":1786547098004,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/packages/core/system-prompt/README.i18n.yaml b/packages/core/system-prompt/README.i18n.yaml index f805ee37b0..fd428146d6 100644 --- a/packages/core/system-prompt/README.i18n.yaml +++ b/packages/core/system-prompt/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/core/system-prompt/README.md -README.md: cedda783d549633f5be9765a9a074e968d99500d -README.zh.md: ab3dbffa099549cb1d5cc2713d903038e917e695 +README.md: 6d0b43322ea0ccf4dddef404c4c862e014819696 +README.zh.md: 08da218ef2fe07ffa63f246dff5382474070c4d9 diff --git a/packages/core/system-prompt/README.md b/packages/core/system-prompt/README.md index cedda783d5..6d0b43322e 100644 --- a/packages/core/system-prompt/README.md +++ b/packages/core/system-prompt/README.md @@ -9,6 +9,7 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem | Key | Default | Meaning | |---|---|---| | `includeHarnessIdentity` | `true` | Include the fixed `You are an AI agent powered by the DeepSeek Harness SDK.` order-−100 opener. Set false only when a compatibility deployment owns the complete system prompt. | +| `includeRuntimeContext` | `true` | Include ordered dynamic contexts in assembly. When false, context providers are not evaluated and contexts added by `system-prompt/assemble` listeners are discarded after the waterfall; other services and their enforcement remain active. | | `persona` | `''` | The global deployment-persona default: the ONE config-authored prompt fragment, rendered as the order-0 `deployment:persona` section unless an agent-scoped contribution shadows it. A template — complete `{{…}}` groups are interpreted strictly against the registered variables (the shipped loop registers `{{model}}`/`{{cwd}}`), with no escape syntax for literal braces yet. Empty ⇒ the section is dropped at render. | | `toolOrder` | — | Explicit model-facing tool order, as a list of `ToolSchema.name`s with one `''` rest entry (`TOOL_ORDER_REST`): listed tools take their listed position, unlisted tools land at the rest entry in lexicographic name order. Absent ⇒ plain lexicographic name order. Applied to the collected tools BEFORE the `system-prompt/assemble` waterfall — like the sections' `order` sort, it canonicalizes what the registry contributed (registration order is a plugin-load artifact), and a waterfall listener that mutates the list owns the determinism of what it emits. Misconfiguration fails loud: a list without exactly one rest entry, or with duplicates, throws at load; a listed name with no registered tool rejects every `assemble()`; a tool provider returning the reserved rest-entry name also rejects. Under the shipped loop the turn fails before any model request. Why a central list and not per-plugin weights: [Explicit model-facing tool order](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md). | @@ -17,9 +18,11 @@ System prompt assembly registry. Plugins contribute ordered sections, tool schem ### Public API - `ctx.systemPrompt.section(section: PromptSection): () => void` Contribute a section. The layer is the calling context's scope: `agent.ctx` contributes to that agent alone, shadowing a same-named global section there. A `complete: true` section becomes the exact complete prompt after the assembly waterfall; more than one effective complete section rejects assembly. Duplicate names within one layer and non-finite orders throw. Disposed with the calling fiber. +- `ctx.systemPrompt.context(context: PromptContext): () => void` Contribute ordered dynamic context for the calling scope. Providers are evaluated for each eligible assembly and become a sourced runtime-context snapshot in model history under the shipped loop. +- `ctx.systemPrompt.suppressRuntimeContext(): () => void` Suppress every dynamic-context contribution for the calling scope. Multiple registrations compose independently; disposing the returned effect restores context when no suppressor remains. - `ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => void` Contribute tool schemas, evaluated at each assembly with that assembly's context. `ToolProviderResult` = `{ schemas, knownNames? }`: `schemas` is the post-restriction visible set; `knownNames` is the pre-restriction universe used by `toolOrder`. A provider must not return a schema named `TOOL_ORDER_REST`. Scoped providers are consulted only for their scope's assemblies. Disposed with the calling fiber. - `ctx.systemPrompt.variable(name: string, provider: (context) => string | undefined): () => void` Contribute a prompt variable, referenced from section text as `{{name}}`. Scoped variables shadow a same-named global for that agent. Duplicate-in-layer or unreferenceable names throw; `undefined` means "no value for this assembly". Disposed with the calling fiber. -- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise` Assemble the prompt for one caller: the global layer merged with `context.scope`'s layer, with tool schemas detached before the transform waterfall. Runs through the scope-filtered `system-prompt/assemble` waterfall, then restores an effective complete section as the sole prompt section. An optional `context.signal` explicitly controls this assembly request; providers and listeners may cooperate with it but must not retain it for another turn. Rejects for multiple complete sections, when a configured `toolOrder` names a tool outside the providers' `knownNames` universe, or when a provider returns the reserved rest-entry name. +- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise` Assemble the prompt for one caller: the global layer merged with `context.scope`'s layer, with tool schemas detached before the transform waterfall. Runs through the scope-filtered `system-prompt/assemble` waterfall, then restores an effective complete section as the sole prompt section and enforces any active runtime-context suppressor. An optional `context.signal` explicitly controls this assembly request; providers and listeners may cooperate with it but must not retain it for another turn. Rejects for multiple complete sections, when a configured `toolOrder` names a tool outside the providers' `knownNames` universe, or when a provider returns the reserved rest-entry name. ### Live events @@ -49,7 +52,7 @@ Design rationale: [the prompt-variables Agent Note](../../../.agents/notes/imple #### What the model sees -By default every assembly starts with the harness identity below, then the configured persona and ordered plugin sections after strict variable interpolation. `includeHarnessIdentity: false` omits only that fixed opener. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The `system-prompt/assemble` waterfall determines the delivered prompt and tool schemas unless one effective section declares itself complete; that exact section then becomes the whole system prompt while the waterfall's contexts, tools, and variables remain. +By default every assembly starts with the harness identity below, then the configured persona and ordered plugin sections after strict variable interpolation. `includeHarnessIdentity: false` omits only that fixed opener. Empty sections disappear; scoped sections and variables can shadow globals for one agent. The `system-prompt/assemble` waterfall determines the delivered prompt and tool schemas unless one effective section declares itself complete; that exact section then becomes the whole system prompt while the waterfall's contexts, tools, and variables remain. Ordered dynamic contexts are separate from system-prompt sections and become sourced user-role snapshots only when present. `includeRuntimeContext: false` or a scoped suppressor removes all such contexts, including listener additions, without disabling the services that own the underlying policy or state. ##### Harness identity diff --git a/packages/core/system-prompt/README.zh.md b/packages/core/system-prompt/README.zh.md index ab3dbffa09..08da218ef2 100644 --- a/packages/core/system-prompt/README.zh.md +++ b/packages/core/system-prompt/README.zh.md @@ -9,6 +9,7 @@ | 键 | 默认值 | 含义 | |---|---|---| | `includeHarnessIdentity` | `true` | 是否包含顺序为 −100 的固定开场白 `You are an AI agent powered by the DeepSeek Harness SDK.`。仅当兼容部署拥有完整系统提示词时设为 false。 | +| `includeRuntimeContext` | `true` | 是否在组装中包含有序动态上下文。设为 false 时不会求值上下文提供方,并会在 waterfall 后丢弃 `system-prompt/assemble` 监听器添加的上下文;其他服务及其强制机制仍然生效。 | | `persona` | `''` | 全局部署 persona 默认值:唯一由配置提供的提示词片段,渲染为顺序为 0 的 `deployment:persona` 段,除非 agent 作用域的贡献将其遮蔽。它是模板,完整的 `{{…}}` 组会严格按已注册变量解释(随附循环注册 `{{model}}`/`{{cwd}}`),目前没有表达字面量花括号的转义语法。为空 ⇒ 渲染时删除该段。 | | `toolOrder` | 无 | 显式的面向模型工具顺序:一个 `ToolSchema.name` 列表,包含一个 `''` 其余项(`TOOL_ORDER_REST`)。已列工具占据列出的位置;未列工具按名称字典序落在其余项位置。缺席 ⇒ 直接按名称字典序排列。在 `system-prompt/assemble` waterfall(瀑布式事件)之前应用于已收集工具;与段的 `order` 排序一样,它会规范化注册表贡献的内容(注册顺序是插件加载产物),而修改列表的 waterfall 监听器拥有其输出的确定性。配置错误会明确失败:列表没有恰好一个其余项或存在重复项,会在加载时抛出;已列名称没有对应已注册工具,会使每次 `assemble()` 被拒绝;工具提供方返回保留的其余项名称也会被拒绝。在随附循环下,轮次会在任何模型请求前失败。为何采用中心列表而非每插件权重,见[显式面向模型工具顺序](../../../.agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md)。 | @@ -17,9 +18,11 @@ ### 公开 API - `ctx.systemPrompt.section(section: PromptSection): () => void`:贡献一个段。层由调用上下文的作用域决定:`agent.ctx` 只为该 agent 贡献,并在该处遮蔽同名全局段。一个 `complete: true` 段会在组装 waterfall 之后成为精确的完整提示词;有效 complete 段超过一个时,组装会被拒绝。同一层中的重复名称和非有限顺序会抛出。随调用 fiber 一并 dispose(资源释放)。 +- `ctx.systemPrompt.context(context: PromptContext): () => void`:为调用作用域贡献有序动态上下文。每次符合条件的组装都会求值提供方,并在随附循环下成为模型历史中带来源的 runtime-context 快照。 +- `ctx.systemPrompt.suppressRuntimeContext(): () => void`:抑制调用作用域的所有动态上下文贡献。多个注册会独立组合;只有当不再存在抑制器时,dispose 返回的 effect 才会恢复上下文。 - `ctx.systemPrompt.tools(provider: (context: AssembleContext) => ToolProviderResult): () => void`:贡献工具 schema;每次组装时使用该次组装的上下文求值。`ToolProviderResult` = `{ schemas, knownNames? }`:`schemas` 是限制后的可见集合;`knownNames` 是限制前由 `toolOrder` 使用的全集。提供方不得返回名为 `TOOL_ORDER_REST` 的 schema。带作用域提供方只在其作用域的组装中查询。随调用 fiber 一并 dispose。 - `ctx.systemPrompt.variable(name: string, provider: (context) => string | undefined): () => void`:贡献提示词变量,在段文本中以 `{{name}}` 引用。带作用域变量会为该 agent 遮蔽同名全局变量。同层重复或无法引用的名称会抛出;`undefined` 表示「本次组装没有值」。随调用 fiber 一并 dispose。 -- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise`:为一个调用方组装提示词:将全局层与 `context.scope` 的层合并,并在变换 waterfall 前分离工具 schema。它经过按作用域筛选的 `system-prompt/assemble` waterfall,之后将一个有效的 complete 段恢复为唯一的提示词段落。可选的 `context.signal` 显式控制本次组装请求;提供方与监听器可以配合该信号,但不得将它保留给另一轮次。存在多个 complete 段、已配置的 `toolOrder` 指名提供方 `knownNames` 全集以外的工具,或提供方返回保留的其余项名称时,调用会被拒绝。 +- `ctx.systemPrompt.assemble(context?: AssembleContext): Promise`:为一个调用方组装提示词:将全局层与 `context.scope` 的层合并,并在变换 waterfall 前分离工具 schema。它经过按作用域筛选的 `system-prompt/assemble` waterfall,之后将一个有效的 complete 段恢复为唯一的提示词段落,并实施任何活动的 runtime-context 抑制器。可选的 `context.signal` 显式控制本次组装请求;提供方与监听器可以配合该信号,但不得将它保留给另一轮次。存在多个 complete 段、已配置的 `toolOrder` 指名提供方 `knownNames` 全集以外的工具,或提供方返回保留的其余项名称时,调用会被拒绝。 @@ -51,7 +54,7 @@ #### 模型看到的内容 -默认情况下,每次组装都从下方 harness 身份开始,然后在严格变量插值后追加已配置 persona 与有序插件段。`includeHarnessIdentity: false` 仅省略这个固定开场白。空段会消失;带作用域的段和变量可以为一个 agent 遮蔽全局项。`system-prompt/assemble` waterfall 决定交付的提示词与工具 schema,除非一个有效段声明自身为 complete;此时,该确切段落会成为完整的系统提示词,而 waterfall 得到的上下文、工具和变量保持不变。 +默认情况下,每次组装都从下方 harness 身份开始,然后在严格变量插值后追加已配置 persona 与有序插件段。`includeHarnessIdentity: false` 仅省略这个固定开场白。空段会消失;带作用域的段和变量可以为一个 agent 遮蔽全局项。`system-prompt/assemble` waterfall 决定交付的提示词与工具 schema,除非一个有效段声明自身为 complete;此时,该确切段落会成为完整的系统提示词,而 waterfall 得到的上下文、工具和变量保持不变。有序动态上下文与系统提示词段落分离,只在存在时才会成为带来源的 user 角色快照。`includeRuntimeContext: false` 或带作用域的抑制器会移除所有这类上下文,包括监听器添加的内容,但不会禁用拥有底层策略或状态的服务。 ##### harness 身份 diff --git a/packages/core/system-prompt/src/index.ts b/packages/core/system-prompt/src/index.ts index 45a613cd8f..b7cb579491 100644 --- a/packages/core/system-prompt/src/index.ts +++ b/packages/core/system-prompt/src/index.ts @@ -186,6 +186,8 @@ function compareToolNames(a: ToolSchema, b: ToolSchema): number { export interface Config { /** Include the fixed DeepSeek Harness identity before the deployment persona (default true). */ includeHarnessIdentity?: boolean + /** Include dynamic runtime-context snapshots in model history (default true). */ + includeRuntimeContext?: boolean /** * Deployment-wide order-0 persona template. A scoped section named * `deployment:persona` shadows it; `{{variable}}` references are strict. @@ -302,6 +304,7 @@ type VariableProvider = (context: AssembleContext) => string | undefined class PromptLayer implements ScopeLayer { readonly sections: NamedEntries readonly contexts: NamedEntries + readonly runtimeContextSuppressors = new AnonymousEntries() readonly toolProviders = new AnonymousEntries() readonly variables: NamedEntries @@ -325,6 +328,7 @@ class PromptLayer implements ScopeLayer { isEmpty(): boolean { return this.sections.isEmpty() && this.contexts.isEmpty() + && this.runtimeContextSuppressors.isEmpty() && this.toolProviders.isEmpty() && this.variables.isEmpty() } @@ -334,6 +338,7 @@ class PromptLayer implements ScopeLayer { export class SystemPrompt extends Service { static Config: z = z.object({ includeHarnessIdentity: z.boolean().default(true), + includeRuntimeContext: z.boolean().default(true), persona: z.string().default(''), // Preserve omission because an explicit empty order lacks the rest marker. toolOrder: z.array(z.string()).default(undefined as unknown as string[]), @@ -362,6 +367,7 @@ export class SystemPrompt extends Service { // The fallback narrows the optional input type; the schema already defaults it. text: config.persona ?? '', }) + if (!(config.includeRuntimeContext ?? true)) this.suppressRuntimeContext() } /** @@ -400,6 +406,20 @@ export class SystemPrompt extends Service { ) } + /** + * Suppress every dynamic runtime-context contribution in the calling + * context's scope without changing the services that own or enforce those + * facts. Multiple suppressors remain independently disposable. + * @returns the exact Cordis effect disposer. + */ + suppressRuntimeContext(): () => void { + return this.layers.effect( + this.ctx, + layer => layer.runtimeContextSuppressors.append(true), + { label: 'systemPrompt.suppressRuntimeContext()' }, + ) + } + /** * Register a tool-schema provider in the calling context's scope. Global and * matching scoped providers both contribute; returning the reserved @@ -446,13 +466,16 @@ export class SystemPrompt extends Service { // Keep configuration failures on the declared asynchronous error path. async assemble(context: AssembleContext = {}): Promise { const scope = context.scope + const scopeLayers = this.layers.chainLayers(scope) + const runtimeContextSuppressed = !this.layers.global.runtimeContextSuppressors.isEmpty() + || scopeLayers.some(layer => !layer.runtimeContextSuppressors.isEmpty()) // Scoped variables shadow globals. const variables: Record = {} for (const [name, provider] of this.layers.global.variables.entries()) { variables[name] = provider(context) } // Scope-chain variables, farthest first, so the nearest scope wins a name. - for (const layer of this.layers.chainLayers(scope)) { + for (const layer of scopeLayers) { for (const [name, provider] of layer.variables.entries()) { variables[name] = provider(context) } @@ -463,7 +486,7 @@ export class SystemPrompt extends Service { // Validate order against pre-restriction names while collecting visible schemas. const providers = [ ...this.layers.global.toolProviders.values(), - ...this.layers.chainLayers(scope).flatMap(layer => [...layer.toolProviders.values()]), + ...scopeLayers.flatMap(layer => [...layer.toolProviders.values()]), ] const collected: ToolSchema[] = [] const knownNames = new Set() @@ -495,12 +518,14 @@ export class SystemPrompt extends Service { }) const assembly: PromptAssembly = { sections, - contexts: [...contextByName.values()] - .sort((a, b) => a.order - b.order) - .map(entry => ({ - name: entry.name, - text: typeof entry.text === 'function' ? entry.text(context) : entry.text, - })), + contexts: runtimeContextSuppressed + ? [] + : [...contextByName.values()] + .sort((a, b) => a.order - b.order) + .map(entry => ({ + name: entry.name, + text: typeof entry.text === 'function' ? entry.text(context) : entry.text, + })), tools: orderTools(collected, this.toolOrder, knownNames), variables, } @@ -508,8 +533,12 @@ export class SystemPrompt extends Service { scopeTarget(this, scope), 'system-prompt/assemble', assembly, context, () => Promise.resolve(assembly), ) - if (completeSection === undefined) return transformed - return { ...transformed, sections: [completeSection] } + if (completeSection === undefined && !runtimeContextSuppressed) return transformed + return { + ...transformed, + sections: completeSection === undefined ? transformed.sections : [completeSection], + contexts: runtimeContextSuppressed ? [] : transformed.contexts, + } } } diff --git a/packages/core/system-prompt/tests/scoped.spec.ts b/packages/core/system-prompt/tests/scoped.spec.ts index 1498966cc6..360fef3a14 100644 --- a/packages/core/system-prompt/tests/scoped.spec.ts +++ b/packages/core/system-prompt/tests/scoped.spec.ts @@ -142,6 +142,23 @@ describe('scoped cache-safe context', () => { expect(renderContextSnapshot(await ctx.systemPrompt.assemble({ scope: scopeKeyOf(scope) }))) .toContain('global policy') }) + + it('suppresses all context for one scope and restores it when disposed', async () => { + const ctx = await mount() + const scope = await mintScope(ctx, 'suppressed-context') + const key = scopeKeyOf(scope) + ctx.systemPrompt.context({ name: 'policy', order: 1, text: 'global policy' }) + const dispose = scope.ctx.systemPrompt.suppressRuntimeContext() + + const suppressed = await ctx.systemPrompt.assemble({ scope: key }) + expect(suppressed.contexts).toEqual([]) + const global = await ctx.systemPrompt.assemble() + expect(renderContextSnapshot(global)).toContain('global policy') + + dispose() + expect(renderContextSnapshot(await ctx.systemPrompt.assemble({ scope: key }))) + .toContain('global policy') + }) }) describe('scoped tool providers and toolOrder × restriction', () => { diff --git a/packages/core/system-prompt/tests/system-prompt.spec.ts b/packages/core/system-prompt/tests/system-prompt.spec.ts index 8ab6c37ea7..c0aa68c494 100644 --- a/packages/core/system-prompt/tests/system-prompt.spec.ts +++ b/packages/core/system-prompt/tests/system-prompt.spec.ts @@ -49,6 +49,25 @@ describe('SystemPrompt', () => { expect(renderPrompt(assembly)).toBe('You are a helpful software engineer assistant.') }) + it('can suppress runtime context without evaluating providers or accepting waterfall additions', async () => { + const ctx = new Context() + await ctx.plugin(SystemPrompt, { includeRuntimeContext: false }) + let providerCalls = 0 + ctx.systemPrompt.context({ + name: 'policy', + order: 0, + text: () => `policy ${++providerCalls}`, + }) + ctx.on('system-prompt/assemble', async (assembly, _context, next) => { + assembly.contexts.push({ name: 'late', text: 'late context' }) + return next() + }) + + const assembly = await ctx.systemPrompt.assemble() + expect(assembly.contexts).toEqual([]) + expect(providerCalls).toBe(0) + }) + it('tolerates a schema-bypassing direct construction (persona omitted)', async () => { // ctx.plugin validates + defaults the config first; a direct construction // skips the schema, so the ctor's `?? ''` narrowing is what fires. diff --git a/packages/examples/agent-spine-demo/README.i18n.yaml b/packages/examples/agent-spine-demo/README.i18n.yaml index 150ddae9ff..b38119060f 100644 --- a/packages/examples/agent-spine-demo/README.i18n.yaml +++ b/packages/examples/agent-spine-demo/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/examples/agent-spine-demo/README.md -README.md: 789715e53038f610d1e2db79cf56f9aabd681fac -README.zh.md: 7a861297d76d18d5e55539334ca8e7ee5ffef640 +README.md: d45a128e786da18d2e2a41c1e4c048270a9c5cc7 +README.zh.md: ae9c523b53a13eae89a670b9177ed996b4fbb928 diff --git a/packages/examples/agent-spine-demo/README.md b/packages/examples/agent-spine-demo/README.md index 789715e530..d45a128e78 100644 --- a/packages/examples/agent-spine-demo/README.md +++ b/packages/examples/agent-spine-demo/README.md @@ -55,11 +55,11 @@ This applies the [Service Definition / Service provider / Consumer separation](. ```ts import type { Config } from '@deepseek-ai/dsh-agent-spine-demo' -// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, tasks?, toolTasks?, goals?, invariants? } +// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, tasks?, toolTasks?, goals?, invariants? } // workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults. ``` -The bundle forwards each field to the child that owns it. App packages supply any pre-created agents: headless and JSON-RPC compositions create `main`, while the ACP app creates agents on demand at `session/new`. Prompt, tool, title, skill, workspace-context, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages; `tasks.maxConcurrentTasksPerOwner` configures the local provider independently of the model-facing `toolTasks` controls. `pickSpineConfig()` copies only fields owned by this bundle, and conflicting `dshHome` values fail during composition. +The bundle forwards each field to the child that owns it. App packages supply any pre-created agents: headless and JSON-RPC compositions create `main`, while the ACP app creates agents on demand at `session/new`. `includeRuntimeContext: false` is forwarded to `dsh-system-prompt` and suppresses all dynamic context snapshots for fresh sessions without disabling their policy services. Prompt, tool, title, skill, workspace-context, invariant, goal, and task settings retain the schemas and defaults documented by their owning packages; `tasks.maxConcurrentTasksPerOwner` configures the local provider independently of the model-facing `toolTasks` controls. `pickSpineConfig()` copies only fields owned by this bundle, and conflicting `dshHome` values fail during composition. For example, `{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` keeps the package-owned companions mounted but suppresses the blocked owner. Blocklist matches override allowlist matches; see [`dsh-invariants`](../../support/invariants/README.md) for regex and lifecycle rules. diff --git a/packages/examples/agent-spine-demo/README.zh.md b/packages/examples/agent-spine-demo/README.zh.md index 7a861297d7..ae9c523b53 100644 --- a/packages/examples/agent-spine-demo/README.zh.md +++ b/packages/examples/agent-spine-demo/README.zh.md @@ -55,11 +55,11 @@ ```ts import type { Config } from '@deepseek-ai/dsh-agent-spine-demo' -// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, tasks?, toolTasks?, goals?, invariants? } +// { agents?, maxParallelToolCalls?, includeHarnessIdentity?, includeRuntimeContext?, persona?, toolOrder?, tools?, dshHome?, sessionTitle?, skills?, workspaceContext, toolBash?, tasks?, toolTasks?, goals?, invariants? } // workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults. ``` -组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent:无头和 JSON-RPC 组合会创建 `main`,ACP 应用则在 `session/new` 按需创建 agent。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值;`tasks.maxConcurrentTasksPerOwner` 配置本地 Service provider,并与面向模型的 `toolTasks` 控制工具相互独立。`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。 +组合包将每个字段转发给拥有它的子节点。应用包提供预创建的 agent:无头和 JSON-RPC 组合会创建 `main`,ACP 应用则在 `session/new` 按需创建 agent。`includeRuntimeContext: false` 会转发给 `dsh-system-prompt`,为新建会话抑制所有动态上下文快照,但不禁用其策略服务。提示词、工具、标题、skill、工作区上下文、不变式、目标和任务设置沿用其所属包记录的 schema 与默认值;`tasks.maxConcurrentTasksPerOwner` 配置本地 Service provider,并与面向模型的 `toolTasks` 控制工具相互独立。`pickSpineConfig()` 只复制该组合包拥有的字段,`dshHome` 值冲突会在组合时失败。 例如,`{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` 会让包拥有的配套插件保持挂载,但抑制被阻止的拥有者。Blocklist 匹配优先于 allowlist 匹配;正则表达式与生命周期规则见 [`dsh-invariants`](../../support/invariants/README.md)。 diff --git a/packages/examples/agent-spine-demo/src/index.ts b/packages/examples/agent-spine-demo/src/index.ts index 2b7644e3cc..4e9ba4fb1f 100644 --- a/packages/examples/agent-spine-demo/src/index.ts +++ b/packages/examples/agent-spine-demo/src/index.ts @@ -69,9 +69,10 @@ export interface GoalConfig { /** * Bundle config: each field forwarded verbatim to the child that owns it — * `agents` to the agent loop (an app that pre-creates no agents, like the ACP - * bridge, simply omits it), `includeHarnessIdentity`, `persona`, and `toolOrder` - * to the system-prompt plugin (the fixed opener, deployment persona, and explicit - * model-facing tool order), the `tools` object to the tool registry (its presentation `mode`), + * bridge, simply omits it), `includeHarnessIdentity`, `includeRuntimeContext`, + * `persona`, and `toolOrder` to the system-prompt plugin (the fixed opener, + * dynamic-context policy, deployment persona, and explicit model-facing tool + * order), the `tools` object to the tool registry (its presentation `mode`), * `dshHome` to bash environment and local skill discovery, `sessionTitle` to * the fallback title service, `skills` to the * skill registry/local provider/tool consumer, `workspaceContext` to the @@ -95,6 +96,8 @@ export interface Config { maxParallelToolCalls?: AgentLoopConfig['maxParallelToolCalls'] /** Whether the system prompt includes the fixed Harness identity (default true). */ includeHarnessIdentity?: SystemPromptConfig['includeHarnessIdentity'] + /** Whether model history includes dynamic runtime-context snapshots (default true). */ + includeRuntimeContext?: SystemPromptConfig['includeRuntimeContext'] /** The deployment persona (see dsh-system-prompt's `Config`). */ persona?: SystemPromptConfig['persona'] /** The explicit model-facing tool order (see dsh-system-prompt's `Config`). */ @@ -180,6 +183,7 @@ export function pickSpineConfig(config: Omit): Omit { it('can omit the bundled bash tool and Harness identity for a compatibility deployment', async () => { const ctx = await mount({ includeHarnessIdentity: false, + includeRuntimeContext: false, persona: 'You are a helpful software engineer assistant.', workspaceContext: false, skills: { enabled: false }, @@ -720,6 +721,8 @@ describe('dsh-agent-spine-demo bundle', () => { }, true) expect(ctx.tools.schemas()).toEqual([]) + ctx.systemPrompt.context({ name: 'policy', order: 0, text: 'hidden policy' }) + expect((await ctx.systemPrompt.assemble()).contexts).toEqual([]) expect(renderPrompt(await ctx.systemPrompt.assemble())) .toBe('You are a helpful software engineer assistant.') @@ -729,7 +732,9 @@ describe('dsh-agent-spine-demo bundle', () => { it('picks shared spine config without leaking entry-point fields', () => { const appConfig = { model: 'entrypoint-only', + maxParallelToolCalls: 3, includeHarnessIdentity: false, + includeRuntimeContext: false, persona: 'You are merged.', toolOrder: ['zulu'], tools: { mode: 'native' as const }, @@ -741,10 +746,13 @@ describe('dsh-agent-spine-demo bundle', () => { tasks: { maxConcurrentTasksPerOwner: 4 }, toolTasks: false as const, invariants: { enabled: false }, + goals: false as const, } expect(agentCore.pickSpineConfig(appConfig)).toEqual({ + maxParallelToolCalls: appConfig.maxParallelToolCalls, includeHarnessIdentity: appConfig.includeHarnessIdentity, + includeRuntimeContext: appConfig.includeRuntimeContext, persona: appConfig.persona, toolOrder: appConfig.toolOrder, tools: appConfig.tools, @@ -756,6 +764,7 @@ describe('dsh-agent-spine-demo bundle', () => { tasks: appConfig.tasks, toolTasks: appConfig.toolTasks, invariants: appConfig.invariants, + goals: appConfig.goals, }) expect(agentCore.pickSpineConfig({ workspaceContext: false })).toEqual({ workspaceContext: false }) }) diff --git a/packages/preset/persona/README.i18n.yaml b/packages/preset/persona/README.i18n.yaml index c839c7dcda..0323741aed 100644 --- a/packages/preset/persona/README.i18n.yaml +++ b/packages/preset/persona/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/preset/persona/README.md -README.md: 742141e65fa8d50b89e6b74e6d21aa8c5bfe98cd -README.zh.md: 61112d1091e428d8c92aa056b7aaa272d3bbbc1d +README.md: 978d01ddbd52095bc2486d27d08fde2d4a8fa79b +README.zh.md: d36af6c028f1258d044608535c6da4eaf2fb92e8 diff --git a/packages/preset/persona/README.md b/packages/preset/persona/README.md index 742141e65f..978d01ddbd 100644 --- a/packages/preset/persona/README.md +++ b/packages/preset/persona/README.md @@ -16,8 +16,9 @@ Mounting this row outside an agent scope collides with the registry's own `deplo |---|---|---| | `text` | required | Persona prose rendered as the `deployment:persona` section | | `complete` | `false` | Restore this persona after assembly as the only system-prompt section | +| `includeRuntimeContext` | `true` | Include dynamic runtime-context snapshots for this agent scope; false suppresses every context contribution without disabling its owning services | -`text` is a template, like any prompt section: complete `{{…}}` groups resolve strictly against registered prompt variables when the prompt renders, not when it assembles. Empty text still occupies the slot, so it shadows the deployment persona away entirely and then disappears at render. With `complete: true`, assembly still resolves contexts, tools, variables, and cooperative listeners, then the prompt registry restores this exact persona as the sole section; no identity, tool guidance, or listener can append prompt text. +`text` is a template, like any prompt section: complete `{{…}}` groups resolve strictly against registered prompt variables when the prompt renders, not when it assembles. Empty text still occupies the slot, so it shadows the deployment persona away entirely and then disappears at render. With `complete: true`, assembly still resolves contexts, tools, variables, and cooperative listeners, then the prompt registry restores this exact persona as the sole section; no identity, tool guidance, or listener can append prompt text. With `includeRuntimeContext: false`, context providers are not evaluated for this scope and contexts added by assembly listeners are discarded. ## Model Experience @@ -25,7 +26,7 @@ Mounting this row outside an agent scope collides with the registry's own `deplo #### What the model sees -The `deployment:persona` section at order 0, immediately after the harness identity opener, carrying exactly this row's configured `text` with prompt variables resolved. For an agent whose preset mounts this row, it replaces whatever persona the deployment configured. In complete mode, the model sees only this rendered section as its system prompt. +The `deployment:persona` section at order 0, immediately after the harness identity opener, carrying exactly this row's configured `text` with prompt variables resolved. For an agent whose preset mounts this row, it replaces whatever persona the deployment configured. In complete mode, the model sees only this rendered section as its system prompt. Runtime context remains enabled by default. When disabled, a fresh agent receives no runtime-context snapshot from sandbox policy, approval policy, delegation, or another system-prompt context provider. #### Token effect diff --git a/packages/preset/persona/README.zh.md b/packages/preset/persona/README.zh.md index 61112d1091..d36af6c028 100644 --- a/packages/preset/persona/README.zh.md +++ b/packages/preset/persona/README.zh.md @@ -16,8 +16,9 @@ |---|---|---| | `text` | 必填 | 作为 `deployment:persona` 段落渲染的人设文本 | | `complete` | `false` | 组装后将此人设恢复为唯一的系统提示词段落 | +| `includeRuntimeContext` | `true` | 是否为此 agent 作用域包含动态 runtime-context 快照;false 会抑制所有上下文贡献,但不禁用拥有它们的服务 | -`text` 与任何提示词段落一样是模板:完整的 `{{…}}` 组在提示词**渲染**时(而非组装时)严格解析为已注册的提示词变量。空文本同样占据该槽位,因此会把部署级人设整个遮蔽掉,然后在渲染时消失。启用 `complete: true` 时,组装仍会解析上下文、工具、变量和协作式监听器,之后提示词注册表将这份确切人设恢复为唯一段落;身份、工具引导或监听器都无法追加提示词文本。 +`text` 与任何提示词段落一样是模板:完整的 `{{…}}` 组在提示词**渲染**时(而非组装时)严格解析为已注册的提示词变量。空文本同样占据该槽位,因此会把部署级人设整个遮蔽掉,然后在渲染时消失。启用 `complete: true` 时,组装仍会解析上下文、工具、变量和协作式监听器,之后提示词注册表将这份确切人设恢复为唯一段落;身份、工具引导或监听器都无法追加提示词文本。启用 `includeRuntimeContext: false` 时,此作用域的上下文提供方不会被求值,组装监听器添加的上下文也会被丢弃。 ## 模型体验 @@ -25,7 +26,7 @@ #### What the model sees -位于 order 0 的 `deployment:persona` 段落,紧随 harness 身份开场白之后,携带本行配置的 `text`,其中的提示词变量已解析。对于其 preset 挂载了本行的 agent,它会替换部署所配置的任何人设。在完整模式下,模型只会看到这个渲染后的段落作为系统提示词。 +位于 order 0 的 `deployment:persona` 段落,紧随 harness 身份开场白之后,携带本行配置的 `text`,其中的提示词变量已解析。对于其 preset 挂载了本行的 agent,它会替换部署所配置的任何人设。在完整模式下,模型只会看到这个渲染后的段落作为系统提示词。Runtime context 默认保持启用。禁用后,新建 agent 不会收到来自沙箱策略、批准策略、委派或其他 system-prompt 上下文提供方的 runtime-context 快照。 #### Token effect diff --git a/packages/preset/persona/src/index.ts b/packages/preset/persona/src/index.ts index 69a53678e9..f45b549656 100644 --- a/packages/preset/persona/src/index.ts +++ b/packages/preset/persona/src/index.ts @@ -40,12 +40,15 @@ export interface Config { text: string /** Make this persona the complete system prompt, suppressing every other section. */ complete?: boolean + /** Suppress dynamic runtime-context snapshots for this persona's agent scope. */ + includeRuntimeContext?: boolean } /** Runtime schema for the persona row. */ export const Config: z = z.object({ text: z.string().required(), complete: z.boolean().default(false), + includeRuntimeContext: z.boolean().default(true), }) /** @@ -61,4 +64,5 @@ export function apply(ctx: Context, config: Config): void { text: config.text, ...(config.complete ? { complete: true } : {}), }), 'persona.section()') + if (!(config.includeRuntimeContext ?? true)) ctx.systemPrompt.suppressRuntimeContext() } diff --git a/packages/preset/persona/tests/persona.spec.ts b/packages/preset/persona/tests/persona.spec.ts index 3fcc30d3a5..3343f7a6f9 100644 --- a/packages/preset/persona/tests/persona.spec.ts +++ b/packages/preset/persona/tests/persona.spec.ts @@ -102,4 +102,41 @@ describe('the persona row', () => { expect(assembly.sections).toEqual([{ name: PERSONA_SECTION, text: 'Only this.' }]) expect(renderPrompt(assembly)).toBe('Only this.') }) + + it('can suppress runtime context for its scope without changing the global assembly', async () => { + const ctx = await harness('deployment identity') + const key: ScopeKey = { agent: 'a1' } + const scope = createScope(ctx, key) + ctx.systemPrompt.context({ name: 'policy', order: 1, text: 'global policy' }) + + const fiber = await scope.ctx.plugin(Persona, { + text: 'Only this.', + includeRuntimeContext: false, + }) + const suppressed = await ctx.systemPrompt.assemble({ scope: key }) + expect(suppressed.contexts).toEqual([]) + const global = await ctx.systemPrompt.assemble() + expect(global.contexts).toEqual([ + { name: 'policy', text: 'global policy' }, + ]) + + await fiber.dispose() + expect((await ctx.systemPrompt.assemble({ scope: key })).contexts).toEqual([ + { name: 'policy', text: 'global policy' }, + ]) + }) + + it('keeps runtime context by default when apply bypasses schema defaults', async () => { + const ctx = await harness('deployment identity') + const key: ScopeKey = { agent: 'a1' } + ctx.systemPrompt.context({ name: 'policy', order: 1, text: 'global policy' }) + + await ctx.plugin(Object.assign((inner: Context) => { + Persona.apply(createScope(inner, key).ctx, { text: 'Scoped identity.' }) + }, { inject: ['systemPrompt'] })) + + expect((await ctx.systemPrompt.assemble({ scope: key })).contexts).toEqual([ + { name: 'policy', text: 'global policy' }, + ]) + }) }) diff --git a/packages/self-modification/tool-cordis/src/api-catalog.ts b/packages/self-modification/tool-cordis/src/api-catalog.ts index 6170c9a196..5a648cada1 100644 --- a/packages/self-modification/tool-cordis/src/api-catalog.ts +++ b/packages/self-modification/tool-cordis/src/api-catalog.ts @@ -1130,6 +1130,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ signature: 'context(context: PromptContext): () => void', jsDoc: '/**\n * Register ordered dynamic context in the calling context\'s scope. Scoped\n * entries shadow global entries with the same name.\n * @param context - the context contribution to register.\n * @returns the exact Cordis effect disposer.\n */', }, + { + signature: 'suppressRuntimeContext(): () => void', + jsDoc: '/**\n * Suppress every dynamic runtime-context contribution in the calling\n * context\'s scope without changing the services that own or enforce those\n * facts. Multiple suppressors remain independently disposable.\n * @returns the exact Cordis effect disposer.\n */', + }, { signature: 'tools(provider: (context: AssembleContext) => ToolProviderResult): () => void', jsDoc: '/**\n * Register a tool-schema provider in the calling context\'s scope. Global and\n * matching scoped providers both contribute; returning the reserved\n * {@link TOOL_ORDER_REST} name makes assembly fail.\n * @param provider - evaluated for each assembly with its context.\n * @returns the exact Cordis effect disposer.\n */', From 63fecf2534c02e3c13f73c0567e593db1deca1e9 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 12 Aug 2026 23:30:51 +0800 Subject: [PATCH 021/160] fix: name run_code's required description argument in its model-facing prose The transport schema requires both `code` and `description`, but the tool description and both SDK instruction flavors described the call as passing a program. `description` was reachable only through the parameter schema, so a model following the prose emitted `{code}` alone and lost the whole written program to an INVALID_ARGS rejection. The length and format guidance stays in RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION alone, so the schema and the prompt cannot drift. Fixes #2426 --- .../feature/2026-06-15-code-mode.i18n.yaml | 4 +-- .../feature/2026-06-15-code-mode.md | 2 ++ .../feature/2026-06-15-code-mode.zh.md | 2 ++ docs/tool-catalog.i18n.yaml | 4 +-- docs/tool-catalog.md | 2 +- docs/tool-catalog.zh.md | 2 +- .../system-prompt.expected.md | 2 +- .../tool-schemas.expected.json | 2 +- .../both-mode-turn/system-prompt.expected.md | 2 +- .../both-mode-turn/tool-schemas.expected.json | 2 +- .../code-mode-turn/system-prompt.expected.md | 2 +- .../code-mode-turn/tool-schemas.expected.json | 2 +- .../advanced-toolchain/session.1.jsonl | 10 +++---- .../advanced-toolchain/session.2.jsonl | 10 +++---- .../advanced-toolchain/session.jsonl | 28 +++++++++---------- packages/core/tools/README.i18n.yaml | 4 +-- packages/core/tools/README.md | 2 +- packages/core/tools/README.zh.md | 2 +- packages/core/tools/src/code-mode.ts | 14 ++++++---- packages/core/tools/src/py-types.ts | 2 +- packages/core/tools/src/ts-types.ts | 2 +- packages/core/tools/tests/code-mode.spec.ts | 5 ++++ packages/core/tools/tests/py-types.spec.ts | 9 ++++++ packages/core/tools/tests/ts-types.spec.ts | 9 ++++++ 24 files changed, 77 insertions(+), 48 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml b/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml index 60e70c2ba5..477957599b 100644 --- a/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-15-code-mode.md -2026-06-15-code-mode.md: f9bde7a85d9cf26d3d6670744c955380fa807e3b -2026-06-15-code-mode.zh.md: 84fc5da63d6049e3533f753cf495aad37b075479 +2026-06-15-code-mode.md: 343ad4cf8191542016b492d6a16cdbf6c26b385c +2026-06-15-code-mode.zh.md: 668def6cf7e74288c3e16253c4ebefb4781db633 diff --git a/.agents/notes/implemented/feature/2026-06-15-code-mode.md b/.agents/notes/implemented/feature/2026-06-15-code-mode.md index f9bde7a85d..343ad4cf81 100644 --- a/.agents/notes/implemented/feature/2026-06-15-code-mode.md +++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.md @@ -87,6 +87,8 @@ The worker runtime provides containment, not a security boundary: model code can The SDK instructs the model to write an async body in the loaded runtime's language (an erasable-TypeScript body by default; a Python `async` body under a Python runtime — see the [language-dispatch note](2026-07-31-code-mode-language-dispatch.md)), call tools through `await tools.name(args)`, catch rejected tool calls when needed, and return or log only the output that should re-enter context. Both flavors state the same contract in their own primitive: independent read-only calls MAY overlap under `Promise.all` (TypeScript) or `asyncio.gather` (Python), mutating calls run alone in submission order, and dependent work sequences with `await`. The declaration prefix can be as large as native schemas, especially in `'both'`, but remains stable for provider caching. +The transport's own `description` and both SDK instruction flavors open by naming `code` and `description` as the call's two required arguments. Prose that describes the call as passing a program leaves the second argument discoverable only through the parameter schema, and a model that emits `{code}` alone loses the whole written program to an `INVALID_ARGS` rejection. + ## Consequences Deployments switching to `'code'` must update any native-only `toolOrder`. Assembly listeners own the integrity of any rewritten protocol messages. Sub-dispatch starts in submission order under a bounded overlap pool, while per-call contexts retain their source, envelope, and metadata through the outer result. diff --git a/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md b/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md index 84fc5da63d..668def6cf7 100644 --- a/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md +++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md @@ -87,6 +87,8 @@ worker 运行时只能约束程序的运行,而不构成安全边界:模型 SDK 指示模型编写一个所加载运行时语言的异步函数体(默认可擦除 TypeScript;Python 运行时下为 Python `async` 函数体——见[语言分发 note](2026-07-31-code-mode-language-dispatch.md)),通过 `await tools.name(args)` 调用工具,在需要时捕获被拒绝的工具调用,并仅 return 或 log 应重新进入上下文的输出。两种 flavor 用各自的原语陈述同一约定:相互独立的只读调用可以(MAY)在 `Promise.all`(TypeScript)或 `asyncio.gather`(Python)下重叠,有副作用的调用按提交顺序单独运行,有依赖的工作用 `await` 排序。声明前缀可能与原生 schema 一样大,尤其在 `'both'` 下,但对提供方缓存保持稳定。 +传输自身的 `description` 与两种 flavor 的 SDK 说明都以点名 `code` 和 `description` 这两个必填参数开头。把该调用描述成「传入一个程序」的散文会让第二个参数只能从参数 schema 中发现,而只发出 `{code}` 的模型会因 `INVALID_ARGS` 被拒,连同已写好的整个程序一起丢失。 + ## 后果 切换到 `'code'` 的部署必须更新任何仅限 native 的 `toolOrder`。组装监听器有责任维护任何被重写的协议消息的完整性。子分发在有界的重叠池下按提交顺序启动,而每次调用的上下文会通过外层结果保留其 source、信封与元数据。 diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index c9726f4ec5..882fb0818d 100644 --- a/docs/tool-catalog.i18n.yaml +++ b/docs/tool-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/tool-catalog.md -tool-catalog.md: 898c5700eddfe49083b2ce0e3e04761298b28bbb -tool-catalog.zh.md: 3b17cf4e3b1b74b0735783cfe899c9c693146c38 +tool-catalog.md: b90a17c7a028b8212fbbcabe2490029de0e44d77 +tool-catalog.zh.md: fff2af027415ca0031926bdf79ce41f006d80a50 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 898c5700ed..b90a17c7a0 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -116,7 +116,7 @@ ask_user_question pauses the tool call until the active UI provider returns a hu ### `run_code` -Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it. +Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it. ```json { diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 3b17cf4e3b..fff2af0274 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -118,7 +118,7 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类 ### `run_code` -针对可用工具执行 TypeScript 程序。请编写异步函数的**函数体**(仅使用可擦除语法;支持顶层 `await` 和 `return`),并根据系统提示词中的声明,以 `await tools.name(args)` 形式调用工具。只有打印或返回的内容会传回,请谨慎筛选。 +针对可用工具执行 TypeScript 程序。接受两个必填参数:`code`,即异步函数的**函数体**(仅使用可擦除语法;支持顶层 `await` 和 `return`);以及 `description`,简要说明该程序做什么。请根据系统提示词中的声明,以 `await tools.name(args)` 形式调用工具。只有打印或返回的内容会传回,请谨慎筛选。 ```json { diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md index 3349deeb59..26ffef13f5 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.expected.md @@ -25,7 +25,7 @@ Use subagent in the background by default. Start independent delegations togethe ## Writing code for run_code -Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program: +`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program: - Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON. - A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue. diff --git a/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json index c5821f832b..e08c449f27 100644 --- a/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json @@ -251,7 +251,7 @@ }, { "name": "run_code", - "description": "Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.", + "description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.", "parameters": { "type": "object", "properties": { diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md index 3771a70950..8501657bbf 100644 --- a/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.expected.md @@ -25,7 +25,7 @@ Use subagent in the background by default. Start independent delegations togethe ## Writing code for run_code -Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program: +`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program: - Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON. - A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue. diff --git a/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json index df0be9cab8..b1d7504245 100644 --- a/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json @@ -194,7 +194,7 @@ }, { "name": "run_code", - "description": "Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.", + "description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.", "parameters": { "type": "object", "properties": { diff --git a/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md b/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md index f3994dc95b..9dc989f1e3 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md +++ b/examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.expected.md @@ -27,7 +27,7 @@ Use subagent in the background by default. Start independent delegations togethe ## Writing code for run_code -Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program: +`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program: - Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON. - A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue. diff --git a/examples/acp-agent/tests/snapshots/code-mode-turn/tool-schemas.expected.json b/examples/acp-agent/tests/snapshots/code-mode-turn/tool-schemas.expected.json index a9ee29aa7a..2a94d503e2 100644 --- a/examples/acp-agent/tests/snapshots/code-mode-turn/tool-schemas.expected.json +++ b/examples/acp-agent/tests/snapshots/code-mode-turn/tool-schemas.expected.json @@ -2,7 +2,7 @@ "initial": [ { "name": "run_code", - "description": "Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.", + "description": "Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.", "parameters": { "type": "object", "properties": { diff --git a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl index 7f0414c53a..11f396a0cc 100644 --- a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl +++ b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl @@ -1,19 +1,19 @@ {"type":"session","version":0,"id":"22222222-2222-4222-8222-222222222222","createdAt":1783950001000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1} -{"type":"agent/inbox/spliced","seq":0,"time":1785498583877,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"55db095e-ab6a-4e9f-aa74-53974c457c15"}]}} +{"type":"agent/inbox/spliced","seq":0,"time":1785498583877,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2de15c96-aba9-4d7c-8426-33d171bb1bf4"}]}} {"type":"turn/start","seq":1,"time":1785821454445,"data":{"turn":1}} {"type":"agent/inbox/spliced","seq":2,"time":1785821454445,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"subagent/descriptor","seq":3,"time":1785821454466,"data":{"version":2,"mode":"one-shot","provider":"spawn","label":"Check direct child"}} {"type":"step/start","seq":4,"time":1785730501506,"data":{"turn":1,"step":1}} -{"type":"user/message","seq":5,"time":1785730501506,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"55db095e-ab6a-4e9f-aa74-53974c457c15"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1786373992181,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"cb0df0e2-55b0-4981-9a30-8c1da928e8b8"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730501506,"data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"2de15c96-aba9-4d7c-8426-33d171bb1bf4"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1786373992181,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"e1d05065-3965-454d-860b-5bc847a3526a"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1786373992181,"data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} -{"type":"request/header","seq":8,"time":1785498583897,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.\n\nVerify your work by running the code or tests. Keep answers brief and factual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.\n\n## Writing code for run_code\n\nPass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:\n\n- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools[\"my-tool\"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.\n- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.\n- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.\n- Emit results with `return` and/or `console.log(...)`. ONLY what you print or return comes back to you — intermediate tool results never enter the conversation, so extract just what you need.\n\nThe available tools:\n\n```ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\ninterface ToolArgsMap {\n /** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. */\n bash: {\n /** The bash command to execute. */\n command: string;\n /** Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\". */\n description: string;\n /** Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry. */\n timeoutMs?: number;\n /** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */\n workdir?: string;\n /** Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies. */\n run_in_background?: boolean;\n } & Record;\n /** Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (all live plugin fibers with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. With `what:\"api\"` or `what:\"events\"`, pass an exact `name` to narrow to one service/event and include its original source JSDoc. */\n cordis_inspect: {\n /** Limit the report to one section. Omit for all sections. */\n what?: \"services\" | \"plugins\" | \"tools\" | \"temporary\" | \"api\" | \"events\";\n /** Exact service key or event name whose original JSDoc to include; valid only with what:\"api\" or what:\"events\". */\n name?: string;\n } & Record;\n /** Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime. */\n cordis_mount: {\n /** JavaScript body returning a temporary Plugin; evaluated now and saved nowhere. */\n code: string;\n } & Record;\n /** Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins. */\n cordis_unmount: {\n /** The temporary Plugin id returned by cordis_mount (for example \"dyn-1\"); valid only in this process and invalid after unmount or restart. */\n id: string;\n } & Record;\n /** Edit an existing UTF-8 text file by replacing literal text. */\n edit: {\n /** Path to edit, resolved by the filesystem backend. */\n file_path: string;\n /** Literal text to replace. Must match exactly. */\n old_string: string;\n /** Literal replacement text. Use an empty string to delete the match. */\n new_string: string;\n /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */\n replace_all?: boolean;\n } & Record;\n /** Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op. */\n interrupt_agent: {\n /** The agent id of the running agent to interrupt. */\n agent_id: string;\n } & Record;\n /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */\n ralph: {\n /** The immutable completion objective for every fresh Ralph round. */\n objective: string;\n /** Optional positive safe-integer round cap, bounded by the deployment ceiling. */\n maxRounds?: number;\n } & Record;\n /** Read a UTF-8 text file and return line-numbered content. */\n read: {\n /** Path to read, resolved by the filesystem backend. */\n file_path: string;\n /** 1-based first line to return. Defaults to 1. */\n offset?: number;\n /** Maximum number of lines to return. Defaults to 2000. */\n limit?: number;\n } & Record;\n /** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered. */\n send_message: {\n /** The subagent id returned when the background subagent was started. */\n subagent_id: string;\n /** The message to deliver to the subagent. */\n message: string;\n } & Record;\n /** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */\n skill: {\n /** The exact skill name from the available skills list. */\n name: string;\n } & Record;\n /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. */\n subagent: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */\n prompt: string;\n /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */\n run_in_background?: boolean;\n } & Record;\n /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result. */\n subagent_fork: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */\n prompt: string;\n } & Record;\n /** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */\n task_kill: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Optional short reason, recorded in the log and forwarded to the task. */\n reason?: string;\n } & Record;\n /** List your background tasks (running and finished) with their ids, kinds, and statuses. */\n task_list: Record;\n /** Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */\n task_output: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive. */\n wait?: boolean;\n /** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */\n timeout_ms?: number;\n } & Record;\n /** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */\n todo_write: {\n /** The COMPLETE task list, replacing any previous list. */\n todos: ({\n /** What the task is — a short imperative line. */\n content: string;\n /** pending (not started) | in_progress (now) | completed (done). */\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n } & Record;\n /** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */\n workflow: {\n /** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `). */\n script: string;\n /** The workflow identity block (plain JSON — never code). */\n meta: {\n /** Short kebab-case workflow name. */\n name: string;\n /** One-line description of what the workflow does. */\n description: string;\n /** Optional guidance on when this workflow applies. */\n whenToUse?: string;\n /** Optional phase declarations matched by phase() calls. */\n phases?: ({\n /** The phase title phase() calls match by exact string. */\n title: string;\n /** Optional one-line description of the phase. */\n detail?: string;\n /** Optional provider override this phase is expected to use. */\n provider?: string;\n /** Optional model override this phase is expected to use. */\n model?: string;\n } & Record)[];\n } & Record;\n /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}). */\n args?: Record;\n } & Record;\n /** Create or fully replace a UTF-8 text file. */\n write: {\n /** Path to write, resolved by the filesystem backend. */\n file_path: string;\n /** Full UTF-8 text content to write. */\n content: string;\n } & Record;\n}\n\ninterface ToolOutputMap {\n bash: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n sandbox?: {\n mode: string;\n denied: boolean;\n enforcement?: string;\n runnerFailed?: boolean;\n };\n };\n cordis_inspect: string;\n cordis_mount: {\n id: string;\n pluginName: string;\n state: \"pending\" | \"loading\" | \"active\" | \"failed\" | \"disposed\" | \"unloading\";\n provides: string[];\n waitingFor: string[];\n };\n cordis_unmount: {\n id: string;\n pluginName: string;\n };\n edit: {\n path: string;\n before: string;\n after: string;\n };\n interrupt_agent: {\n accepted: boolean;\n };\n ralph: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n read: {\n path: string;\n offset: number;\n lines: {\n number: number;\n text: string;\n }[];\n totalLines: number;\n };\n send_message: {\n messageId: string;\n };\n skill: {\n name: string;\n provider: string;\n resourceBase?: {\n kind: \"directory\";\n path: string;\n } | {\n kind: \"url\";\n url: string;\n } | {\n kind: \"opaque\";\n description: string;\n };\n content: string;\n };\n subagent: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n subagent_fork: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n task_kill: {\n outcome: \"cancellation-requested\" | \"already-finished\";\n task: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n task_list: ({\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n })[];\n task_output: {\n text: string;\n task: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n todo_write: {\n todos: ({\n content: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n counts: {\n pending: number;\n inProgress: number;\n completed: number;\n };\n };\n workflow: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n write: {\n path: string;\n operation: \"create\" | \"update\";\n before: string | null;\n after: string;\n };\n}\n\ntype ToolName = keyof ToolOutputMap\n\ndeclare class ToolCallError extends Error {\n readonly name: \"ToolCallError\";\n readonly toolName: ToolName;\n}\n\ndeclare const tools: {\n [K in ToolName]: (args: ToolArgsMap[K]) => Promise;\n}\n```","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."}},"required":["command","description"]}},{"name":"cordis_inspect","description":"Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (all live plugin fibers with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. With `what:\"api\"` or `what:\"events\"`, pass an exact `name` to narrow to one service/event and include its original source JSDoc.","parameters":{"type":"object","properties":{"what":{"type":"string","description":"Limit the report to one section. Omit for all sections.","enum":["services","plugins","tools","temporary","api","events"]},"name":{"type":"string","description":"Exact service key or event name whose original JSDoc to include; valid only with what:\"api\" or what:\"events\"."}}}},{"name":"cordis_mount","description":"Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"JavaScript body returning a temporary Plugin; evaluated now and saved nowhere."}},"required":["code"]}},{"name":"cordis_unmount","description":"Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins.","parameters":{"type":"object","properties":{"id":{"type":"string","description":"The temporary Plugin id returned by cordis_mount (for example \"dyn-1\"); valid only in this process and invalid after unmount or restart."}},"required":["id"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"run_code","description":"Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"The program: the body of an async TypeScript function."},"description":{"type":"string","description":"Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."}},"required":["code","description"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"task_kill","description":"Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the task."}},"required":["task_id"]}},{"name":"task_list","description":"List your background tasks (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"task_output","description":"Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["task_id"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} +{"type":"request/header","seq":8,"time":1785498583897,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.\n\nVerify your work by running the code or tests. Keep answers brief and factual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.\n\n## Writing code for run_code\n\n`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:\n\n- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools[\"my-tool\"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.\n- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.\n- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.\n- Emit results with `return` and/or `console.log(...)`. ONLY what you print or return comes back to you — intermediate tool results never enter the conversation, so extract just what you need.\n\nThe available tools:\n\n```ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\ninterface ToolArgsMap {\n /** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. */\n bash: {\n /** The bash command to execute. */\n command: string;\n /** Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\". */\n description: string;\n /** Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry. */\n timeoutMs?: number;\n /** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */\n workdir?: string;\n /** Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies. */\n run_in_background?: boolean;\n } & Record;\n /** Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (all live plugin fibers with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. With `what:\"api\"` or `what:\"events\"`, pass an exact `name` to narrow to one service/event and include its original source JSDoc. */\n cordis_inspect: {\n /** Limit the report to one section. Omit for all sections. */\n what?: \"services\" | \"plugins\" | \"tools\" | \"temporary\" | \"api\" | \"events\";\n /** Exact service key or event name whose original JSDoc to include; valid only with what:\"api\" or what:\"events\". */\n name?: string;\n } & Record;\n /** Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime. */\n cordis_mount: {\n /** JavaScript body returning a temporary Plugin; evaluated now and saved nowhere. */\n code: string;\n } & Record;\n /** Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins. */\n cordis_unmount: {\n /** The temporary Plugin id returned by cordis_mount (for example \"dyn-1\"); valid only in this process and invalid after unmount or restart. */\n id: string;\n } & Record;\n /** Edit an existing UTF-8 text file by replacing literal text. */\n edit: {\n /** Path to edit, resolved by the filesystem backend. */\n file_path: string;\n /** Literal text to replace. Must match exactly. */\n old_string: string;\n /** Literal replacement text. Use an empty string to delete the match. */\n new_string: string;\n /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */\n replace_all?: boolean;\n } & Record;\n /** Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op. */\n interrupt_agent: {\n /** The agent id of the running agent to interrupt. */\n agent_id: string;\n } & Record;\n /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */\n ralph: {\n /** The immutable completion objective for every fresh Ralph round. */\n objective: string;\n /** Optional positive safe-integer round cap, bounded by the deployment ceiling. */\n maxRounds?: number;\n } & Record;\n /** Read a UTF-8 text file and return line-numbered content. */\n read: {\n /** Path to read, resolved by the filesystem backend. */\n file_path: string;\n /** 1-based first line to return. Defaults to 1. */\n offset?: number;\n /** Maximum number of lines to return. Defaults to 2000. */\n limit?: number;\n } & Record;\n /** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered. */\n send_message: {\n /** The subagent id returned when the background subagent was started. */\n subagent_id: string;\n /** The message to deliver to the subagent. */\n message: string;\n } & Record;\n /** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */\n skill: {\n /** The exact skill name from the available skills list. */\n name: string;\n } & Record;\n /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. */\n subagent: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */\n prompt: string;\n /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */\n run_in_background?: boolean;\n } & Record;\n /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result. */\n subagent_fork: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */\n prompt: string;\n } & Record;\n /** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */\n task_kill: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Optional short reason, recorded in the log and forwarded to the task. */\n reason?: string;\n } & Record;\n /** List your background tasks (running and finished) with their ids, kinds, and statuses. */\n task_list: Record;\n /** Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */\n task_output: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive. */\n wait?: boolean;\n /** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */\n timeout_ms?: number;\n } & Record;\n /** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */\n todo_write: {\n /** The COMPLETE task list, replacing any previous list. */\n todos: ({\n /** What the task is — a short imperative line. */\n content: string;\n /** pending (not started) | in_progress (now) | completed (done). */\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n } & Record;\n /** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */\n workflow: {\n /** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `). */\n script: string;\n /** The workflow identity block (plain JSON — never code). */\n meta: {\n /** Short kebab-case workflow name. */\n name: string;\n /** One-line description of what the workflow does. */\n description: string;\n /** Optional guidance on when this workflow applies. */\n whenToUse?: string;\n /** Optional phase declarations matched by phase() calls. */\n phases?: ({\n /** The phase title phase() calls match by exact string. */\n title: string;\n /** Optional one-line description of the phase. */\n detail?: string;\n /** Optional provider override this phase is expected to use. */\n provider?: string;\n /** Optional model override this phase is expected to use. */\n model?: string;\n } & Record)[];\n } & Record;\n /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}). */\n args?: Record;\n } & Record;\n /** Create or fully replace a UTF-8 text file. */\n write: {\n /** Path to write, resolved by the filesystem backend. */\n file_path: string;\n /** Full UTF-8 text content to write. */\n content: string;\n } & Record;\n}\n\ninterface ToolOutputMap {\n bash: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n sandbox?: {\n mode: string;\n denied: boolean;\n enforcement?: string;\n runnerFailed?: boolean;\n };\n };\n cordis_inspect: string;\n cordis_mount: {\n id: string;\n pluginName: string;\n state: \"pending\" | \"loading\" | \"active\" | \"failed\" | \"disposed\" | \"unloading\";\n provides: string[];\n waitingFor: string[];\n };\n cordis_unmount: {\n id: string;\n pluginName: string;\n };\n edit: {\n path: string;\n before: string;\n after: string;\n };\n interrupt_agent: {\n accepted: boolean;\n };\n ralph: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n read: {\n path: string;\n offset: number;\n lines: {\n number: number;\n text: string;\n }[];\n totalLines: number;\n };\n send_message: {\n messageId: string;\n };\n skill: {\n name: string;\n provider: string;\n resourceBase?: {\n kind: \"directory\";\n path: string;\n } | {\n kind: \"url\";\n url: string;\n } | {\n kind: \"opaque\";\n description: string;\n };\n content: string;\n };\n subagent: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n subagent_fork: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n task_kill: {\n outcome: \"cancellation-requested\" | \"already-finished\";\n task: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n task_list: ({\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n })[];\n task_output: {\n text: string;\n task: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n todo_write: {\n todos: ({\n content: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n counts: {\n pending: number;\n inProgress: number;\n completed: number;\n };\n };\n workflow: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n write: {\n path: string;\n operation: \"create\" | \"update\";\n before: string | null;\n after: string;\n };\n}\n\ntype ToolName = keyof ToolOutputMap\n\ndeclare class ToolCallError extends Error {\n readonly name: \"ToolCallError\";\n readonly toolName: ToolName;\n}\n\ndeclare const tools: {\n [K in ToolName]: (args: ToolArgsMap[K]) => Promise;\n}\n```","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."}},"required":["command","description"]}},{"name":"cordis_inspect","description":"Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (all live plugin fibers with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. With `what:\"api\"` or `what:\"events\"`, pass an exact `name` to narrow to one service/event and include its original source JSDoc.","parameters":{"type":"object","properties":{"what":{"type":"string","description":"Limit the report to one section. Omit for all sections.","enum":["services","plugins","tools","temporary","api","events"]},"name":{"type":"string","description":"Exact service key or event name whose original JSDoc to include; valid only with what:\"api\" or what:\"events\"."}}}},{"name":"cordis_mount","description":"Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"JavaScript body returning a temporary Plugin; evaluated now and saved nowhere."}},"required":["code"]}},{"name":"cordis_unmount","description":"Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins.","parameters":{"type":"object","properties":{"id":{"type":"string","description":"The temporary Plugin id returned by cordis_mount (for example \"dyn-1\"); valid only in this process and invalid after unmount or restart."}},"required":["id"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"run_code","description":"Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"The program: the body of an async TypeScript function."},"description":{"type":"string","description":"Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."}},"required":["code","description"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"task_kill","description":"Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the task."}},"required":["task_id"]}},{"name":"task_list","description":"List your background tasks (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"task_output","description":"Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["task_id"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} {"type":"request/context","seq":9,"time":1785730501507,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/chunk","seq":10,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","seq":11,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"DIRECT_CHILD_OK"}}} {"type":"assistant/chunk","seq":12,"time":1783957884564,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DIRECT_CHILD_OK"}}}} {"type":"assistant/chunk","seq":13,"time":1785498583897,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":14,"time":1785730501507,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":15,"time":1785730501507,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"bcfce159-0aa7-45ef-8239-8222d55d3a8c"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"} +{"type":"assistant/message","seq":15,"time":1785730501507,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3818bc0b-162c-4732-9479-38fd3f7b8346"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"} {"type":"step/end","seq":16,"time":1785730501507,"data":{"turn":1,"step":1}} {"type":"turn/end","seq":17,"time":1785730501507,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl index 10ede98385..0e451f53c4 100644 --- a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl +++ b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl @@ -1,19 +1,19 @@ {"type":"session","version":0,"id":"33333333-3333-4333-8333-333333333333","createdAt":1783950002000,"cwd":"{{cwd}}","parentSession":"11111111-1111-4111-8111-111111111111","origin":"subagent","delegationDepth":1} -{"type":"agent/inbox/spliced","seq":0,"time":1785498584048,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"1ce5ce75-bd55-4372-8466-99b3f5b245bb"}]}} +{"type":"agent/inbox/spliced","seq":0,"time":1785498584048,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"e4481091-3d3d-410a-bdfd-ba2b2357bfbe"}]}} {"type":"turn/start","seq":1,"time":1785821454599,"data":{"turn":1}} {"type":"agent/inbox/spliced","seq":2,"time":1785821454599,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"subagent/descriptor","seq":3,"time":1785821454618,"data":{"version":2,"mode":"one-shot","provider":"spawn"}} {"type":"step/start","seq":4,"time":1785730501645,"data":{"turn":1,"step":1}} -{"type":"user/message","seq":5,"time":1785730501645,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"1ce5ce75-bd55-4372-8466-99b3f5b245bb"},"surfaceOp":"append"} -{"type":"user/message","seq":6,"time":1786373992411,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"f310d4b7-47c3-4d53-a6f2-91ebe89ecd8e"},"surfaceOp":"append"} +{"type":"user/message","seq":5,"time":1785730501645,"data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"e4481091-3d3d-410a-bdfd-ba2b2357bfbe"},"surfaceOp":"append"} +{"type":"user/message","seq":6,"time":1786373992411,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt","form":"snapshot","sections":[{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"bbafb446-3003-4973-a9dd-a6d41fda6507"},"surfaceOp":"append"} {"type":"session/title","seq":7,"time":1786373992411,"data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[5],"source":{"kind":"fallback"}}} -{"type":"request/header","seq":8,"time":1785498584067,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.\n\nVerify your work by running the code or tests. Keep answers brief and factual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.\n\n## Writing code for run_code\n\nPass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:\n\n- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools[\"my-tool\"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.\n- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.\n- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.\n- Emit results with `return` and/or `console.log(...)`. ONLY what you print or return comes back to you — intermediate tool results never enter the conversation, so extract just what you need.\n\nThe available tools:\n\n```ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\ninterface ToolArgsMap {\n /** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. */\n bash: {\n /** The bash command to execute. */\n command: string;\n /** Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\". */\n description: string;\n /** Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry. */\n timeoutMs?: number;\n /** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */\n workdir?: string;\n /** Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies. */\n run_in_background?: boolean;\n } & Record;\n /** Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (all live plugin fibers with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. With `what:\"api\"` or `what:\"events\"`, pass an exact `name` to narrow to one service/event and include its original source JSDoc. */\n cordis_inspect: {\n /** Limit the report to one section. Omit for all sections. */\n what?: \"services\" | \"plugins\" | \"tools\" | \"temporary\" | \"api\" | \"events\";\n /** Exact service key or event name whose original JSDoc to include; valid only with what:\"api\" or what:\"events\". */\n name?: string;\n } & Record;\n /** Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime. */\n cordis_mount: {\n /** JavaScript body returning a temporary Plugin; evaluated now and saved nowhere. */\n code: string;\n } & Record;\n /** Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins. */\n cordis_unmount: {\n /** The temporary Plugin id returned by cordis_mount (for example \"dyn-1\"); valid only in this process and invalid after unmount or restart. */\n id: string;\n } & Record;\n /** Edit an existing UTF-8 text file by replacing literal text. */\n edit: {\n /** Path to edit, resolved by the filesystem backend. */\n file_path: string;\n /** Literal text to replace. Must match exactly. */\n old_string: string;\n /** Literal replacement text. Use an empty string to delete the match. */\n new_string: string;\n /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */\n replace_all?: boolean;\n } & Record;\n /** Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op. */\n interrupt_agent: {\n /** The agent id of the running agent to interrupt. */\n agent_id: string;\n } & Record;\n /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */\n ralph: {\n /** The immutable completion objective for every fresh Ralph round. */\n objective: string;\n /** Optional positive safe-integer round cap, bounded by the deployment ceiling. */\n maxRounds?: number;\n } & Record;\n /** Read a UTF-8 text file and return line-numbered content. */\n read: {\n /** Path to read, resolved by the filesystem backend. */\n file_path: string;\n /** 1-based first line to return. Defaults to 1. */\n offset?: number;\n /** Maximum number of lines to return. Defaults to 2000. */\n limit?: number;\n } & Record;\n /** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered. */\n send_message: {\n /** The subagent id returned when the background subagent was started. */\n subagent_id: string;\n /** The message to deliver to the subagent. */\n message: string;\n } & Record;\n /** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */\n skill: {\n /** The exact skill name from the available skills list. */\n name: string;\n } & Record;\n /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. */\n subagent: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */\n prompt: string;\n /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */\n run_in_background?: boolean;\n } & Record;\n /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result. */\n subagent_fork: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */\n prompt: string;\n } & Record;\n /** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */\n task_kill: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Optional short reason, recorded in the log and forwarded to the task. */\n reason?: string;\n } & Record;\n /** List your background tasks (running and finished) with their ids, kinds, and statuses. */\n task_list: Record;\n /** Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */\n task_output: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive. */\n wait?: boolean;\n /** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */\n timeout_ms?: number;\n } & Record;\n /** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */\n todo_write: {\n /** The COMPLETE task list, replacing any previous list. */\n todos: ({\n /** What the task is — a short imperative line. */\n content: string;\n /** pending (not started) | in_progress (now) | completed (done). */\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n } & Record;\n /** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */\n workflow: {\n /** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `). */\n script: string;\n /** The workflow identity block (plain JSON — never code). */\n meta: {\n /** Short kebab-case workflow name. */\n name: string;\n /** One-line description of what the workflow does. */\n description: string;\n /** Optional guidance on when this workflow applies. */\n whenToUse?: string;\n /** Optional phase declarations matched by phase() calls. */\n phases?: ({\n /** The phase title phase() calls match by exact string. */\n title: string;\n /** Optional one-line description of the phase. */\n detail?: string;\n /** Optional provider override this phase is expected to use. */\n provider?: string;\n /** Optional model override this phase is expected to use. */\n model?: string;\n } & Record)[];\n } & Record;\n /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}). */\n args?: Record;\n } & Record;\n /** Create or fully replace a UTF-8 text file. */\n write: {\n /** Path to write, resolved by the filesystem backend. */\n file_path: string;\n /** Full UTF-8 text content to write. */\n content: string;\n } & Record;\n}\n\ninterface ToolOutputMap {\n bash: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n sandbox?: {\n mode: string;\n denied: boolean;\n enforcement?: string;\n runnerFailed?: boolean;\n };\n };\n cordis_inspect: string;\n cordis_mount: {\n id: string;\n pluginName: string;\n state: \"pending\" | \"loading\" | \"active\" | \"failed\" | \"disposed\" | \"unloading\";\n provides: string[];\n waitingFor: string[];\n };\n cordis_unmount: {\n id: string;\n pluginName: string;\n };\n edit: {\n path: string;\n before: string;\n after: string;\n };\n interrupt_agent: {\n accepted: boolean;\n };\n ralph: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n read: {\n path: string;\n offset: number;\n lines: {\n number: number;\n text: string;\n }[];\n totalLines: number;\n };\n send_message: {\n messageId: string;\n };\n skill: {\n name: string;\n provider: string;\n resourceBase?: {\n kind: \"directory\";\n path: string;\n } | {\n kind: \"url\";\n url: string;\n } | {\n kind: \"opaque\";\n description: string;\n };\n content: string;\n };\n subagent: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n subagent_fork: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n task_kill: {\n outcome: \"cancellation-requested\" | \"already-finished\";\n task: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n task_list: ({\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n })[];\n task_output: {\n text: string;\n task: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n todo_write: {\n todos: ({\n content: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n counts: {\n pending: number;\n inProgress: number;\n completed: number;\n };\n };\n workflow: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n write: {\n path: string;\n operation: \"create\" | \"update\";\n before: string | null;\n after: string;\n };\n}\n\ntype ToolName = keyof ToolOutputMap\n\ndeclare class ToolCallError extends Error {\n readonly name: \"ToolCallError\";\n readonly toolName: ToolName;\n}\n\ndeclare const tools: {\n [K in ToolName]: (args: ToolArgsMap[K]) => Promise;\n}\n```","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."}},"required":["command","description"]}},{"name":"cordis_inspect","description":"Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (all live plugin fibers with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. With `what:\"api\"` or `what:\"events\"`, pass an exact `name` to narrow to one service/event and include its original source JSDoc.","parameters":{"type":"object","properties":{"what":{"type":"string","description":"Limit the report to one section. Omit for all sections.","enum":["services","plugins","tools","temporary","api","events"]},"name":{"type":"string","description":"Exact service key or event name whose original JSDoc to include; valid only with what:\"api\" or what:\"events\"."}}}},{"name":"cordis_mount","description":"Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"JavaScript body returning a temporary Plugin; evaluated now and saved nowhere."}},"required":["code"]}},{"name":"cordis_unmount","description":"Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins.","parameters":{"type":"object","properties":{"id":{"type":"string","description":"The temporary Plugin id returned by cordis_mount (for example \"dyn-1\"); valid only in this process and invalid after unmount or restart."}},"required":["id"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"run_code","description":"Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"The program: the body of an async TypeScript function."},"description":{"type":"string","description":"Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."}},"required":["code","description"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"task_kill","description":"Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the task."}},"required":["task_id"]}},{"name":"task_list","description":"List your background tasks (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"task_output","description":"Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["task_id"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} +{"type":"request/header","seq":8,"time":1785498584067,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.\n\nVerify your work by running the code or tests. Keep answers brief and factual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.\n\n## Writing code for run_code\n\n`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:\n\n- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools[\"my-tool\"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.\n- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.\n- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.\n- Emit results with `return` and/or `console.log(...)`. ONLY what you print or return comes back to you — intermediate tool results never enter the conversation, so extract just what you need.\n\nThe available tools:\n\n```ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\ninterface ToolArgsMap {\n /** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. */\n bash: {\n /** The bash command to execute. */\n command: string;\n /** Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\". */\n description: string;\n /** Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry. */\n timeoutMs?: number;\n /** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */\n workdir?: string;\n /** Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies. */\n run_in_background?: boolean;\n } & Record;\n /** Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (all live plugin fibers with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. With `what:\"api\"` or `what:\"events\"`, pass an exact `name` to narrow to one service/event and include its original source JSDoc. */\n cordis_inspect: {\n /** Limit the report to one section. Omit for all sections. */\n what?: \"services\" | \"plugins\" | \"tools\" | \"temporary\" | \"api\" | \"events\";\n /** Exact service key or event name whose original JSDoc to include; valid only with what:\"api\" or what:\"events\". */\n name?: string;\n } & Record;\n /** Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime. */\n cordis_mount: {\n /** JavaScript body returning a temporary Plugin; evaluated now and saved nowhere. */\n code: string;\n } & Record;\n /** Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins. */\n cordis_unmount: {\n /** The temporary Plugin id returned by cordis_mount (for example \"dyn-1\"); valid only in this process and invalid after unmount or restart. */\n id: string;\n } & Record;\n /** Edit an existing UTF-8 text file by replacing literal text. */\n edit: {\n /** Path to edit, resolved by the filesystem backend. */\n file_path: string;\n /** Literal text to replace. Must match exactly. */\n old_string: string;\n /** Literal replacement text. Use an empty string to delete the match. */\n new_string: string;\n /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */\n replace_all?: boolean;\n } & Record;\n /** Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op. */\n interrupt_agent: {\n /** The agent id of the running agent to interrupt. */\n agent_id: string;\n } & Record;\n /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */\n ralph: {\n /** The immutable completion objective for every fresh Ralph round. */\n objective: string;\n /** Optional positive safe-integer round cap, bounded by the deployment ceiling. */\n maxRounds?: number;\n } & Record;\n /** Read a UTF-8 text file and return line-numbered content. */\n read: {\n /** Path to read, resolved by the filesystem backend. */\n file_path: string;\n /** 1-based first line to return. Defaults to 1. */\n offset?: number;\n /** Maximum number of lines to return. Defaults to 2000. */\n limit?: number;\n } & Record;\n /** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered. */\n send_message: {\n /** The subagent id returned when the background subagent was started. */\n subagent_id: string;\n /** The message to deliver to the subagent. */\n message: string;\n } & Record;\n /** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */\n skill: {\n /** The exact skill name from the available skills list. */\n name: string;\n } & Record;\n /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. */\n subagent: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */\n prompt: string;\n /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */\n run_in_background?: boolean;\n } & Record;\n /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result. */\n subagent_fork: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */\n prompt: string;\n } & Record;\n /** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */\n task_kill: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Optional short reason, recorded in the log and forwarded to the task. */\n reason?: string;\n } & Record;\n /** List your background tasks (running and finished) with their ids, kinds, and statuses. */\n task_list: Record;\n /** Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */\n task_output: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive. */\n wait?: boolean;\n /** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */\n timeout_ms?: number;\n } & Record;\n /** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */\n todo_write: {\n /** The COMPLETE task list, replacing any previous list. */\n todos: ({\n /** What the task is — a short imperative line. */\n content: string;\n /** pending (not started) | in_progress (now) | completed (done). */\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n } & Record;\n /** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */\n workflow: {\n /** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `). */\n script: string;\n /** The workflow identity block (plain JSON — never code). */\n meta: {\n /** Short kebab-case workflow name. */\n name: string;\n /** One-line description of what the workflow does. */\n description: string;\n /** Optional guidance on when this workflow applies. */\n whenToUse?: string;\n /** Optional phase declarations matched by phase() calls. */\n phases?: ({\n /** The phase title phase() calls match by exact string. */\n title: string;\n /** Optional one-line description of the phase. */\n detail?: string;\n /** Optional provider override this phase is expected to use. */\n provider?: string;\n /** Optional model override this phase is expected to use. */\n model?: string;\n } & Record)[];\n } & Record;\n /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}). */\n args?: Record;\n } & Record;\n /** Create or fully replace a UTF-8 text file. */\n write: {\n /** Path to write, resolved by the filesystem backend. */\n file_path: string;\n /** Full UTF-8 text content to write. */\n content: string;\n } & Record;\n}\n\ninterface ToolOutputMap {\n bash: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n sandbox?: {\n mode: string;\n denied: boolean;\n enforcement?: string;\n runnerFailed?: boolean;\n };\n };\n cordis_inspect: string;\n cordis_mount: {\n id: string;\n pluginName: string;\n state: \"pending\" | \"loading\" | \"active\" | \"failed\" | \"disposed\" | \"unloading\";\n provides: string[];\n waitingFor: string[];\n };\n cordis_unmount: {\n id: string;\n pluginName: string;\n };\n edit: {\n path: string;\n before: string;\n after: string;\n };\n interrupt_agent: {\n accepted: boolean;\n };\n ralph: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n read: {\n path: string;\n offset: number;\n lines: {\n number: number;\n text: string;\n }[];\n totalLines: number;\n };\n send_message: {\n messageId: string;\n };\n skill: {\n name: string;\n provider: string;\n resourceBase?: {\n kind: \"directory\";\n path: string;\n } | {\n kind: \"url\";\n url: string;\n } | {\n kind: \"opaque\";\n description: string;\n };\n content: string;\n };\n subagent: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n subagent_fork: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n task_kill: {\n outcome: \"cancellation-requested\" | \"already-finished\";\n task: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n task_list: ({\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n })[];\n task_output: {\n text: string;\n task: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n todo_write: {\n todos: ({\n content: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n counts: {\n pending: number;\n inProgress: number;\n completed: number;\n };\n };\n workflow: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n write: {\n path: string;\n operation: \"create\" | \"update\";\n before: string | null;\n after: string;\n };\n}\n\ntype ToolName = keyof ToolOutputMap\n\ndeclare class ToolCallError extends Error {\n readonly name: \"ToolCallError\";\n readonly toolName: ToolName;\n}\n\ndeclare const tools: {\n [K in ToolName]: (args: ToolArgsMap[K]) => Promise;\n}\n```","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."}},"required":["command","description"]}},{"name":"cordis_inspect","description":"Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (all live plugin fibers with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. With `what:\"api\"` or `what:\"events\"`, pass an exact `name` to narrow to one service/event and include its original source JSDoc.","parameters":{"type":"object","properties":{"what":{"type":"string","description":"Limit the report to one section. Omit for all sections.","enum":["services","plugins","tools","temporary","api","events"]},"name":{"type":"string","description":"Exact service key or event name whose original JSDoc to include; valid only with what:\"api\" or what:\"events\"."}}}},{"name":"cordis_mount","description":"Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"JavaScript body returning a temporary Plugin; evaluated now and saved nowhere."}},"required":["code"]}},{"name":"cordis_unmount","description":"Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins.","parameters":{"type":"object","properties":{"id":{"type":"string","description":"The temporary Plugin id returned by cordis_mount (for example \"dyn-1\"); valid only in this process and invalid after unmount or restart."}},"required":["id"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"run_code","description":"Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"The program: the body of an async TypeScript function."},"description":{"type":"string","description":"Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."}},"required":["code","description"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"task_kill","description":"Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the task."}},"required":["task_id"]}},{"name":"task_list","description":"List your background tasks (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"task_output","description":"Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["task_id"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} {"type":"request/context","seq":9,"time":1785730501646,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/chunk","seq":10,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} {"type":"assistant/chunk","seq":11,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"WORKFLOW_CHILD_OK"}}} {"type":"assistant/chunk","seq":12,"time":1783957884701,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"WORKFLOW_CHILD_OK"}}}} {"type":"assistant/chunk","seq":13,"time":1785498584067,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":14,"time":1785730501646,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":15,"time":1785730501646,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"1d8ec7e3-56a0-4d7c-99af-146add0a2de1"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"} +{"type":"assistant/message","seq":15,"time":1785730501646,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"WORKFLOW_CHILD_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"0a72dd70-1910-4d7d-981c-dc8784aa99f8"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"} {"type":"step/end","seq":16,"time":1785730501646,"data":{"turn":1,"step":1}} {"type":"turn/end","seq":17,"time":1785730501646,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl index 3671d32fb6..cf12af4a5e 100644 --- a/examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl +++ b/examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl @@ -1,20 +1,20 @@ {"type":"session","version":0,"id":"11111111-1111-4111-8111-111111111111","createdAt":1783950000000,"cwd":"{{cwd}}","delegationDepth":0} -{"type":"agent/inbox/spliced","seq":0,"time":1785498583746,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Run this advanced flow exactly once: try a no-op temporary Cordis Plugin named snapshot-marker; use run_code to inspect the live temporary Plugins through tools.cordis_inspect; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; stop dyn-1; then reply with exactly ADVANCED_HEADLESS_OK."}],"source":{"kind":"user"},"role":"user","id":"7b9df077-cb93-4354-b6f6-f46bb5d55c31"}]}} +{"type":"agent/inbox/spliced","seq":0,"time":1785498583746,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Run this advanced flow exactly once: try a no-op temporary Cordis Plugin named snapshot-marker; use run_code to inspect the live temporary Plugins through tools.cordis_inspect; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; stop dyn-1; then reply with exactly ADVANCED_HEADLESS_OK."}],"source":{"kind":"user"},"role":"user","id":"b87ed956-7342-4b8b-a7da-71cca4a08137"}]}} {"type":"turn/start","seq":1,"time":1785821454304,"data":{"turn":1}} {"type":"agent/inbox/spliced","seq":2,"time":1785821454304,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","seq":3,"time":1783957884486,"data":{"turn":1,"step":1}} -{"type":"user/message","seq":4,"time":1785498583779,"data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: try a no-op temporary Cordis Plugin named snapshot-marker; use run_code to inspect the live temporary Plugins through tools.cordis_inspect; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; stop dyn-1; then reply with exactly ADVANCED_HEADLESS_OK."}],"source":{"kind":"user"},"role":"user","id":"7b9df077-cb93-4354-b6f6-f46bb5d55c31"},"surfaceOp":"append"} +{"type":"user/message","seq":4,"time":1785498583779,"data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: try a no-op temporary Cordis Plugin named snapshot-marker; use run_code to inspect the live temporary Plugins through tools.cordis_inspect; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; stop dyn-1; then reply with exactly ADVANCED_HEADLESS_OK."}],"source":{"kind":"user"},"role":"user","id":"b87ed956-7342-4b8b-a7da-71cca4a08137"},"surfaceOp":"append"} {"type":"session/title","seq":5,"time":1785498583779,"data":{"title":"Run this advanced flow exactly","messageSeqs":[4],"source":{"kind":"fallback"}}} -{"type":"request/header","seq":6,"time":1785498583782,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.\n\nVerify your work by running the code or tests. Keep answers brief and factual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.\n\n## Writing code for run_code\n\nPass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program:\n\n- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools[\"my-tool\"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.\n- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.\n- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.\n- Emit results with `return` and/or `console.log(...)`. ONLY what you print or return comes back to you — intermediate tool results never enter the conversation, so extract just what you need.\n\nThe available tools:\n\n```ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\ninterface ToolArgsMap {\n /** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. */\n bash: {\n /** The bash command to execute. */\n command: string;\n /** Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\". */\n description: string;\n /** Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry. */\n timeoutMs?: number;\n /** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */\n workdir?: string;\n /** Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies. */\n run_in_background?: boolean;\n } & Record;\n /** Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (all live plugin fibers with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. With `what:\"api\"` or `what:\"events\"`, pass an exact `name` to narrow to one service/event and include its original source JSDoc. */\n cordis_inspect: {\n /** Limit the report to one section. Omit for all sections. */\n what?: \"services\" | \"plugins\" | \"tools\" | \"temporary\" | \"api\" | \"events\";\n /** Exact service key or event name whose original JSDoc to include; valid only with what:\"api\" or what:\"events\". */\n name?: string;\n } & Record;\n /** Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime. */\n cordis_mount: {\n /** JavaScript body returning a temporary Plugin; evaluated now and saved nowhere. */\n code: string;\n } & Record;\n /** Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins. */\n cordis_unmount: {\n /** The temporary Plugin id returned by cordis_mount (for example \"dyn-1\"); valid only in this process and invalid after unmount or restart. */\n id: string;\n } & Record;\n /** Edit an existing UTF-8 text file by replacing literal text. */\n edit: {\n /** Path to edit, resolved by the filesystem backend. */\n file_path: string;\n /** Literal text to replace. Must match exactly. */\n old_string: string;\n /** Literal replacement text. Use an empty string to delete the match. */\n new_string: string;\n /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */\n replace_all?: boolean;\n } & Record;\n /** Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op. */\n interrupt_agent: {\n /** The agent id of the running agent to interrupt. */\n agent_id: string;\n } & Record;\n /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */\n ralph: {\n /** The immutable completion objective for every fresh Ralph round. */\n objective: string;\n /** Optional positive safe-integer round cap, bounded by the deployment ceiling. */\n maxRounds?: number;\n } & Record;\n /** Read a UTF-8 text file and return line-numbered content. */\n read: {\n /** Path to read, resolved by the filesystem backend. */\n file_path: string;\n /** 1-based first line to return. Defaults to 1. */\n offset?: number;\n /** Maximum number of lines to return. Defaults to 2000. */\n limit?: number;\n } & Record;\n /** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered. */\n send_message: {\n /** The subagent id returned when the background subagent was started. */\n subagent_id: string;\n /** The message to deliver to the subagent. */\n message: string;\n } & Record;\n /** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */\n skill: {\n /** The exact skill name from the available skills list. */\n name: string;\n } & Record;\n /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. */\n subagent: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */\n prompt: string;\n /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */\n run_in_background?: boolean;\n } & Record;\n /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result. */\n subagent_fork: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */\n prompt: string;\n } & Record;\n /** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */\n task_kill: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Optional short reason, recorded in the log and forwarded to the task. */\n reason?: string;\n } & Record;\n /** List your background tasks (running and finished) with their ids, kinds, and statuses. */\n task_list: Record;\n /** Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */\n task_output: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive. */\n wait?: boolean;\n /** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */\n timeout_ms?: number;\n } & Record;\n /** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */\n todo_write: {\n /** The COMPLETE task list, replacing any previous list. */\n todos: ({\n /** What the task is — a short imperative line. */\n content: string;\n /** pending (not started) | in_progress (now) | completed (done). */\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n } & Record;\n /** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */\n workflow: {\n /** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `). */\n script: string;\n /** The workflow identity block (plain JSON — never code). */\n meta: {\n /** Short kebab-case workflow name. */\n name: string;\n /** One-line description of what the workflow does. */\n description: string;\n /** Optional guidance on when this workflow applies. */\n whenToUse?: string;\n /** Optional phase declarations matched by phase() calls. */\n phases?: ({\n /** The phase title phase() calls match by exact string. */\n title: string;\n /** Optional one-line description of the phase. */\n detail?: string;\n /** Optional provider override this phase is expected to use. */\n provider?: string;\n /** Optional model override this phase is expected to use. */\n model?: string;\n } & Record)[];\n } & Record;\n /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}). */\n args?: Record;\n } & Record;\n /** Create or fully replace a UTF-8 text file. */\n write: {\n /** Path to write, resolved by the filesystem backend. */\n file_path: string;\n /** Full UTF-8 text content to write. */\n content: string;\n } & Record;\n}\n\ninterface ToolOutputMap {\n bash: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n sandbox?: {\n mode: string;\n denied: boolean;\n enforcement?: string;\n runnerFailed?: boolean;\n };\n };\n cordis_inspect: string;\n cordis_mount: {\n id: string;\n pluginName: string;\n state: \"pending\" | \"loading\" | \"active\" | \"failed\" | \"disposed\" | \"unloading\";\n provides: string[];\n waitingFor: string[];\n };\n cordis_unmount: {\n id: string;\n pluginName: string;\n };\n edit: {\n path: string;\n before: string;\n after: string;\n };\n interrupt_agent: {\n accepted: boolean;\n };\n ralph: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n read: {\n path: string;\n offset: number;\n lines: {\n number: number;\n text: string;\n }[];\n totalLines: number;\n };\n send_message: {\n messageId: string;\n };\n skill: {\n name: string;\n provider: string;\n resourceBase?: {\n kind: \"directory\";\n path: string;\n } | {\n kind: \"url\";\n url: string;\n } | {\n kind: \"opaque\";\n description: string;\n };\n content: string;\n };\n subagent: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n subagent_fork: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n task_kill: {\n outcome: \"cancellation-requested\" | \"already-finished\";\n task: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n task_list: ({\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n })[];\n task_output: {\n text: string;\n task: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n todo_write: {\n todos: ({\n content: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n counts: {\n pending: number;\n inProgress: number;\n completed: number;\n };\n };\n workflow: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n write: {\n path: string;\n operation: \"create\" | \"update\";\n before: string | null;\n after: string;\n };\n}\n\ntype ToolName = keyof ToolOutputMap\n\ndeclare class ToolCallError extends Error {\n readonly name: \"ToolCallError\";\n readonly toolName: ToolName;\n}\n\ndeclare const tools: {\n [K in ToolName]: (args: ToolArgsMap[K]) => Promise;\n}\n```","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."}},"required":["command","description"]}},{"name":"cordis_inspect","description":"Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (all live plugin fibers with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. With `what:\"api\"` or `what:\"events\"`, pass an exact `name` to narrow to one service/event and include its original source JSDoc.","parameters":{"type":"object","properties":{"what":{"type":"string","description":"Limit the report to one section. Omit for all sections.","enum":["services","plugins","tools","temporary","api","events"]},"name":{"type":"string","description":"Exact service key or event name whose original JSDoc to include; valid only with what:\"api\" or what:\"events\"."}}}},{"name":"cordis_mount","description":"Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"JavaScript body returning a temporary Plugin; evaluated now and saved nowhere."}},"required":["code"]}},{"name":"cordis_unmount","description":"Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins.","parameters":{"type":"object","properties":{"id":{"type":"string","description":"The temporary Plugin id returned by cordis_mount (for example \"dyn-1\"); valid only in this process and invalid after unmount or restart."}},"required":["id"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"run_code","description":"Execute a TypeScript program against the available tools. Write the BODY of an async function (erasable syntax only; top-level `await` and `return` work) and call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"The program: the body of an async TypeScript function."},"description":{"type":"string","description":"Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."}},"required":["code","description"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"task_kill","description":"Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the task."}},"required":["task_id"]}},{"name":"task_list","description":"List your background tasks (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"task_output","description":"Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["task_id"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} +{"type":"request/header","seq":6,"time":1785498583782,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou are headless-agent, a coding assistant powered by the deepseek-v4-flash model. Your working directory is {{cwd}}.\n\nVerify your work by running the code or tests. Keep answers brief and factual.\n\n\nUse the read tool — not shell commands like cat — to inspect text files. Results include line numbers. Use offset and limit to continue reading large files.\n\nUse the write tool to create files or completely replace file contents. Existing files are overwritten, so read an existing file first (the default fs-policy requires it) and prefer edit for targeted changes.\n\nUse the edit tool for targeted changes to existing UTF-8 text files. It replaces literal old_string with new_string; by default old_string must appear exactly once. If old_string appears multiple times, provide a more specific old_string or set replace_all to true. Read the file first (the default fs-policy requires it), unless you just created or edited it in this session.\n\nCheck the [exit code: N] marker on every bash result; investigate failures before moving on.\n\nTrack every background task id you start. You are notified in-session when a task finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running task's work. Before giving a final answer, collect every still-relevant task with task_output (set wait: true only when you are genuinely blocked on it), and task_kill tasks that stopped mattering.\n\nUse the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls.\n\nUse the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.\n\nUse subagent in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set `run_in_background: false` only when your next action depends on that subagent's result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.\n\n## Writing code for run_code\n\n`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program:\n\n- Call tools as `await tools.name(args)` — quoted access for exotic names: `tools[\"my-tool\"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON.\n- A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue.\n- Independent read-only calls MAY overlap under `Promise.all` (safe calls run concurrently; mutating calls run alone, in submission order). Sequence dependent work with `await`.\n- Emit results with `return` and/or `console.log(...)`. ONLY what you print or return comes back to you — intermediate tool results never enter the conversation, so extract just what you need.\n\nThe available tools:\n\n```ts\ntype JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\ninterface ToolArgsMap {\n /** Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`. */\n bash: {\n /** The bash command to execute. */\n command: string;\n /** Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\". */\n description: string;\n /** Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry. */\n timeoutMs?: number;\n /** Working directory for this command. Defaults to the session workspace; a relative path is resolved against it. */\n workdir?: string;\n /** Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies. */\n run_in_background?: boolean;\n } & Record;\n /** Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (all live plugin fibers with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. With `what:\"api\"` or `what:\"events\"`, pass an exact `name` to narrow to one service/event and include its original source JSDoc. */\n cordis_inspect: {\n /** Limit the report to one section. Omit for all sections. */\n what?: \"services\" | \"plugins\" | \"tools\" | \"temporary\" | \"api\" | \"events\";\n /** Exact service key or event name whose original JSDoc to include; valid only with what:\"api\" or what:\"events\". */\n name?: string;\n } & Record;\n /** Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime. */\n cordis_mount: {\n /** JavaScript body returning a temporary Plugin; evaluated now and saved nowhere. */\n code: string;\n } & Record;\n /** Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins. */\n cordis_unmount: {\n /** The temporary Plugin id returned by cordis_mount (for example \"dyn-1\"); valid only in this process and invalid after unmount or restart. */\n id: string;\n } & Record;\n /** Edit an existing UTF-8 text file by replacing literal text. */\n edit: {\n /** Path to edit, resolved by the filesystem backend. */\n file_path: string;\n /** Literal text to replace. Must match exactly. */\n old_string: string;\n /** Literal replacement text. Use an empty string to delete the match. */\n new_string: string;\n /** Replace all matches. Defaults to false; when false, old_string must appear exactly once. */\n replace_all?: boolean;\n } & Record;\n /** Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op. */\n interrupt_agent: {\n /** The agent id of the running agent to interrupt. */\n agent_id: string;\n } & Record;\n /** Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools. */\n ralph: {\n /** The immutable completion objective for every fresh Ralph round. */\n objective: string;\n /** Optional positive safe-integer round cap, bounded by the deployment ceiling. */\n maxRounds?: number;\n } & Record;\n /** Read a UTF-8 text file and return line-numbered content. */\n read: {\n /** Path to read, resolved by the filesystem backend. */\n file_path: string;\n /** 1-based first line to return. Defaults to 1. */\n offset?: number;\n /** Maximum number of lines to return. Defaults to 2000. */\n limit?: number;\n } & Record;\n /** Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered. */\n send_message: {\n /** The subagent id returned when the background subagent was started. */\n subagent_id: string;\n /** The message to deliver to the subagent. */\n message: string;\n } & Record;\n /** Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill. */\n skill: {\n /** The exact skill name from the available skills list. */\n name: string;\n } & Record;\n /** Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result. */\n subagent: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */\n prompt: string;\n /** Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it. */\n run_in_background?: boolean;\n } & Record;\n /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result. */\n subagent_fork: {\n /** A short (3-5 word) description of the delegated task, for display. */\n description: string;\n /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */\n prompt: string;\n } & Record;\n /** Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops. */\n task_kill: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Optional short reason, recorded in the log and forwarded to the task. */\n reason?: string;\n } & Record;\n /** List your background tasks (running and finished) with their ids, kinds, and statuses. */\n task_list: Record;\n /** Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap. */\n task_output: {\n /** Task id returned by the tool that started the background work. */\n task_id: string;\n /** Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive. */\n wait?: boolean;\n /** Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum. */\n timeout_ms?: number;\n } & Record;\n /** Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished). */\n todo_write: {\n /** The COMPLETE task list, replacing any previous list. */\n todos: ({\n /** What the task is — a short imperative line. */\n content: string;\n /** pending (not started) | in_progress (now) | completed (done). */\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n } & Record;\n /** Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn. The workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result. Script-body hooks: - `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly. - `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages. - `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`. - `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim. Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`. Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes. */\n workflow: {\n /** The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `). */\n script: string;\n /** The workflow identity block (plain JSON — never code). */\n meta: {\n /** Short kebab-case workflow name. */\n name: string;\n /** One-line description of what the workflow does. */\n description: string;\n /** Optional guidance on when this workflow applies. */\n whenToUse?: string;\n /** Optional phase declarations matched by phase() calls. */\n phases?: ({\n /** The phase title phase() calls match by exact string. */\n title: string;\n /** Optional one-line description of the phase. */\n detail?: string;\n /** Optional provider override this phase is expected to use. */\n provider?: string;\n /** Optional model override this phase is expected to use. */\n model?: string;\n } & Record)[];\n } & Record;\n /** Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}). */\n args?: Record;\n } & Record;\n /** Create or fully replace a UTF-8 text file. */\n write: {\n /** Path to write, resolved by the filesystem backend. */\n file_path: string;\n /** Full UTF-8 text content to write. */\n content: string;\n } & Record;\n}\n\ninterface ToolOutputMap {\n bash: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"foreground\";\n exitCode: number | null;\n signal: string | null;\n timedOut: boolean;\n aborted: boolean;\n timeoutMs: number;\n stdout: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n stderr: {\n text: string;\n truncated: boolean;\n spillPath?: string;\n };\n sandbox?: {\n mode: string;\n denied: boolean;\n enforcement?: string;\n runnerFailed?: boolean;\n };\n };\n cordis_inspect: string;\n cordis_mount: {\n id: string;\n pluginName: string;\n state: \"pending\" | \"loading\" | \"active\" | \"failed\" | \"disposed\" | \"unloading\";\n provides: string[];\n waitingFor: string[];\n };\n cordis_unmount: {\n id: string;\n pluginName: string;\n };\n edit: {\n path: string;\n before: string;\n after: string;\n };\n interrupt_agent: {\n accepted: boolean;\n };\n ralph: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n read: {\n path: string;\n offset: number;\n lines: {\n number: number;\n text: string;\n }[];\n totalLines: number;\n };\n send_message: {\n messageId: string;\n };\n skill: {\n name: string;\n provider: string;\n resourceBase?: {\n kind: \"directory\";\n path: string;\n } | {\n kind: \"url\";\n url: string;\n } | {\n kind: \"opaque\";\n description: string;\n };\n content: string;\n };\n subagent: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n subagent_fork: {\n kind: \"background\";\n taskId: string;\n } | {\n kind: \"continuable\";\n subagentId: string;\n } | {\n kind: \"foreground\";\n runId: string;\n output: JsonValue[];\n };\n task_kill: {\n outcome: \"cancellation-requested\" | \"already-finished\";\n task: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n task_list: ({\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n })[];\n task_output: {\n text: string;\n task: {\n id: string;\n kind: string;\n label: string;\n status: \"running\" | \"stopping\" | \"completed\" | \"killed\" | \"failed\";\n detail?: string;\n startedAt: number;\n finishedAt?: number;\n };\n };\n todo_write: {\n todos: ({\n content: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n })[];\n counts: {\n pending: number;\n inProgress: number;\n completed: number;\n };\n };\n workflow: {\n runId: string;\n agentsStarted: number;\n result: JsonValue;\n };\n write: {\n path: string;\n operation: \"create\" | \"update\";\n before: string | null;\n after: string;\n };\n}\n\ntype ToolName = keyof ToolOutputMap\n\ndeclare class ToolCallError extends Error {\n readonly name: \"ToolCallError\";\n readonly toolName: ToolName;\n}\n\ndeclare const tools: {\n [K in ToolName]: (args: ToolArgsMap[K]) => Promise;\n}\n```","tools":[{"name":"bash","description":"Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`.","parameters":{"type":"object","properties":{"command":{"type":"string","description":"The bash command to execute."},"description":{"type":"string","description":"Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."},"timeoutMs":{"type":"number","description":"Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."},"workdir":{"type":"string","description":"Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."},"run_in_background":{"type":"boolean","description":"Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies."}},"required":["command","description"]}},{"name":"cordis_inspect","description":"Inspect the live Cordis runtime in the current DSH process. Read-only. Sections: `services` (every provided ctx service and the plugin fiber that owns it), `plugins` (all live plugin fibers with their lifecycle states), `tools` (the model-facing tools currently registered, i.e. what you can call), `temporary` (only temporary Plugins created by cordis_mount: id, name, state, provided services, awaited services, and lifetime), `api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), `events` (every harness event with its dispatch mode and exact signature — pick listener targets here). Temporary Plugins exist only in memory, remain active across later turns, and disappear after cordis_unmount, toolset unload, or DSH restart; they are not restored automatically. The `temporary` section is a subset of `plugins`. Omit `what` to get all six sections. With `what:\"api\"` or `what:\"events\"`, pass an exact `name` to narrow to one service/event and include its original source JSDoc.","parameters":{"type":"object","properties":{"what":{"type":"string","description":"Limit the report to one section. Omit for all sections.","enum":["services","plugins","tools","temporary","api","events"]},"name":{"type":"string","description":"Exact service key or event name whose original JSDoc to include; valid only with what:\"api\" or what:\"events\"."}}}},{"name":"cordis_mount","description":"Mount a temporary Cordis Plugin in the current DSH process. This creates an in-memory runtime Plugin, not an installed or configured Plugin. It remains active across later turns until cordis_unmount, toolset unload, or DSH restart. It does not create files, install a package, change cordis.yml or personal/project config, survive restart, or automatically become permanent. To keep it, ask the Agent to implement an SDK Plugin or installable profile bundle through the regular development workflow. It may affect other sessions in the same process; the sandbox is not a security boundary, and injected services reach the real runtime. `code` runs now as the body of an async JavaScript function in an isolated sandbox and MUST `return` a plugin. Two forms: FUNCTION form `return (ctx) => { … }` — declares no inject, so it can register tools, listen to events, and provide services, but reaching ANY service (e.g. ctx.bash) throws; use it only when you need no services. OBJECT form `return { name?, inject: ['bash', 'llm', …], apply(ctx) { … } }` — declares dependencies, and cordis activates the plugin only after the services exist; PREFER this form. You may reach ONLY the services you list in inject: an undeclared service throws even if it exists, because an undeclared dependency would not be cleaned up if its provider is unmounted. BEFORE calling a service from your code, read cordis_inspect what:\"api\" — it lists method signatures AND the type shapes of their arguments/returns (do not guess a field's type; e.g. a bash run's stdout is an object, not a string). Inside `apply`, use the standard cordis API: `ctx.on(event, listener)` to observe events (see cordis_inspect what:\"events\"), or call `harness.registerTool(ctx, harness.defineTool({ name, description, parameters: { text: { type: 'string', required: true } }, output: { schema: { type: 'string' }, render(_args, value) { return [{ type: 'text', text: value }] } }, async execute(args) { return args.text } }))` to give yourself a new tool — it becomes callable on your NEXT step. Tool parameters: each key IS a property — { type: 'string'|'number'|'integer'|'boolean'|'null'|'object'|'array'|'json', required?: true, description?, enum?, const?, items?, properties? }; every direct DSL object declares additionalProperties: true|false, and oneOf: [schema, schema, ...] replaces type for an exact-one union. A raw JSON-Schema { type: 'object', properties, required?: […] } wrapper is also accepted with open-by-default objects. A tool's `execute` MUST return the lossless JSON value declared by `output.schema`; `output.render(args, value)` separately returns Native/model content blocks. Temporary Plugins can COMPOSE: one Plugin may `ctx.provide('name', value)` a service and another may declare `inject: ['name']` to consume it — the consumer stays pending until the provider exists and returns to pending when the provider is unmounted. Everything registered inside `apply` is cleaned up automatically by cordis_unmount. Sandbox globals: `console` (tagged `[cordis:]`, writes through to the harness terminal), `harness.defineTool`, `harness.registerTool`, `btoa`, `atob`, `TextEncoder`, `TextDecoder`. Node APIs are DISABLED — do filesystem/network/timer work through the cordis services, never Node built-ins: `require`, `setTimeout`/`setInterval`, and `fetch` throw redirect errors; `process` and `Buffer` are undefined. Instead use inject: ['fs'] + ctx.fs for files, inject: ['web'] + ctx.web for HTTP, inject: ['bash'] + ctx.bash for processes, and inject: ['timer'] + ctx.setTimeout/ctx.setInterval for timing (fiber effects, auto-cleaned when unmounted) — cordis_inspect what:\"api\" shows what THIS runtime provides. Write PLAIN JavaScript, not TypeScript (no `as`, no type annotations). Cautions: (1) waterfall events (e.g. tools/pre-execute) hand the listener a trailing `next` callback which MUST be called — returning without `next()` SHORT-CIRCUITS the call; prefer plain notification events unless you intend to intercept. (2) Never await something that only resolves after the current turn (your code runs INSIDE a tool call of that turn — it would deadlock). (3) Your `ctx` is a restricted façade: you can register tools, observe events, provide/consume services, and use timers, but framework internals (ctx.root, ctx.fiber, ctx.extend, ctx.plugin, …) are withheld. It is not a security boundary though — the services you inject (e.g. ctx.bash) reach the real runtime.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"JavaScript body returning a temporary Plugin; evaluated now and saved nowhere."}},"required":["code"]}},{"name":"cordis_unmount","description":"Unmount a current-process temporary Plugin created by cordis_mount. Waits for its tools, listeners, services, timers, and other owned effects to clean up completely. Only dyn-N temporary ids are accepted; this cannot remove Loader, configured, or installed Plugins.","parameters":{"type":"object","properties":{"id":{"type":"string","description":"The temporary Plugin id returned by cordis_mount (for example \"dyn-1\"); valid only in this process and invalid after unmount or restart."}},"required":["id"]}},{"name":"edit","description":"Edit an existing UTF-8 text file by replacing literal text.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to edit, resolved by the filesystem backend."},"old_string":{"type":"string","description":"Literal text to replace. Must match exactly."},"new_string":{"type":"string","description":"Literal replacement text. Use an empty string to delete the match."},"replace_all":{"type":"boolean","description":"Replace all matches. Defaults to false; when false, old_string must appear exactly once."}},"required":["file_path","old_string","new_string"]}},{"name":"interrupt_agent","description":"Request cancellation of a background agent's current turn by its agent id. The target may be your direct child or a deeper agent created under you. Only the current turn stops: messages already queued for the agent stay parked until a later send_message, agents it started keep running, and the agent itself stays available for follow-ups. This call returns as soon as the stop request is accepted, so the target may keep running briefly; interrupting an agent that already finished is an accepted no-op.","parameters":{"type":"object","properties":{"agent_id":{"type":"string","description":"The agent id of the running agent to interrupt."}},"required":["agent_id"]}},{"name":"ralph","description":"Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.","parameters":{"type":"object","properties":{"objective":{"type":"string","description":"The immutable completion objective for every fresh Ralph round."},"maxRounds":{"type":"number","description":"Optional positive safe-integer round cap, bounded by the deployment ceiling."}},"required":["objective"]}},{"name":"read","description":"Read a UTF-8 text file and return line-numbered content.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to read, resolved by the filesystem backend."},"offset":{"type":"number","description":"1-based first line to return. Defaults to 1."},"limit":{"type":"number","description":"Maximum number of lines to return. Defaults to 2000."}},"required":["file_path"]}},{"name":"run_code","description":"Execute a TypeScript program against the available tools. Takes two required arguments: `code`, the BODY of an async function (erasable syntax only; top-level `await` and `return` work), and `description`, a short summary of what the program does. Call tools as `await tools.name(args)` per the declarations in the system prompt. Only what you print or return comes back — curate it.","parameters":{"type":"object","properties":{"code":{"type":"string","description":"The program: the body of an async TypeScript function."},"description":{"type":"string","description":"Clear, concise description of what this program does in active voice, 5-10 words (shown in the UI). Examples: \"Count TODO markers across packages\"; \"Read failing test and its fixture\"; \"Rename config key in every cordis.yml\"."}},"required":["code","description"]}},{"name":"send_message","description":"Send a message to a background subagent by its subagent id, continuing the same conversation. It becomes the subagent's next turn: if it is still working, the message waits until its current turn finishes, so it cannot redirect work already underway. This call returns no answer from the subagent — only confirmation that the message was delivered — so use it to give it more work. A failure means the message was NOT delivered.","parameters":{"type":"object","properties":{"subagent_id":{"type":"string","description":"The subagent id returned when the background subagent was started."},"message":{"type":"string","description":"The message to deliver to the subagent."}},"required":["subagent_id","message"]}},{"name":"skill","description":"Load the full instructions for an available skill. Call this with the exact skill name from the session skill catalog before acting on a task that names or clearly matches that skill.","parameters":{"type":"object","properties":{"name":{"type":"string","description":"The exact skill name from the available skills list."}},"required":["name"]}},{"name":"subagent","description":"Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. Give it a complete, standalone prompt: it does not see this conversation. This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs."},"run_in_background":{"type":"boolean","description":"Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it."}},"required":["description","prompt"]}},{"name":"subagent_fork","description":"Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This call waits for the subagent and returns its result.","parameters":{"type":"object","properties":{"description":{"type":"string","description":"A short (3-5 word) description of the delegated task, for display."},"prompt":{"type":"string","description":"The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new."}},"required":["description","prompt"]}},{"name":"task_kill","description":"Request cancellation of a running background task by task id. Returns immediately; the task settles as killed once its work actually stops.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"reason":{"type":"string","description":"Optional short reason, recorded in the log and forwarded to the task."}},"required":["task_id"]}},{"name":"task_list","description":"List your background tasks (running and finished) with their ids, kinds, and statuses.","parameters":{"type":"object","properties":{}}},{"name":"task_output","description":"Read a background task. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.","parameters":{"type":"object","properties":{"task_id":{"type":"string","description":"Task id returned by the tool that started the background work."},"wait":{"type":"boolean","description":"Block until the task reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the task alive."},"timeout_ms":{"type":"number","description":"Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."}},"required":["task_id"]}},{"name":"todo_write","description":"Record and update a structured task list for the current work. Send the ENTIRE list every call — it REPLACES the previous list (there are no partial updates, no per-item edits). Use it to plan multi-step work and show progress: add one todo per concrete step before you start. Mark every todo being actively worked on `in_progress` — several at once when work genuinely runs in parallel (e.g. concurrent subagents or background commands), one for sequential work; while work remains, at least one task should be `in_progress`. Mark a todo `completed` the moment it is done (do not batch completions), and allow no `in_progress` item only once all work is complete. Skip the list for trivial single-step tasks. Statuses: `pending` (not started), `in_progress` (being worked on now), `completed` (finished).","parameters":{"type":"object","properties":{"todos":{"type":"array","description":"The COMPLETE task list, replacing any previous list.","items":{"type":"object","additionalProperties":false,"properties":{"content":{"type":"string","description":"What the task is — a short imperative line."},"status":{"type":"string","description":"pending (not started) | in_progress (now) | completed (done).","enum":["pending","in_progress","completed"]}},"required":["content","status"]}}},"required":["todos"]}},{"name":"workflow","description":"Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.\n\nThe workflow's identity rides the `meta` parameter as JSON: required `name` (short kebab-case) and `description` strings, optional `whenToUse` string and `phases` array (`{title, detail?, provider?, model?}`). The `script` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO `export const meta` statement — meta is a parameter, not code), running with top-level await; end with `return ` — the value must be JSON-serializable and is this tool's result.\n\nScript-body hooks:\n- `agent(prompt, opts?): Promise` — run one subagent to completion. Without `opts.schema` it resolves to the child's final text; with `opts.schema` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const/oneOf — no pattern/format/numeric bounds) it resolves to the validated object. Resolves `null` when the child fails (filter with `.filter(Boolean)`). Other opts: `label` (display), `phase` (progress group), and independent `provider`/`model` LLM target overrides (either may be provided alone). Anything else (`effort`/`isolation`/`agentType`) is rejected loudly.\n- `pipeline(items, ...stages): Promise` — run each item through the stages independently with NO barrier between stages (prefer this for multi-stage work). Each stage receives `(prev, item, index)`. An ordinary stage throw drops that ITEM to `null` and skips its remaining stages.\n- `parallel(thunks): Promise` — run zero-argument functions concurrently and await ALL of them (a barrier; use only when a stage genuinely needs every prior result together). A throwing thunk resolves to `null`.\n- `phase(title)` — start a progress phase; `log(message)` — narrate progress; `args` — the tool call's `args` input, verbatim.\n\nMisused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item `null`.\n\nConstraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.","parameters":{"type":"object","properties":{"script":{"type":"string","description":"The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return `)."},"meta":{"type":"object","description":"The workflow identity block (plain JSON — never code).","additionalProperties":true,"properties":{"name":{"type":"string","description":"Short kebab-case workflow name."},"description":{"type":"string","description":"One-line description of what the workflow does."},"whenToUse":{"type":"string","description":"Optional guidance on when this workflow applies."},"phases":{"type":"array","description":"Optional phase declarations matched by phase() calls.","items":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string","description":"The phase title phase() calls match by exact string."},"detail":{"type":"string","description":"Optional one-line description of the phase."},"provider":{"type":"string","description":"Optional provider override this phase is expected to use."},"model":{"type":"string","description":"Optional model override this phase is expected to use."}},"required":["title"]}}},"required":["name","description"]},"args":{"type":"object","description":"Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]}).","additionalProperties":true}},"required":["script","meta"]}},{"name":"write","description":"Create or fully replace a UTF-8 text file.","parameters":{"type":"object","properties":{"file_path":{"type":"string","description":"Path to write, resolved by the filesystem backend."},"content":{"type":"string","description":"Full UTF-8 text content to write."}},"required":["file_path","content"]}}]},"reason":"initial"}} {"type":"request/context","seq":7,"time":1785730501403,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"assistant/chunk","seq":8,"time":1783950000007,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} {"type":"assistant/chunk","seq":9,"time":1783950000008,"data":{"turn":1,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-mount","name":"cordis_mount","argumentsDelta":"{\"code\":\"return { name: 'snapshot-marker', apply() {} }\"}"}}} {"type":"assistant/chunk","seq":10,"time":1783950000009,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\":\"return { name: 'snapshot-marker', apply() {} }\"}"}}}} {"type":"assistant/chunk","seq":11,"time":1785498583784,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":12,"time":1785730501404,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":13,"time":1785730501404,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\":\"return { name: 'snapshot-marker', apply() {} }\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3d281cb7-5491-4819-98ac-fe5499d13344"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"} +{"type":"assistant/message","seq":13,"time":1785730501404,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-mount","name":"cordis_mount","arguments":"{\"code\":\"return { name: 'snapshot-marker', apply() {} }\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"0b0e5f02-7f85-4522-9d9d-a239c09bb14b"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"} {"type":"tool/call","seq":14,"time":1785730501404,"data":{"turn":1,"step":1,"callId":"advanced-mount","name":"cordis_mount","arguments":"{\"code\":\"return { name: 'snapshot-marker', apply() {} }\"}"}} -{"type":"tool/result","seq":15,"time":1785730501413,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-mount"},"content":[{"type":"tool-result","toolCallId":"advanced-mount","content":[{"type":"text","text":"Temporary Plugin dyn-1 is running (plugin \"snapshot-marker\"; available until unmounted or DSH restarts)."}],"isError":false}],"role":"user","id":"ef790863-ba32-4cec-95fa-84759f23b65b"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"tool/result","seq":15,"time":1785730501413,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"advanced-mount"},"content":[{"type":"tool-result","toolCallId":"advanced-mount","content":[{"type":"text","text":"Temporary Plugin dyn-1 is running (plugin \"snapshot-marker\"; available until unmounted or DSH restarts)."}],"isError":false}],"role":"user","id":"13429f8e-5f31-4883-8e04-62a480a29d1a"}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","seq":16,"time":1785730501413,"data":{"turn":1,"step":1}} {"type":"step/start","seq":17,"time":1785730501423,"data":{"turn":1,"step":2}} {"type":"assistant/chunk","seq":18,"time":1783950000017,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -22,11 +22,11 @@ {"type":"assistant/chunk","seq":20,"time":1783950000019,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.cordis_inspect({ what: 'temporary' })\", \"description\": \"Run the scripted inspection program\"}"}}}} {"type":"assistant/chunk","seq":21,"time":1785498583804,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":22,"time":1785730501424,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":23,"time":1785730501424,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.cordis_inspect({ what: 'temporary' })\", \"description\": \"Run the scripted inspection program\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3c8fb0fe-4ad9-4619-8583-656112a508c9"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"} +{"type":"assistant/message","seq":23,"time":1785730501424,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.cordis_inspect({ what: 'temporary' })\", \"description\": \"Run the scripted inspection program\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"15c14e30-7b10-4421-8388-91af6c5ec8d0"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[18,19,20,21,22],"surfaceOp":"append"} {"type":"tool/call","seq":24,"time":1785730501424,"data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.cordis_inspect({ what: 'temporary' })\", \"description\": \"Run the scripted inspection program\"}"}} {"type":"tool/code-dispatch-start","seq":25,"time":1785730501473,"data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_inspect","arguments":{"what":"temporary"}}} {"type":"tool/code-dispatch","seq":26,"time":1785730501474,"data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:code:1","name":"cordis_inspect","arguments":{"what":"temporary"},"isError":false,"content":[{"type":"text","text":"## Temporary Plugins\n- Temporary Plugin dyn-1: snapshot-marker [running] — provides: none; waiting for: none; lifetime: until unmounted or DSH restarts"}]}} -{"type":"tool/result","seq":27,"time":1785730501475,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"## Temporary Plugins\n- Temporary Plugin dyn-1: snapshot-marker [running] — provides: none; waiting for: none; lifetime: until unmounted or DSH restarts"}],"isError":false}],"role":"user","id":"5157b02e-5ba5-4672-ac18-37b38a5283ac"}},"sourceEventSeqs":[24],"surfaceOp":"append"} +{"type":"tool/result","seq":27,"time":1785730501475,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool","callId":"advanced-code"},"content":[{"type":"tool-result","toolCallId":"advanced-code","content":[{"type":"text","text":"## Temporary Plugins\n- Temporary Plugin dyn-1: snapshot-marker [running] — provides: none; waiting for: none; lifetime: until unmounted or DSH restarts"}],"isError":false}],"role":"user","id":"e14ca026-504e-4507-b996-b66cfc9f9875"}},"sourceEventSeqs":[24],"surfaceOp":"append"} {"type":"step/end","seq":28,"time":1785730501475,"data":{"turn":1,"step":2}} {"type":"step/start","seq":29,"time":1785730501483,"data":{"turn":1,"step":3}} {"type":"assistant/chunk","seq":30,"time":1783950000029,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -34,9 +34,9 @@ {"type":"assistant/chunk","seq":32,"time":1785037378923,"data":{"turn":1,"step":3,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}}} {"type":"assistant/chunk","seq":33,"time":1785498583869,"data":{"turn":1,"step":3,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":34,"time":1785730501484,"data":{"turn":1,"step":3,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":35,"time":1785730501484,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"cae8cb67-2cf4-41c0-9112-69c583169796"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[30,31,32,33,34],"surfaceOp":"append"} +{"type":"assistant/message","seq":35,"time":1785730501484,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"3f25768e-3d1a-4a29-9663-b849a682c17e"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[30,31,32,33,34],"surfaceOp":"append"} {"type":"tool/call","seq":36,"time":1785730501484,"data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}} -{"type":"tool/result","seq":37,"time":1785730501508,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"1b549053-598f-4595-b7b7-61794756da33"}},"sourceEventSeqs":[36],"surfaceOp":"append"} +{"type":"tool/result","seq":37,"time":1785730501508,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool","callId":"advanced-direct-child"},"content":[{"type":"tool-result","toolCallId":"advanced-direct-child","content":[{"type":"text","text":"DIRECT_CHILD_OK"}],"isError":false}],"role":"user","id":"a427a45e-8365-40fc-9f88-476b05aae2c4"}},"sourceEventSeqs":[36],"surfaceOp":"append"} {"type":"step/end","seq":38,"time":1785730501508,"data":{"turn":1,"step":3}} {"type":"step/start","seq":39,"time":1785730501521,"data":{"turn":1,"step":4}} {"type":"assistant/chunk","seq":40,"time":1783957884594,"data":{"turn":1,"step":4,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -44,13 +44,13 @@ {"type":"assistant/chunk","seq":42,"time":1785037378946,"data":{"turn":1,"step":4,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}}}} {"type":"assistant/chunk","seq":43,"time":1785498583919,"data":{"turn":1,"step":4,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":44,"time":1785730501522,"data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":45,"time":1785730501522,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"bc51af07-d025-4345-912b-9e533e639085"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[40,41,42,43,44],"surfaceOp":"append"} +{"type":"assistant/message","seq":45,"time":1785730501522,"data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"0fb1adfd-fde9-4660-972f-803a385b6687"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[40,41,42,43,44],"surfaceOp":"append"} {"type":"tool/call","seq":46,"time":1785730501522,"data":{"turn":1,"step":4,"callId":"advanced-workflow","name":"workflow","arguments":"{\"script\":\"phase('Delegate')\\nconst reply = await agent('Reply with exactly WORKFLOW_CHILD_OK and nothing else.', { label: 'workflow-child' })\\nreturn { reply }\",\"meta\":{\"name\":\"advanced-headless-snapshot\",\"description\":\"exercise one workflow child through the headless agent\"}}"}} {"type":"tool-workflow/run-start","seq":47,"time":1786359174028,"data":{"runId":"668432bb-f01c-41e7-841e-30d8deab7b55","name":"advanced-headless-snapshot"}} {"type":"tool-workflow/agent-start","seq":48,"time":1786359174210,"data":{"runId":"668432bb-f01c-41e7-841e-30d8deab7b55","seq":1,"label":"workflow-child","phase":"Delegate","childId":"33333333-3333-4333-8333-333333333333"}} {"type":"tool-workflow/agent-end","seq":49,"time":1786359174230,"data":{"runId":"668432bb-f01c-41e7-841e-30d8deab7b55","seq":1,"outcome":"completed"}} {"type":"tool-workflow/run-end","seq":50,"time":1786359174232,"data":{"runId":"668432bb-f01c-41e7-841e-30d8deab7b55","stopReason":"completed"}} -{"type":"tool/result","seq":51,"time":1786359174232,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-headless-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"6ea4325f-622f-4381-8f12-3a6fe5606a50"}},"sourceEventSeqs":[46],"surfaceOp":"append"} +{"type":"tool/result","seq":51,"time":1786359174232,"data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"advanced-workflow"},"content":[{"type":"tool-result","toolCallId":"advanced-workflow","content":[{"type":"text","text":"workflow \"advanced-headless-snapshot\" completed (1 agent).\nReturn value:\n{\n \"reply\": \"WORKFLOW_CHILD_OK\"\n}"}],"isError":false}],"role":"user","id":"9fe92917-9724-4129-8973-9193ed5ff2f1"}},"sourceEventSeqs":[46],"surfaceOp":"append"} {"type":"step/end","seq":52,"time":1786359174233,"data":{"turn":1,"step":4}} {"type":"step/start","seq":53,"time":1786359174239,"data":{"turn":1,"step":5}} {"type":"assistant/chunk","seq":54,"time":1785730501661,"data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}} @@ -58,9 +58,9 @@ {"type":"assistant/chunk","seq":56,"time":1786359174239,"data":{"turn":1,"step":5,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\":\"dyn-1\"}"}}}} {"type":"assistant/chunk","seq":57,"time":1786359174239,"data":{"turn":1,"step":5,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":58,"time":1786359174239,"data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}} -{"type":"assistant/message","seq":59,"time":1786359174239,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\":\"dyn-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"bda6f601-43d0-4c07-b561-4d50322d645b"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[54,55,56,57,58],"surfaceOp":"append"} +{"type":"assistant/message","seq":59,"time":1786359174239,"data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\":\"dyn-1\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4e132ffc-e437-4f02-bbd4-01a7f7809070"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[54,55,56,57,58],"surfaceOp":"append"} {"type":"tool/call","seq":60,"time":1786359174239,"data":{"turn":1,"step":5,"callId":"advanced-unmount","name":"cordis_unmount","arguments":"{\"id\":\"dyn-1\"}"}} -{"type":"tool/result","seq":61,"time":1786359174243,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-unmount"},"content":[{"type":"tool-result","toolCallId":"advanced-unmount","content":[{"type":"text","text":"Temporary Plugin dyn-1 was unmounted and removed."}],"isError":false}],"role":"user","id":"d31a7b99-409f-4d2c-9584-93cc453c12b5"}},"sourceEventSeqs":[60],"surfaceOp":"append"} +{"type":"tool/result","seq":61,"time":1786359174243,"data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"advanced-unmount"},"content":[{"type":"tool-result","toolCallId":"advanced-unmount","content":[{"type":"text","text":"Temporary Plugin dyn-1 was unmounted and removed."}],"isError":false}],"role":"user","id":"755a3021-cd56-437f-b9fb-7db74d116c9d"}},"sourceEventSeqs":[60],"surfaceOp":"append"} {"type":"step/end","seq":62,"time":1786359174243,"data":{"turn":1,"step":5}} {"type":"step/start","seq":63,"time":1786359174248,"data":{"turn":1,"step":6}} {"type":"assistant/chunk","seq":64,"time":1785730501679,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"text"}}} @@ -68,6 +68,6 @@ {"type":"assistant/chunk","seq":66,"time":1786359174249,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_HEADLESS_OK"}}}} {"type":"assistant/chunk","seq":67,"time":1786359174249,"data":{"turn":1,"step":6,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}} {"type":"assistant/chunk","seq":68,"time":1786359174249,"data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"stop"}}}} -{"type":"assistant/message","seq":69,"time":1786359174249,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_HEADLESS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"4ae96a19-c45a-4096-befc-47004b88fc3e"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[64,65,66,67,68],"surfaceOp":"append"} +{"type":"assistant/message","seq":69,"time":1786359174249,"data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"text","text":"ADVANCED_HEADLESS_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"72d6c5d3-85b7-4d62-b9df-253834141892"},"usage":{"inputTokens":3,"outputTokens":3}},"sourceEventSeqs":[64,65,66,67,68],"surfaceOp":"append"} {"type":"step/end","seq":70,"time":1786359174249,"data":{"turn":1,"step":6}} {"type":"turn/end","seq":71,"time":1786359174249,"data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/packages/core/tools/README.i18n.yaml b/packages/core/tools/README.i18n.yaml index 7011884239..421a19ec48 100644 --- a/packages/core/tools/README.i18n.yaml +++ b/packages/core/tools/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/core/tools/README.md -README.md: 44eb25b79436a75f08406102fc1e3734e59b1001 -README.zh.md: a47e7c54b0cd3fc3c5146a9a8be4d1406f9ef8aa +README.md: 3dddeb51a9f5b98a01a5e0170dacde471be67094 +README.zh.md: deb01899fa909e5d430415fa98ea49bd543b2749 diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index 44eb25b794..3dddeb51a9 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -155,7 +155,7 @@ Code Mode exposes the generated [`run_code` schema](../../../docs/tool-catalog.m ```markdown ## Writing code for run_code -Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program: +`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program: - Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON. - A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue. diff --git a/packages/core/tools/README.zh.md b/packages/core/tools/README.zh.md index a47e7c54b0..deb01899fa 100644 --- a/packages/core/tools/README.zh.md +++ b/packages/core/tools/README.zh.md @@ -155,7 +155,7 @@ Code Mode 会公开生成的 [`run_code` schema](../../../docs/tool-catalog.md#d ```markdown ## Writing code for run_code -Pass `run_code` the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program: +`run_code` takes two required arguments: `code` — the body of an async TypeScript function (erasable syntax only — no `enum` or namespaces; type annotations are advisory, the code runs type-stripped) — and `description`, a short summary of what the program does. Inside the program: - Call tools as `await tools.name(args)` — quoted access for exotic names: `tools["my-tool"](args)`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON. - A FAILED tool call rejects with `ToolCallError`, whose `toolName` identifies the failed tool and whose `message` is human-readable — `try/catch` it to handle and continue. diff --git a/packages/core/tools/src/code-mode.ts b/packages/core/tools/src/code-mode.ts index 42c55ece03..c997e7d2c2 100644 --- a/packages/core/tools/src/code-mode.ts +++ b/packages/core/tools/src/code-mode.ts @@ -45,10 +45,11 @@ interface RunCodeFlavor { */ const TYPESCRIPT_FLAVOR: RunCodeFlavor = { description: - 'Execute a TypeScript program against the available tools. Write the BODY of an ' - + 'async function (erasable syntax only; top-level `await` and `return` work) and ' - + 'call tools as `await tools.name(args)` per the declarations in the system prompt. ' - + 'Only what you print or return comes back — curate it.', + 'Execute a TypeScript program against the available tools. Takes two required ' + + 'arguments: `code`, the BODY of an async function (erasable syntax only; top-level ' + + '`await` and `return` work), and `description`, a short summary of what the program ' + + 'does. Call tools as `await tools.name(args)` per the declarations in the system ' + + 'prompt. Only what you print or return comes back — curate it.', codeDescription: 'The program: the body of an async TypeScript function.', } @@ -59,8 +60,9 @@ const TYPESCRIPT_FLAVOR: RunCodeFlavor = { */ const PYTHON_FLAVOR: RunCodeFlavor = { description: - 'Execute a Python program against the available tools. Write the BODY of an ' - + 'async function (top-level `await` and `return` work) and call tools as ' + 'Execute a Python program against the available tools. Takes two required ' + + 'arguments: `code`, the BODY of an async function (top-level `await` and `return` ' + + 'work), and `description`, a short summary of what the program does. Call tools as ' + '`await tools.name(args)` per the declarations in the system prompt. Answer ' + 'with `print(...)` and/or `return ` — only that comes back, so curate it.', codeDescription: 'The program: the body of an async Python function.', diff --git a/packages/core/tools/src/py-types.ts b/packages/core/tools/src/py-types.ts index 4898ec80e1..6141df14ed 100644 --- a/packages/core/tools/src/py-types.ts +++ b/packages/core/tools/src/py-types.ts @@ -733,7 +733,7 @@ export function jsonSchemaToPy(schema: unknown): string { /** The fixed model-facing usage contract rendered above the declarations. */ const SDK_INSTRUCTIONS = `## Writing code for run_code -Pass \`run_code\` the body of an async Python function (top-level \`await\` and \`return\` both work). At run time exactly two of the names declared below are bound: \`tools\` and \`ToolCallError\`. Everything else is a STATIC STUB describing argument and return types — in particular the \`TypedDict\` classes do NOT exist at run time, so build arguments as plain \`dict\`/\`list\` JSON values: \`await tools.name({"field": 1})\`, never \`FooArgs(field=1)\`, which raises \`NameError\`. Inside the program: +\`run_code\` takes two required arguments: \`code\` — the body of an async Python function (top-level \`await\` and \`return\` both work) — and \`description\`, a short summary of what the program does. At run time exactly two of the names declared below are bound: \`tools\` and \`ToolCallError\`. Everything else is a STATIC STUB describing argument and return types — in particular the \`TypedDict\` classes do NOT exist at run time, so build arguments as plain \`dict\`/\`list\` JSON values: \`await tools.name({"field": 1})\`, never \`FooArgs(field=1)\`, which raises \`NameError\`. Inside the program: - Call tools as \`await tools.name(args)\` — subscript access for exotic, reserved, or underscore-leading names: \`await tools["my-tool"](args)\`. Every call resolves to the tool's typed canonical JSON value (each method's return type below). Tool arguments must be lossless JSON. - A FAILED tool call raises \`ToolCallError\`, whose \`toolName\` identifies the failed tool and whose message is human-readable — wrap in \`try/except\` to handle and continue. diff --git a/packages/core/tools/src/ts-types.ts b/packages/core/tools/src/ts-types.ts index 9b0d096a22..bbd5cd7666 100644 --- a/packages/core/tools/src/ts-types.ts +++ b/packages/core/tools/src/ts-types.ts @@ -249,7 +249,7 @@ export function jsonSchemaToTs(schema: unknown, indent = 0): string { /** The fixed model-facing usage contract rendered above the declarations (see the Code Mode Agent Note's "What the model sees"). */ const SDK_INSTRUCTIONS = `## Writing code for run_code -Pass \`run_code\` the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped). Inside the program: +\`run_code\` takes two required arguments: \`code\` — the body of an async TypeScript function (erasable syntax only — no \`enum\` or namespaces; type annotations are advisory, the code runs type-stripped) — and \`description\`, a short summary of what the program does. Inside the program: - Call tools as \`await tools.name(args)\` — quoted access for exotic names: \`tools["my-tool"](args)\`. Every call resolves to the tool's typed canonical JSON value. Tool arguments must be lossless JSON. - A FAILED tool call rejects with \`ToolCallError\`, whose \`toolName\` identifies the failed tool and whose \`message\` is human-readable — \`try/catch\` it to handle and continue. diff --git a/packages/core/tools/tests/code-mode.spec.ts b/packages/core/tools/tests/code-mode.spec.ts index 4379f7e0b2..edd456154a 100644 --- a/packages/core/tools/tests/code-mode.spec.ts +++ b/packages/core/tools/tests/code-mode.spec.ts @@ -399,6 +399,10 @@ describe('mode-aware wire contribution', () => { const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME) expect(runCodeSchema?.description).toContain('Execute a TypeScript program') expect(runCodeSchema?.description).toContain('BODY of an') + // Both required arguments are named here, not only in the parameter + // schema: prose that describes the call as "pass the program" is what + // leads a model to emit `{code}` alone and fail INVALID_ARGS. + expect(runCodeSchema?.description).toContain('`description`') const codeParam = (runCodeSchema?.parameters as { properties: { code: { description: string } } }).properties.code expect(codeParam.description).toBe('The program: the body of an async TypeScript function.') }) @@ -410,6 +414,7 @@ describe('mode-aware wire contribution', () => { const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME) expect(runCodeSchema?.description).toContain('Execute a Python program') expect(runCodeSchema?.description).toContain('`return `') + expect(runCodeSchema?.description).toContain('`description`') expect(runCodeSchema?.description).not.toContain('TypeScript') const codeParam = (runCodeSchema?.parameters as { properties: { code: { description: string } } }).properties.code expect(codeParam.description).toBe('The program: the body of an async Python function.') diff --git a/packages/core/tools/tests/py-types.spec.ts b/packages/core/tools/tests/py-types.spec.ts index 3439cb6a37..0caf9daab6 100644 --- a/packages/core/tools/tests/py-types.spec.ts +++ b/packages/core/tools/tests/py-types.spec.ts @@ -166,6 +166,15 @@ describe('renderToolsSdkPy', () => { expect(text).toContain('tools: Tools') }) + it('names both required call arguments, not just the program', () => { + // The schema requires `code` AND `description`; instructions that mention + // only the program let a model emit `{code}` alone and fail INVALID_ARGS. + const text = renderToolsSdkPy([bash]) + expect(text).toContain('`code`') + expect(text).toContain('`description`') + expect(text).toContain('two required arguments') + }) + it('renders required as plain fields and optional as NotRequired, with per-field description comments', () => { const tool: ToolSdkSchema = { name: 'search', diff --git a/packages/core/tools/tests/ts-types.spec.ts b/packages/core/tools/tests/ts-types.spec.ts index 4c4954752d..8b4e145060 100644 --- a/packages/core/tools/tests/ts-types.spec.ts +++ b/packages/core/tools/tests/ts-types.spec.ts @@ -148,6 +148,15 @@ describe('renderToolsSdk', () => { expect(text).toContain('lossless JSON') }) + it('names both required call arguments, not just the program', () => { + // The schema requires `code` AND `description`; instructions that mention + // only the program let a model emit `{code}` alone and fail INVALID_ARGS. + const text = renderToolsSdk([bash]) + expect(text).toContain('`code`') + expect(text).toContain('`description`') + expect(text).toContain('two required arguments') + }) + it('is deterministic: same tool set, byte-identical text regardless of input order', () => { expect(renderToolsSdk([bash, exotic])).toBe(renderToolsSdk([exotic, bash])) // Equal names sort stably (the comparator's equal arm). From d6fc55ad863916ade46102860bfc1ced516c8205 Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Wed, 12 Aug 2026 08:39:09 -0700 Subject: [PATCH 022/160] docs: refresh generated references --- docs/config-catalog.i18n.yaml | 4 ++-- docs/config-catalog.zh.md | 2 +- docs/module-graph.i18n.yaml | 4 ++-- docs/module-graph.md | 3 ++- docs/module-graph.zh.md | 3 ++- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index 33fae42ac2..bb542a0ab7 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/config-catalog.md -config-catalog.md: f075a5c22049df1696a1849cd88bd3b783adde21 -config-catalog.zh.md: 952a77df3ba279155b4364a083917f816efd2d63 +config-catalog.md: bdec02cae2afd13bbe9ef041d95c6c30164f8fc0 +config-catalog.zh.md: 11769b07652859058d16ec7309411e2f05eca254 diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 952a77df3b..11769b0765 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -2807,7 +2807,7 @@ export interface Config { - `@deepseek-ai/dsh-client-ui-agent-preset`([`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts)) - `@deepseek-ai/dsh-client-ui-command`([`packages/client/ui-command/src/index.ts`](../packages/client/ui-command/src/index.ts)) - `@deepseek-ai/dsh-client-ui-conversation`([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts)) -- `@deepseek-ai/dsh-client-ui-deliverables`([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts)) +- `@deepseek-ai/dsh-client-ui-deliverables` — 需要 `systemPrompt`([`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts)) - `@deepseek-ai/dsh-client-ui-directory-picker`([`packages/client/ui-directory-picker/src/index.ts`](../packages/client/ui-directory-picker/src/index.ts)) - `@deepseek-ai/dsh-client-ui-directory-picker-native`([`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts)) - `@deepseek-ai/dsh-client-ui-feedback`([`packages/client/ui-feedback/src/index.ts`](../packages/client/ui-feedback/src/index.ts)) diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index b528a4356d..7f7c5d902f 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: 5197a184f2be283e3bb57de91d4a4d3cb22e51b2 -module-graph.zh.md: 3b312b731d32ed71674d800c35a5868e0b5d94ee +module-graph.md: bee4af67d5c2895bb03f584daca7fc7cf43a5cd3 +module-graph.zh.md: 506645d45fae4b5dcd301e11c4b2b40a9361bb09 diff --git a/docs/module-graph.md b/docs/module-graph.md index 5197a184f2..bee4af67d5 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -1269,6 +1269,7 @@ flowchart TD pkg_client_ui_deliverables --> pkg_client_ui_conversation pkg_client_ui_deliverables --> pkg_client_ui_slots pkg_client_ui_deliverables --> pkg_invariants + pkg_client_ui_deliverables --> pkg_system_prompt pkg_client_ui_feedback --> pkg_api_remotes pkg_client_ui_feedback --> pkg_client_connection pkg_client_ui_feedback --> pkg_client_locale @@ -1579,7 +1580,7 @@ flowchart TD | [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | | [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`api-remotes`](../packages/api/remotes), [`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-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | | [`client-ui-command`](../packages/client/ui-command) | `client` | [`api-remotes`](../packages/api/remotes), [`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), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants) | -| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `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) | +| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `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), [`system-prompt`](../packages/core/system-prompt) | | [`client-ui-feedback`](../packages/client/ui-feedback) | `client` | [`api-remotes`](../packages/api/remotes), [`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), [`invariants`](../packages/support/invariants), [`message-feedback`](../packages/feedback/message-feedback), [`type-meta`](../packages/typert/type-meta) | | [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`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), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) | | [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`api-remotes`](../packages/api/remotes), [`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), [`invariants`](../packages/support/invariants), [`plan-mode`](../packages/plan/plan-mode) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 3b312b731d..506645d45f 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -1271,6 +1271,7 @@ flowchart TD pkg_client_ui_deliverables --> pkg_client_ui_conversation pkg_client_ui_deliverables --> pkg_client_ui_slots pkg_client_ui_deliverables --> pkg_invariants + pkg_client_ui_deliverables --> pkg_system_prompt pkg_client_ui_feedback --> pkg_api_remotes pkg_client_ui_feedback --> pkg_client_connection pkg_client_ui_feedback --> pkg_client_locale @@ -1581,7 +1582,7 @@ flowchart TD | [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | | [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`api-remotes`](../packages/api/remotes), [`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-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | | [`client-ui-command`](../packages/client/ui-command) | `client` | [`api-remotes`](../packages/api/remotes), [`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), [`commands`](../packages/interaction/commands), [`invariants`](../packages/support/invariants) | -| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `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) | +| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `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), [`system-prompt`](../packages/core/system-prompt) | | [`client-ui-feedback`](../packages/client/ui-feedback) | `client` | [`api-remotes`](../packages/api/remotes), [`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), [`invariants`](../packages/support/invariants), [`message-feedback`](../packages/feedback/message-feedback), [`type-meta`](../packages/typert/type-meta) | | [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`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), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) | | [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`api-remotes`](../packages/api/remotes), [`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), [`invariants`](../packages/support/invariants), [`plan-mode`](../packages/plan/plan-mode) | From 238e900776ff0743f0a0b6a83414c526e46e844a Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Wed, 12 Aug 2026 08:43:25 -0700 Subject: [PATCH 023/160] ci: retrigger after stale merge ref From 3ef05f8a8524dc1dc754f4d0bd7d343f520c2d27 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 12 Aug 2026 23:51:46 +0800 Subject: [PATCH 024/160] review: address ds-review-bot findings on composer geometry - Rewrite the 08-04 gutter-reservation note in place: the reservation is Chat's alone and the overlay branch points to the seat-width compensation note; the Testing section now describes the uncompensated control. - Cross-reference the earlier rejection of the seat inset in the 08-12 note and record the engine-resolved-vs-fixed-8px drift as accepted residual cost instead of asserting Firefox resolves 8px. - Pair --dsh-scrollbar-width with the mirrored ::-webkit-scrollbar rule and the compensation consumer in the scrollbar-styles gate; document the variable in the ui-theme README. - Rename the e2e control to compareTabsWithoutCompensation and correct the vacuity-guard comment; rewrap the WorkspaceBrowser doc comment. --- ...-composer-tab-gutter-reservation.i18n.yaml | 4 +- ...6-08-04-composer-tab-gutter-reservation.md | 16 ++--- ...8-04-composer-tab-gutter-reservation.zh.md | 16 ++--- ...-overlay-seat-width-compensation.i18n.yaml | 4 +- ...omposer-overlay-seat-width-compensation.md | 7 +- ...oser-overlay-seat-width-compensation.zh.md | 7 +- apps/web/tests/composer-tab-geometry.e2e.ts | 28 ++++---- .../skeleton/ConversationRoot.module.css | 6 +- packages/client/ui-theme/README.i18n.yaml | 4 +- packages/client/ui-theme/README.md | 2 +- packages/client/ui-theme/README.zh.md | 2 +- .../tests/scrollbar-styles.client.spec.ts | 70 +++++++++++++++++-- .../src/client/WorkspaceBrowser.tsx | 8 +-- 13 files changed, 120 insertions(+), 54 deletions(-) diff --git a/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.i18n.yaml index 0082a6f991..ccae7043a7 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md -2026-08-04-composer-tab-gutter-reservation.md: 2c2813371af073eee4bd80ce7813e77af3c0c319 -2026-08-04-composer-tab-gutter-reservation.zh.md: e450def2482218028875a48fce8842c88b5ba82e +2026-08-04-composer-tab-gutter-reservation.md: 8bd9fb2d86982d82b44a82c55b7303fcd9a5bf4d +2026-08-04-composer-tab-gutter-reservation.zh.md: 4b70aeb1d3777384907c345968971fcf75b3e74d diff --git a/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md b/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md index 2c2813371a..8bd9fb2d86 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md +++ b/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md @@ -2,8 +2,6 @@ Status: implemented -> Superseded for overlay views by [the seat-width compensation](2026-08-12-composer-overlay-seat-width-compensation.md): the overlay branch no longer reserves a gutter, and the seat compensates for the bar instead. Chat keeps the reservation described here. - English | [中文](2026-08-04-composer-tab-gutter-reservation.zh.md) ## Problem @@ -16,13 +14,11 @@ So for as long as the transcript overflowed — the ordinary state of any sessio ## Decision -`.scrollBody` declares `scrollbar-gutter: stable` unconditionally, and the overlay branch declares the same box a scroll container on both axes — `overflow-x: hidden; overflow-y: auto` — instead of `overflow: hidden`. +`.scrollBody` declares `scrollbar-gutter: stable` for the Chat state, and the overlay branch overrides it with `scrollbar-gutter: auto` while staying a scroll container on both axes — `overflow-x: hidden; overflow-y: auto`. The reservation is Chat's alone: it holds the seat's content box at the same width whether or not the transcript overflows, so the card never jumps as a growing transcript starts to scroll, nor between the hero phase and the first scrolling turn. The overlay branch reserves nothing — the view owns its own scrollers, so a gutter there would only narrow the view's content — and its seat compensates for the bar instead ([the seat-width compensation](2026-08-12-composer-overlay-seat-width-compensation.md)). -The two halves are one change. The reservation is what makes both states measure against the same width; declaring the overlay branch a scroll container is what makes the reservation reach it. `stable` rather than `auto` because `auto` reserves only while the box actually overflows, and the difference between overflowing and not is precisely the difference between the two tabs — an `auto` gutter would state the bug rather than fix it. +`stable` rather than `auto` because `auto` reserves only while the box actually overflows, and the difference between overflowing and not is precisely the difference between Chat's two phases — an `auto` gutter would state the bug rather than fix it. -The overlay state is a scroll container that nothing scrolls: the view fills it (`flex: 1 1 0` with its own clip) and the seat is out of flow, so no gesture and no clipping behavior changes. What changes is which declarations the engine honours. WebKit applies `scrollbar-gutter` to an `overflow-y: auto` box and ignores it on a hidden one — measured on this app's own composer layers and recorded in [the composer scrollport note](2026-07-31-composer-text-layers-share-one-scrollport.md) — so a reservation left on a hidden box would hold in Chromium and silently not in Safari. - -The horizontal axis is declared rather than left to compute: a box that scrolls on one axis computes `visible` on the other to `auto`, and would grow a horizontal scrollbar of its own the first time a view's content reached past the column. +The reservation lives on an `overflow-y: auto` box, and that form is load-bearing: WebKit applies `scrollbar-gutter` to an `overflow-y: auto` box and ignores it on a hidden one — measured on this app's own composer layers and recorded in [the composer scrollport note](2026-07-31-composer-text-layers-share-one-scrollport.md) — so a reservation on a hidden box would hold in Chromium and silently not in Safari. The overlay branch keeps its `overflow-y: auto` form too, as a clipping box nothing scrolls out of: a single-axis scroller computes the other axis to `auto`, so the horizontal axis is declared `hidden` rather than left to compute, and would otherwise grow a horizontal scrollbar of its own the first time a view's content reached past the column. The reservation is worth what it costs only because the bar takes layout space here at all, which is not the browser's default behavior but this client's: `::-webkit-scrollbar` carries a width in ui-theme's sheet ([themed scrollbars](2026-07-28-themed-scrollbars-and-reserved-gutter.md)), and the sidebar's session list already reserves its own gutter for the same reason. @@ -39,7 +35,7 @@ The reservation is worth what it costs only because the bar takes layout space h ## Consequences - Chat's content column is permanently 8px narrower — in the hero phase and while the transcript is short as well, where no bar is drawn. That is the trade: one card position at every content height, instead of the widest possible column. -- The fix covers three transitions with one declaration, because all three are the same difference: Chat ↔ Trajectory, short ↔ scrolling transcript within Chat, and hero ↔ first scrolling turn. +- The card holds one position across three transitions, by two mechanisms: the reservation keeps Chat's seat at one width across its own phases (short ↔ scrolling transcript, hero ↔ first scrolling turn), and the overlay seat's compensation matches it on the Chat ↔ Trajectory transition ([the seat-width compensation](2026-08-12-composer-overlay-seat-width-compensation.md)). - The overlay state is now a scroll container. Nothing in it can overflow today; a future view that let its content exceed the column would scroll this box instead of clipping, and would need its own clip the way the Trajectory view already has one. - The committed golden records the reserved band, so a change to the sheet's `::-webkit-scrollbar` width — the value that decides how wide the reservation is — arrives as a reviewable diff in this scenario as well as in the sidebar's. @@ -47,6 +43,6 @@ The reservation is worth what it costs only because the bar takes layout space h `apps/web/tests/composer-tab-geometry.e2e.ts` measures the input card's rectangle in both tabs, at a viewport where the card sits at its width cap and one where it shrinks with the column, and asserts the two rectangles are the same rectangle. Only a real engine reports this: jsdom gives every element a zero-sized box and no scrollbar, so a unit spec could assert the declarations exist but not that the two states land in the same place. For the same reason no CSS-text spec accompanies it — it would restate the declarations without adding a fact the browser lane does not already establish. -The scenario launches chromium without Playwright's default `--hide-scrollbars`, which is load-bearing: under that argument a bar consumes no layout width, both tabs agree before this change as much as after it, and every comparison in the file holds vacuously. Measured, the pre-fix cascade leaves both bands at 0 under the argument, and at 8 and 0 with it dropped. +The scenario launches chromium without Playwright's default `--hide-scrollbars`, which is load-bearing: under that argument a bar consumes no layout width, so the tabs agree with and without the compensation and every comparison in the file holds vacuously. Measured, both bands sit at 0 under the argument and at 8 and 0 with it dropped. -The pre-fix cascade is then applied in the page — `scrollbar-gutter: auto` on the scroller, `overflow: hidden` on the overlay branch — and the same two tabs measured through it, which is what separates a card that does not move from a tab switch that never reached the layout. It reproduces the reported symptom as a number: 4px on each edge, half the 8px band. The golden records that control beside the fixed state, so the fixture carries the difference the change removes rather than only its absence. +The uncompensated cascade is then applied in the page — the overlay seat's `right` compensation dropped to 0 via `!important`, Chat's reservation untouched — and the same two tabs measured through it, which is what separates a card that does not move from a tab switch that never reached the layout. It reproduces the reported symptom as a number: 4px on each edge, half the 8px band. The golden records that control beside the fixed state, so the fixture carries the difference the change removes rather than only its absence. diff --git a/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.zh.md b/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.zh.md index e450def248..4b70aeb1d3 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.zh.md @@ -2,8 +2,6 @@ Status: implemented -> 对覆盖视图已被 [座位宽度补偿](2026-08-12-composer-overlay-seat-width-compensation.md) 取代:覆盖分支不再预留滚动条槽,座位改为补偿滚动条宽度。Chat 仍保留本文所述的预留。 - [English](2026-08-04-composer-tab-gutter-reservation.md) | 中文 ## 问题 @@ -16,13 +14,11 @@ composer 座位在组件树中只有一个节点、一个位置,但它究竟 ## 决策 -`.scrollBody` 无条件声明 `scrollbar-gutter: stable`,overlay 分支则把同一个盒子在两个轴向上都声明为滚动容器——`overflow-x: hidden; overflow-y: auto`——而不再是 `overflow: hidden`。 +`.scrollBody` 为 Chat 状态声明 `scrollbar-gutter: stable`,覆盖分支则将其覆盖为 `scrollbar-gutter: auto`,同时保持为双轴滚动容器——`overflow-x: hidden; overflow-y: auto`。这条预留只属于 Chat:它让座位的内容盒在 transcript 是否溢出时都保持同一宽度,因此卡片不会在 transcript 增长到开始滚动的那一刻跳动,也不会在 hero 态与第一个可滚动轮次之间跳动。覆盖分支不预留任何槽位——视图自己滚动,槽位只会白白收窄视图内容——它的座位改为补偿滚动条宽度([座位宽度补偿](2026-08-12-composer-overlay-seat-width-compensation.md))。 -这两半是同一处改动。预留使两种状态依附于同一个宽度;把 overlay 分支声明为滚动容器,才使这条预留真正抵达它。选 `stable` 而非 `auto`,是因为 `auto` 只在盒子确实溢出时才预留,而「溢出与否」恰恰就是两个标签页之间的那点差别——`auto` 的写法只是把缺陷重述一遍,并不能修掉它。 +选 `stable` 而非 `auto`,是因为 `auto` 只在盒子确实溢出时才预留,而「溢出与否」恰恰就是 Chat 两种相位之间的那点差别——`auto` 的写法只是把缺陷重述一遍,并不能修掉它。 -overlay 状态是一个没有任何东西会去滚动它的滚动容器:视图把它填满(`flex: 1 1 0`,且自带裁剪),座位不在常规流中,因此没有任何手势与裁剪行为发生变化。变化的是引擎会认哪些声明。WebKit 对 `overflow-y: auto` 的盒子应用 `scrollbar-gutter`,对 hidden 的盒子则忽略它——这是在本应用 composer 自身的图层上实测所得,并记录于 [composer 滚动视口记录](2026-07-31-composer-text-layers-share-one-scrollport.md)——所以把预留留在一个 hidden 盒子上,会在 Chromium 上成立,在 Safari 上悄无声息地不成立。 - -横向轴是显式声明的,而不是交给推导:单轴滚动的盒子会把另一轴的 `visible` 计算为 `auto`,于是只要某个视图的内容第一次伸出列外,它就会长出自己的横向滚动条。 +这条预留位于 `overflow-y: auto` 的盒子上,而这个形式是承重的:WebKit 对 `overflow-y: auto` 的盒子应用 `scrollbar-gutter`,对 hidden 的盒子则忽略它——这是在本应用 composer 自身的图层上实测所得,并记录于 [composer 滚动视口记录](2026-07-31-composer-text-layers-share-one-scrollport.md)——所以把预留放在 hidden 盒子上,会在 Chromium 上成立,在 Safari 上悄无声息地不成立。覆盖分支同样保留 `overflow-y: auto` 的形式,作为没有任何内容会滚出去的裁剪盒:单轴滚动的盒子会把另一轴的 `visible` 计算为 `auto`,因此横向轴显式声明为 `hidden` 而不是交给推导,否则某个视图的内容第一次伸出列外时,它就会长出自己的横向滚动条。 这条预留之所以值回它的代价,前提是滚动条在这里确实占布局空间——这并非浏览器的默认行为,而是本客户端的选择:ui-theme 的样式表给 `::-webkit-scrollbar` 声明了宽度([滚动条主题化](2026-07-28-themed-scrollbars-and-reserved-gutter.md)),侧边栏的会话列表也正是出于同一原因预留了自己的滚动条槽。 @@ -39,7 +35,7 @@ overlay 状态是一个没有任何东西会去滚动它的滚动容器:视图 ## 后果 - Chat 的内容列永久变窄 8px——hero 态与 transcript 尚短、根本不绘制滚动条时同样如此。这就是这笔交易:以最宽的列换取卡片在任何内容高度下都只有一个位置。 -- 一条声明覆盖三种切换,因为这三者本就是同一个差异:Chat ↔ Trajectory、Chat 内部 transcript 较短 ↔ transcript 可滚动,以及 hero ↔ 第一个可滚动轮次。 +- 卡片在三种切换下保持同一位置,由两种机制达成:预留让 Chat 的座位在自身各相位间保持同一宽度(transcript 较短 ↔ 可滚动、hero ↔ 第一个可滚动轮次),Chat ↔ Trajectory 的切换则由覆盖座位的补偿来对齐([座位宽度补偿](2026-08-12-composer-overlay-seat-width-compensation.md))。 - overlay 状态现在是一个滚动容器。今天其中没有任何内容会溢出;将来若有视图允许自身内容超出会话列,这个盒子会滚动而不是裁剪,那个视图就需要像 Trajectory 视图那样自带裁剪。 - 提交的 golden 记录了预留条带,因此样式表中 `::-webkit-scrollbar` 宽度的变化——决定这条预留有多宽的那个值——会在本场景中与在侧边栏场景中一样,以可评审的 diff 形式出现。 @@ -47,6 +43,6 @@ overlay 状态是一个没有任何东西会去滚动它的滚动容器:视图 `apps/web/tests/composer-tab-geometry.e2e.ts` 在两个标签页下测量输入卡片的矩形,分别取卡片处于宽度上限的视口与卡片随列收缩的视口,并断言这两个矩形是同一个矩形。只有真实引擎能报告这件事:jsdom 给每个元素的盒子尺寸都是零,也没有滚动条,因此单元测试只能断言那些声明存在,无法断言两种状态落在同一位置。出于同一原因,本次没有附带读取 CSS 文本的单元测试——它只会把声明复述一遍,并不会补上浏览器车道尚未确立的事实。 -该场景启动 chromium 时去掉了 Playwright 默认的 `--hide-scrollbars`,这一点是承重的:带上该参数时滚动条不占任何布局宽度,两个标签页在改动前后同样一致,文件中的每一处比较都会空洞地通过。实测:带上该参数时,改动前的层叠让两条预留带的宽度都是 0;去掉它则分别是 8 与 0。 +该场景启动 chromium 时去掉了 Playwright 默认的 `--hide-scrollbars`,这一点是承重的:带上该参数时滚动条不占任何布局宽度,因此两个标签页在有补偿与无补偿时同样一致,文件中的每一处比较都会空洞地通过。实测:带上该参数时两条预留带的宽度都是 0;去掉它则分别是 8 与 0。 -随后,改动前的层叠会被注入页面——滚动容器上 `scrollbar-gutter: auto`,overlay 分支上 `overflow: hidden`——并在其下测量同样的两个标签页,这正是把「卡片确实没动」与「标签页切换根本没到达布局」区分开的那一步。它把上报的症状复现为一个数字:每条边 4px,恰是 8px 带宽的一半。golden 把这份对照与修复后的状态并排记录,因此 fixture(测试前置数据)承载的是这次改动所消除的那个差值,而不仅仅是它的缺席。 +随后,未补偿的级联会被注入页面——通过 `!important` 把覆盖座位的 `right` 补偿降为 0,Chat 的预留保持不变——并在其下测量同样的两个标签页,这正是把「卡片确实没动」与「标签页切换根本没到达布局」区分开的那一步。它把上报的症状复现为一个数字:每条边 4px,恰是 8px 带宽的一半。golden 把这份对照与修复后的状态并排记录,因此 fixture(测试前置数据)承载的是这次改动所消除的那个差值,而不仅仅是它的缺席。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.i18n.yaml index fd88ecbc93..a1904a4b03 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md -2026-08-12-composer-overlay-seat-width-compensation.md: 85281ed4301046672ff4c02320b196a397e6d163 -2026-08-12-composer-overlay-seat-width-compensation.zh.md: 7bcf060e651a828068ef207caaebd01fa2ffd7ae +2026-08-12-composer-overlay-seat-width-compensation.md: 0ec4d1272ac1adab5b724dccf44f567e15cc3368 +2026-08-12-composer-overlay-seat-width-compensation.zh.md: 2f66771240fe62e15c481342d55a82dd09cfb864 diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md b/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md index 85281ed430..0ec4d1272a 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md +++ b/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md @@ -14,7 +14,7 @@ The trajectory table made that cost visible: its full-width row divider lines st The reservation now belongs to Chat alone. The overlay branch declares `scrollbar-gutter: auto`, so the view's content spans the full column; the overlay composer seat (absolutely positioned against the padding box) gives back the bar's width with `right: var(--dsh-scrollbar-width)`, so the input card still measures the same width as Chat's seat and does not move between tabs. -The compensation value is not a literal: ui-theme's scrollbar.css defines `--dsh-scrollbar-width` (8px on the WebKit path) beside the `::-webkit-scrollbar` rule it mirrors, and the seat reads that variable. A change to the sheet's bar width reaches the compensation in the same reviewable diff as the bar itself. +The compensation value is not a literal: ui-theme's scrollbar.css defines `--dsh-scrollbar-width` (8px on the WebKit path) beside the `::-webkit-scrollbar` rule it mirrors, and the seat reads that variable. The scrollbar-styles spec pairs the variable with the mirrored rule and with the compensation consumer, so a change to the sheet's bar width without the variable — or to the variable without the consumer — fails the gate, not just review. ## Alternatives considered @@ -24,12 +24,15 @@ The compensation value is not a literal: ui-theme's scrollbar.css defines `--dsh **Accept the 4px card shift.** Dropping the reservation without compensating the seat would move the input card on every tab switch, which is exactly the symptom the earlier note fixed. Rejected: the card position is a deliberate cross-tab invariant. +**Inset the overlay seat by the bar's width.** The [gutter-reservation note](2026-08-04-composer-tab-gutter-reservation.md) rejected exactly this, and this note adopts it; what changed is the rejection's premise. The number was the engine's, not ours — the WebKit path draws the sheet's 8px bar while the Firefox path draws whatever `scrollbar-width: thin` resolves to — so a hardcoded inset would line the two states up in Chromium and drift elsewhere. The overlay branch reserved an engine-resolved gutter of its own back then, so an inset had to match that width exactly. Today the overlay branch reserves nothing, so the compensation is the overlay side's only mechanism, and the literal half of the rejection is answered by making the 8px a variable that mirrors the `::-webkit-scrollbar` rule in the same diff. The Firefox half remains: Chat reserves the engine-resolved width while the compensation stays fixed, and the residual drift where the two differ is recorded as an accepted cost in Consequences. + ## Consequences - Chat keeps its reserved gutter and its stable card position; nothing changes on that tab. - Overlay views (trajectory) span the full column; the trajectory ledger's divider lines reach the pane edge. - The input card still holds one horizontal position across the Chat and Trajectory tabs, now by two mechanisms instead of one: Chat reserves, the overlay seat compensates. -- `--dsh-scrollbar-width` becomes a public ui-theme variable read outside ui-theme; the scrollbar-styles spec's indirection checks only scan `--dsh-scrollbar-thumb{,-hover}` rebinds, so the width variable is not covered by the pair gate. +- Chat reserves the engine-resolved width while the overlay seat compensates a fixed 8px. Where the two differ — the Firefox path resolves `scrollbar-width: thin` per platform, and the e2e runs only on Chromium — the card drifts by half the difference on tab switch. Accepted residual cost, recorded here rather than asserted away: no measurement of the Firefox thin width on the target platforms exists in this change. +- `--dsh-scrollbar-width` becomes a public ui-theme variable read outside ui-theme; the scrollbar-styles spec pairs it with the mirrored `::-webkit-scrollbar` width rule and with the compensation consumer, closing the indirection-gate gap the variable would otherwise leave. ## Testing diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.zh.md b/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.zh.md index 7bcf060e65..2f66771240 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.zh.md @@ -14,7 +14,7 @@ trajectory 表格让这个代价显形:整行分隔线在面板右边缘前 8p 预留现在只属于 Chat。覆盖分支声明 `scrollbar-gutter: auto`,视图内容占满整列;覆盖分支的 composer 座位(相对 padding box 绝对定位)用 `right: var(--dsh-scrollbar-width)` 让出滚动条宽度,使输入卡仍与 Chat 座位测得相同宽度,切换标签页时不移动。 -补偿值不是字面量:ui-theme 的 scrollbar.css 在它镜像的 `::-webkit-scrollbar` 规则旁定义 `--dsh-scrollbar-width`(WebKit 路径 8px),座位读取该变量。样式表滚动条宽度一变,补偿就会与滚动条本身出现在同一次可审阅的 diff 中。 +补偿值不是字面量:ui-theme 的 scrollbar.css 在它镜像的 `::-webkit-scrollbar` 规则旁定义 `--dsh-scrollbar-width`(WebKit 路径 8px),座位读取该变量。scrollbar-styles 规格把该变量与其镜像规则、以及补偿消费者配对检查,因此样式表滚动条宽度一变却不同步变量——或变量一变却不同步消费者——都会让门禁失败,而不只是评审时发现。 ## 备选方案 @@ -24,12 +24,15 @@ trajectory 表格让这个代价显形:整行分隔线在面板右边缘前 8p **接受 4px 卡片位移。** 去掉预留却不补偿座位,会在每次切换标签页时移动输入卡——正是前一份 note 修复的症状。已拒绝:卡片位置是刻意保持的跨标签页不变量。 +**把 overlay 座位按滚动条宽度内缩。** [滚动条槽预留 note](2026-08-04-composer-tab-gutter-reservation.md) 当初否决的正是这个方案,本 note 采纳了它;变的是否决的前提。这个数字属于引擎而不属于我们——WebKit 路径绘制样式表里的 8px 滚动条,Firefox 路径绘制 `scrollbar-width: thin` 解析出的宽度——因此硬编码的内缩会让两种状态在 Chromium 上对齐、在别处继续漂移。当初 overlay 分支自己预留的是引擎解析出的槽宽,内缩必须精确匹配那个宽度。如今 overlay 分支不预留任何槽位,补偿成为覆盖侧唯一的机制;否决的字面量那一半,通过把 8px 变成与 `::-webkit-scrollbar` 规则同处一个 diff 的变量来回应。Firefox 那一半仍然存在:Chat 预留引擎解析宽度,补偿保持固定 8px,两者不等之处的残余漂移作为接受的代价记录在后果中。 + ## 后果 - Chat 保留滚动条槽与稳定的卡片位置;该标签页无任何变化。 - 覆盖视图(trajectory)占满整列;trajectory 台账的分隔线到达面板右边缘。 - 输入卡在 Chat 与 Trajectory 标签页间仍保持同一水平位置,现在由两种机制而非一种达成:Chat 预留,覆盖座位补偿。 -- `--dsh-scrollbar-width` 成为 ui-theme 对外、且被 ui-theme 之外读取的变量;scrollbar-styles 规格的间接层检查只扫描 `--dsh-scrollbar-thumb{,-hover}` 重绑,宽度变量不受成对门禁覆盖。 +- Chat 预留引擎解析宽度,覆盖座位补偿固定的 8px。两者不等之处——Firefox 路径按平台解析 `scrollbar-width: thin`,而 e2e 只在 Chromium 上运行——卡片在切换标签页时会漂移半个差值。这是接受的残余代价,如实记录于此而不断言消除:本次改动并未提供目标平台 Firefox thin 宽度的实测。 +- `--dsh-scrollbar-width` 成为 ui-theme 对外、且被 ui-theme 之外读取的变量;scrollbar-styles 规格把它与镜像的 `::-webkit-scrollbar` 宽度规则、以及补偿消费者配对检查,补上了该变量本会留下的间接层门禁缺口。 ## 测试 diff --git a/apps/web/tests/composer-tab-geometry.e2e.ts b/apps/web/tests/composer-tab-geometry.e2e.ts index b491b1257c..0f7b01c7ff 100644 --- a/apps/web/tests/composer-tab-geometry.e2e.ts +++ b/apps/web/tests/composer-tab-geometry.e2e.ts @@ -28,13 +28,13 @@ // The browser is launched WITHOUT Playwright's default `--hide-scrollbars`, // which is load-bearing rather than incidental. Under that argument a scroll // container's bar consumes no layout width at all, so the two tabs agree with -// and without the reservation and every comparison below holds vacuously — -// measured: the unreserved cascade leaves both tabs' bands at 0 there, against -// 8 and 0 with the argument dropped. Dropping it is also the faithful +// and without the compensation and every comparison below holds vacuously — +// measured: the uncompensated cascade leaves both tabs' bands at 0 there, +// against 8 and 0 with the argument dropped. Dropping it is also the faithful // configuration: ui-theme's scrollbar.css gives `::-webkit-scrollbar` a width, // and a bar that occupies layout space is what the product actually draws. // -// The scenario runs that unreserved cascade in the page — the overlay seat's +// The scenario runs that uncompensated cascade in the page — the overlay seat's // `right` compensation dropped to 0 — and measures the same two tabs through // it, which is what keeps the equal rectangles above from being explained by a // tab switch that never reached the layout. It is the reported symptom as a @@ -227,7 +227,7 @@ async function compareTabs(page: Page): Promise { * @param page - the page under test. * @returns the comparison as the column lays out without the compensation. */ -async function compareTabsWithoutReservation(page: Page): Promise { +async function compareTabsWithoutCompensation(page: Page): Promise { await page.evaluate(({ id, css }) => { const style = document.createElement('style') style.id = id @@ -327,9 +327,13 @@ describe('web e2e: input card position across view tabs', () => { it('reserves the gutter in Chat and lets Trajectory own its width', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-composer-tab-geometry-band')) await setMeasuredViewport(page, WIDE_VIEWPORT, false) - // Vacuity guard, in two parts. A transcript that does not overflow gives - // Chat no scrollbar, and a hidden or overlaid bar gives it no width; either - // would make the tabs agree without the compensation doing anything. + // Vacuity guard. The scenario must be able to fail: on an engine that + // does not implement `scrollbar-gutter`, Chat reserves nothing and the + // overlay seat's fixed compensation stands alone, manufacturing an 8px + // deviation the equal-rectangle assertions would catch. `stable` reserves + // even without overflow, so a short transcript is not a vacuous case; the + // poll still pins the measurement to the overflowing state the product + // ships. await expect.poll(async () => (await measureTab(page)).scrolls, { timeout: 10_000 }).toBe(true) const comparison = await compareTabs(page) expect(comparison.chat.band).toBeGreaterThan(0) @@ -356,7 +360,7 @@ describe('web e2e: input card position across view tabs', () => { await setMeasuredViewport(page, WIDE_VIEWPORT, false) const comparison = await compareTabs(page) // The reported symptom as a number. At this viewport the card sits at its - // width cap, so the unreserved cascade's shift shows up as a centring + // width cap, so the uncompensated cascade's shift shows up as a centring // difference — half the band on each edge — rather than as a width change. expect(comparison.leftShift).toBe(0) expect(comparison.rightShift).toBe(0) @@ -387,11 +391,11 @@ describe('web e2e: input card position across view tabs', () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-composer-tab-geometry-control')) await setMeasuredViewport(page, WIDE_VIEWPORT, false) // The control: without it, equal rectangles could also mean the tab switch - // never reached the layout. Under the unreserved cascade the overlay seat + // never reached the layout. Under the uncompensated cascade the overlay seat // loses its `right` compensation and measures the full padding box, so the // card moves by half the band on each edge. Chat's own reservation is // untouched — that is the side that must not change. - const comparison = await compareTabsWithoutReservation(page) + const comparison = await compareTabsWithoutCompensation(page) expect(comparison.chat.gutter).toBe('stable') expect(comparison.chat.band).toBeGreaterThan(0) expect(comparison.trajectory.band).toBe(0) @@ -411,7 +415,7 @@ describe('web e2e: input card position across view tabs', () => { await setMeasuredViewport(page, NARROW_VIEWPORT, true) const narrow = await compareTabs(page) await setMeasuredViewport(page, WIDE_VIEWPORT, false) - const control = await compareTabsWithoutReservation(page) + const control = await compareTabsWithoutCompensation(page) await compareOrRefreshGolden(GEOMETRY_EXPECTED, renderGeometry(wide, narrow, control), MODE) expect(tripwire.pageErrors).toEqual([]) }, 60_000) diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css index c6618fc624..20ade47c8a 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css @@ -297,8 +297,10 @@ by the bar's width; the overlay seat is against the padding box, so it must give back the same width to keep the input card in place across tabs. Reads ui-theme's --dsh-scrollbar-width, which mirrors the WebKit - bar's layout width; Firefox's thin bar resolves to the same 8px on the - platforms this app targets, so the compensation holds there too. */ + bar's layout width. Chat reserves the engine-resolved width, so on an + engine where the resolved width differs from this fixed 8px the card + drifts by half the difference — accepted residual risk, recorded in + ([decision](../../../../../../.agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md)). */ right: var(--dsh-scrollbar-width); bottom: 0; left: 0; diff --git a/packages/client/ui-theme/README.i18n.yaml b/packages/client/ui-theme/README.i18n.yaml index e2bdc28ecb..00c049e8df 100644 --- a/packages/client/ui-theme/README.i18n.yaml +++ b/packages/client/ui-theme/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-theme/README.md -README.md: df4d5e0370962bf6f2a8ac0a7b88d669225dc5c5 -README.zh.md: fb8e937979a5168803be78a49e1bc1e7eacd7a47 +README.md: c24f2ae5692fdd05134ce780d21569b87570d1c7 +README.zh.md: 518b8930027c88bd85139a8a7325cea1fd40b40c diff --git a/packages/client/ui-theme/README.md b/packages/client/ui-theme/README.md index df4d5e0370..c24f2ae569 100644 --- a/packages/client/ui-theme/README.md +++ b/packages/client/ui-theme/README.md @@ -8,7 +8,7 @@ When the host composition includes an HTTP server, the host half injects a synch `src/styles/` holds five sheets, all imported by the web shell's `base.css`: `base.css`, `design-platform.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. `scrollbar.css` is the sole consumer of the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them. -Scrollbar rebinding contract: `scrollbar.css` binds `--dsh-scrollbar-thumb` and `--dsh-scrollbar-thumb-hover` on `body` to the l1 (base-surface) tokens, and both rendering paths read that pair. An elevated surface (menu, popover, dialog) sets `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` and `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)` on its own container; one rebind retints whichever path the engine took. The pair's other legal target is `transparent`, which draws no thumb at all — [ui-sidebar](../ui-sidebar/README.md) rebinds its column that way while the pointer is elsewhere. A rebind to the l1 pair is not a rebind; it restates the base-surface default. +Scrollbar rebinding contract: `scrollbar.css` binds `--dsh-scrollbar-thumb` and `--dsh-scrollbar-thumb-hover` on `body` to the l1 (base-surface) tokens, and both rendering paths read that pair. An elevated surface (menu, popover, dialog) sets `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` and `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)` on its own container; one rebind retints whichever path the engine took. The pair's other legal target is `transparent`, which draws no thumb at all — [ui-sidebar](../ui-sidebar/README.md) rebinds its column that way while the pointer is elsewhere. A rebind to the l1 pair is not a rebind; it restates the base-surface default. `--dsh-scrollbar-width` mirrors the WebKit bar's layout width for surfaces that align themselves beside a space-consuming bar — [ui-conversation](../ui-conversation/README.md) reads it for the overlay composer seat's `right` offset — and the scrollbar-styles spec pairs it with the mirrored rule and the consumer. The two paths are mutually exclusive by construction. `scrollbar-width`/`scrollbar-color` sit inside `@supports not selector(::-webkit-scrollbar)` because a non-`auto` value of either makes Chromium and Safari discard every `::-webkit-scrollbar*` rule for that element, `::-webkit-scrollbar-thumb:hover` included — declaring both unconditionally leaves `--dsh-scrollbar-thumb-hover` with no rendering anywhere. Firefox therefore takes the standard properties and WebKit-based engines take the pseudo-elements, so the hover token only ever renders through the pseudo-element path. Reasoning and the measured computed values: [the scrollbar Agent Note](../../../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md). diff --git a/packages/client/ui-theme/README.zh.md b/packages/client/ui-theme/README.zh.md index fb8e937979..518b893002 100644 --- a/packages/client/ui-theme/README.zh.md +++ b/packages/client/ui-theme/README.zh.md @@ -8,7 +8,7 @@ `src/styles/` 下有五张样式表,全部由 web 壳的 `base.css` 导入:`base.css`、`design-platform.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。`scrollbar.css` 是 `--dsw-alias-scrollbar-*` token 的唯一消费方,必须排在声明这些 token 的 `design-platform.css` 之后。 -滚动条重新绑定约定:`scrollbar.css` 在 `body` 上把 `--dsh-scrollbar-thumb` 与 `--dsh-scrollbar-thumb-hover` 绑定到 l1(基础表面)token,两条渲染路径都读取这一组变量。高层级表面(菜单、浮层、对话框)在自己的容器上设置 `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` 与 `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)`;一次重新绑定即可为引擎实际走的那条路径换色。这组变量的另一个合法目标是 `transparent`,即完全不绘制滑块——[ui-sidebar](../ui-sidebar/README.md) 在指针不在栏内时就这样重新绑定自己的列。绑回 l1 那组不算重新绑定,它只是重述基础表面的默认值。 +滚动条重新绑定约定:`scrollbar.css` 在 `body` 上把 `--dsh-scrollbar-thumb` 与 `--dsh-scrollbar-thumb-hover` 绑定到 l1(基础表面)token,两条渲染路径都读取这一组变量。高层级表面(菜单、浮层、对话框)在自己的容器上设置 `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` 与 `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)`;一次重新绑定即可为引擎实际走的那条路径换色。这组变量的另一个合法目标是 `transparent`,即完全不绘制滑块——[ui-sidebar](../ui-sidebar/README.md) 在指针不在栏内时就这样重新绑定自己的列。绑回 l1 那组不算重新绑定,它只是重述基础表面的默认值。`--dsh-scrollbar-width` 镜像 WebKit 滚动条的布局宽度,供需要与占布局宽度的滚动条对齐的表面使用——[ui-conversation](../ui-conversation/README.md) 用它作为覆盖 composer 座位 `right` 偏移——scrollbar-styles 规格把它与镜像规则及消费者配对检查。 两条路径在构造上互斥。`scrollbar-width`/`scrollbar-color` 写在 `@supports not selector(::-webkit-scrollbar)` 之内,因为这两个属性中的任一个只要取非 `auto` 值,Chromium 与 Safari 就会丢弃该元素上的全部 `::-webkit-scrollbar*` 规则,`::-webkit-scrollbar-thumb:hover` 也在其中——若无条件地同时声明,`--dsh-scrollbar-thumb-hover` 在任何引擎上都不会被渲染。因此 Firefox 走标准属性,WebKit 系引擎走伪元素,hover token 只经由伪元素这条路径渲染。相关原理与实测计算值见[滚动条 Agent Note](../../../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md)。 diff --git a/packages/client/ui-theme/tests/scrollbar-styles.client.spec.ts b/packages/client/ui-theme/tests/scrollbar-styles.client.spec.ts index b6ed3f0c90..e9234846eb 100644 --- a/packages/client/ui-theme/tests/scrollbar-styles.client.spec.ts +++ b/packages/client/ui-theme/tests/scrollbar-styles.client.spec.ts @@ -2,10 +2,11 @@ * Scrollbar stylesheet contract, asserted against the CSS text on disk: every * --dsw-alias-scrollbar-* token design-platform.css defines has a consumer, * scrollbar.css binds the base-surface pair through the rebindable - * indirection, and elevated surfaces rebind that indirection in complete - * pairs. The expected token set is scanned out of design-platform.css, so - * adding, renaming, or dropping a scrollbar token moves these assertions with - * it. + * indirection, the width variable mirrors the ::-webkit-scrollbar rule for + * consumers that align beside the bar, and elevated surfaces rebind that + * indirection in complete pairs. The expected token set is scanned out of + * design-platform.css, so adding, renaming, or dropping a scrollbar token + * moves these assertions with it. */ import { readdirSync, readFileSync } from 'node:fs' import { join } from 'node:path' @@ -341,6 +342,67 @@ describe('scrollbar.css base-surface binding', () => { }) }) +describe('scrollbar.css width variable', () => { + const WIDTH_VARIABLE = `${INDIRECTION_PREFIX}width` + + it('defines the width variable on body as a static length', () => { + // The overlay seat compensation reads a fixed number, not a second + // indirection: the mirror check below compares the WebKit rule against + // this value, so a var()-to-var() chain would compare one indirection to + // another instead of pinning the number. + const value = scrollbarRules + .filter(rule => rule.selectors.includes('body')) + .flatMap(rule => rule.declarations) + .findLast(([property]) => property === WIDTH_VARIABLE)?.[1] + expect(value, WIDTH_VARIABLE).toBeDefined() + expect(value, WIDTH_VARIABLE).toMatch(/^\d+(?:\.\d+)?px$/) + }) + + it('mirrors the ::-webkit-scrollbar width rule with the variable value', () => { + // The compensation stays aligned with the WebKit bar only while both read + // the same number. A change to one side without the other puts the overlay + // seat a band off from Chat on WebKit engines. + const variableValue = scrollbarRules + .filter(rule => rule.selectors.includes('body')) + .flatMap(rule => rule.declarations) + .findLast(([property]) => property === WIDTH_VARIABLE)?.[1] + const webkitWidth = scrollbarRules + .filter(rule => rule.selectors.includes('::-webkit-scrollbar')) + .flatMap(rule => rule.declarations) + .findLast(([property]) => property === 'width')?.[1] + expect(webkitWidth, '::-webkit-scrollbar width').toBeDefined() + expect(webkitWidth).toBe(variableValue) + }) + + it('every reader of the width variable outside ui-theme references a defined variable', () => { + // The consumer is ConversationRoot's overlay composer seat + // (`right: var(--dsh-scrollbar-width)`); a rename in scrollbar.css without + // the consumer, or a typo in the consumer, leaves the value + // guaranteed-invalid and the seat loses the band. The equal-rectangle e2e + // would catch it only on an engine that draws the bar, so the sheet + // contract states it here. + const defined = new Set( + scrollbarRules + .flatMap(rule => rule.declarations) + .filter(([property]) => property.startsWith(INDIRECTION_PREFIX)) + .map(([property]) => property), + ) + expect(defined).toContain(WIDTH_VARIABLE) + const readers: string[] = [] + for (const file of packageStylesheets()) { + if (file === fileURLToPath(new URL('scrollbar.css', STYLES))) continue + for (const rule of parseRules(readFileSync(file, 'utf8'))) { + for (const [property, value] of rule.declarations) { + for (const name of varReferences(value)) { + if (name === WIDTH_VARIABLE) readers.push(`${file} ${rule.selectors.join(', ')}: ${property}`) + } + } + } + } + expect(readers.length, 'compensation consumer').toBeGreaterThan(0) + }) +}) + describe('scrollbar.css selectors', () => { const scrollbarColorSelectors = scrollbarRules .filter(rule => rule.declarations.some(([property]) => property === 'scrollbar-color')) diff --git a/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx b/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx index ebd0e11a33..f98950ed70 100644 --- a/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx +++ b/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx @@ -4,10 +4,10 @@ * workspace), search, the grouped tree or flat list, and the workspace * dialogs. Wide state renders the full browser; rail state renders the two * region icons (search / add workspace) as 36px controls on the shell's shared - * rail entry path, each requesting expansion through the owner share. Adding is the header - * button's one action, so it raises the directory flow with no menu in - * between; the flow and its error dialog live in WorkspacePicker (same - * package — direct composition, no slot between them). + * rail entry path, each requesting expansion through the owner share. Adding + * is the header button's one action, so it raises the directory flow with no + * menu in between; the flow and its error dialog live in WorkspacePicker + * (same package — direct composition, no slot between them). */ import { useEffect, useMemo, useRef, useState } from 'react' import clsx from 'clsx' From 7e4b8b1676588e4415e4490a44cab03cc76e501a Mon Sep 17 00:00:00 2001 From: j-xiang Date: Thu, 13 Aug 2026 00:12:44 +0800 Subject: [PATCH 025/160] docs(i18n): human-polish key Chinese READMEs --- README.i18n.yaml | 2 +- README.zh.md | 3 +- apps/cli/README.i18n.yaml | 2 +- apps/cli/README.zh.md | 24 ++++++------ apps/cli/reference/README.i18n.yaml | 2 +- apps/cli/reference/README.zh.md | 25 ++++++------ packages/core/tools/README.i18n.yaml | 2 +- packages/core/tools/README.zh.md | 38 +++++++++---------- python/sdk/README.i18n.yaml | 2 +- python/sdk/README.zh.md | 16 ++++---- .../request-response.expected.json | 2 +- 11 files changed, 58 insertions(+), 60 deletions(-) diff --git a/README.i18n.yaml b/README.i18n.yaml index 816b9ec26c..6a566a9abb 100644 --- a/README.i18n.yaml +++ b/README.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write README.md README.md: 05eee108fe57f75671a74cec5f3858b1abdfcef2 -README.zh.md: 1a25075e63f13a1f92c678b88b45e2ecca4c4c77 +README.zh.md: a27d995c2c68525c10d287fb74474bbef17a3f57 diff --git a/README.zh.md b/README.zh.md index 1a25075e63..a27d995c2c 100644 --- a/README.zh.md +++ b/README.zh.md @@ -10,8 +10,7 @@ DeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 DeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。 -Session Log 默认留在本地。设置 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 可仅在提交反馈时共享 Session Log,设置 `DSH_TELEMETRY_MODE=FULL` 可持续上传;`FULL` 同时会启用 dsh-sdk 命令遥测,上报匿名 ID、命令结果以及脱敏后的项目配置。请通过内部企业微信群反馈问题和建议。 - +会话日志默认保存在本地。设置 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 后,仅在提交反馈时共享对应的会话日志;设置 `DSH_TELEMETRY_MODE=FULL` 后,会持续上传会话日志。`FULL` 还会启用 dsh-sdk 命令遥测,上报匿名 ID、命令执行结果和脱敏后的项目配置。请通过内部企业微信群反馈问题和建议。 ## 运行 diff --git a/apps/cli/README.i18n.yaml b/apps/cli/README.i18n.yaml index fa2fcf2bd4..ac025294ca 100644 --- a/apps/cli/README.i18n.yaml +++ b/apps/cli/README.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/README.md README.md: d36183c545475020207bbf23e58d3c98b07ac6a6 -README.zh.md: 4693339e836d81c0b069704fb43451fb22accfdb +README.zh.md: 9e5507ad767640b413d00cf16f1becc0f540be2b diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md index 4693339e83..9e5507ad76 100644 --- a/apps/cli/README.zh.md +++ b/apps/cli/README.zh.md @@ -2,22 +2,22 @@ [English](README.md) | 中文 -`dsh` 命令是 profile 的产品启动器:profile 是按序叠放的插件组合包 patch 层,之上再叠加用户自己的覆盖层。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。 +`dsh` 是 DeepSeek Harness 中用于启动 profile 的命令;profile 由多个插件组合包 patch 层按顺序叠加而成,其上再应用用户自己的覆盖配置。[`src/args.ts`](src/args.ts) 负责命令语法,[`src/bin.ts`](src/bin.ts) 只加载选中的运行器。无效命令、来自其他模式的选项、配置错误和启动失败都会以非零状态退出。 ## 入口模式 | 命令 | 用途 | |---|---| | `dsh --profile ` | 启动位于 `$DSH_HOME/profiles/` 的指定 profile。 | -| `dsh --profile headless "task"` | 运行一个新的持久化会话,打印最终答案并退出。 | +| `dsh --profile headless "task"` | 运行一个全新的持久化会话,打印最终答案并退出。 | | `dsh web` | `--profile web` 的别名。 | | `dsh plugin --profile ` | 通过在 profile 目录中转发给 pnpm 来管理该 profile 的插件。 | -调用目录是默认 workspace 根目录。`web` 和 `headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。 +运行命令时所在的目录将作为默认 workspace 根目录。`web` 和 `headless` profile 在首次使用时会从随附模板自动初始化;其他任何 profile 都必须通过 `dsh plugin` 创建。 ## 应用参数 -启动器只解析属于自己的 flag,并把其后的一切交给启动起来的 profile,任何注入它的应用插件都可以解析这份共享的不可变快照([`dsh-cmdline`](../../packages/boot/cmdline/README.md))。因此启动器的 flag 必须写在前面,而启动器不认识的第一个 token 就是应用参数的起点: +启动器只解析自身的 flag,并将其后的所有内容交给已启动的 profile;注入该 profile 的任意应用插件都可以解析这份共享的不可变快照([`dsh-cmdline`](../../packages/boot/cmdline/README.md))。因此,启动器的 flag 必须写在最前面;启动器无法识别的第一个 token 标志着应用参数的开始: ```sh dsh --profile web --port 8080 # --port belongs to the web app @@ -29,19 +29,19 @@ dsh --help # the launcher's own help ## Profile -profile 目录包含一个 `package.json`(树外插件依赖,加上 profile manifest(元数据清单)`dsh.profile` 及其有序的 `bundles` 列表)和一个 `cordis.patch.yml`(用户自己的 patch 层)。 +profile 目录包含一个 `package.json`,其中记录另行安装的插件依赖,以及 profile manifest(元数据清单)`dsh.profile` 和其中按顺序排列的 `bundles` 列表;还包含一个 `cordis.patch.yml`,其中保存用户自己的 patch 层。 -配置树在空根之上组合: -- 先按 `dsh.profile.bundles` 顺序应用各组合包的 patch -- 然后是 profile 的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml` -- 最后是 `--patch` overlay +配置树以空根为起点,依次叠加以下配置层: +- `dsh.profile.bundles` 中各组合包的 patch +- profile 自身的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml` +- `--patch` 指定的覆盖层 -`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自己的 `node_modules` 解析;pnpm 把树外插件安装在后者。 +`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自身的 `node_modules` 解析;pnpm 会将另行安装的插件放入该目录。 使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。 -[CLI(命令行界面)行为参考](reference/README.md)负责确切的层优先级、flag、关闭行为、部署默认值和源码执行。 +层的确切优先级、flag、关闭行为、部署默认值和源码执行方式,以 [CLI(命令行界面)行为参考](reference/README.md)为准。 ## 开发 -生产运行需要已构建的包与前端产物。请在仓库根目录单独运行 `pnpm run build`,然后使用 `pnpm dsh ` 运行 TypeScript 入口并转发所有参数;模块解析约定由[源码执行参考](reference/README.md#source-execution)负责。 +生产运行需要已构建的包与前端产物。请在仓库根目录单独运行 `pnpm run build`,然后使用 `pnpm dsh ` 运行 TypeScript 入口并转发所有参数;模块解析约定以[源码执行参考](reference/README.md#source-execution)为准。 diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index c23be209db..ebbede237a 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/reference/README.md README.md: 17d63fe73ea2b3bda74e9c4da91555b34c21f4ab -README.zh.md: 452d024c1411c4c239b5fc54ec2586e1891e74f3 +README.zh.md: 83945a09073057b8c220da9fc1a6b2b84b98f599 diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index 452d024c14..83945a0907 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -2,25 +2,25 @@ [English](README.md) | 中文 -本参考定义 profile、web 别名、插件管理和配置 dump 命令模式。参数由 [`src/args.ts`](../src/args.ts) 统一解析,[`src/bin.ts`](../src/bin.ts) 只动态导入选中的运行器。 +本参考定义 profile 启动、web 别名、插件管理和配置 dump 等命令模式。argv 由 [`src/args.ts`](../src/args.ts) 统一解析一次,[`src/bin.ts`](../src/bin.ts) 只会动态导入选中的运行器。 ## Profile 启动 -`dsh --profile ` 启动位于 `$DSH_HOME/profiles/` 的 profile。生效配置树在空根节点之上按以下顺序逐层组合:profile manifest(元数据清单)的 `dsh.profile.bundles` 列表所列的各个组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(各 profile 共享的机器本地偏好,因此优先级高于逐 profile 的层)、以及按 argv 顺序的各个 `--patch ` overlay。后应用的层按行胜出;patch 替换目标行完整的 `config` 值,而不是深度合并各键,并且可以插入新行。配置解析、schema 校验、模块解析或插件启动失败会得到报告并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。 +`dsh --profile ` 启动位于 `$DSH_HOME/profiles/` 的 profile。生效配置树以空根节点为起点,依次叠加 profile manifest(元数据清单)的 `dsh.profile.bundles` 列表中指定的各组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(这是各 profile 共享的机器本地偏好,因此优先于逐 profile 配置层),以及按 argv 顺序指定的各个 `--patch ` 覆盖层。对同一配置行,后应用的层优先。patch 会替换目标行的整个 `config` 值,而不是深度合并其中的键;patch 也可以插入新行。配置解析、schema 校验、模块解析或插件启动失败时,系统会报告错误并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。 -组合包名称先从 dsh 安装解析,再从 profile 目录解析。因此内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`)总是来自与正在运行的 `dsh` 相同的安装;树外组合包来自 profile 由 pnpm 管理的 `node_modules`。任何 patch 行中的裸插件 `name` 通过 profile 目录的 Node 父目录逐级查找解析,该查找可达到持续维护的安装后备目录 `$DSH_HOME/profiles/node_modules`(安装的应用和组合包所依赖的每个包对应一个符号链接,每次启动时修复)。 +组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`)始终来自当前运行的 `dsh` 所属的安装;另行安装的组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。该目录为 dsh 安装中的应用和组合包所依赖的每个包各维护一个符号链接,并在每次启动时修复这些链接。 `web` 和 `headless` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app;`headless`:base + headless)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile add `。 ### 应用参数 -启动器自己的 flag 写在最前面,并在它不认识的第一个 token 处结束;从那里开始的一切都通过 `ctx.cmdlineArgs` 原样交给启动起来的 profile,任何注入它的应用插件都可以解析([`dsh-cmdline`](../../../packages/boot/cmdline/README.md))。因此 `dsh --profile web --port 8080` 到达的是 web 应用的 `--port`,`dsh --profile web --help` 打印的是该应用的 help 且什么也不启动,而 `dsh --help`(没有可以交付的 profile)打印的是启动器自己的 help。`-V`/`--version` 写在应用参数边界之前时会打印启动器的版本。 +启动器自身的 flag 必须写在最前面,并在遇到第一个无法识别的 token 时结束;从该 token 开始的所有内容都会通过 `ctx.cmdlineArgs` 原样交给已启动的 profile,注入该 profile 的任意应用插件都可以解析这些内容([`dsh-cmdline`](../../../packages/boot/cmdline/README.md))。因此,`dsh --profile web --port 8080` 会将 `--port` 交给 web 应用;`dsh --profile web --help` 只打印该应用的帮助信息,不启动应用;`dsh --help` 没有可供交付参数的 profile,因此会打印启动器自身的帮助信息。`-V`/`--version` 位于应用参数边界之前时,会打印启动器的版本。 -一套组合只挂载一次。普通插件注入 `cmdlineArgs`、解析本应用参数,并把结果作为服务提供出去;由 flag 配置的每一行都会注入该服务,Loader 会等服务激活后再求值该行配置(`port: !!js ctx.webStartup.port ?? 3080`),因此 flag 胜过写在它旁边的值。该优先级要求配置行保留这一表达式;若用户 patch 用字面量替换整份 `config`,运行时读取也会随之消失。help 和被拒绝的参数会请求退出——拒绝时以非零状态,help 时以 0——且不会激活依赖提供方服务的行。在线编辑 `cordis.patch.yml` 会针对仍然在线的服务重新求值表达式,因此不会重置已在服务的端口。 +每套组合只会挂载一次。普通插件注入 `cmdlineArgs`,解析所属应用的参数,并将解析结果作为服务提供。每个从 flag 取值的配置行都会注入该服务;Loader 会等到服务激活后,再对该行的配置求值(`port: !!js ctx.webStartup.port ?? 3080`),因此 flag 的优先级高于配置行中写明的值。要维持这一优先级,配置行必须保留该表达式;如果用户 patch 用字面量替换整个 `config`,也会随之移除运行时读取。帮助参数和被拒绝的参数都会请求退出:参数被拒绝时以非零状态退出,显示帮助时以 0 退出;依赖该提供方服务的配置行不会激活。在线编辑 `cordis.patch.yml` 时,系统会根据仍在运行的服务重新计算表达式,因此不会重置当前正在使用的端口。 启动器的 flag 必须写在应用参数之前,且启动器的解析器会消耗掉一个 `--`:必须以字面量 `--` 送达应用的参数需要写成 `-- --`。如果应用的第一个参数恰好等于 `web` 或 `plugin`,会选择对应的子命令。`ctx.cmdlineArgs.get()` 是共享的不可变读取:多个插件可以解析同一份快照,没有读取方的 profile 则会忽略自己的应用参数。 -随附的各应用持有这些命令行: +随附的应用接受以下命令行参数: | Profile | 参数 | |---|---| @@ -36,11 +36,11 @@ dsh --profile web --dump-default-config dsh --profile web --patch ./extra.yml --dump-config ``` -`--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml` 和 `--patch` overlay。两者都会打印注释,标明每行由哪个文件提供,以及哪些 overlay 修改过它;`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。dump 从不运行应用命令行提供方,因此它展示的是任何应用参数被解析之前的组合配置树,并拒绝携带应用参数的调用。 +`--dump-default-config` 只打印组合包各层;`--dump-config` 额外加上 profile 的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml` 和 `--patch` overlay。两者都会打印注释,标明每行由哪个文件提供,以及哪些 overlay 修改过它;`!!js` 表达式保持未求值,找不到目标的 patch 会报告到 stderr。dump 操作不会运行应用的命令行参数提供方,因此展示的是解析任何应用参数之前的组合配置树;如果调用中包含应用参数,dump 会拒绝该调用。 ## 插件管理 -`dsh plugin --profile ` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,`dsh.profile.bundles` 都会与已安装状态对齐:每个解析到 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的包的依赖加入层栈(因此让包获得该声明的 `update` 会将其激活),没有组合包声明的依赖保持为普通依赖并给出一次性警告,已移除的依赖则退出层栈。 +`dsh plugin --profile ` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,系统都会根据当前安装状态更新 `dsh.profile.bundles`:如果某项依赖解析到的包在 manifest(元数据清单)中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,该依赖就会加入配置层栈;如果某项依赖在 `update` 后获得该声明,也会随即激活。没有组合包声明的依赖仍作为普通依赖保留,并显示一次性警告;已移除的依赖则从配置层栈中删除。 ```sh dsh plugin --profile tui add github:deepseek-harness/turtle-ui @@ -48,7 +48,7 @@ dsh plugin --profile tui remove turtle-ui dsh --profile tui ``` -Git 托管、随附源码的插件在安装期间通过其 `prepare` 脚本构建,而 pnpm ≥10 在消费方允许之前会阻止该脚本:首次 `add` 会失败并给出 pnpm 的 `allowBuilds` 提示(以及 dsh 指向该 profile 的 `pnpm-workspace.yaml` 的指引);把打印出的键复制到那里并重新运行即可。安装已构建的 tarball 或本地 checkout 不需要任何允许。 +随源码发布的 Git 托管插件会在安装期间通过 `prepare` 脚本构建,而 pnpm ≥10 默认会阻止该脚本,直到使用方明确允许。首次运行 `add` 会失败,并显示 pnpm 的 `allowBuilds` 提示;dsh 还会提示应修改该 profile 的 `pnpm-workspace.yaml`。将输出的键复制到该文件后,重新运行命令即可。安装已经构建好的 tarball 或本地 checkout 时,无需加入 `allowBuilds`。 ## Web 别名 @@ -63,9 +63,9 @@ dsh web --help 生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`。绑定所有网络接口时,还会信任机器自动发现的 LAN IP 字面量;`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。 -进程关闭时会给插件树最多 5 秒完成 dispose。第一次 `SIGINT`/`SIGTERM` 启动该优雅排空——`SIGTERM` 是监督进程的普通停止请求,在所有 surface 上以 0 退出,`SIGINT` 报告 130;第二次信号强制立即退出。如果一次性运行正常结束时已经卡在 dispose 中,第一次 `Ctrl+C` 就会升格并立即退出,而不会被吞掉。 +进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT` 或 `SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。 -所有模式都将调用目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。每次 profile 启动都监视两个 `cordis.patch.yml` 层(profile 与 home)的有效编辑并以事务方式重新应用;一次性 surface 经由有界关闭退出,关闭会先 dispose 监视器。 +所有模式都将运行命令时所在的目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。每次启动 profile 时,系统都会监视 profile 与 home 两个 `cordis.patch.yml` 配置层的有效变更,并以事务方式重新应用;一次性运行模式通过有界关闭流程退出,该流程会先 dispose 监视器。 新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取、网络访问和进程可见性不受限制。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。 @@ -75,8 +75,7 @@ dsh web --help 基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和已禁用的会话遥测。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`;只有 patch 层插入提供方并启用 `web_fetch` 后,该工具才可用。 -会话遥测默认留在本地。`DSH_TELEMETRY_MODE=FULL` 将每条已投影会话事件作为 OTLP/HTTP 日志流式发送,`DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 则仅在记录反馈时上传会话日志后缀。`DSH_TELEMETRY_OTLP_URL` 选择其他 collector,任何非空 `DSH_TELEMETRY_DISABLED` 仍是具有最高优先级的硬性退出开关。随附基础配置没有遥测脱敏规则,因此显式启用的导出可能包含消息文本、工具参数与结果以及 workspace 路径;该部署决策由[默认关闭 Agent Note](../../../.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md)负责。 - +会话遥测默认留在本地。`DSH_TELEMETRY_MODE=FULL` 将每条已投影会话事件作为 OTLP/HTTP 日志流式发送,`DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 则仅在记录反馈时上传会话日志后缀。`DSH_TELEMETRY_OTLP_URL` 选择其他 collector。任何非空的 `DSH_TELEMETRY_DISABLED` 都是具有最终效力的遥测强制关闭开关。随附基础配置没有遥测脱敏规则,因此显式启用的导出可能包含消息文本、工具参数和结果,以及 workspace 路径;相关部署决策见[默认关闭 Agent Note](../../../.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md)。 通过 `dsh plugin --profile add ` 安装外部插件组合包。安装的包拥有其依赖,并贡献其声明的 `cordis.patch.yml` 层。CLI 还随附 `@deepseek-ai/dsh-mcp-client` 作为供 patch 层使用的依赖,但默认不启用 MCP 服务器,因为每条服务器命令都是 agent(智能体)沙箱之外的受信任可执行代码。 diff --git a/packages/core/tools/README.i18n.yaml b/packages/core/tools/README.i18n.yaml index 7011884239..9cda431253 100644 --- a/packages/core/tools/README.i18n.yaml +++ b/packages/core/tools/README.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/core/tools/README.md README.md: 44eb25b79436a75f08406102fc1e3734e59b1001 -README.zh.md: a47e7c54b0cd3fc3c5146a9a8be4d1406f9ef8aa +README.zh.md: d77b4ee5248b19418c27674faf7afd52957c6382 diff --git a/packages/core/tools/README.zh.md b/packages/core/tools/README.zh.md index a47e7c54b0..d77b4ee524 100644 --- a/packages/core/tools/README.zh.md +++ b/packages/core/tools/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -工具注册表与执行流水线。工具插件注册各自的 schema 和执行器;agent loop(智能体循环)依次让每次调用经过 `tools/pre-execute`(可扩展的允许/拒绝门禁)→ 已注册的单调守卫 → `tools/execute`(供超时/重试/指标插件使用的环绕分发包装层)→ `tools/post-execute`(检查/替换结果、附加上下文)→ 定义自身的 `finalizeContent` 终结步骤 → 仅观测的 `tools/result` 通知。注册表还负责决定如何向模型呈现其工具:`mode` 配置可以选择原生 Function Calling(函数调用)、[Code Mode](#code-mode),或同时选择两者;单个 agent 可用 `presentAs` 为自己遮蔽该默认值。 +工具注册表与执行流水线。工具插件注册各自的 schema 和执行器;agent loop(智能体循环)依次让每次调用经过 `tools/pre-execute`(可扩展的允许/拒绝门禁)→ 已注册的单调守卫 → `tools/execute`(供超时/重试/指标插件使用的环绕分发包装层)→ `tools/post-execute`(检查/替换结果、附加上下文)→ 由工具定义持有的 `finalizeContent` 边界 → 仅观测的 `tools/result` 通知。注册表还决定以何种方式向模型呈现工具:`mode` 配置可以选择原生 Function Calling(函数调用)、[Code Mode](#code-mode),或同时选择两者;单个 agent 可用 `presentAs` 为自己遮蔽该默认值。 ## 服务:`ToolRegistry`(ctx 键:`tools`) @@ -13,17 +13,17 @@ tools: mode: native # native (default) | code | both ``` -`native` 以函数定义的形式贡献可见工具。`code` 贡献保留的 `run_code` 传输、生成的 `tools:sdk` 段,以及声明「只有 `run_code` 可被直接调用」的 `tools:code-only` 规则——执行器随后强制该规则,模型直呼其他任何工具名都会在策略运行之前解析为 `UNKNOWN_TOOL`;`both` 同时贡献两种形式,且不声明该规则,因为它的原生调用确实会执行。这是「未作声明的 agent」的默认值——agent preset 用 [`dsh-agent-tool-mode`](../agent-tool-mode/README.md) 为自己选择。不能注册、遮蔽、限制或移除该保留传输,且无论配置何种模式,该名称都是保留的,因为任何 agent 都可能选择 code 模式。非原生模式要求所加载 `ctx.codeRuntime` 的 `language` 有已注册的 SDK 渲染器——TypeScript 经 [`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md) 交付;Python 渲染器内置,驱动任何报告 `language: 'python'` 的运行时(第一方 `dsh-code-runtime-python` 后端另行交付)。没有渲染器的运行时语言会导致提示词组装明确失败;如果 `systemPrompt.toolOrder` 条目指向当前模式未贡献的工具,系统会拒绝组装提示词。`system-prompt/assemble` 监听器可以替换注册表贡献;它返回的组装结果具有权威性,因此该监听器负责保留可用的 Code Mode 协议。 +`native` 以函数定义的形式贡献可见工具。`code` 会提供预留的 `run_code` 传输、生成的 `tools:sdk` 段,以及声明「只有 `run_code` 可被直接调用」的 `tools:code-only` 规则。执行器随后强制执行该规则:模型直接调用其他任何工具时,会在策略运行前将该调用解析为 `UNKNOWN_TOOL`;`both` 同时提供两种形式,且不声明该规则,因为其中的原生调用确实可以执行。没有单独声明呈现模式的 agent 默认采用此配置;agent preset 可通过 [`dsh-agent-tool-mode`](../agent-tool-mode/README.md) 自行选择呈现模式。不能注册、遮蔽、限制或移除该保留传输,且无论配置何种模式,该名称都是保留的,因为任何 agent 都可能选择 code 模式。非原生模式要求所加载 `ctx.codeRuntime` 的 `language` 有已注册的 SDK 渲染器——TypeScript 经 [`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md) 交付;Python 渲染器内置,驱动任何报告 `language: 'python'` 的运行时(第一方 `dsh-code-runtime-python` 后端另行交付)。没有渲染器的运行时语言会导致提示词组装明确失败;如果 `systemPrompt.toolOrder` 条目指向当前模式未贡献的工具,系统会拒绝组装提示词。`system-prompt/assemble` 监听器可以替换注册表贡献;它返回的组装结果具有权威性,因此该监听器负责保留可用的 Code Mode 协议。 ### 公开 API -- `ctx.tools.register(definition: ToolDefinition): () => void`:注册一个受信任、带类型的同进程定义,其中必须包含规范的 `output` 声明。所在层由调用上下文的作用域决定:普通插件上下文会全局注册;agent 的 `agent.ctx` 只为该 agent 注册,并在此处遮蔽同名全局工具。同一层内名称重复会抛出;非原生模式还会拒绝保留的 `run_code` 传输名称。缺失或不受支持的输出声明,以及非正数或非有限的 `timeoutMs`,都会使注册失败。可选的同步 `finalizeContent` 回调会在调用开始时创建快照;在所有流水线结果(包括实体化其他结果字段时发现的错误)规范化之后,它只能替换最终面向模型的内容。随调用 fiber dispose(资源释放)。 -- `ctx.tools.presentAs(mode: ToolPresentationMode): () => void`:为本 agent 选择面向模型的呈现方式,仅对该 agent 遮蔽 `mode` 配置;从普通上下文调用会抛出(进程级呈现方式是那个配置字段),同一 scope 内第二次声明也会抛出。code 类模式还会为该 agent 注册它自己的 `tools:sdk` 段。清单本身不变——`schemas(agent)` 报告的仍是该 agent 的能力,仅组装结果中的工具会被折叠。随调用方 fiber dispose。 +- `ctx.tools.register(definition: ToolDefinition): () => void`:注册一个受信任、带类型的同进程定义,其中必须包含规范的 `output` 声明。所在层由调用上下文的作用域决定:普通插件上下文会全局注册;agent 的 `agent.ctx` 只为该 agent 注册,并在此处遮蔽同名全局工具。同一层内名称重复会抛出;非原生模式还会拒绝保留的 `run_code` 传输名称。缺失或不受支持的输出声明,以及非正数或非有限的 `timeoutMs`,都会使注册失败。可选的同步 `finalizeContent` 回调会在调用开始时纳入快照;在所有流水线结果(包括实体化其他结果字段时发现的错误)规范化之后,它只能替换最终面向模型的内容。该注册会随调用方 fiber 一同 dispose(资源释放)。 +- `ctx.tools.presentAs(mode: ToolPresentationMode): () => void`:为本 agent 选择面向模型的呈现方式,仅对该 agent 遮蔽 `mode` 配置;从普通上下文调用会抛出(进程级呈现方式是那个配置字段),同一 scope 内第二次声明也会抛出。code 类模式还会为该 agent 注册它自己的 `tools:sdk` 段。工具目录保持不变:`schemas(agent)` 仍会报告该 agent 的能力;只有组装结果中的工具列表会按所选呈现方式收束。随调用方 fiber dispose。 - `ctx.tools.restrict(filter)`:对全局工具应用 agent 作用域的允许/拒绝掩码;从普通上下文调用会抛出。筛选器在注册时创建快照;多个掩码取交集,随后再合并作用域本地工具。拒绝掩码会接纳后来出现且未点名的全局工具,而允许掩码会排除后来出现的名称。未知、本地或保留名称以及空筛选器都会被拒绝。这是实时可见性组合,不是权限边界;参见[作用域安全非目标](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals)。 -- `ctx.tools.get(name: string, scope?: ScopeKey): ToolDefinition | undefined`:按某个作用域所见的结果解析(应用遮蔽;被限制掉的全局工具视为不存在)。呈现器会传入发起调用的 agent,使卡片与实际执行内容一致。 +- `ctx.tools.get(name: string, scope?: ScopeKey): ToolDefinition | undefined`:返回指定作用域可见的解析结果,其中已应用名称遮蔽;被作用域限制排除的全局工具会被视为不存在。呈现器会传入发起调用的 agent,使卡片与实际执行内容一致。 - `ctx.tools.schemas(scope?: ScopeKey): ToolSchema[]`:返回该作用域可见的所有 schema(不含 `execute` 函数)。已交付工具的 schema 收录在 [docs/tool-catalog.md](../../../docs/tool-catalog.md) 中;该目录通过启动每个工具插件并采集此方法的结果生成(参见[工具 schema 目录 Agent Note](../../../.agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md))。 - `ctx.tools.guard(guard: ToolGuard): () => void`:在 `tools/pre-execute` 之后注册单调同步执行守卫:返回理由会拒绝调用,返回 `undefined` 则保持原决定。普通上下文守卫全局生效;`agent.ctx` 守卫只对该 agent 生效。后续 waterfall(瀑布式事件)监听器无法将守卫的拒绝重新变为允许。随调用 fiber dispose。 -- `ctx.tools.execute(exec)`:以无损方式快照并冻结参数,分配不透明 token,运行完整的策略/分发/结果流水线,然后在最终观测前独立快照权威结果。无效参数会进入同一结果路径,但不会到达策略或工具主体。环绕包装层只能替换 `signal`;注册表会在调用主体前立即重新融合调用方的原始信号。 +- `ctx.tools.execute(exec)`:以无损方式快照并冻结参数,分配不透明 token,运行完整的策略/分发/结果流水线,然后在最终观测前独立快照权威结果。无效参数会进入同一结果路径,但不会到达策略或工具主体。环绕包装层只能替换 `signal`;注册表会在进入工具主体之前,立即将调用方的原始信号重新合并到当前信号中。 - `ctx.tools.executionMode(exec)`:返回 `parallel` 的唯一条件是可见定义的 `isConcurrencySafe(exec.arguments)` 分类器恰好返回 `true`;未知、隐藏、未声明、无效或抛出异常的分类结果均为独占。 ### 注入的服务 @@ -32,11 +32,11 @@ tools: ### 取消 -取消采用协作方式,并等待完全停稳。每次类型化调用都提供由调用方拥有的 `AbortSignal`;工具主体通过必填的只读 `exec.signal` 接收它,只有 `tools/execute` 包装层可以临时替换这个必填信号。注册表会在替换期间保留调用方取消,并且绝不会在已启动的同进程 Promise 尚未结算时提前返回。工具主体调用前发生的取消为 `ABORTED_BEFORE_DISPATCH`;调用主体后的取消只能把成功结果替换为 `ABORTED`。拒绝、包装层失败、工具失败、后置策略失败或由超时机制产生的 `TOOL_TIMEOUT` 仍保留更具体的结果。入口处已中止的调用会实体化并冻结参数,随后跳过所有策略和分发阶段,只发布一个结果。每个异步工具都必须观测或转发该信号,并且只能在自身拥有的工作停止后结算。[工具取消 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md) 规定完整约定和强制终止边界。 +取消采用协作方式,并等待完全停稳。每次类型化调用都提供由调用方拥有的 `AbortSignal`;工具主体通过必填的只读 `exec.signal` 接收它,只有 `tools/execute` 包装层可以临时替换这个必填信号。注册表会在替换期间保留调用方取消,并且绝不会在已启动的同进程 Promise 尚未结算时提前返回。工具主体调用前发生的取消为 `ABORTED_BEFORE_DISPATCH`;工具主体被调用后发生的取消,只能将成功结果替换为 `ABORTED`。拒绝、包装层失败、工具失败、后置策略失败或由超时机制产生的 `TOOL_TIMEOUT` 仍保留更具体的结果。入口处已中止的调用会实体化并冻结参数,随后跳过所有策略和分发阶段,只发布一个结果。每个异步工具都必须观测或转发该信号,并且只能在其负责的工作停止后结算。[工具取消 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md) 规定完整约定和强制终止边界。 ### 实时事件 -实时注册表流水线先经过 3 个可变换的 waterfall,再经过由定义拥有的内容终结器,最后发布仅供观测的 `tools/result` 事件;注册表变更有意作为不过滤的共享状态通知。确切签名、分发 mode、作用域筛选和失败隔离约定位于 [tools.md](../../../docs/subsystems/tools.md#cordis-surface) 的生成区块,完整顺序则在生成的[工具执行流水线](../../../docs/tool-execution-pipeline.md)中可视化。`tools/result` 是实时事件;名称相近的 `tool/result` 是 agent loop 随后追加的持久会话事件。 +实时注册表流水线先经过 3 道可转换的 waterfall,再经过由工具定义持有的内容终结器,最后发布仅供观测的 `tools/result` 事件;注册表变更通知有意不作过滤,并作为共享状态通知发布。确切签名、分发 mode、作用域筛选和失败隔离约定位于 [tools.md](../../../docs/subsystems/tools.md#cordis-surface) 的生成区块,完整顺序则在生成的[工具执行流水线](../../../docs/tool-execution-pipeline.md)中可视化。`tools/result` 是实时事件;名称相近的 `tool/result` 是 agent loop 随后追加的持久会话事件。 ### 关键类型 @@ -102,16 +102,16 @@ ctx.tools.register(defineTool({ ### 强制执行的原始 JSON Schema 子集 -`JsonSchemaNode` 是工具输出、Code Mode 生成、subagent 和工作流共享的原始对应类型。它允许任意 JSON 根、一个仅含 annotation 的无约束 JSON 节点,以及恰好匹配一个分支的 `oneOf`;annotation 必须保持为无损 JSON。`assertSupportedJsonSchema()` 拒绝不受支持的构造,而 `validateJsonSchemaValue()` 返回带路径的违规信息。subagent 和工作流通过 `assertObjectJsonSchema()` 与 `ObjectJsonSchema` 保留调用方定义的对象根要求,而不是依赖共享词汇的限制。 +`JsonSchemaNode` 是工具输出、Code Mode 生成、subagent 和工作流共享的原始 JSON Schema 对应类型。它允许任意 JSON 根、仅含注解且不施加约束的 JSON 节点,以及恰好匹配一个分支的 `oneOf`;注解必须保持为无损 JSON。`assertSupportedJsonSchema()` 拒绝不受支持的构造,而 `validateJsonSchemaValue()` 返回带路径的违规信息。subagent 和工作流通过 `assertObjectJsonSchema()` 与 `ObjectJsonSchema` 保留调用方定义的对象根要求,而不是依赖共享词汇的限制。 -### 工具拥有的 UI 呈现 +### 由工具定义的 UI 呈现 -工具可以选择拥有纯 `presentCall()` 和 `presentResult()` 呈现意图,使 UI 无需特殊处理工具名称: +工具可以选择通过纯函数 `presentCall()` 和 `presentResult()` 定义呈现意图,使 UI 无需针对工具名称编写特殊逻辑: - 调用视图为 `{ card: 'generic', title, kind?, rawInput?, content?, locations? }`、`{ card: 'terminal', title, description?, cwd? }` 或 `{ card: 'diff', title, diffs, locations? }`。 -- 结果视图为 `{ card: 'generic', title?, content? }`、`{ card: 'terminal', title?, output?, exitCode?, signal? }`、`{ card: 'diff', title?, diffs }`、`{ card: 'search', shape, title?, truncated, total, … }`(已完成的发现型搜索——`shape: 'matches'`(grep)为按文件分组的匹配,`shape: 'paths'`(glob)为扁平路径列表,配 `truncated`/`total` 使 UI 永不把被截断的结果当作完整结果呈现;该视图不携带结果文本,且搜索没有 `card: 'search'` 的调用时对应视图)、`{ card: 'read', title?, path, offset, lines, totalLines, lang?, content? }`(已完成的文件读取→带行号、可选语法高亮的代码视图;`offset` 是窗口请求的 1-based 起始行,即使 `lines` 为空也保留;`lines` 是 `{ number, text }[]`,保留每一行的文件行号,`content` 是无读取能力的 UI 回退时使用的去信封文本)或 `{ card: 'web', kind: 'search' | 'fetch', title?, … }`(已完成的 web 检索;`kind` 各分支携带结构化的搜索来源或抓取摘要,不具备 `web` 能力的 UI 回退到原始结果内容)。 +- 结果视图为 `{ card: 'generic', title?, content? }`、`{ card: 'terminal', title?, output?, exitCode?, signal? }`、`{ card: 'diff', title?, diffs }`、`{ card: 'search', shape, title?, truncated, total, … }`(已完成的发现型搜索——`shape: 'matches'`(grep)为按文件分组的匹配,`shape: 'paths'`(glob)为扁平路径列表,配 `truncated`/`total` 使 UI 永不把被截断的结果当作完整结果呈现;该视图不携带结果文本,且搜索没有 `card: 'search'` 的调用时对应视图)、`{ card: 'read', title?, path, offset, lines, totalLines, lang?, content? }`(已完成的文件读取→带行号、可选语法高亮的代码视图;`offset` 是窗口请求的 1-based 起始行,即使 `lines` 为空也保留;`lines` 是 `{ number, text }[]`,保留每一行的文件行号,`content` 是去除读取结果外层封装后的正文,供不支持读取视图的 UI 回退显示)或 `{ card: 'web', kind: 'search' | 'fetch', title?, … }`(已完成的 web 检索;`kind` 各分支携带结构化的搜索来源或抓取摘要,不具备 `web` 能力的 UI 回退到原始结果内容)。 -返回 `undefined` 会选择通用回退。呈现器只依赖其参数和持久结果,因为 UI 会在实时流式输出和日志回放期间调用它们。`output.presentationMeta(args, value)` 为直接接口调用派生 JSON 元数据;该元数据随 `tool/result` 持久化并传回 `presentResult`,而规范值本身仍只存在于执行局部,绝不会回放。嵌套 Code 分发不会计算元数据。`defineTool` 会软验证较旧的日志参数并回退,而不会使回放崩溃。`dsh-tool-bash` 与 `dsh-tool-fs` 是参考实现;[规范输出 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md) 规定值/呈现拆分,[呈现意图 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md) 规定卡片词汇。 +返回 `undefined` 会选择通用回退。呈现器只依赖其参数和持久结果,因为 UI 会在实时流式输出和日志回放期间调用它们。`output.presentationMeta(args, value)` 为直接的顶层调用派生 JSON 元数据;该元数据随 `tool/result` 持久化并传回 `presentResult`,而规范值本身仍只存在于执行局部,绝不会回放。嵌套 Code 分发不会计算元数据。`defineTool` 会软验证较旧的日志参数并回退,而不会使回放崩溃。`dsh-tool-bash` 与 `dsh-tool-fs` 是参考实现;[规范输出 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md) 规定值/呈现拆分,[呈现意图 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md) 规定卡片词汇。 ### Code Mode @@ -119,10 +119,10 @@ ctx.tools.register(defineTool({ 在 `code`(而非 `both`)下,该传输同时也是模型唯一可用的入口:模型直呼其他任何可见工具名,都会在创建执行时、早于 `tools/pre-execute`、审批 `ask` 和 guards 解析为 `UNKNOWN_TOOL`,因此没有任何一方会观察或批准一个注定失败的调用。拒绝信息会给出正确路径(`only \`run_code\` is callable directly — call \`\` from inside a \`run_code\` program instead`),因为同一份提示词刚刚声明过那个工具,只说 `unknown tool` 会被读成部署损坏。SDK 子分发携带外层执行的 `parent` token,不受此限制,因此程序保留 SDK 声明的全部绑定。参见[执行器塌缩 note](../../../.agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.md)、[Code Mode 基础](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)、[类型化返回约定](../../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md)和[代码运行时 seam](../../code-runtime/README.md)。可以运行 `pnpm run demo:code-mode` 试用。 -- **SDK 段**(`tools:sdk`,顺序 150):一个惰性提示词段,每次组装时都会重新生成与所加载运行时语言相符的 SDK 文本。TypeScript 形态发出 `JsonValue`、精确的 `ToolArgsMap` / `ToolOutputMap`、`ToolName`、`ToolCallError` 声明、面向调用作用域可见最终能力的映射 `tools` 命名空间(特殊名称使用带引号的键),以及固定用法说明;Python 形态(`ctx.codeRuntime.language === 'python'`)发出等价的具名 `TypedDict` 与一个带相同用法说明的 `tools` 对象。其输出具有确定性:工具按字典序排列;工具集合不变时,文本逐字节相同(有利于前缀 cache)。两个代码生成器都已导出,且绝不会在提示词组装期间抛出:`jsonSchemaToTs` 处理统一 schema 的每种构造并将不受支持的原始构造降级为 `unknown`;`jsonSchemaToPy` 同理,降级为 `Any`(当某字段名不是合法的 `TypedDict` 属性时,或在 SDK 渲染之外被调用时——`TypedDict` 声明所需的命名上下文由该渲染提供——整个对象降级为 `dict[str, Any]`)。 -- **分发桥接层**(`run_code` 的 execute):每个绑定调用都会在分发前快照为无损 JSON(`undefined`、`BigInt`、循环、稀疏数组、`-0` 和特殊对象会使该次调用被拒绝),经由每次运行独有、复用原生并发约定的池调度——调用严格按提交顺序启动,连续的 `isConcurrencySafe` 调用最多可重叠经校验的 `maxParallelSubCalls` 配置个(默认 10;设为 `1` 即恢复串行分发),被分类为独占的调用先排空池、单独运行并阻挡其后的调用——以外层执行的不透明 token 作为 `parent`,并经过完整的 pre-execute → guards → execute → post-execute → result 流水线。成功会返回策略处理后的最终规范值;失败以一条消息到达 worker,并成为 `ToolCallError(toolName, message)`。每个已启动的子调用在进入流水线时记录一条 `tool/code-dispatch-start` 事件(确定性 id `:code:`,按提交顺序编号),并以一条携带完整模型可见 `content`/`isError` 结果的 `tool/code-dispatch` 事件完结(采用 `tool/result` 词汇,因此 UI 会沿原生路径呈现子调用——这对事件的 `time` 字段承载每个子调用的计时);因 run 结算而被放弃的排队调用两者都不记录。`deriveMessages()` 既不公开这两个事件,也不持久化规范值。token 关联让以提交为语义的观察器能够把内部成功延迟到最终 `run_code` 结果,而无需公开实时外层执行;普通工具副作用不会回滚。每个子调用的 `additionalContexts` 条目都会按分发顺序通过外层 `ToolRunContext` 延迟;循环只在父级 `run_code` 结果之后追加这些上下文,从而保持相邻关系,并且即使程序后来失败,也会保留各自的来源/元数据。 +- **SDK 段**(`tools:sdk`,顺序 150):一个在组装时求值的提示词段,每次组装都会重新生成与所加载运行时语言相符的 SDK 文本。TypeScript 形态会生成 `JsonValue`、精确的 `ToolArgsMap` / `ToolOutputMap`、`ToolName`、`ToolCallError` 声明,以及映射调用作用域最终可见工具的 `tools` 命名空间(特殊名称使用带引号的键),并附带固定的使用说明;Python 形态(`ctx.codeRuntime.language === 'python'`)发出等价的具名 `TypedDict` 与一个带相同用法说明的 `tools` 对象。其输出具有确定性:工具按字典序排列;工具集合不变时,文本逐字节相同(有利于前缀 cache)。两个代码生成器都已导出,且绝不会在提示词组装期间抛出:`jsonSchemaToTs` 处理统一 schema 的每种构造并将不受支持的原始构造降级为 `unknown`;`jsonSchemaToPy` 同理,降级为 `Any`(当某字段名不是合法的 `TypedDict` 属性时,或在 SDK 渲染之外被调用时——`TypedDict` 声明所需的命名上下文由该渲染提供——整个对象降级为 `dict[str, Any]`)。 +- **分发桥接层**(`run_code` 的 execute):每个绑定调用都会在分发前快照为无损 JSON(`undefined`、`BigInt`、循环、稀疏数组、`-0` 和特殊对象会使该次调用被拒绝),经由每次运行独有、复用原生并发约定的池调度——调用严格按提交顺序启动,连续的 `isConcurrencySafe` 调用最多可重叠经校验的 `maxParallelSubCalls` 配置个(默认 10;设为 `1` 即恢复串行分发),被分类为独占的调用先排空池、单独运行并阻挡其后的调用——以外层执行的不透明 token 作为 `parent`,并经过完整的 pre-execute → guards → execute → post-execute → result 流水线。成功会返回策略处理后的最终规范值;失败以一条消息到达 worker,并成为 `ToolCallError(toolName, message)`。每个已启动的子调用在进入流水线时记录一条 `tool/code-dispatch-start` 事件(确定性 id `:code:`,按提交顺序编号),并以一条携带完整模型可见 `content`/`isError` 结果的 `tool/code-dispatch` 事件完结(采用 `tool/result` 词汇,因此 UI 会沿原生路径呈现子调用——这对事件的 `time` 字段承载每个子调用的计时);因 run 结算而被放弃的排队调用两者都不记录。`deriveMessages()` 既不公开这两个事件,也不持久化规范值。token 关联使按提交语义工作的观察器可以延后提交内部调用的成功结果,直到最终 `run_code` 结果确定,而无需接收实时可变的外层执行对象;普通工具副作用不会回滚。每个子调用的 `additionalContexts` 条目都会按分发顺序通过外层 `ToolRunContext` 延迟;循环只在父级 `run_code` 结果之后追加这些上下文,从而保持相邻关系,并且即使程序后来失败,也会保留各自的来源/元数据。 - **结算纪律**:桥接层拥有一个运行作用域的中止机制;该中止会跟随传入的外层信号,并在运行因任何原因结算时触发,因此预算耗尽会中止正在运行的子工具,而不会将其遗留。桥接层随后会在返回之前排空队列,使每个 `tool/code-dispatch` 都落在仍打开的轮次内。失败的运行会抛出 `CodeRunFailedError`(`code: 'CODE_RUN_FAILED'`,message = 失败类型 + 已捕获日志),流水线会将其转换为模型可据以自我修正的结构化 `isError`。 -- **结果大小**:中间绑定值会完整传入 worker 进程,且没有逐绑定字节上限。`run_code` 返回规范的 `{ logs: string[], result?: JsonValue }`;字符串原样呈现,其他所有存在的 JSON 根都通过栈安全的美化 JSON 遍历呈现,总缩进最多为 10 个字符(更深的子树保持紧凑),`null` 保持显式,而缺少 `result` 表示程序返回 `undefined`。worker 可配置的 `maxOutputBytes`(默认 64 MiB)只应用于组合序列化后的外层日志数组、完成值或失败消息载荷;固定的结果 envelope 语法和呈现空白不计入该上限。无效和超限的完成会明确失败,只有此外层结果可以使用普通 spill。 +- **结果大小**:中间绑定值会完整传入 worker 进程,且没有逐绑定字节上限。`run_code` 返回规范的 `{ logs: string[], result?: JsonValue }`;字符串原样呈现,其他所有存在的 JSON 根都通过栈安全的美化 JSON 遍历呈现,总缩进最多为 10 个字符(更深的子树保持紧凑),`null` 保持显式,而缺少 `result` 表示程序返回 `undefined`。worker 可配置的 `maxOutputBytes`(默认 64 MiB)只应用于组合序列化后的外层日志数组、完成值或失败消息载荷;固定的结果封装语法和呈现空白不计入该上限。无效和超限的完成会明确失败,只有这个外层结果可以按常规 spill 机制处理。 ### 并行执行 @@ -177,7 +177,7 @@ The available tools: #### 模型看到的内容 -循环会保留模型发出的参数和注册表的最终内容。任何抛出或被拒绝的调用都会恰好变为 `Error: `。Code Mode 只返回外层程序打印的行和呈现后的返回值;两者都为空时返回 `(run_code completed with no output)`;失败时返回 `Error: code run failed (): `,并根据是否存在已捕获内容,在其后附加 `Captured output:` 与捕获的行。内部分发事件只保留在日志中;后置执行监听器可以在结果之后追加带来源归属的上下文。 +循环会保留模型发出的参数和注册表的最终内容。任何抛出异常或遭到拒绝的调用,都会转换为确切的 `Error: `。Code Mode 只返回外层程序打印的行和呈现后的返回值;两者都为空时返回 `(run_code completed with no output)`;失败时返回 `Error: code run failed (): `,并根据是否存在已捕获内容,在其后附加 `Captured output:` 与捕获的行。内部分发事件只保留在日志中;后置执行监听器可以在结果之后追加带来源归属的上下文。 #### Token 影响 @@ -192,7 +192,7 @@ The available tools: - **并发策略不是事件门禁**:`executionMode()` 直接读取已解析的工具定义;插件只能在自身拥有的定义上声明分类器。 - **`tools/pre-execute` 有意不允许改写 `exec.arguments`**:否则日志记录和呈现的参数会与实际运行内容失去同步;改写设计记录在[拟议的 Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md)中。 - **调用方定义的 subagent 与工作流结构化输出仍要求对象根**:这是消费方层面的守卫;共享 schema 词汇和工具输出支持任意 JSON 根。 -- **定义上的 `timeoutMs` 仅为声明**:注册表绝不会强制执行截止时间;要强制执行,必须使用 `@deepseek-ai/dsh-timeout-policy` 包装层。 -- **Code Mode 的 SDK 语言跟随已加载的那个运行时,且呈现方式按 agent 而非按工具**:`mode: code`/`both` 会拒绝组装提示词,除非 `ctx.codeRuntime.language` 有已注册的 SDK 渲染器(TypeScript 或 Python);作用域限制/遮蔽与 `presentAs` 会选择每个 agent 的可见绑定及其形态,但在同一个 agent 内不能让一个工具仅使用 Native,而另一个仅使用 Code。 +- **定义中的 `timeoutMs` 仅作声明之用**:注册表绝不会强制执行截止时间;要强制执行,必须使用 `@deepseek-ai/dsh-timeout-policy` 包装层。 +- **Code Mode 的 SDK 语言由当前加载的运行时决定,且呈现方式按 agent 而非按工具**:`mode: code`/`both` 会拒绝组装提示词,除非 `ctx.codeRuntime.language` 有已注册的 SDK 渲染器(TypeScript 或 Python);作用域限制/遮蔽与 `presentAs` 会选择每个 agent 的可见绑定及其形态,但在同一个 agent 内不能让一个工具仅使用 Native,而另一个仅使用 Code。 - **Code Mode 中间值只存在于执行局部,且没有字节上限**:这些规范的类型化值无法从会话回放重建,并可能耗尽进程或 worker 内存;只有外层 `run_code` 输出受 worker 可配置的硬上限约束。每个子调用的持久日志副本则确实有上限:`tools/code-dispatch-log` waterfall 允许 spill 策略把过大的 `tool/code-dispatch` 内容替换为预览加定位符([原理](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md))。 - **每次运行都会获得全新的 `run_code` 状态**:MVP 不采用持久 REPL 风格内核(跨调用状态不会出现在日志中);参见 [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)。 diff --git a/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index 42aa01ec6e..fa73fe380f 100644 --- a/python/sdk/README.i18n.yaml +++ b/python/sdk/README.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write python/sdk/README.md README.md: 70b9d6391644d10ee7d5c29ce122632786e3bbcc -README.zh.md: 1d1a23576cc8029dacbb2df0e3d1d9fc2ce27426 +README.zh.md: 63824ed2a4427835cbbd5ef3491bc7cb0fb94f21 diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index 1d1a23576c..63824ed2a4 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -19,9 +19,9 @@ with DeepSeekHarness() as harness: result = harness.run("Say hi.") ``` -`DeepSeekHarness` 会保留延迟启动的运行时子进程,以供多次调用复用。请像上例一样将其用作上下文管理器,或在用完后显式调用 `close()`。 +`DeepSeekHarness` 会保留其按需启动的运行时子进程,以便在多次调用之间复用。请像上例一样将其用作上下文管理器,或在使用完毕后显式调用 `close()`。 -默认情况下,SDK 启动 `deepseek-harness-runtime-bin` 包内置的单文件 `dsh-jsonrpc-agent` 可执行程序,并通过 `DSH_CORDIS_CONFIG` 注入该包的默认配置(stdio JSON-RPC 服务器、`agent-core`、预载的 DeepSeek 适配器、配有显式组合语义检查点策略的 JSONL 会话持久化、本地 bash)。要运行自己的插件组合,请在配置里保留 `@deepseek-ai/dsh-jsonrpc` 条目,并传入 Cordis 配置路径。 +默认情况下,SDK 会启动 `deepseek-harness-runtime-bin` 包内置的单文件可执行程序 `dsh-jsonrpc-agent`,并通过 `DSH_CORDIS_CONFIG` 注入该包的默认配置,其中包括 stdio JSON-RPC 服务器、agent core(智能体核心)、预载的 DeepSeek 适配器、采用显式组合语义检查点策略的 JSONL 会话持久化,以及本地 bash。要运行自己的插件组合,请在配置中保留 `@deepseek-ai/dsh-jsonrpc` 配置项,并传入 Cordis 配置文件路径。 ```py from deepseek_harness import DeepSeekHarness @@ -35,14 +35,14 @@ with DeepSeekHarness( result = harness.run("Make the requested code change.") ``` -`provider` 用于选择当前 Cordis 组合已注册的提供方路由;`model` 是该适配器解析的模型 ID。`max_tokens` 是可选的正整数,用于限制根 agent(智能体)及其进程内后代每次请求的输出 token;省略时由提供方默认值控制。压缩摘要继续使用压缩插件单独配置的上限。内置默认组合注册 `deepseek-official`。自定义组合可以挂载 `llm-pi-ai`,在其中配置各提供方的凭据与端点,再选择 pi-ai 已安装目录中的任意提供方/模型组合。 +`provider` 选择指定 Cordis 组合所注册的提供方路由;`model` 是该适配器解析出的模型 ID。`max_tokens` 是一个可选的正值,用于限制根 agent(智能体)及其进程内后代在每次请求中输出的 token 数量;省略该参数时,由提供方的默认行为决定输出上限。压缩摘要继续使用压缩插件单独配置的上限。内置默认组合注册 `deepseek-official`。自定义组合可以挂载 `llm-pi-ai`,在其中配置各提供方专属的凭据和端点,并选择 pi-ai 已安装 catalog 中存在的任意提供方/模型组合。 -[Python SDK 教程](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/python-sdk.md)提供不使用 Web UI 的顺序安装与首次运行路径。[`jsonrpc-agent` 示例](https://github.com/deepseek-ai/deepseek-harness/blob/master/examples/jsonrpc-agent/README.md)归属该教程使用的完整独立 Cordis 文件。 +[Python SDK 教程](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/python-sdk.md)提供一套无需使用 Web UI、按步骤完成安装和首次运行的流程。该教程所用的完整独立 Cordis 配置文件位于 [`jsonrpc-agent` 示例](https://github.com/deepseek-ai/deepseek-harness/blob/master/examples/jsonrpc-agent/README.md)中。 -`Session.run()` 拥有一个从提示词进入持久 inbox 时开始、到整个 agent 下一次进入空闲状态为止的活动区间,并返回 `RunResult(session_id, final_response, finish_reason, events, notifications, session_root)`。`final_response` 是该区间内根会话最后提交的助手文本。`finish_reason` 是该区间内根会话最后一个 `turn/end` 的 `kind`,例如 `completed`、`max-tokens` 或 `error`;没有轮次结束时为 `None`。缺少字符串 `data.reason.kind` 的 `turn/end` 违反运行时协议,并会抛出 `SdkProtocolError`。两个结果字段描述的都是自有活动区间,而不是因果上归属于该提示词的输出或结束原因。steering(中途引导)、注入的上下文和其他排队工作都可能在进入空闲状态前参与其中。 +`Session.run()` 的活动区间从其提示词被持久 inbox 接收时开始,到整个 agent 下一次进入空闲状态时结束,并返回 `RunResult(session_id, final_response, finish_reason, events, notifications, session_root)`。`final_response` 是该区间内根会话最后提交的助手文本。`finish_reason` 是该区间内根会话最后一个 `turn/end` 的 `kind`,例如 `completed`、`max-tokens` 或 `error`;没有轮次结束时为 `None`。缺少字符串 `data.reason.kind` 的 `turn/end` 违反运行时协议,并会抛出 `SdkProtocolError`。这两个结果字段描述的是 `Session.run()` 所界定的活动区间,并不表示某项输出或结束原因在因果上归属于该提示词。steering(中途引导)、注入的上下文和其他排队工作,也可能在 agent 进入空闲状态前参与这段活动。 -`HarnessClient` 会在运行时进程的生命周期内保留已发现的 subagent(子 agent)祖先关系。每次执行 `Session.run()` 时,`RunResult.notifications` 与 `on_notification` 会按协议传输顺序收到根会话及所有已知后代的通知,其中包括嵌套 subagent 的生命周期事件与会话事件。`RunResult.events` 只包含根会话事件,因此后代消息不会覆盖根会话回复。底层 `session_prompt()` 会立即返回已排队消息的 `MessageId`;绕过 `Session.run()` 的调用方必须自行负责后续的活动边界。 +`HarnessClient` 会在运行时进程的整个生命周期内保留已发现的 subagent 谱系。每次执行 `Session.run()` 时,`RunResult.notifications` 与 `on_notification` 会按协议传输顺序收到根会话及所有已知后代的通知,其中包括嵌套 subagent 的生命周期事件与会话事件。`RunResult.events` 只包含根会话事件,因此后代消息不会覆盖根会话回复。底层 `session_prompt()` 会立即返回已排队消息的 `MessageId`;绕过 `Session.run()` 的调用方必须自行负责后续的活动边界。 -同样的行为也可以通过 `DSH_CORDIS_CONFIG` 为运行时子进程选定。注入逻辑位于 `HarnessClient.start()`,因此底层客户端的默认启动也具有此行为:当启动解析到内置运行时,且 `cordis` 与非空的 `DSH_CORDIS_CONFIG` 均未设置时(运行时把空值视为缺省,注入检查与之一致),使用内置的默认配置;显式给出 `runtime_bin`、`bridge_bin` 或 `launch_args_override` 则完全禁用注入。运行时载体(生产用 exe 与仅限开发的 `node` 闭包)及其获取方式见 [sdk-runtime README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/README.md)。 +也可以通过 `DSH_CORDIS_CONFIG` 为运行时子进程指定配置。注入逻辑位于 `HarnessClient.start()`,因此底层客户端按默认方式启动时也具有该行为:如果启动方式最终解析为内置运行时,且既没有设置 `cordis`,也没有设置非空的 `DSH_CORDIS_CONFIG`(运行时将空值视为未设置,注入检查也是如此),系统就会使用内置默认配置;显式指定 `runtime_bin`、`bridge_bin` 或 `launch_args_override` 时,则会完全禁用该注入。运行时载体(生产用 exe 与仅限开发的 `node` 闭包)及其获取方式见 [sdk-runtime README](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk-runtime/README.md)。 -`cwd` 与 `runtime_cwd` 会在启动子进程、注入环境变量和协议握手前解析为绝对路径。公开 API 只暴露真正生效的选项:部署的角色设定与持久化配置归 `cordis.yml` 管理,而 `session_root` 继续作为设置 `DSH_SESSION_ROOT` 的高层便捷选项。 +`cwd` 与 `runtime_cwd` 会在启动子进程、注入环境变量和协议握手前解析为绝对路径。公开 API 只暴露由 SDK 直接应用的选项:部署 persona 和持久化配置应在 `cordis.yml` 中定义;`session_root` 则保留为设置 `DSH_SESSION_ROOT` 的高层便捷参数。 diff --git a/scripts/snapshots/translation-prompt-v4/request-response.expected.json b/scripts/snapshots/translation-prompt-v4/request-response.expected.json index 5027739b50..497fd95a86 100644 --- a/scripts/snapshots/translation-prompt-v4/request-response.expected.json +++ b/scripts/snapshots/translation-prompt-v4/request-response.expected.json @@ -12,7 +12,7 @@ }, { "role": "assistant", - "content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent(编程智能体)。\n\n它采用了**一切皆插件**的架构。\n\n## 内测说明\n\nDeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。\n\nSession Log 默认留在本地。设置 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 可仅在提交反馈时共享 Session Log,设置 `DSH_TELEMETRY_MODE=FULL` 可持续上传;`FULL` 同时会启用 dsh-sdk 命令遥测,上报匿名 ID、命令结果以及脱敏后的项目配置。请通过内部企业微信群反馈问题和建议。\n\n\n## 运行\n\n请先安装 Node.js(版本要求:`^22.19` 或 `>=24`)和 pnpm 11,然后运行已发布的包:\n\n```sh\nnpx @deepseek-ai/dsh web\n```\n\n该命令会初始化 Web profile 并打印 Web UI 地址,默认地址为 `http://127.0.0.1:3080`。打开该地址,在**设置 → 模型**中添加 DeepSeek API 密钥,然后启动一个会话。运行命令时所在的目录将作为默认工作区;你可以尝试输入 `Summarize this repository and identify its main packages.`。\n\n下一步请阅读 [Web UI 指南](docs/user/guide/index.md)。\n\n### 从源码运行\n\n如需从仓库源码运行:\n\n```sh\ngit clone https://github.com/deepseek-ai/deepseek-harness.git\ncd deepseek-harness\npnpm install\npnpm run build\npnpm dsh web\n```\n\n`pnpm run build` 会准备仓库产物。`pnpm dsh web` 不会重新构建,而是直接启动同一个 Web UI。\n\n## Profile 与插件\n\nprofile 由一组按顺序排列的插件组合包构成。随附的 `web` profile 用于运行 `dsh web`。使用 `dsh plugin --profile ` 管理 profile;该命令会在对应 profile 目录中将剩余参数转发给 pnpm:\n\n```sh\nnpx -p @deepseek-ai/dsh dsh plugin --profile web add \nnpx -p @deepseek-ai/dsh dsh plugin --profile web remove \n```\n\n`add`、`remove`、`update`、`why` 等 pnpm 命令均可直接使用。该命令会先初始化不存在的 profile,再修改其中的包,并根据声明了 `dsh.bundle` 的已安装包更新 bundle 列表。具体行为见 [CLI 参考](apps/cli/reference/README.md#plugin-management)。\n\n[CLI(命令行界面)参考](apps/cli/README.md)介绍 headless 执行与自定义 profile。[Python SDK](python/README.md) 和[示例](examples/README.md)介绍程序化组合与自定义组合。\n\n## 社区\n\n扫描二维码,或打开 DeepSeek Harness 微信社区申请页面 申请加入。\n\n

    \n \"DeepSeek\n

    \n\n## 开发\n\n请先阅读[开发指南](docs/development.md);修改包之前,请阅读[架构文档](docs/architecture.md)。\n\n面向 agent:遵循 [AGENTS.md](AGENTS.md)。\n\n## 许可证\n\n[BSD 3-Clause](LICENSE)\n\n第三方依赖及其许可证见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。\n\n## 参与贡献\n\n向本仓库贡献前,请阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。\n" + "content": "# DeepSeek Harness\n\n[English](README.md) | 中文\n\nDeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent(编程智能体)。\n\n它采用了**一切皆插件**的架构。\n\n## 内测说明\n\nDeepSeek Harness 正处于内部测试阶段,功能和接口可能发生变化。\n\n会话日志默认保存在本地。设置 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 后,仅在提交反馈时共享对应的会话日志;设置 `DSH_TELEMETRY_MODE=FULL` 后,会持续上传会话日志。`FULL` 还会启用 dsh-sdk 命令遥测,上报匿名 ID、命令执行结果和脱敏后的项目配置。请通过内部企业微信群反馈问题和建议。\n\n## 运行\n\n请先安装 Node.js(版本要求:`^22.19` 或 `>=24`)和 pnpm 11,然后运行已发布的包:\n\n```sh\nnpx @deepseek-ai/dsh web\n```\n\n该命令会初始化 Web profile 并打印 Web UI 地址,默认地址为 `http://127.0.0.1:3080`。打开该地址,在**设置 → 模型**中添加 DeepSeek API 密钥,然后启动一个会话。运行命令时所在的目录将作为默认工作区;你可以尝试输入 `Summarize this repository and identify its main packages.`。\n\n下一步请阅读 [Web UI 指南](docs/user/guide/index.md)。\n\n### 从源码运行\n\n如需从仓库源码运行:\n\n```sh\ngit clone https://github.com/deepseek-ai/deepseek-harness.git\ncd deepseek-harness\npnpm install\npnpm run build\npnpm dsh web\n```\n\n`pnpm run build` 会准备仓库产物。`pnpm dsh web` 不会重新构建,而是直接启动同一个 Web UI。\n\n## Profile 与插件\n\nprofile 由一组按顺序排列的插件组合包构成。随附的 `web` profile 用于运行 `dsh web`。使用 `dsh plugin --profile ` 管理 profile;该命令会在对应 profile 目录中将剩余参数转发给 pnpm:\n\n```sh\nnpx -p @deepseek-ai/dsh dsh plugin --profile web add \nnpx -p @deepseek-ai/dsh dsh plugin --profile web remove \n```\n\n`add`、`remove`、`update`、`why` 等 pnpm 命令均可直接使用。该命令会先初始化不存在的 profile,再修改其中的包,并根据声明了 `dsh.bundle` 的已安装包更新 bundle 列表。具体行为见 [CLI 参考](apps/cli/reference/README.md#plugin-management)。\n\n[CLI(命令行界面)参考](apps/cli/README.md)介绍 headless 执行与自定义 profile。[Python SDK](python/README.md) 和[示例](examples/README.md)介绍程序化组合与自定义组合。\n\n## 社区\n\n扫描二维码,或打开 DeepSeek Harness 微信社区申请页面 申请加入。\n\n

    \n \"DeepSeek\n

    \n\n## 开发\n\n请先阅读[开发指南](docs/development.md);修改包之前,请阅读[架构文档](docs/architecture.md)。\n\n面向 agent:遵循 [AGENTS.md](AGENTS.md)。\n\n## 许可证\n\n[BSD 3-Clause](LICENSE)\n\n第三方依赖及其许可证见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。\n\n## 参与贡献\n\n向本仓库贡献前,请阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。\n" }, { "role": "user", From 486c87238130e87cb1e0cc5905ed02268eef50e3 Mon Sep 17 00:00:00 2001 From: j-xiang Date: Thu, 13 Aug 2026 00:42:11 +0800 Subject: [PATCH 026/160] docs(i18n): address focused README review --- apps/cli/README.i18n.yaml | 2 +- apps/cli/README.zh.md | 4 ++-- apps/cli/reference/README.i18n.yaml | 2 +- apps/cli/reference/README.zh.md | 4 ++-- packages/core/tools/README.i18n.yaml | 2 +- packages/core/tools/README.zh.md | 4 ++-- python/sdk/README.i18n.yaml | 2 +- python/sdk/README.zh.md | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/cli/README.i18n.yaml b/apps/cli/README.i18n.yaml index ac025294ca..269cfe0c8c 100644 --- a/apps/cli/README.i18n.yaml +++ b/apps/cli/README.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/README.md README.md: d36183c545475020207bbf23e58d3c98b07ac6a6 -README.zh.md: 9e5507ad767640b413d00cf16f1becc0f540be2b +README.zh.md: 3a87f6750ee4c1a3c94e1438b29911adef615d2c diff --git a/apps/cli/README.zh.md b/apps/cli/README.zh.md index 9e5507ad76..3a87f6750e 100644 --- a/apps/cli/README.zh.md +++ b/apps/cli/README.zh.md @@ -29,14 +29,14 @@ dsh --help # the launcher's own help ## Profile -profile 目录包含一个 `package.json`,其中记录另行安装的插件依赖,以及 profile manifest(元数据清单)`dsh.profile` 和其中按顺序排列的 `bundles` 列表;还包含一个 `cordis.patch.yml`,其中保存用户自己的 patch 层。 +profile 目录包含一个 `package.json`,其中记录树外插件依赖,以及 profile manifest(元数据清单)`dsh.profile` 和其中按顺序排列的 `bundles` 列表;还包含一个 `cordis.patch.yml`,其中保存用户自己的 patch 层。 配置树以空根为起点,依次叠加以下配置层: - `dsh.profile.bundles` 中各组合包的 patch - profile 自身的 `cordis.patch.yml`,然后是 home 级的 `$DSH_HOME/cordis.patch.yml` - `--patch` 指定的覆盖层 -`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自身的 `node_modules` 解析;pnpm 会将另行安装的插件放入该目录。 +`dsh.profile.bundles` 中列出的组合包先从 dsh 安装目录解析(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`),再从 profile 自身的 `node_modules` 解析;pnpm 会将树外插件安装到该目录。 使用 `--dump-default-config` 和 `--dump-config` 可在不启动的情况下检查组合后的配置树。 diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index ebbede237a..cbc7fe54f9 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/reference/README.md README.md: 17d63fe73ea2b3bda74e9c4da91555b34c21f4ab -README.zh.md: 83945a09073057b8c220da9fc1a6b2b84b98f599 +README.zh.md: 31e0143a0ab9007cedcb0c49788c26cc61caa8a3 diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index 83945a0907..31e0143a0a 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -8,7 +8,7 @@ `dsh --profile ` 启动位于 `$DSH_HOME/profiles/` 的 profile。生效配置树以空根节点为起点,依次叠加 profile manifest(元数据清单)的 `dsh.profile.bundles` 列表中指定的各组合包 patch、profile 自身的 `cordis.patch.yml`、home 级的 `$DSH_HOME/cordis.patch.yml`(这是各 profile 共享的机器本地偏好,因此优先于逐 profile 配置层),以及按 argv 顺序指定的各个 `--patch ` 覆盖层。对同一配置行,后应用的层优先。patch 会替换目标行的整个 `config` 值,而不是深度合并其中的键;patch 也可以插入新行。配置解析、schema 校验、模块解析或插件启动失败时,系统会报告错误并以非零状态退出。收到 SIGINT 或 SIGTERM 时,挂载的根节点会先 dispose(资源释放)再退出。 -组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`)始终来自当前运行的 `dsh` 所属的安装;另行安装的组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。该目录为 dsh 安装中的应用和组合包所依赖的每个包各维护一个符号链接,并在每次启动时修复这些链接。 +组合包名称先从 dsh 安装目录解析,再从 profile 目录解析。因此,内置组合包(`@deepseek-ai/dsh-base`、`@deepseek-ai/dsh-web-app`、`@deepseek-ai/dsh-headless`)始终来自当前运行的 `dsh` 所属的安装;树外组合包则来自 profile 中由 pnpm 管理的 `node_modules`。patch 行中的裸插件 `name` 会从 profile 目录开始,按照 Node 的模块解析规则逐级向父目录查找,直至由 dsh 维护的安装后备目录 `$DSH_HOME/profiles/node_modules`。该目录为 dsh 安装中的应用和组合包所依赖的每个包各维护一个符号链接,并在每次启动时修复这些链接。 `web` 和 `headless` profile 首次使用时会从随附模板自动初始化(`web`:base + web-app;`headless`:base + headless)。其他缺失的 profile 会显式报错,并提示运行 `dsh plugin --profile add `。 @@ -40,7 +40,7 @@ dsh --profile web --patch ./extra.yml --dump-config ## 插件管理 -`dsh plugin --profile ` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,系统都会根据当前安装状态更新 `dsh.profile.bundles`:如果某项依赖解析到的包在 manifest(元数据清单)中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,该依赖就会加入配置层栈;如果某项依赖在 `update` 后获得该声明,也会随即激活。没有组合包声明的依赖仍作为普通依赖保留,并显示一次性警告;已移除的依赖则从配置层栈中删除。 +`dsh plugin --profile ` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,系统都会根据当前安装状态更新 `dsh.profile.bundles`:如果某项依赖解析到的包在 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,该依赖就会加入配置层栈;如果某项依赖在 `update` 后获得该声明,也会随即激活。没有组合包声明的依赖仍作为普通依赖保留,并显示一次性警告;已移除的依赖则从配置层栈中删除。 ```sh dsh plugin --profile tui add github:deepseek-harness/turtle-ui diff --git a/packages/core/tools/README.i18n.yaml b/packages/core/tools/README.i18n.yaml index 9cda431253..3e3802d74d 100644 --- a/packages/core/tools/README.i18n.yaml +++ b/packages/core/tools/README.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/core/tools/README.md README.md: 44eb25b79436a75f08406102fc1e3734e59b1001 -README.zh.md: d77b4ee5248b19418c27674faf7afd52957c6382 +README.zh.md: a395f453bab5ade3530219f4883575b55c1068ff diff --git a/packages/core/tools/README.zh.md b/packages/core/tools/README.zh.md index d77b4ee524..a395f453ba 100644 --- a/packages/core/tools/README.zh.md +++ b/packages/core/tools/README.zh.md @@ -13,7 +13,7 @@ tools: mode: native # native (default) | code | both ``` -`native` 以函数定义的形式贡献可见工具。`code` 会提供预留的 `run_code` 传输、生成的 `tools:sdk` 段,以及声明「只有 `run_code` 可被直接调用」的 `tools:code-only` 规则。执行器随后强制执行该规则:模型直接调用其他任何工具时,会在策略运行前将该调用解析为 `UNKNOWN_TOOL`;`both` 同时提供两种形式,且不声明该规则,因为其中的原生调用确实可以执行。没有单独声明呈现模式的 agent 默认采用此配置;agent preset 可通过 [`dsh-agent-tool-mode`](../agent-tool-mode/README.md) 自行选择呈现模式。不能注册、遮蔽、限制或移除该保留传输,且无论配置何种模式,该名称都是保留的,因为任何 agent 都可能选择 code 模式。非原生模式要求所加载 `ctx.codeRuntime` 的 `language` 有已注册的 SDK 渲染器——TypeScript 经 [`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md) 交付;Python 渲染器内置,驱动任何报告 `language: 'python'` 的运行时(第一方 `dsh-code-runtime-python` 后端另行交付)。没有渲染器的运行时语言会导致提示词组装明确失败;如果 `systemPrompt.toolOrder` 条目指向当前模式未贡献的工具,系统会拒绝组装提示词。`system-prompt/assemble` 监听器可以替换注册表贡献;它返回的组装结果具有权威性,因此该监听器负责保留可用的 Code Mode 协议。 +`native` 以函数定义的形式贡献可见工具。`code` 会提供保留的 `run_code` 传输、生成的 `tools:sdk` 段,以及声明「只有 `run_code` 可被直接调用」的 `tools:code-only` 规则。执行器随后强制执行该规则:模型直接调用其他任何工具时,会在策略运行前将该调用解析为 `UNKNOWN_TOOL`;`both` 同时提供两种形式,且不声明该规则,因为其中的原生调用确实可以执行。没有单独声明呈现模式的 agent 默认采用此配置;agent preset 可通过 [`dsh-agent-tool-mode`](../agent-tool-mode/README.md) 自行选择呈现模式。不能注册、遮蔽、限制或移除该保留传输,且无论配置何种模式,该名称都是保留的,因为任何 agent 都可能选择 code 模式。非原生模式要求所加载 `ctx.codeRuntime` 的 `language` 有已注册的 SDK 渲染器——TypeScript 经 [`dsh-code-runtime-worker`](../../code-runtime/code-runtime-worker/README.md) 交付;Python 渲染器内置,驱动任何报告 `language: 'python'` 的运行时(第一方 `dsh-code-runtime-python` 后端另行交付)。没有渲染器的运行时语言会导致提示词组装明确失败;如果 `systemPrompt.toolOrder` 条目指向当前模式未贡献的工具,系统会拒绝组装提示词。`system-prompt/assemble` 监听器可以替换注册表贡献;它返回的组装结果具有权威性,因此该监听器负责保留可用的 Code Mode 协议。 ### 公开 API @@ -120,7 +120,7 @@ ctx.tools.register(defineTool({ 在 `code`(而非 `both`)下,该传输同时也是模型唯一可用的入口:模型直呼其他任何可见工具名,都会在创建执行时、早于 `tools/pre-execute`、审批 `ask` 和 guards 解析为 `UNKNOWN_TOOL`,因此没有任何一方会观察或批准一个注定失败的调用。拒绝信息会给出正确路径(`only \`run_code\` is callable directly — call \`\` from inside a \`run_code\` program instead`),因为同一份提示词刚刚声明过那个工具,只说 `unknown tool` 会被读成部署损坏。SDK 子分发携带外层执行的 `parent` token,不受此限制,因此程序保留 SDK 声明的全部绑定。参见[执行器塌缩 note](../../../.agents/notes/implemented/bug-fix/2026-08-07-code-mode-executor-collapse.md)、[Code Mode 基础](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)、[类型化返回约定](../../../.agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.md)和[代码运行时 seam](../../code-runtime/README.md)。可以运行 `pnpm run demo:code-mode` 试用。 - **SDK 段**(`tools:sdk`,顺序 150):一个在组装时求值的提示词段,每次组装都会重新生成与所加载运行时语言相符的 SDK 文本。TypeScript 形态会生成 `JsonValue`、精确的 `ToolArgsMap` / `ToolOutputMap`、`ToolName`、`ToolCallError` 声明,以及映射调用作用域最终可见工具的 `tools` 命名空间(特殊名称使用带引号的键),并附带固定的使用说明;Python 形态(`ctx.codeRuntime.language === 'python'`)发出等价的具名 `TypedDict` 与一个带相同用法说明的 `tools` 对象。其输出具有确定性:工具按字典序排列;工具集合不变时,文本逐字节相同(有利于前缀 cache)。两个代码生成器都已导出,且绝不会在提示词组装期间抛出:`jsonSchemaToTs` 处理统一 schema 的每种构造并将不受支持的原始构造降级为 `unknown`;`jsonSchemaToPy` 同理,降级为 `Any`(当某字段名不是合法的 `TypedDict` 属性时,或在 SDK 渲染之外被调用时——`TypedDict` 声明所需的命名上下文由该渲染提供——整个对象降级为 `dict[str, Any]`)。 -- **分发桥接层**(`run_code` 的 execute):每个绑定调用都会在分发前快照为无损 JSON(`undefined`、`BigInt`、循环、稀疏数组、`-0` 和特殊对象会使该次调用被拒绝),经由每次运行独有、复用原生并发约定的池调度——调用严格按提交顺序启动,连续的 `isConcurrencySafe` 调用最多可重叠经校验的 `maxParallelSubCalls` 配置个(默认 10;设为 `1` 即恢复串行分发),被分类为独占的调用先排空池、单独运行并阻挡其后的调用——以外层执行的不透明 token 作为 `parent`,并经过完整的 pre-execute → guards → execute → post-execute → result 流水线。成功会返回策略处理后的最终规范值;失败以一条消息到达 worker,并成为 `ToolCallError(toolName, message)`。每个已启动的子调用在进入流水线时记录一条 `tool/code-dispatch-start` 事件(确定性 id `:code:`,按提交顺序编号),并以一条携带完整模型可见 `content`/`isError` 结果的 `tool/code-dispatch` 事件完结(采用 `tool/result` 词汇,因此 UI 会沿原生路径呈现子调用——这对事件的 `time` 字段承载每个子调用的计时);因 run 结算而被放弃的排队调用两者都不记录。`deriveMessages()` 既不公开这两个事件,也不持久化规范值。token 关联使按提交语义工作的观察器可以延后提交内部调用的成功结果,直到最终 `run_code` 结果确定,而无需接收实时可变的外层执行对象;普通工具副作用不会回滚。每个子调用的 `additionalContexts` 条目都会按分发顺序通过外层 `ToolRunContext` 延迟;循环只在父级 `run_code` 结果之后追加这些上下文,从而保持相邻关系,并且即使程序后来失败,也会保留各自的来源/元数据。 +- **分发桥接层**(`run_code` 的 execute):每个绑定调用都会在分发前快照为无损 JSON(`undefined`、`BigInt`、循环、稀疏数组、`-0` 和特殊对象会使该次调用被拒绝),经由每次运行独有、复用原生并发约定的池调度——调用严格按提交顺序启动,连续的 `isConcurrencySafe` 调用最多可重叠经校验的 `maxParallelSubCalls` 配置个(默认 10;设为 `1` 即恢复串行分发),被分类为独占的调用先排空池、单独运行并阻挡其后的调用——以外层执行的不透明 token 作为 `parent`,并经过完整的 pre-execute → guards → execute → post-execute → result 流水线。成功会返回策略处理后的最终规范值;失败以一条消息到达 worker,并成为 `ToolCallError(toolName, message)`。每个已启动的子调用在进入流水线时记录一条 `tool/code-dispatch-start` 事件(确定性 id `:code:`,按提交顺序编号),并以一条携带完整模型可见 `content`/`isError` 结果的 `tool/code-dispatch` 事件完结(采用 `tool/result` 词汇,因此 UI 会沿原生路径呈现子调用——这对事件的 `time` 字段承载每个子调用的计时);因 run 结算而被放弃的排队调用两者都不记录。`deriveMessages()` 既不公开这两个事件,也不持久化规范值。token 关联使按提交语义工作的观察器可以延后提交内部调用的成功结果,直到最终 `run_code` 结果确定,而无需暴露进行中的外层执行;普通工具副作用不会回滚。每个子调用的 `additionalContexts` 条目都会按分发顺序通过外层 `ToolRunContext` 延迟;循环只在父级 `run_code` 结果之后追加这些上下文,从而保持相邻关系,并且即使程序后来失败,也会保留各自的来源/元数据。 - **结算纪律**:桥接层拥有一个运行作用域的中止机制;该中止会跟随传入的外层信号,并在运行因任何原因结算时触发,因此预算耗尽会中止正在运行的子工具,而不会将其遗留。桥接层随后会在返回之前排空队列,使每个 `tool/code-dispatch` 都落在仍打开的轮次内。失败的运行会抛出 `CodeRunFailedError`(`code: 'CODE_RUN_FAILED'`,message = 失败类型 + 已捕获日志),流水线会将其转换为模型可据以自我修正的结构化 `isError`。 - **结果大小**:中间绑定值会完整传入 worker 进程,且没有逐绑定字节上限。`run_code` 返回规范的 `{ logs: string[], result?: JsonValue }`;字符串原样呈现,其他所有存在的 JSON 根都通过栈安全的美化 JSON 遍历呈现,总缩进最多为 10 个字符(更深的子树保持紧凑),`null` 保持显式,而缺少 `result` 表示程序返回 `undefined`。worker 可配置的 `maxOutputBytes`(默认 64 MiB)只应用于组合序列化后的外层日志数组、完成值或失败消息载荷;固定的结果封装语法和呈现空白不计入该上限。无效和超限的完成会明确失败,只有这个外层结果可以按常规 spill 机制处理。 diff --git a/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index fa73fe380f..50e4ae9591 100644 --- a/python/sdk/README.i18n.yaml +++ b/python/sdk/README.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write python/sdk/README.md README.md: 70b9d6391644d10ee7d5c29ce122632786e3bbcc -README.zh.md: 63824ed2a4427835cbbd5ef3491bc7cb0fb94f21 +README.zh.md: 02217d62b8f24dc6806356c93f63574f2fb9f129 diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index 63824ed2a4..02217d62b8 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -35,7 +35,7 @@ with DeepSeekHarness( result = harness.run("Make the requested code change.") ``` -`provider` 选择指定 Cordis 组合所注册的提供方路由;`model` 是该适配器解析出的模型 ID。`max_tokens` 是一个可选的正值,用于限制根 agent(智能体)及其进程内后代在每次请求中输出的 token 数量;省略该参数时,由提供方的默认行为决定输出上限。压缩摘要继续使用压缩插件单独配置的上限。内置默认组合注册 `deepseek-official`。自定义组合可以挂载 `llm-pi-ai`,在其中配置各提供方专属的凭据和端点,并选择 pi-ai 已安装 catalog 中存在的任意提供方/模型组合。 +`provider` 选择指定 Cordis 组合所注册的提供方路由;`model` 是该适配器解析出的模型 ID。`max_tokens` 是一个可选的正整数,用于限制根 agent 及其进程内后代在每次请求中输出的 token 数量;省略该参数时,由提供方的默认行为决定输出上限。压缩摘要继续使用压缩插件单独配置的上限。内置默认组合注册 `deepseek-official`。自定义组合可以挂载 `llm-pi-ai`,在其中配置各提供方专属的凭据和端点,并选择 pi-ai 已安装 catalog 中存在的任意提供方/模型组合。 [Python SDK 教程](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/guide/python-sdk.md)提供一套无需使用 Web UI、按步骤完成安装和首次运行的流程。该教程所用的完整独立 Cordis 配置文件位于 [`jsonrpc-agent` 示例](https://github.com/deepseek-ai/deepseek-harness/blob/master/examples/jsonrpc-agent/README.md)中。 From a2d0f7f41121ee81911dd1badbf248edd3f2ab70 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:36:22 +0800 Subject: [PATCH 027/160] refactor: apply repository naming contract Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change. --- ...026-06-11-event-sourced-sessions.i18n.yaml | 4 +- .../2026-06-11-event-sourced-sessions.md | 2 +- .../2026-06-11-event-sourced-sessions.zh.md | 2 +- .../2026-06-13-capability-seams.i18n.yaml | 4 +- .../2026-06-13-capability-seams.md | 8 +- .../2026-06-13-capability-seams.zh.md | 6 +- .../2026-06-14-session-persistence.i18n.yaml | 4 +- .../2026-06-14-session-persistence.md | 2 +- .../2026-06-14-session-persistence.zh.md | 2 +- ...06-17-filesystem-capability-seam.i18n.yaml | 4 +- .../2026-06-17-filesystem-capability-seam.md | 32 +- ...026-06-17-filesystem-capability-seam.zh.md | 32 +- ...ifecycle-and-ownership-contracts.i18n.yaml | 4 +- ...agent-lifecycle-and-ownership-contracts.md | 8 +- ...nt-lifecycle-and-ownership-contracts.zh.md | 4 +- .../2026-06-18-session-surface.i18n.yaml | 4 +- .../2026-06-18-session-surface.md | 2 +- .../2026-06-18-session-surface.zh.md | 2 +- .../2026-06-20-branded-ids.i18n.yaml | 4 +- .../architecture/2026-06-20-branded-ids.md | 16 +- .../architecture/2026-06-20-branded-ids.zh.md | 16 +- ...eneric-long-running-tool-runtime.i18n.yaml | 4 +- ...06-20-generic-long-running-tool-runtime.md | 64 +- ...20-generic-long-running-tool-runtime.zh.md | 62 +- ...-21-bounded-llm-request-recovery.i18n.yaml | 4 +- ...2026-06-21-bounded-llm-request-recovery.md | 4 +- ...6-06-21-bounded-llm-request-recovery.zh.md | 4 +- ...andatory-app-attribution-headers.i18n.yaml | 4 +- ...06-21-mandatory-app-attribution-headers.md | 2 +- ...21-mandatory-app-attribution-headers.zh.md | 2 +- .../2026-06-24-web-capability-seam.i18n.yaml | 4 +- .../2026-06-24-web-capability-seam.md | 32 +- .../2026-06-24-web-capability-seam.zh.md | 32 +- ...06-26-file-context-as-event-gate.i18n.yaml | 4 +- .../2026-06-26-file-context-as-event-gate.md | 58 +- ...026-06-26-file-context-as-event-gate.zh.md | 58 +- ...ash-stdin-env-trusted-plugin-api.i18n.yaml | 4 +- ...06-30-bash-stdin-env-trusted-plugin-api.md | 6 +- ...30-bash-stdin-env-trusted-plugin-api.zh.md | 6 +- ...6-07-02-tool-render-intent-union.i18n.yaml | 4 +- .../2026-07-02-tool-render-intent-union.md | 4 +- .../2026-07-02-tool-render-intent-union.zh.md | 2 +- ...bles-and-tool-guidance-ownership.i18n.yaml | 4 +- ...t-variables-and-tool-guidance-ownership.md | 4 +- ...ariables-and-tool-guidance-ownership.zh.md | 4 +- ...6-07-05-reconstructable-requests.i18n.yaml | 4 +- .../2026-07-05-reconstructable-requests.md | 4 +- .../2026-07-05-reconstructable-requests.zh.md | 4 +- ...6-07-06-timeout-deadline-library.i18n.yaml | 4 +- .../2026-07-06-timeout-deadline-library.md | 10 +- .../2026-07-06-timeout-deadline-library.zh.md | 10 +- ...06-tool-result-retention-library.i18n.yaml | 4 +- ...026-07-06-tool-result-retention-library.md | 8 +- ...-07-06-tool-result-retention-library.zh.md | 6 +- ...6-07-07-tool-call-timeout-policy.i18n.yaml | 4 +- .../2026-07-07-tool-call-timeout-policy.md | 20 +- .../2026-07-07-tool-call-timeout-policy.zh.md | 20 +- ...26-07-08-tool-output-spill-files.i18n.yaml | 4 +- .../2026-07-08-tool-output-spill-files.md | 12 +- .../2026-07-08-tool-output-spill-files.zh.md | 12 +- ...n-pressure-and-overflow-recovery.i18n.yaml | 4 +- ...mpaction-pressure-and-overflow-recovery.md | 16 +- ...ction-pressure-and-overflow-recovery.zh.md | 16 +- ...cutable-sdk-runtime-distribution.i18n.yaml | 4 +- ...ile-executable-sdk-runtime-distribution.md | 16 +- ...-executable-sdk-runtime-distribution.zh.md | 16 +- ...07-12-agent-scope-runtime-design.i18n.yaml | 4 +- .../2026-07-12-agent-scope-runtime-design.md | 10 +- ...026-07-12-agent-scope-runtime-design.zh.md | 10 +- .../2026-07-12-scoped-layers-store.i18n.yaml | 4 +- .../2026-07-12-scoped-layers-store.md | 10 +- .../2026-07-12-scoped-layers-store.zh.md | 10 +- ...-14-provider-routed-llm-adapters.i18n.yaml | 4 +- ...2026-07-14-provider-routed-llm-adapters.md | 8 +- ...6-07-14-provider-routed-llm-adapters.zh.md | 8 +- ...2026-07-15-agent-initiator-scope.i18n.yaml | 2 +- .../2026-07-15-agent-initiator-scope.md | 2 +- ...-model-catalog-and-acp-selection.i18n.yaml | 4 +- ...-15-llm-model-catalog-and-acp-selection.md | 8 +- ...-llm-model-catalog-and-acp-selection.zh.md | 8 +- .../2026-07-15-lsp-capability-seam.i18n.yaml | 4 +- .../2026-07-15-lsp-capability-seam.md | 14 +- .../2026-07-15-lsp-capability-seam.zh.md | 14 +- ...07-15-replay-token-meter-service.i18n.yaml | 4 +- .../2026-07-15-replay-token-meter-service.md | 16 +- ...026-07-15-replay-token-meter-service.zh.md | 18 +- ...19-gui-layering-and-rpc-protocol.i18n.yaml | 4 +- ...026-07-19-gui-layering-and-rpc-protocol.md | 8 +- ...-07-19-gui-layering-and-rpc-protocol.zh.md | 8 +- ...kage-invariant-runtime-contracts.i18n.yaml | 4 +- ...-19-package-invariant-runtime-contracts.md | 10 +- ...-package-invariant-runtime-contracts.zh.md | 10 +- ...0-canonical-tool-output-contract.i18n.yaml | 4 +- ...26-07-20-canonical-tool-output-contract.md | 10 +- ...07-20-canonical-tool-output-contract.zh.md | 6 +- ...el-context-and-compaction-policy.i18n.yaml | 4 +- ...ted-model-context-and-compaction-policy.md | 12 +- ...-model-context-and-compaction-policy.zh.md | 12 +- ...2-slot-type-chain-implementation.i18n.yaml | 4 +- ...26-07-22-slot-type-chain-implementation.md | 2 +- ...07-22-slot-type-chain-implementation.zh.md | 2 +- ...7-23-client-plugin-loading-model.i18n.yaml | 4 +- .../2026-07-23-client-plugin-loading-model.md | 8 +- ...26-07-23-client-plugin-loading-model.zh.md | 8 +- ...ed-reasoning-effort-capabilities.i18n.yaml | 4 +- ...ter-owned-reasoning-effort-capabilities.md | 4 +- ...-owned-reasoning-effort-capabilities.zh.md | 4 +- ...-24-single-harness-home-resolver.i18n.yaml | 4 +- ...2026-07-24-single-harness-home-resolver.md | 10 +- ...6-07-24-single-harness-home-resolver.zh.md | 10 +- ...tree-boot-and-transport-layering.i18n.yaml | 4 +- ...config-tree-boot-and-transport-layering.md | 2 +- ...fig-tree-boot-and-transport-layering.zh.md | 2 +- ...ession-scope-and-provide-channel.i18n.yaml | 4 +- ...lient-session-scope-and-provide-channel.md | 4 +- ...nt-session-scope-and-provide-channel.zh.md | 4 +- ...eb-command-surfaces-and-assembly.i18n.yaml | 4 +- ...07-25-web-command-surfaces-and-assembly.md | 10 +- ...25-web-command-surfaces-and-assembly.zh.md | 10 +- ...input-machine-and-slash-pipeline.i18n.yaml | 4 +- ...25-web-input-machine-and-slash-pipeline.md | 18 +- ...web-input-machine-and-slash-pipeline.zh.md | 18 +- ...=> 2026-07-26-job-registry-seam.i18n.yaml} | 6 +- .../2026-07-26-job-registry-seam.md | 35 + .../2026-07-26-job-registry-seam.zh.md | 35 + .../2026-07-26-subprocess-seam.i18n.yaml | 4 +- .../2026-07-26-subprocess-seam.md | 18 +- .../2026-07-26-subprocess-seam.zh.md | 16 +- .../2026-07-26-task-registry-seam.md | 35 - .../2026-07-26-task-registry-seam.zh.md | 35 - ...ompiler-independent-typert-model.i18n.yaml | 4 +- ...07-27-compiler-independent-typert-model.md | 2 +- ...27-compiler-independent-typert-model.zh.md | 2 +- ...rtable-execution-world-consumers.i18n.yaml | 4 +- ...7-28-portable-execution-world-consumers.md | 8 +- ...8-portable-execution-world-consumers.zh.md | 8 +- .../2026-07-28-user-settings-seam.i18n.yaml | 4 +- .../2026-07-28-user-settings-seam.md | 2 +- .../2026-07-28-user-settings-seam.zh.md | 2 +- ...-07-29-dsh-source-launch-tsx-esm.i18n.yaml | 4 +- .../2026-07-29-dsh-source-launch-tsx-esm.md | 2 +- ...2026-07-29-dsh-source-launch-tsx-esm.zh.md | 2 +- .../2026-07-29-package-regrouping.i18n.yaml | 4 +- .../2026-07-29-package-regrouping.md | 33 +- .../2026-07-29-package-regrouping.zh.md | 33 +- ...-token-usage-and-request-context.i18n.yaml | 4 +- ...ojected-token-usage-and-request-context.md | 2 +- ...cted-token-usage-and-request-context.zh.md | 2 +- ...est-level-llm-config-credentials.i18n.yaml | 4 +- ...29-request-level-llm-config-credentials.md | 2 +- ...request-level-llm-config-credentials.zh.md | 2 +- ...-29-terminal-llm-stream-failures.i18n.yaml | 4 +- ...2026-07-29-terminal-llm-stream-failures.md | 8 +- ...6-07-29-terminal-llm-stream-failures.zh.md | 8 +- ...adapter-owned-max-token-defaults.i18n.yaml | 4 +- ...-07-30-adapter-owned-max-token-defaults.md | 4 +- ...-30-adapter-owned-max-token-defaults.zh.md | 4 +- ...26-07-30-config-plane-boundaries.i18n.yaml | 4 +- .../2026-07-30-config-plane-boundaries.md | 2 +- .../2026-07-30-config-plane-boundaries.zh.md | 2 +- ...undaries-and-atomic-registration.i18n.yaml | 4 +- ...tial-boundaries-and-atomic-registration.md | 4 +- ...l-boundaries-and-atomic-registration.zh.md | 4 +- ...30-session-end-seed-log-boundary.i18n.yaml | 4 +- ...026-07-30-session-end-seed-log-boundary.md | 10 +- ...-07-30-session-end-seed-log-boundary.zh.md | 10 +- ...30-settings-write-path-integrity.i18n.yaml | 4 +- ...026-07-30-settings-write-path-integrity.md | 2 +- ...-07-30-settings-write-path-integrity.zh.md | 2 +- .../2026-07-30-web-config-plane.i18n.yaml | 4 +- .../2026-07-30-web-config-plane.md | 4 +- .../2026-07-30-web-config-plane.zh.md | 4 +- ...claimed-pre-step-inbox-lifecycle.i18n.yaml | 4 +- ...-07-31-claimed-pre-step-inbox-lifecycle.md | 2 +- ...-31-claimed-pre-step-inbox-lifecycle.zh.md | 2 +- ...runtime-portable-identifier-seam.i18n.yaml | 4 +- ...1-code-runtime-portable-identifier-seam.md | 2 +- ...ode-runtime-portable-identifier-seam.zh.md | 2 +- ...-07-31-goal-owned-durable-events.i18n.yaml | 4 +- .../2026-07-31-goal-owned-durable-events.md | 4 +- ...2026-07-31-goal-owned-durable-events.zh.md | 4 +- ...26-08-01-packaged-ripgrep-search.i18n.yaml | 4 +- .../2026-08-01-packaged-ripgrep-search.md | 2 +- .../2026-08-01-packaged-ripgrep-search.zh.md | 2 +- ...08-02-typert-remote-method-calls.i18n.yaml | 4 +- .../2026-08-02-typert-remote-method-calls.md | 116 +- ...026-08-02-typert-remote-method-calls.zh.md | 116 +- ...4-configuration-source-ownership.i18n.yaml | 4 +- ...26-08-04-configuration-source-ownership.md | 4 +- ...08-04-configuration-source-ownership.zh.md | 4 +- ...-a-provider-from-the-models-page.i18n.yaml | 4 +- ...claring-a-provider-from-the-models-page.md | 2 +- ...ring-a-provider-from-the-models-page.zh.md | 2 +- ...026-08-05-profile-plugin-bundles.i18n.yaml | 4 +- .../2026-08-05-profile-plugin-bundles.md | 2 +- .../2026-08-05-profile-plugin-bundles.zh.md | 2 +- ...08-05-slot-declaration-injection.i18n.yaml | 4 +- .../2026-08-05-slot-declaration-injection.md | 4 +- ...026-08-05-slot-declaration-injection.zh.md | 4 +- ...ubagent-list-identity-projection.i18n.yaml | 4 +- ...08-06-subagent-list-identity-projection.md | 2 +- ...06-subagent-list-identity-projection.zh.md | 2 +- ...hared-feedback-telemetry-user-id.i18n.yaml | 4 +- ...08-07-shared-feedback-telemetry-user-id.md | 4 +- ...07-shared-feedback-telemetry-user-id.zh.md | 4 +- ...08-08-per-preset-standing-mounts.i18n.yaml | 4 +- .../2026-08-08-per-preset-standing-mounts.md | 8 +- ...026-08-08-per-preset-standing-mounts.zh.md | 8 +- ...lient-conversation-node-assembly.i18n.yaml | 4 +- ...08-09-client-conversation-node-assembly.md | 2 +- ...09-client-conversation-node-assembly.zh.md | 2 +- ...headless-direct-core-entry-point.i18n.yaml | 4 +- ...-08-09-headless-direct-core-entry-point.md | 2 +- ...-09-headless-direct-core-entry-point.zh.md | 2 +- ...026-08-09-layered-skill-registry.i18n.yaml | 4 +- .../2026-08-09-layered-skill-registry.md | 4 +- .../2026-08-09-layered-skill-registry.zh.md | 4 +- ...8-10-fork-children-stay-one-shot.i18n.yaml | 4 +- .../2026-08-10-fork-children-stay-one-shot.md | 6 +- ...26-08-10-fork-children-stay-one-shot.zh.md | 6 +- ...st-plane-ownership-after-presets.i18n.yaml | 4 +- ...8-10-host-plane-ownership-after-presets.md | 4 +- ...0-host-plane-ownership-after-presets.zh.md | 4 +- ...6-08-10-message-feedback-sidecar.i18n.yaml | 4 +- .../2026-08-10-message-feedback-sidecar.md | 2 +- .../2026-08-10-message-feedback-sidecar.zh.md | 2 +- ...2026-08-10-remote-event-delivery.i18n.yaml | 4 +- .../2026-08-10-remote-event-delivery.md | 36 +- .../2026-08-10-remote-event-delivery.zh.md | 36 +- .../2026-08-11-plugin-settings-tabs.i18n.yaml | 4 +- .../2026-08-11-plugin-settings-tabs.md | 10 +- .../2026-08-11-plugin-settings-tabs.zh.md | 10 +- ...20-error-cause-chain-diagnostics.i18n.yaml | 4 +- ...026-07-20-error-cause-chain-diagnostics.md | 2 +- ...-07-20-error-cause-chain-diagnostics.zh.md | 2 +- ...ction-summary-prefix-cache-reuse.i18n.yaml | 4 +- ...1-compaction-summary-prefix-cache-reuse.md | 12 +- ...ompaction-summary-prefix-cache-reuse.zh.md | 12 +- ...mpty-model-response-is-retryable.i18n.yaml | 4 +- ...07-24-empty-model-response-is-retryable.md | 2 +- ...24-empty-model-response-is-retryable.zh.md | 2 +- ...2026-07-28-web-gui-feedback-loop.i18n.yaml | 2 +- .../2026-07-28-web-gui-feedback-loop.md | 2 +- ...9-human-transcript-append-origin.i18n.yaml | 4 +- ...26-07-29-human-transcript-append-origin.md | 12 +- ...07-29-human-transcript-append-origin.zh.md | 12 +- ...select-custom-answer-composition.i18n.yaml | 2 +- ...-multi-select-custom-answer-composition.md | 2 +- ...ranscript-log-ordered-projection.i18n.yaml | 4 +- ...0-web-transcript-log-ordered-projection.md | 16 +- ...eb-transcript-log-ordered-projection.zh.md | 16 +- ...-fork-anchor-floors-to-event-seq.i18n.yaml | 4 +- ...6-07-31-fork-anchor-floors-to-event-seq.md | 2 +- ...7-31-fork-anchor-floors-to-event-seq.zh.md | 2 +- ...same-basename-workspace-adoption.i18n.yaml | 4 +- ...-07-31-same-basename-workspace-adoption.md | 2 +- ...-31-same-basename-workspace-adoption.zh.md | 2 +- ...-ask-user-delegated-caller-guard.i18n.yaml | 4 +- ...6-08-01-ask-user-delegated-caller-guard.md | 6 +- ...8-01-ask-user-delegated-caller-guard.zh.md | 6 +- ...3-cli-signal-shutdown-escalation.i18n.yaml | 2 +- ...26-08-03-cli-signal-shutdown-escalation.md | 2 +- ...3-hmr-initial-scan-boot-deadlock.i18n.yaml | 2 +- ...26-08-03-hmr-initial-scan-boot-deadlock.md | 2 +- ...ontext-meter-blind-to-compaction.i18n.yaml | 4 +- ...08-05-context-meter-blind-to-compaction.md | 4 +- ...05-context-meter-blind-to-compaction.zh.md | 4 +- ...-08-06-api-key-format-validation.i18n.yaml | 4 +- .../2026-08-06-api-key-format-validation.md | 8 +- ...2026-08-06-api-key-format-validation.zh.md | 8 +- ...8-06-host-backed-web-preferences.i18n.yaml | 4 +- .../2026-08-06-host-backed-web-preferences.md | 2 +- ...26-08-06-host-backed-web-preferences.zh.md | 2 +- ...rding-step-owned-takeover-chrome.i18n.yaml | 4 +- ...6-onboarding-step-owned-takeover-chrome.md | 2 +- ...nboarding-step-owned-takeover-chrome.zh.md | 2 +- ...e-unpriced-replace-compatibility.i18n.yaml | 4 +- ...-surface-unpriced-replace-compatibility.md | 2 +- ...rface-unpriced-replace-compatibility.zh.md | 2 +- ...07-cancel-convergence-wake-latch.i18n.yaml | 2 +- ...026-08-07-cancel-convergence-wake-latch.md | 2 +- ...8-07-code-mode-executor-collapse.i18n.yaml | 4 +- .../2026-08-07-code-mode-executor-collapse.md | 4 +- ...26-08-07-code-mode-executor-collapse.zh.md | 4 +- ...9-filesystem-absence-observation.i18n.yaml | 4 +- ...26-08-09-filesystem-absence-observation.md | 4 +- ...08-09-filesystem-absence-observation.zh.md | 4 +- ...-agents-join-their-parent-preset.i18n.yaml | 4 +- ...0-child-agents-join-their-parent-preset.md | 6 +- ...hild-agents-join-their-parent-preset.zh.md | 6 +- ...08-10-pre-plugin-theme-bootstrap.i18n.yaml | 4 +- .../2026-08-10-pre-plugin-theme-bootstrap.md | 10 +- ...026-08-10-pre-plugin-theme-bootstrap.zh.md | 10 +- ...sh-catalog-follows-preset-switch.i18n.yaml | 4 +- ...-10-slash-catalog-follows-preset-switch.md | 10 +- ...-slash-catalog-follows-preset-switch.zh.md | 10 +- ...ounded-background-job-admission.i18n.yaml} | 6 +- ...08-11-bounded-background-job-admission.md} | 24 +- ...11-bounded-background-job-admission.zh.md} | 20 +- ...nt-validates-its-own-composition.i18n.yaml | 4 +- ...ing-agent-validates-its-own-composition.md | 8 +- ...-agent-validates-its-own-composition.zh.md | 8 +- ...chronous-subprocess-exit-cleanup.i18n.yaml | 4 +- ...-11-synchronous-subprocess-exit-cleanup.md | 2 +- ...-synchronous-subprocess-exit-cleanup.zh.md | 2 +- .../2026-06-14-acp-multi-session.i18n.yaml | 4 +- .../feature/2026-06-14-acp-multi-session.md | 10 +- .../2026-06-14-acp-multi-session.zh.md | 4 +- .../feature/2026-06-15-code-mode.i18n.yaml | 4 +- .../feature/2026-06-15-code-mode.md | 10 +- .../feature/2026-06-15-code-mode.zh.md | 10 +- ...26-06-17-filesystem-tool-schemas.i18n.yaml | 4 +- .../2026-06-17-filesystem-tool-schemas.md | 12 +- .../2026-06-17-filesystem-tool-schemas.zh.md | 12 +- ...06-18-compaction-capability-seam.i18n.yaml | 4 +- .../2026-06-18-compaction-capability-seam.md | 58 +- ...026-06-18-compaction-capability-seam.zh.md | 60 +- ...6-06-21-subagent-capability-seam.i18n.yaml | 4 +- .../2026-06-21-subagent-capability-seam.md | 16 +- .../2026-06-21-subagent-capability-seam.zh.md | 16 +- .../2026-06-24-workspace-context.i18n.yaml | 4 +- .../feature/2026-06-24-workspace-context.md | 10 +- .../2026-06-24-workspace-context.zh.md | 10 +- .../feature/2026-06-30-hook-bridges.i18n.yaml | 4 +- .../feature/2026-06-30-hook-bridges.md | 6 +- .../feature/2026-06-30-hook-bridges.zh.md | 6 +- .../2026-06-30-hook-protocol-lib.i18n.yaml | 4 +- .../feature/2026-06-30-hook-protocol-lib.md | 6 +- .../2026-06-30-hook-protocol-lib.zh.md | 6 +- ...30-interception-extension-points.i18n.yaml | 4 +- ...026-06-30-interception-extension-points.md | 4 +- ...-06-30-interception-extension-points.zh.md | 4 +- ...026-06-30-session-store-fork-api.i18n.yaml | 4 +- .../2026-06-30-session-store-fork-api.md | 4 +- .../2026-06-30-session-store-fork-api.zh.md | 4 +- .../2026-07-05-dynamic-workflows.i18n.yaml | 4 +- .../feature/2026-07-05-dynamic-workflows.md | 10 +- .../2026-07-05-dynamic-workflows.zh.md | 8 +- .../feature/2026-07-05-skill-system.i18n.yaml | 4 +- .../feature/2026-07-05-skill-system.md | 4 +- .../feature/2026-07-05-skill-system.zh.md | 4 +- .../2026-07-06-approval-seam.i18n.yaml | 4 +- .../feature/2026-07-06-approval-seam.md | 4 +- .../feature/2026-07-06-approval-seam.zh.md | 4 +- .../2026-07-06-explicit-tool-order.i18n.yaml | 4 +- .../feature/2026-07-06-explicit-tool-order.md | 4 +- .../2026-07-06-explicit-tool-order.zh.md | 4 +- .../feature/2026-07-06-sandbox.i18n.yaml | 4 +- .../implemented/feature/2026-07-06-sandbox.md | 26 +- .../feature/2026-07-06-sandbox.zh.md | 26 +- .../2026-07-07-mcp-client-plugin.i18n.yaml | 4 +- .../feature/2026-07-07-mcp-client-plugin.md | 6 +- .../2026-07-07-mcp-client-plugin.zh.md | 6 +- ...-07-08-background-subagent-tasks.i18n.yaml | 4 +- .../2026-07-08-background-subagent-tasks.md | 14 +- ...2026-07-08-background-subagent-tasks.zh.md | 8 +- ...-self-referential-cordis-toolset.i18n.yaml | 4 +- ...6-07-08-self-referential-cordis-toolset.md | 10 +- ...7-08-self-referential-cordis-toolset.zh.md | 10 +- ...ession-identity-and-log-location.i18n.yaml | 4 +- ...agent-session-identity-and-log-location.md | 8 +- ...nt-session-identity-and-log-location.zh.md | 8 +- ...-10-parallel-tool-call-execution.i18n.yaml | 4 +- ...2026-07-10-parallel-tool-call-execution.md | 2 +- ...6-07-10-parallel-tool-call-execution.zh.md | 2 +- ...nt-persona-tool-filter-and-depth.i18n.yaml | 4 +- ...-subagent-persona-tool-filter-and-depth.md | 6 +- ...bagent-persona-tool-filter-and-depth.zh.md | 6 +- ...26-07-14-cross-family-fs-sandbox.i18n.yaml | 4 +- .../2026-07-14-cross-family-fs-sandbox.md | 20 +- .../2026-07-14-cross-family-fs-sandbox.zh.md | 20 +- .../2026-07-16-harness-level-loop.i18n.yaml | 4 +- .../feature/2026-07-16-harness-level-loop.md | 10 +- .../2026-07-16-harness-level-loop.zh.md | 4 +- ...26-07-16-persistent-pty-sessions.i18n.yaml | 4 +- .../2026-07-16-persistent-pty-sessions.md | 42 +- .../2026-07-16-persistent-pty-sessions.zh.md | 42 +- ...-fresh-agent-ralph-workflow-tool.i18n.yaml | 4 +- ...6-07-19-fresh-agent-ralph-workflow-tool.md | 2 +- ...7-19-fresh-agent-ralph-workflow-tool.zh.md | 2 +- ...9-same-session-goal-round-driver.i18n.yaml | 4 +- ...26-07-19-same-session-goal-round-driver.md | 2 +- ...07-19-same-session-goal-round-driver.zh.md | 2 +- ...-20-code-mode-typed-tool-returns.i18n.yaml | 4 +- ...2026-07-20-code-mode-typed-tool-returns.md | 8 +- ...6-07-20-code-mode-typed-tool-returns.zh.md | 8 +- ...26-07-20-dsh-cli-personal-config.i18n.yaml | 4 +- .../2026-07-20-dsh-cli-personal-config.md | 4 +- .../2026-07-20-dsh-cli-personal-config.zh.md | 4 +- ...continuable-background-subagents.i18n.yaml | 4 +- ...-07-21-continuable-background-subagents.md | 46 +- ...-21-continuable-background-subagents.zh.md | 36 +- ...6-07-21-cross-session-references.i18n.yaml | 4 +- .../2026-07-21-cross-session-references.md | 6 +- .../2026-07-21-cross-session-references.zh.md | 6 +- ...7-21-follow-instruction-symlinks.i18n.yaml | 4 +- .../2026-07-21-follow-instruction-symlinks.md | 6 +- ...26-07-21-follow-instruction-symlinks.zh.md | 6 +- ...07-21-instruction-load-all-dedup.i18n.yaml | 4 +- .../2026-07-21-instruction-load-all-dedup.md | 2 +- ...026-07-21-instruction-load-all-dedup.zh.md | 2 +- ...-07-21-local-instruction-overlay.i18n.yaml | 4 +- .../2026-07-21-local-instruction-overlay.md | 2 +- ...2026-07-21-local-instruction-overlay.zh.md | 2 +- ...-07-21-log-backed-session-titles.i18n.yaml | 2 +- .../2026-07-21-log-backed-session-titles.md | 10 +- ...subagent-catalog-and-list-agents.i18n.yaml | 4 +- ...urable-subagent-catalog-and-list-agents.md | 14 +- ...ble-subagent-catalog-and-list-agents.zh.md | 14 +- ...ge-input-and-durable-attachments.i18n.yaml | 4 +- ...dal-image-input-and-durable-attachments.md | 8 +- ...-image-input-and-durable-attachments.zh.md | 8 +- ...3-session-telemetry-otel-revival.i18n.yaml | 4 +- ...26-07-23-session-telemetry-otel-revival.md | 6 +- ...07-23-session-telemetry-otel-revival.zh.md | 6 +- ...7-23-web-permission-and-approval.i18n.yaml | 4 +- .../2026-07-23-web-permission-and-approval.md | 10 +- ...26-07-23-web-permission-and-approval.zh.md | 10 +- .../2026-07-23-web-todo-display.i18n.yaml | 4 +- .../feature/2026-07-23-web-todo-display.md | 2 +- .../feature/2026-07-23-web-todo-display.zh.md | 2 +- ...07-24-web-session-model-selector.i18n.yaml | 4 +- .../2026-07-24-web-session-model-selector.md | 4 +- ...026-07-24-web-session-model-selector.zh.md | 4 +- ...-07-25-workspace-ui-product-flow.i18n.yaml | 4 +- .../2026-07-25-workspace-ui-product-flow.md | 2 +- ...2026-07-25-workspace-ui-product-flow.zh.md | 2 +- ...7-26-code-dispatch-ui-foundation.i18n.yaml | 2 +- .../2026-07-26-code-dispatch-ui-foundation.md | 2 +- ...ative-workspace-directory-picker.i18n.yaml | 4 +- ...07-27-native-workspace-directory-picker.md | 2 +- ...27-native-workspace-directory-picker.zh.md | 2 +- ...-07-27-skill-catalog-hot-refresh.i18n.yaml | 4 +- .../2026-07-27-skill-catalog-hot-refresh.md | 2 +- ...2026-07-27-skill-catalog-hot-refresh.zh.md | 2 +- ...2026-07-27-tmux-location-context.i18n.yaml | 4 +- .../2026-07-27-tmux-location-context.md | 6 +- .../2026-07-27-tmux-location-context.zh.md | 6 +- ...ipt-sdk-and-sdk-subagent-backend.i18n.yaml | 4 +- ...typescript-sdk-and-sdk-subagent-backend.md | 8 +- ...escript-sdk-and-sdk-subagent-backend.zh.md | 8 +- .../2026-07-27-web-session-search.i18n.yaml | 4 +- .../feature/2026-07-27-web-session-search.md | 2 +- .../2026-07-27-web-session-search.zh.md | 2 +- ...07-27-web-subagent-conversations.i18n.yaml | 4 +- .../2026-07-27-web-subagent-conversations.md | 4 +- ...026-07-27-web-subagent-conversations.zh.md | 4 +- ...-workspace-registration-deletion.i18n.yaml | 4 +- ...6-07-27-workspace-registration-deletion.md | 2 +- ...7-27-workspace-registration-deletion.zh.md | 2 +- ...ntinuable-subagent-conversations.i18n.yaml | 4 +- ...7-28-continuable-subagent-conversations.md | 10 +- ...8-continuable-subagent-conversations.zh.md | 4 +- .../2026-07-28-feedback-command.i18n.yaml | 4 +- .../feature/2026-07-28-feedback-command.md | 4 +- .../feature/2026-07-28-feedback-command.zh.md | 4 +- ...2026-07-28-sdk-max-output-tokens.i18n.yaml | 4 +- .../2026-07-28-sdk-max-output-tokens.md | 4 +- .../2026-07-28-sdk-max-output-tokens.zh.md | 4 +- ...-07-28-tool-call-file-open-in-os.i18n.yaml | 4 +- .../2026-07-28-tool-call-file-open-in-os.md | 2 +- ...2026-07-28-tool-call-file-open-in-os.zh.md | 2 +- ...29-ask-question-web-presentation.i18n.yaml | 4 +- ...026-07-29-ask-question-web-presentation.md | 4 +- ...-07-29-ask-question-web-presentation.zh.md | 4 +- ...rsistent-bash-str-replace-editor.i18n.yaml | 4 +- ...7-29-persistent-bash-str-replace-editor.md | 2 +- ...9-persistent-bash-str-replace-editor.zh.md | 2 +- ...continuable-subagent-report-tool.i18n.yaml | 2 +- ...-07-30-continuable-subagent-report-tool.md | 2 +- ...seek-onboarding-credential-setup.i18n.yaml | 4 +- ...30-deepseek-onboarding-credential-setup.md | 4 +- ...deepseek-onboarding-credential-setup.zh.md | 4 +- ...-plan-review-presentation-intent.i18n.yaml | 4 +- ...6-07-30-plan-review-presentation-intent.md | 12 +- ...7-30-plan-review-presentation-intent.zh.md | 12 +- ...6-07-30-queued-manual-compaction.i18n.yaml | 4 +- .../2026-07-30-queued-manual-compaction.md | 28 +- .../2026-07-30-queued-manual-compaction.zh.md | 28 +- ...versioned-gui-welcome-onboarding.i18n.yaml | 4 +- ...-07-30-versioned-gui-welcome-onboarding.md | 2 +- ...-30-versioned-gui-welcome-onboarding.zh.md | 2 +- .../2026-07-30-web-read-card.i18n.yaml | 4 +- .../feature/2026-07-30-web-read-card.md | 2 +- .../feature/2026-07-30-web-read-card.zh.md | 2 +- ...1-browser-derived-initial-locale.i18n.yaml | 4 +- ...26-07-31-browser-derived-initial-locale.md | 4 +- ...07-31-browser-derived-initial-locale.zh.md | 4 +- ...7-31-code-mode-language-dispatch.i18n.yaml | 4 +- .../2026-07-31-code-mode-language-dispatch.md | 6 +- ...26-07-31-code-mode-language-dispatch.zh.md | 6 +- ...31-even-out-shipped-tool-rosters.i18n.yaml | 4 +- ...026-07-31-even-out-shipped-tool-rosters.md | 10 +- ...-07-31-even-out-shipped-tool-rosters.zh.md | 10 +- ...-31-gui-full-access-confirmation.i18n.yaml | 4 +- ...2026-07-31-gui-full-access-confirmation.md | 2 +- ...6-07-31-gui-full-access-confirmation.zh.md | 2 +- ...mission-default-for-new-sessions.i18n.yaml | 4 +- ...-31-permission-default-for-new-sessions.md | 4 +- ...-permission-default-for-new-sessions.zh.md | 4 +- ...07-31-session-archive-global-set.i18n.yaml | 4 +- .../2026-07-31-session-archive-global-set.md | 2 +- ...026-07-31-session-archive-global-set.zh.md | 2 +- ...7-31-telemetry-anonymous-user-id.i18n.yaml | 4 +- .../2026-07-31-telemetry-anonymous-user-id.md | 18 +- ...26-07-31-telemetry-anonymous-user-id.zh.md | 16 +- .../2026-07-31-web-default-search.i18n.yaml | 4 +- .../feature/2026-07-31-web-default-search.md | 6 +- .../2026-07-31-web-default-search.zh.md | 4 +- ...7-31-web-telemetry-default-mount.i18n.yaml | 4 +- .../2026-07-31-web-telemetry-default-mount.md | 4 +- ...26-07-31-web-telemetry-default-mount.zh.md | 4 +- ...-workspace-write-surface-default.i18n.yaml | 4 +- ...6-07-31-workspace-write-surface-default.md | 2 +- ...7-31-workspace-write-surface-default.zh.md | 2 +- ...026-08-01-pwsh-tool-and-executor.i18n.yaml | 4 +- .../2026-08-01-pwsh-tool-and-executor.md | 10 +- .../2026-08-01-pwsh-tool-and-executor.zh.md | 8 +- .../2026-08-01-windows-pwsh-default.i18n.yaml | 4 +- .../2026-08-01-windows-pwsh-default.md | 6 +- .../2026-08-01-windows-pwsh-default.zh.md | 6 +- ...2026-08-02-pwsh-tool-bash-parity.i18n.yaml | 4 +- .../2026-08-02-pwsh-tool-bash-parity.md | 12 +- .../2026-08-02-pwsh-tool-bash-parity.zh.md | 12 +- .../2026-08-03-fs-tool-error-remedy.i18n.yaml | 4 +- .../2026-08-03-fs-tool-error-remedy.md | 2 +- .../2026-08-03-fs-tool-error-remedy.zh.md | 2 +- ...b-context-source-and-steer-marks.i18n.yaml | 4 +- ...8-04-web-context-source-and-steer-marks.md | 2 +- ...4-web-context-source-and-steer-marks.zh.md | 2 +- ...26-08-05-context-form-vocabulary.i18n.yaml | 4 +- .../2026-08-05-context-form-vocabulary.md | 6 +- .../2026-08-05-context-form-vocabulary.zh.md | 6 +- .../2026-08-05-durable-web-schedule.i18n.yaml | 4 +- .../2026-08-05-durable-web-schedule.md | 4 +- .../2026-08-05-durable-web-schedule.zh.md | 4 +- ...feedback-gated-session-telemetry.i18n.yaml | 4 +- ...-08-05-feedback-gated-session-telemetry.md | 2 +- ...-05-feedback-gated-session-telemetry.zh.md | 2 +- ...8-05-per-agent-tool-presentation.i18n.yaml | 4 +- .../2026-08-05-per-agent-tool-presentation.md | 6 +- ...26-08-05-per-agent-tool-presentation.zh.md | 6 +- .../2026-08-05-pwsh-ui-bash-parity.i18n.yaml | 4 +- .../feature/2026-08-05-pwsh-ui-bash-parity.md | 10 +- .../2026-08-05-pwsh-ui-bash-parity.zh.md | 8 +- ...26-08-06-bundled-dsh-badge-skill.i18n.yaml | 4 +- .../2026-08-06-bundled-dsh-badge-skill.md | 2 +- .../2026-08-06-bundled-dsh-badge-skill.zh.md | 2 +- ...6-continuable-subagent-interrupt.i18n.yaml | 4 +- ...26-08-06-continuable-subagent-interrupt.md | 4 +- ...08-06-continuable-subagent-interrupt.zh.md | 4 +- ...ned-subagent-settlement-delivery.i18n.yaml | 4 +- ...ager-owned-subagent-settlement-delivery.md | 4 +- ...r-owned-subagent-settlement-delivery.zh.md | 4 +- .../2026-08-06-web-install-manifest.i18n.yaml | 4 +- .../2026-08-06-web-install-manifest.md | 4 +- .../2026-08-06-web-install-manifest.zh.md | 4 +- ...default-model-follows-the-picker.i18n.yaml | 4 +- ...-08-07-default-model-follows-the-picker.md | 4 +- ...-07-default-model-follows-the-picker.zh.md | 4 +- ...knowledgement-sharing-disclosure.i18n.yaml | 4 +- ...back-acknowledgement-sharing-disclosure.md | 4 +- ...k-acknowledgement-sharing-disclosure.zh.md | 4 +- ...8-user-explicit-skill-invocation.i18n.yaml | 4 +- ...26-08-08-user-explicit-skill-invocation.md | 2 +- ...08-08-user-explicit-skill-invocation.zh.md | 2 +- ...8-08-web-background-job-display.i18n.yaml} | 6 +- .../2026-08-08-web-background-job-display.md | 136 ++ ...026-08-08-web-background-job-display.zh.md | 136 ++ .../2026-08-08-web-background-task-display.md | 136 -- ...26-08-08-web-background-task-display.zh.md | 136 -- ...ows-acl-restricted-token-sandbox.i18n.yaml | 4 +- ...08-windows-acl-restricted-token-sandbox.md | 2 +- ...windows-acl-restricted-token-sandbox.zh.md | 2 +- ...09-parallel-subagent-delegations.i18n.yaml | 4 +- ...026-08-09-parallel-subagent-delegations.md | 4 +- ...-08-09-parallel-subagent-delegations.zh.md | 4 +- ...able-subagent-policy-inheritance.i18n.yaml | 4 +- ...continuable-subagent-policy-inheritance.md | 2 +- ...tinuable-subagent-policy-inheritance.zh.md | 2 +- ...2026-08-10-telemetry-default-off.i18n.yaml | 2 +- .../2026-08-10-telemetry-default-off.md | 2 +- ...6-08-10-web-plugin-configuration.i18n.yaml | 4 +- .../2026-08-10-web-plugin-configuration.md | 4 +- .../2026-08-10-web-plugin-configuration.zh.md | 4 +- ...und-first-continuable-delegation.i18n.yaml | 2 +- ...background-first-continuable-delegation.md | 2 +- ...-completion-wakes-an-idle-owner.i18n.yaml} | 6 +- ...und-job-completion-wakes-an-idle-owner.md} | 30 +- ...-job-completion-wakes-an-idle-owner.zh.md} | 24 +- ...-deepseek-request-user-id-header.i18n.yaml | 4 +- ...6-08-11-deepseek-request-user-id-header.md | 10 +- ...8-11-deepseek-request-user-id-header.zh.md | 8 +- ...-11-message-feedback-web-surface.i18n.yaml | 4 +- ...2026-08-11-message-feedback-web-surface.md | 4 +- ...6-08-11-message-feedback-web-surface.zh.md | 4 +- ...l-profiles-bare-two-tool-runtime.i18n.yaml | 4 +- ...-minimal-profiles-bare-two-tool-runtime.md | 4 +- ...nimal-profiles-bare-two-tool-runtime.zh.md | 4 +- ...11-web-export-command-and-dialog.i18n.yaml | 4 +- ...026-08-11-web-export-command-and-dialog.md | 2 +- ...-08-11-web-export-command-and-dialog.zh.md | 2 +- .../2026-06-17-ts-build-config.i18n.yaml | 4 +- .../process/2026-06-17-ts-build-config.md | 4 +- .../process/2026-06-17-ts-build-config.zh.md | 4 +- ...-20-core-data-structures-catalog.i18n.yaml | 4 +- ...2026-06-20-core-data-structures-catalog.md | 6 +- ...6-06-20-core-data-structures-catalog.zh.md | 6 +- .../2026-07-02-tool-schema-catalog.i18n.yaml | 4 +- .../process/2026-07-02-tool-schema-catalog.md | 4 +- .../2026-07-02-tool-schema-catalog.zh.md | 4 +- ...rial-cross-platform-ci-reference.i18n.yaml | 4 +- ...7-21-serial-cross-platform-ci-reference.md | 4 +- ...1-serial-cross-platform-ci-reference.zh.md | 4 +- .../2026-07-22-fast-local-git-hooks.i18n.yaml | 4 +- .../2026-07-22-fast-local-git-hooks.md | 2 +- .../2026-07-22-fast-local-git-hooks.zh.md | 2 +- ...subsystem-cordis-surface-regions.i18n.yaml | 4 +- ...28-per-subsystem-cordis-surface-regions.md | 6 +- ...per-subsystem-cordis-surface-regions.zh.md | 6 +- ...30-generated-third-party-notices.i18n.yaml | 4 +- ...026-07-30-generated-third-party-notices.md | 2 +- ...-07-30-generated-third-party-notices.zh.md | 2 +- ...remotes-generated-contract-build.i18n.yaml | 4 +- ...08-api-remotes-generated-contract-build.md | 12 +- ...api-remotes-generated-contract-build.zh.md | 12 +- ...08-unified-github-label-taxonomy.i18n.yaml | 2 +- ...026-08-08-unified-github-label-taxonomy.md | 2 +- ...2026-08-10-npm-release-sequences.i18n.yaml | 4 +- .../2026-08-10-npm-release-sequences.md | 4 +- .../2026-08-10-npm-release-sequences.zh.md | 4 +- .../2026-06-26-fsspec-style-fs-seam.i18n.yaml | 4 +- .../2026-06-26-fsspec-style-fs-seam.md | 22 +- .../2026-06-26-fsspec-style-fs-seam.zh.md | 22 +- ...4-tighten-hook-protocol-contract.i18n.yaml | 4 +- ...26-07-04-tighten-hook-protocol-contract.md | 4 +- ...07-04-tighten-hook-protocol-contract.zh.md | 4 +- ...-20-remove-stdio-and-echo-agents.i18n.yaml | 4 +- ...2026-07-20-remove-stdio-and-echo-agents.md | 2 +- ...6-07-20-remove-stdio-and-echo-agents.zh.md | 2 +- ...nwrap-injected-content-envelopes.i18n.yaml | 4 +- ...07-20-unwrap-injected-content-envelopes.md | 8 +- ...20-unwrap-injected-content-envelopes.zh.md | 8 +- ...lan-specific-collaboration-state.i18n.yaml | 2 +- ...07-22-plan-specific-collaboration-state.md | 6 +- ...-23-acp-automation-only-protocol.i18n.yaml | 2 +- ...2026-07-23-acp-automation-only-protocol.md | 4 +- ...6-merge-subagent-control-service.i18n.yaml | 4 +- ...26-07-26-merge-subagent-control-service.md | 10 +- ...07-26-merge-subagent-control-service.zh.md | 4 +- ...subagent-continuation-operations.i18n.yaml | 4 +- ...-named-subagent-continuation-operations.md | 4 +- ...med-subagent-continuation-operations.zh.md | 4 +- ...07-27-request-error-retry-action.i18n.yaml | 4 +- .../2026-07-27-request-error-retry-action.md | 2 +- ...026-07-27-request-error-retry-action.zh.md | 2 +- ...-remove-synthetic-log-only-turns.i18n.yaml | 4 +- ...6-07-28-remove-synthetic-log-only-turns.md | 2 +- ...7-28-remove-synthetic-log-only-turns.zh.md | 2 +- ...7-29-shared-base-config-overlays.i18n.yaml | 4 +- .../2026-07-29-shared-base-config-overlays.md | 4 +- ...26-07-29-shared-base-config-overlays.zh.md | 4 +- ...-31-one-route-to-add-a-workspace.i18n.yaml | 4 +- ...2026-07-31-one-route-to-add-a-workspace.md | 2 +- ...6-07-31-one-route-to-add-a-workspace.zh.md | 2 +- .../2026-08-04-remove-tui-package.i18n.yaml | 4 +- .../2026-08-04-remove-tui-package.md | 4 +- .../2026-08-04-remove-tui-package.zh.md | 2 +- ...6-buffer-free-feedback-telemetry.i18n.yaml | 4 +- ...26-08-06-buffer-free-feedback-telemetry.md | 2 +- ...08-06-buffer-free-feedback-telemetry.zh.md | 2 +- ...conversational-schedule-delivery.i18n.yaml | 4 +- ...-08-09-conversational-schedule-delivery.md | 2 +- ...-09-conversational-schedule-delivery.zh.md | 2 +- ...6-08-09-remove-repository-plugin.i18n.yaml | 4 +- .../2026-08-09-remove-repository-plugin.md | 4 +- .../2026-08-09-remove-repository-plugin.zh.md | 4 +- ...-11-remove-sdk-project-toolchain.i18n.yaml | 4 +- ...2026-08-11-remove-sdk-project-toolchain.md | 4 +- ...6-08-11-remove-sdk-project-toolchain.zh.md | 4 +- ...eparate-source-launch-from-build.i18n.yaml | 4 +- ...08-12-separate-source-launch-from-build.md | 2 +- ...12-separate-source-launch-from-build.zh.md | 2 +- .../2026-06-19-acp-snapshot-tests.i18n.yaml | 4 +- .../testing/2026-06-19-acp-snapshot-tests.md | 2 +- .../2026-06-19-acp-snapshot-tests.zh.md | 2 +- ...-fork-child-replay-seed-boundary.i18n.yaml | 4 +- ...6-06-22-fork-child-replay-seed-boundary.md | 4 +- ...6-22-fork-child-replay-seed-boundary.zh.md | 4 +- ...6-06-22-subagent-snapshot-replay.i18n.yaml | 4 +- .../2026-06-22-subagent-snapshot-replay.md | 4 +- .../2026-06-22-subagent-snapshot-replay.zh.md | 4 +- ...6-07-24-web-gui-browser-e2e-lane.i18n.yaml | 4 +- .../2026-07-24-web-gui-browser-e2e-lane.md | 10 +- .../2026-07-24-web-gui-browser-e2e-lane.zh.md | 10 +- .../2026-06-16-typed-event-schemas.i18n.yaml | 4 +- .../2026-06-16-typed-event-schemas.md | 2 +- .../2026-06-16-typed-event-schemas.zh.md | 2 +- ...on-through-tool-capability-seams.i18n.yaml | 2 +- ...cellation-through-tool-capability-seams.md | 2 +- ...-domain-kv-storage-and-workspace.i18n.yaml | 4 +- ...6-07-24-domain-kv-storage-and-workspace.md | 12 +- ...7-24-domain-kv-storage-and-workspace.zh.md | 12 +- ...-25-client-settings-locale-theme.i18n.yaml | 4 +- ...2026-07-25-client-settings-locale-theme.md | 12 +- ...6-07-25-client-settings-locale-theme.zh.md | 12 +- ...ssion-projection-and-command-log.i18n.yaml | 4 +- ...7-27-session-projection-and-command-log.md | 2 +- ...7-session-projection-and-command-log.zh.md | 2 +- ...root-and-derived-medium-recovery.i18n.yaml | 4 +- ...torage-root-and-derived-medium-recovery.md | 2 +- ...age-root-and-derived-medium-recovery.zh.md | 2 +- ...-unary-apiproxy-remote-migration.i18n.yaml | 4 +- ...6-08-10-unary-apiproxy-remote-migration.md | 26 +- ...8-10-unary-apiproxy-remote-migration.zh.md | 26 +- ...aming-contract-and-rename-ledger.i18n.yaml | 4 +- ...itory-naming-contract-and-rename-ledger.md | 34 +- ...ry-naming-contract-and-rename-ledger.zh.md | 34 +- ...2026-07-06-recallable-compaction.i18n.yaml | 4 +- .../2026-07-06-recallable-compaction.md | 18 +- .../2026-07-06-recallable-compaction.zh.md | 18 +- .../feature/2026-08-04-task-surface.i18n.yaml | 2 +- .../feature/2026-08-04-task-surface.md | 2 +- ...-07-04-prune-dead-core-spine-api.i18n.yaml | 4 +- .../2026-07-04-prune-dead-core-spine-api.md | 12 +- ...2026-07-04-prune-dead-core-spine-api.zh.md | 12 +- ...6-07-19-make-jsonrpc-directional.i18n.yaml | 4 +- .../2026-07-19-make-jsonrpc-directional.md | 2 +- .../2026-07-19-make-jsonrpc-directional.zh.md | 2 +- ...-20-drop-bash-output-spill-files.i18n.yaml | 2 +- ...2026-06-20-drop-bash-output-spill-files.md | 4 +- ...-prune-unused-skill-registry-api.i18n.yaml | 4 +- ...6-07-12-prune-unused-skill-registry-api.md | 2 +- ...7-12-prune-unused-skill-registry-api.zh.md | 2 +- ...19-fold-compaction-package-split.i18n.yaml | 4 +- ...026-07-19-fold-compaction-package-split.md | 12 +- ...-07-19-fold-compaction-package-split.zh.md | 12 +- ...-promises-for-hand-rolled-sleeps.i18n.yaml | 4 +- ...n-timer-promises-for-hand-rolled-sleeps.md | 12 +- ...imer-promises-for-hand-rolled-sleeps.zh.md | 12 +- ...ency-swaps-rejected-by-nih-audit.i18n.yaml | 4 +- ...-dependency-swaps-rejected-by-nih-audit.md | 10 +- ...pendency-swaps-rejected-by-nih-audit.zh.md | 10 +- .../skills/dsh-find-simplifications/SKILL.md | 4 +- .../dsh-prose-standard/references/examples.md | 2 +- .github/workflows/ci.yml | 8 +- .github/workflows/sandbox.yml | 2 +- AGENTS.md | 13 +- README.i18n.yaml | 4 +- README.md | 2 +- README.zh.md | 2 +- apps/cli/README.i18n.yaml | 4 +- apps/cli/README.md | 2 +- apps/cli/README.zh.md | 2 +- apps/cli/composition.md | 106 +- .../agent-presets/code/agent.cordis.yml | 48 +- .../agent-presets/cordis/agent.cordis.yml | 40 +- .../editing-cordis-compositions/SKILL.md | 6 +- .../agent-presets/minimal/agent.cordis.yml | 8 +- .../agent-presets/standard/agent.cordis.yml | 42 +- apps/cli/package.json | 30 +- apps/cli/reference/README.i18n.yaml | 4 +- apps/cli/reference/README.md | 2 +- apps/cli/reference/README.zh.md | 2 +- apps/cli/src/profile-boot.ts | 10 +- apps/cli/tests/fixtures/dsh-badge/cordis.yml | 4 +- .../fixtures/dsh-badge/default.cordis.yml | 4 +- apps/cli/tests/fixtures/dsh-badge/snapshot.ts | 2 +- apps/cli/tests/memory-mcp-configs.spec.ts | 4 +- apps/cli/tests/telemetry-switch.spec.ts | 2 +- apps/cli/tests/web-agent-presets.e2e.ts | 19 +- apps/cli/tsconfig.json | 10 +- apps/web/package.json | 2 +- apps/web/tests/agent-preset-selection.e2e.ts | 2 +- apps/web/tests/assembled-boot.ts | 2 +- ...list.e2e.ts => background-job-list.e2e.ts} | 40 +- apps/web/tests/chat-scroll-contract.e2e.ts | 2 +- apps/web/tests/goal-multi-turn-actions.e2e.ts | 10 +- apps/web/tests/hmr-live.e2e.ts | 4 +- apps/web/tests/lifecycle-chrome.e2e.ts | 2 +- apps/web/tests/minimal-preset.snapshot.ts | 2 +- apps/web/tests/pin-browse-picker.overlay.yml | 4 +- apps/web/tests/pwsh-terminal.overlay.yml | 2 +- apps/web/tests/question-composer.e2e.ts | 2 +- apps/web/tests/scaffold.ts | 30 +- apps/web/tests/schedule-after.e2e.ts | 8 +- apps/web/tests/seeded-history.e2e.ts | 20 +- apps/web/tests/settings-chrome.e2e.ts | 2 +- apps/web/tests/shipped-composition.e2e.ts | 24 +- .../tests/sidebar-subagent-activity.e2e.ts | 6 +- apps/web/tests/smoke-real.e2e.ts | 4 +- .../running.expected.md | 2 +- .../settled.expected.md | 2 +- .../feedback-command/ack.expected.md | 4 +- .../system-prompt.expected.md | 4 +- .../replay.override.json | 4 +- .../goal-multi-turn-actions/session.jsonl | 12 +- .../goal-multi-turn-actions/ui.expected.md | 4 +- .../permission-policy-context/session.jsonl | 2 +- .../seeded-history/feedback-row.expected.md | 6 +- .../owner-running.expected.md | 2 +- .../web-surface-prompt.expected.md | 2 +- apps/web/tests/startup-auto-selection.e2e.ts | 2 +- apps/web/tests/workspace-management.e2e.ts | 32 +- apps/web/tsconfig.json | 2 +- docs/agent-lifecycle.i18n.yaml | 4 +- docs/agent-lifecycle.md | 2 +- docs/agent-lifecycle.zh.md | 2 +- docs/api-gateway.i18n.yaml | 4 +- docs/api-gateway.md | 34 +- docs/api-gateway.zh.md | 34 +- docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 6 +- docs/architecture.zh.md | 6 +- docs/capability-seams.i18n.yaml | 4 +- docs/capability-seams.md | 264 +-- docs/capability-seams.zh.md | 284 +-- docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 553 +++--- docs/config-catalog.zh.md | 529 +++--- docs/cookbook/adding-a-package.i18n.yaml | 4 +- docs/cookbook/adding-a-package.md | 30 +- docs/cookbook/adding-a-package.zh.md | 30 +- docs/cookbook/adding-a-tool.i18n.yaml | 4 +- docs/cookbook/adding-a-tool.md | 6 +- docs/cookbook/adding-a-tool.zh.md | 6 +- docs/cookbook/adding-an-llm-adapter.i18n.yaml | 4 +- docs/cookbook/adding-an-llm-adapter.md | 2 +- docs/cookbook/adding-an-llm-adapter.zh.md | 2 +- docs/cookbook/extension-cookbook.i18n.yaml | 4 +- docs/cookbook/extension-cookbook.md | 14 +- docs/cookbook/extension-cookbook.zh.md | 12 +- docs/cordis-primer.i18n.yaml | 4 +- docs/cordis-primer.md | 2 +- docs/cordis-primer.zh.md | 2 +- docs/cordis-tutorial/index.i18n.yaml | 4 +- docs/cordis-tutorial/index.md | 2 +- docs/cordis-tutorial/index.zh.md | 2 +- docs/defensive-patterns.i18n.yaml | 4 +- docs/defensive-patterns.md | 4 +- docs/defensive-patterns.zh.md | 2 +- docs/development.i18n.yaml | 4 +- docs/development.md | 8 +- docs/development.zh.md | 8 +- docs/event-producer-consumer.i18n.yaml | 4 +- docs/event-producer-consumer.md | 54 +- docs/event-producer-consumer.zh.md | 54 +- docs/glossary.i18n.yaml | 4 +- docs/glossary.md | 6 +- docs/glossary.zh.md | 6 +- docs/i18n/style-samples.md | 2 +- docs/i18n/terminology.md | 20 +- docs/module-graph.i18n.yaml | 4 +- docs/module-graph.md | 1390 +++++++------- docs/module-graph.zh.md | 1390 +++++++------- docs/persistence-catalog.i18n.yaml | 4 +- docs/persistence-catalog.md | 44 +- docs/persistence-catalog.zh.md | 44 +- ...0003-web-agent-gui-feedback-loop.i18n.yaml | 2 +- .../0003-web-agent-gui-feedback-loop.md | 2 +- ...ice-misclassified-child-failures.i18n.yaml | 4 +- ...ial-notice-misclassified-child-failures.md | 4 +- ...-notice-misclassified-child-failures.zh.md | 4 +- docs/subsystems/README.i18n.yaml | 4 +- docs/subsystems/README.md | 18 +- docs/subsystems/README.zh.md | 18 +- docs/subsystems/approval.i18n.yaml | 4 +- docs/subsystems/approval.md | 2 +- docs/subsystems/approval.zh.md | 2 +- docs/subsystems/client-modules.i18n.yaml | 4 +- docs/subsystems/client-modules.md | 8 +- docs/subsystems/client-modules.zh.md | 8 +- docs/subsystems/code-runtime.i18n.yaml | 4 +- docs/subsystems/code-runtime.md | 2 +- docs/subsystems/code-runtime.zh.md | 2 +- docs/subsystems/commands.i18n.yaml | 4 +- docs/subsystems/commands.md | 4 +- docs/subsystems/commands.zh.md | 4 +- docs/subsystems/compaction.i18n.yaml | 4 +- docs/subsystems/compaction.md | 52 +- docs/subsystems/compaction.zh.md | 52 +- docs/subsystems/core.i18n.yaml | 4 +- docs/subsystems/core.md | 6 +- docs/subsystems/core.zh.md | 6 +- docs/subsystems/credentials.i18n.yaml | 4 +- docs/subsystems/credentials.md | 8 +- docs/subsystems/credentials.zh.md | 8 +- docs/subsystems/feedback.i18n.yaml | 4 +- docs/subsystems/feedback.md | 6 +- docs/subsystems/feedback.zh.md | 6 +- docs/subsystems/filesystem.i18n.yaml | 4 +- docs/subsystems/filesystem.md | 20 +- docs/subsystems/filesystem.zh.md | 20 +- docs/subsystems/invariants.i18n.yaml | 4 +- docs/subsystems/invariants.md | 14 +- docs/subsystems/invariants.zh.md | 14 +- .../{pty.i18n.yaml => jobs.i18n.yaml} | 6 +- docs/subsystems/{tasks.md => jobs.md} | 146 +- docs/subsystems/{tasks.zh.md => jobs.zh.md} | 146 +- docs/subsystems/llm-streaming.i18n.yaml | 4 +- docs/subsystems/llm-streaming.md | 22 +- docs/subsystems/llm-streaming.zh.md | 22 +- docs/subsystems/lsp.i18n.yaml | 4 +- docs/subsystems/lsp.md | 2 +- docs/subsystems/lsp.zh.md | 2 +- docs/subsystems/permission-presets.i18n.yaml | 6 + .../{permission.md => permission-presets.md} | 20 +- ...mission.zh.md => permission-presets.zh.md} | 20 +- docs/subsystems/plan.i18n.yaml | 4 +- docs/subsystems/plan.md | 10 +- docs/subsystems/plan.zh.md | 10 +- docs/subsystems/sandbox.i18n.yaml | 4 +- docs/subsystems/sandbox.md | 4 +- docs/subsystems/sandbox.zh.md | 4 +- docs/subsystems/schedule.i18n.yaml | 4 +- docs/subsystems/schedule.md | 4 +- docs/subsystems/schedule.zh.md | 4 +- docs/subsystems/session-query.i18n.yaml | 4 +- docs/subsystems/session-query.md | 4 +- docs/subsystems/session-query.zh.md | 4 +- docs/subsystems/session-reference.i18n.yaml | 4 +- docs/subsystems/session-reference.md | 4 +- docs/subsystems/session-reference.zh.md | 4 +- docs/subsystems/session-telemetry.i18n.yaml | 6 + .../{telemetry.md => session-telemetry.md} | 60 +- ...elemetry.zh.md => session-telemetry.zh.md} | 58 +- docs/subsystems/session-title.i18n.yaml | 4 +- docs/subsystems/session-title.md | 2 +- docs/subsystems/session-title.zh.md | 2 +- docs/subsystems/session.i18n.yaml | 4 +- docs/subsystems/session.md | 14 +- docs/subsystems/session.zh.md | 14 +- docs/subsystems/settings.i18n.yaml | 4 +- docs/subsystems/settings.md | 8 +- docs/subsystems/settings.zh.md | 8 +- .../{bash.i18n.yaml => shell.i18n.yaml} | 6 +- docs/subsystems/{bash.md => shell.md} | 90 +- docs/subsystems/{bash.zh.md => shell.zh.md} | 86 +- docs/subsystems/skills.i18n.yaml | 4 +- docs/subsystems/skills.md | 8 +- docs/subsystems/skills.zh.md | 8 +- docs/subsystems/spill.i18n.yaml | 4 +- docs/subsystems/spill.md | 2 +- docs/subsystems/spill.zh.md | 2 +- docs/subsystems/subagent.i18n.yaml | 4 +- docs/subsystems/subagent.md | 30 +- docs/subsystems/subagent.zh.md | 30 +- docs/subsystems/subprocess.i18n.yaml | 4 +- docs/subsystems/subprocess.md | 16 +- docs/subsystems/subprocess.zh.md | 16 +- docs/subsystems/telemetry.i18n.yaml | 6 - .../{tasks.i18n.yaml => terminal.i18n.yaml} | 6 +- docs/subsystems/{pty.md => terminal.md} | 70 +- docs/subsystems/{pty.zh.md => terminal.zh.md} | 70 +- docs/subsystems/token-meter.i18n.yaml | 4 +- docs/subsystems/token-meter.md | 4 +- docs/subsystems/token-meter.zh.md | 4 +- docs/subsystems/tools.i18n.yaml | 4 +- docs/subsystems/tools.md | 22 +- docs/subsystems/tools.zh.md | 22 +- docs/subsystems/typert.i18n.yaml | 4 +- docs/subsystems/typert.md | 52 +- docs/subsystems/typert.zh.md | 52 +- docs/subsystems/user-interaction.i18n.yaml | 6 - ...ver.i18n.yaml => user-questions.i18n.yaml} | 6 +- ...{user-interaction.md => user-questions.md} | 30 +- ...interaction.zh.md => user-questions.zh.md} | 30 +- ...mission.i18n.yaml => web-server.i18n.yaml} | 6 +- .../{http-server.md => web-server.md} | 12 +- .../{http-server.zh.md => web-server.zh.md} | 12 +- docs/subsystems/web.i18n.yaml | 4 +- docs/subsystems/web.md | 10 +- docs/subsystems/web.zh.md | 10 +- docs/subsystems/workflow.i18n.yaml | 4 +- docs/subsystems/workflow.md | 8 +- docs/subsystems/workflow.zh.md | 8 +- docs/subsystems/workspace.i18n.yaml | 4 +- docs/subsystems/workspace.md | 12 +- docs/subsystems/workspace.zh.md | 12 +- docs/testing.i18n.yaml | 4 +- docs/testing.md | 6 +- docs/testing.zh.md | 6 +- docs/tool-catalog.i18n.yaml | 4 +- docs/tool-catalog.md | 130 +- docs/tool-catalog.zh.md | 120 +- docs/user/develop/framework/events.i18n.yaml | 4 +- docs/user/develop/framework/events.md | 2 +- docs/user/develop/framework/events.zh.md | 2 +- docs/user/develop/framework/service.i18n.yaml | 4 +- docs/user/develop/framework/service.md | 2 +- docs/user/develop/framework/service.zh.md | 2 +- docs/user/develop/practice/index.i18n.yaml | 4 +- docs/user/develop/practice/index.md | 4 +- docs/user/develop/practice/index.zh.md | 4 +- .../acp-agent/advanced.cordis.snapshot.yml | 2 +- examples/acp-agent/advanced.cordis.yml | 2 +- ...=> agent-instructions.cordis.snapshot.yml} | 2 +- ...rdis.yml => agent-instructions.cordis.yml} | 0 ...kground-job-admission.cordis.snapshot.yml} | 4 +- ...ml => background-job-admission.cordis.yml} | 2 +- .../acp-agent/both-mode.cordis.snapshot.yml | 2 +- examples/acp-agent/both-mode.cordis.yml | 2 +- .../child-question.cordis.snapshot.yml | 4 +- examples/acp-agent/child-question.cordis.yml | 4 +- ...mode-workspace-context.cordis.snapshot.yml | 2 +- .../code-mode-workspace-context.cordis.yml | 4 +- .../acp-agent/code-mode.cordis.snapshot.yml | 2 +- examples/acp-agent/code-mode.cordis.yml | 2 +- examples/acp-agent/composition.md | 42 +- examples/acp-agent/cordis.yml | 34 +- examples/acp-agent/pty-snapshot-backend.mjs | 4 +- examples/acp-agent/pty.cordis.snapshot.yml | 6 +- examples/acp-agent/pty.cordis.yml | 10 +- .../session-query.cordis.snapshot.yml | 2 +- examples/acp-agent/session-query.cordis.yml | 2 +- .../session-title.cordis.snapshot.yml | 2 +- examples/acp-agent/session-title.cordis.yml | 4 +- examples/acp-agent/tests/acp.snapshot.ts | 20 +- .../tests/fixtures/child-question-tripwire.ts | 6 +- .../{bash => shell}/tool-pwsh/cordis.yml | 10 +- .../{bash => shell}/tool-pwsh/driver.ts | 6 +- .../fixtures/workspace-context-compaction.ts | 4 +- .../tests/fs-search.cordis.snapshot.yml | 2 +- examples/acp-agent/tests/fs-search.cordis.yml | 2 +- .../input.json | 0 .../replay.override.json | 4 +- .../session.expected.jsonl | 24 +- .../session.jsonl | 0 .../stdout.expected.jsonl | 0 examples/acp-agent/tests/goal.snapshot.ts | 4 +- .../acp-agent/tests/lsp.cordis.snapshot.yml | 6 +- examples/acp-agent/tests/lsp.cordis.yml | 6 +- .../acp-agent/tests/pwsh.cordis.snapshot.yml | 6 +- examples/acp-agent/tests/pwsh.cordis.yml | 6 +- .../advanced-toolchain/session.1.jsonl | 2 +- .../advanced-toolchain/session.2.jsonl | 2 +- .../system-prompt.expected.md | 122 +- .../tool-schemas.expected.json | 110 +- .../input.json | 0 .../replay.override.json | 0 .../session.jsonl | 10 +- .../stdout.expected.jsonl | 0 .../system-prompt.expected.md | 8 +- .../workspace/.dsh-project | 0 .../workspace/AGENTS.canonical.md | 0 .../workspace/AGENTS.md | 0 .../workspace/nested/AGENTS.canonical.md | 0 .../workspace/nested/AGENTS.md | 0 .../workspace/nested/task.txt | 0 .../input.json | 2 +- .../replay.override.json | 12 +- .../session.jsonl | 36 +- .../stdout.expected.jsonl | 0 .../both-mode-turn/system-prompt.expected.md | 120 +- .../both-mode-turn/tool-schemas.expected.json | 108 +- .../code-mode-turn/system-prompt.expected.md | 120 +- .../code-mode-workspace-context/session.jsonl | 6 +- .../cordis-inspect-jsdoc/session.jsonl | 2 +- .../system-prompt.expected.md | 2 +- .../tool-schemas.expected.json | 4 +- .../hook-cc-posttool-block/session.jsonl | 8 +- .../hook-cc-posttool-context/session.jsonl | 8 +- .../hook-cc-pretool-ask/session.jsonl | 4 +- .../hook-cc-pretool-deny/session.jsonl | 4 +- .../session.jsonl | 6 +- .../hook-cc-stop-continue/session.jsonl | 12 +- .../lsp-definition/system-prompt.expected.md | 8 +- .../lsp-definition/tool-schemas.expected.json | 108 +- .../missing-sandbox-runner/input.json | 2 +- .../missing-sandbox-runner/session.jsonl | 18 +- .../snapshots/packed-chunks/session.jsonl | 4 +- .../tool-schemas.expected.json | 108 +- .../system-prompt.expected.md | 8 +- .../tool-schemas.expected.json | 108 +- .../pty-tools/system-prompt.expected.md | 10 +- .../pty-tools/tool-schemas.expected.json | 112 +- .../pwsh-tool-turn/system-prompt.expected.md | 4 +- .../pwsh-tool-turn/tool-schemas.expected.json | 28 +- .../read-image/tool-schemas.expected.json | 108 +- .../input.json | 0 .../session.jsonl | 8 +- .../stdout.expected.jsonl | 0 .../system-prompt.expected.md | 8 +- .../tool-schemas.expected.json | 108 +- .../session-title-after-turn/session.jsonl | 4 +- .../session.1.jsonl | 2 +- .../tool-schemas.expected.json | 108 +- .../system-prompt.1.expected.md | 8 +- .../tool-schemas.1.expected.json | 108 +- .../system-prompt.1.expected.md | 8 +- .../tool-schemas.1.expected.json | 108 +- .../session.1.jsonl | 2 +- .../session.2.jsonl | 2 +- .../input.json | 0 .../session.1.jsonl | 0 .../session.jsonl | 0 .../stdout.expected.jsonl | 0 .../system-prompt.1.expected.md | 8 +- .../tool-schemas.1.expected.json | 108 +- .../snapshots/subagent-mixed/session.1.jsonl | 2 +- .../snapshots/subagent-mixed/session.2.jsonl | 2 +- .../snapshots/subagent-multi/session.1.jsonl | 2 +- .../snapshots/subagent-multi/session.2.jsonl | 2 +- .../subagent-parallel/session.1.jsonl | 8 +- .../subagent-parallel/session.2.jsonl | 8 +- .../system-prompt.1.expected.md | 8 +- .../tool-schemas.1.expected.json | 108 +- .../input.json | 0 .../session.1.jsonl | 0 .../session.jsonl | 0 .../stdout.expected.jsonl | 0 .../text-turn/system-prompt.expected.md | 8 +- .../text-turn/tool-schemas.expected.json | 108 +- .../web-fetch/system-prompt.expected.md | 8 +- .../web-fetch/tool-schemas.expected.json | 108 +- .../acp-agent/web-fetch-fixture-server.mjs | 2 +- examples/acp-agent/web.cordis.snapshot.yml | 4 +- examples/acp-agent/web.cordis.yml | 4 +- .../advanced.cordis.snapshot.yml | 2 +- examples/headless-agent/advanced.cordis.yml | 2 +- .../compaction.cordis.snapshot.yml | 4 +- examples/headless-agent/composition.md | 34 +- examples/headless-agent/cordis.yml | 22 +- examples/headless-agent/e2b.cordis.yml | 14 +- .../headless-agent/pty.cordis.snapshot.yml | 6 +- .../semantic-checkpoint.cordis.snapshot.yml | 2 +- .../subagent-diagnostic.cordis.snapshot.yml | 4 +- .../subagent-inheritance.cordis.snapshot.yml | 10 +- .../headless-agent/tests/code-mode.e2e.ts | 80 +- .../headless-agent/tests/compaction.e2e.ts | 8 +- .../tests/fixtures/cli.cordis.yml | 2 +- .../tests/fixtures/e2b/e2b/bin.ts | 26 +- .../tests/fixtures/e2b/e2b/cordis.yml | 10 +- ...er.ts => session-telemetry-otel-driver.ts} | 4 +- ....yml => session-telemetry-otel.cordis.yml} | 4 +- .../tests/fixtures/telemetry-redact-rule.ts | 4 +- .../workspace-context-resume-agent.ts | 2 +- examples/headless-agent/tests/harness.ts | 28 +- .../headless-agent/tests/headless.snapshot.ts | 14 +- .../tests/semantic-checkpoint.snapshot.ts | 4 +- .../tests/session-format-guard.snapshot.ts | 4 +- .../advanced-toolchain/session.1.jsonl | 10 +- .../advanced-toolchain/session.2.jsonl | 10 +- .../advanced-toolchain/session.jsonl | 28 +- .../snapshots/compaction-recovery/input.json | 2 +- .../compaction-recovery/session.jsonl | 20 +- .../stream-json.expected.jsonl | 10 +- .../headless-profile/session.expected.jsonl | 2 +- .../tests/snapshots/pty-tools/session.jsonl | 34 +- .../stream-json.expected.jsonl | 4 +- .../parent.expected.jsonl | 4 +- .../tests/subagent-diagnostic.snapshot.ts | 6 +- .../tests/subagent-inheritance.snapshot.ts | 4 +- .../offline-edit/session.expected.jsonl | 4 +- .../precedence-change/session.expected.jsonl | 4 +- .../workspace-context-resume.snapshot.ts | 20 +- ...rkspace-context-resume.cordis.snapshot.yml | 2 +- examples/jsonrpc-agent/README.i18n.yaml | 2 +- examples/jsonrpc-agent/README.md | 2 +- examples/jsonrpc-agent/cordis.yml | 18 +- examples/jsonrpc-agent/minimal.cordis.yml | 12 +- .../subagent-dsh-sdk/child.cordis.yml | 6 +- .../jsonrpc-agent/tests/keyless-smoke.e2e.ts | 2 +- examples/jsonrpc-agent/tests/sdk.snapshot.ts | 2 +- .../notifications.expected.jsonl | 0 .../result.expected.json | 0 .../session.1.jsonl | 0 .../session.jsonl | 0 examples/package.json | 56 +- examples/web-cordis/README.i18n.yaml | 4 +- examples/web-cordis/README.md | 4 +- examples/web-cordis/README.zh.md | 4 +- examples/web-schedule/cordis.yml | 4 +- knip.json | 34 +- package.json | 4 +- packages/AGENTS.md | 4 +- packages/README.i18n.yaml | 4 +- packages/README.md | 15 +- packages/README.zh.md | 15 +- packages/acp/acp/tsconfig.json | 2 +- packages/api/README.i18n.yaml | 4 +- packages/api/README.md | 6 +- packages/api/README.zh.md | 6 +- packages/api/gateway/README.i18n.yaml | 4 +- packages/api/gateway/README.md | 10 +- packages/api/gateway/README.zh.md | 10 +- packages/api/gateway/package.json | 4 +- packages/api/gateway/src/client/index.ts | 52 +- packages/api/gateway/src/index.ts | 42 +- packages/api/gateway/src/invariant.ts | 2 +- packages/api/gateway/src/types.ts | 4 +- .../api/gateway/tests/gateway.client.spec.ts | 30 +- .../api/gateway/tests/gateway.host.spec.ts | 72 +- packages/api/gateway/tsconfig.client.json | 2 +- packages/api/gateway/tsconfig.host.json | 4 +- packages/api/remotes/README.i18n.yaml | 4 +- packages/api/remotes/README.md | 8 +- packages/api/remotes/README.zh.md | 8 +- packages/api/remotes/package.json | 2 +- packages/api/remotes/src/agent-lookup.ts | 8 +- packages/api/remotes/src/client/index.ts | 10 +- packages/api/remotes/src/index.ts | 4 +- packages/api/remotes/src/invariant.ts | 2 +- packages/api/remotes/src/types.ts | 4 +- .../api/remotes/tests/agent-lookup.spec.ts | 4 +- packages/api/remotes/tests/built-lib.e2e.ts | 2 +- packages/api/remotes/tsconfig.client.json | 2 +- packages/api/remotes/tsconfig.host.json | 4 +- .../attachment/attachment-local/package.json | 4 +- .../attachment/attachment-local/src/index.ts | 2 +- .../attachment/attachment-local/tsconfig.json | 4 +- packages/attachment/attachment/tsconfig.json | 2 +- packages/bash/bash-env/README.md | 51 - packages/bash/bash-env/README.zh.md | 51 - packages/bash/bash/README.zh.md | 53 - packages/bash/pwsh-local/README.i18n.yaml | 6 - packages/bash/pwsh-sandbox/README.i18n.yaml | 6 - packages/bash/tool-bash/README.i18n.yaml | 6 - packages/bash/tool-pwsh/README.i18n.yaml | 6 - packages/boot/app-boot/README.i18n.yaml | 4 +- packages/boot/app-boot/README.md | 2 +- packages/boot/app-boot/README.zh.md | 2 +- packages/boot/app-boot/package.json | 8 +- packages/boot/app-boot/src/index.ts | 10 +- packages/boot/app-boot/src/profile.ts | 6 +- packages/boot/app-boot/tests/app-boot.spec.ts | 2 +- packages/boot/app-boot/tsconfig.json | 6 +- packages/boot/cmdline/tsconfig.json | 2 +- packages/bundle/base/cordis.patch.yml | 70 +- packages/bundle/base/package.json | 36 +- packages/bundle/base/tests/base.spec.ts | 2 +- packages/bundle/base/tsconfig.json | 2 +- packages/bundle/headless/cordis.patch.yml | 2 +- packages/bundle/headless/package.json | 2 +- .../bundle/headless/tests/headless.spec.ts | 4 +- packages/bundle/headless/tsconfig.json | 2 +- packages/bundle/web-app/README.i18n.yaml | 4 +- packages/bundle/web-app/README.md | 2 +- packages/bundle/web-app/README.zh.md | 2 +- packages/bundle/web-app/cordis.patch.yml | 80 +- packages/bundle/web-app/package.json | 34 +- packages/bundle/web-app/src/index.ts | 26 +- packages/bundle/web-app/tests/web-app.spec.ts | 32 +- packages/bundle/web-app/tsconfig.json | 4 +- packages/client/AGENTS.md | 4 +- packages/client/README.i18n.yaml | 4 +- packages/client/README.md | 20 +- packages/client/README.zh.md | 20 +- packages/client/connection/README.i18n.yaml | 4 +- packages/client/connection/README.md | 2 +- packages/client/connection/README.zh.md | 2 +- packages/client/connection/src/client/api.ts | 2 +- .../client/connection/src/client/fixture.ts | 4 +- .../client/connection/src/client/index.ts | 2 +- packages/client/connection/src/index.ts | 8 +- packages/client/connection/src/rpc-host.ts | 2 +- .../connection/tests/node-half.host.spec.ts | 18 +- .../client/connection/tsconfig.client.json | 2 +- packages/client/connection/tsconfig.host.json | 2 +- packages/client/hmr/src/index.ts | 20 +- .../client/hmr/tests/node-half.client.spec.ts | 18 +- packages/client/hmr/tsconfig.json | 2 +- packages/client/locale/README.i18n.yaml | 4 +- packages/client/locale/README.md | 2 +- packages/client/locale/README.zh.md | 2 +- packages/client/locale/src/client/index.ts | 6 +- .../client/locale/tests/apply.client.spec.ts | 22 +- .../client/locale/tests/host.client.spec.ts | 4 +- .../locale/tests/invariant.client.spec.ts | 16 +- .../locale/tests/language-row.client.spec.tsx | 2 +- .../client/locale/tests/locale.client.spec.ts | 8 +- packages/client/locale/tsconfig.json | 2 +- packages/client/modules/src/index.ts | 18 +- packages/client/modules/src/invariant.ts | 2 +- .../modules/tests/node-half.client.spec.ts | 14 +- packages/client/modules/tsconfig.json | 2 +- packages/client/runtime/README.i18n.yaml | 4 +- packages/client/runtime/README.md | 16 +- packages/client/runtime/README.zh.md | 16 +- packages/client/runtime/package.json | 6 +- .../client/runtime/src/client/agents/scope.ts | 4 +- .../runtime/src/client/contract/session.ts | 2 +- .../src/client/contract/sessions-port.ts | 2 +- packages/client/runtime/src/client/index.ts | 32 +- .../src/client/sessions/context-provenance.ts | 2 +- .../src/client/sessions/conversation.ts | 4 +- .../runtime/src/client/sessions/manager.ts | 26 +- .../runtime/src/client/sessions/provide.ts | 4 +- .../runtime/src/client/sessions/service.ts | 30 +- .../runtime/src/client/sessions/session.ts | 6 +- packages/client/runtime/src/client/slots.ts | 8 +- .../runtime/src/client/workspaces/service.ts | 4 +- .../runtime/tests/client-apply.client.spec.ts | 16 +- .../tests/context-provenance.client.spec.ts | Bin 4572 -> 4540 bytes .../conversation-assembler.client.spec.ts | 4 +- .../conversation-registry.client.spec.ts | 4 +- .../runtime/tests/event-script.client.ts | 4 +- .../runtime/tests/invariant.client.spec.ts | 10 +- .../runtime/tests/manager.client.spec.ts | 18 +- .../tests/sessions-service.client.spec.ts | 8 +- .../tests/slots-service.client.spec.ts | 8 +- .../tests/workspaces-service.client.spec.ts | 58 +- packages/client/runtime/tsconfig.json | 6 +- .../tests/invariant.client.spec.ts | 4 +- packages/client/schema-form/tsconfig.json | 2 +- .../tests/apply.client.spec.ts | 14 +- .../tests/invariant.client.spec.ts | 4 +- packages/client/ui-agent-preset/tsconfig.json | 4 +- .../tests/invariant.client.spec.ts | 4 +- packages/client/ui-attachment/tsconfig.json | 2 +- packages/client/ui-command/tsdown.config.ts | 3 - .../README.i18n.yaml | 6 +- .../{ui-command => ui-commands}/README.md | 8 +- .../{ui-command => ui-commands}/README.zh.md | 8 +- .../{ui-command => ui-commands}/package.json | 10 +- .../src/client/PopupSelectView.module.css | 0 .../src/client/PopupSelectView.tsx | 0 .../src/client/contract.ts | 10 +- .../src/client/directory.ts | 0 .../src/client/index.ts | 22 +- .../src/client/locales.ts | 0 .../src/client/popup.ts | 8 +- .../src/client/service.ts | 38 +- .../src/css-modules.d.ts | 0 .../{ui-command => ui-commands}/src/index.ts | 2 +- .../src/invariant.ts | 8 +- .../tests/browser-plugin.client.spec.ts | 38 +- .../tests/directory.client.spec.ts | 0 .../tests/popup-view.client.spec.tsx | 0 .../tests/popup.client.spec.ts | 0 .../tests/service.client.spec.ts | 20 +- .../{ui-command => ui-commands}/tsconfig.json | 4 +- .../tsdown.config.ts | 2 +- .../client/ui-conversation/README.i18n.yaml | 4 +- packages/client/ui-conversation/README.md | 12 +- packages/client/ui-conversation/README.zh.md | 10 +- packages/client/ui-conversation/package.json | 10 +- .../ui-conversation/src/client/apply.ts | 22 +- .../src/client/chat/ChatView.tsx | 2 +- .../src/client/chat/CompactionItem.tsx | 2 +- .../src/client/contract/slots.ts | 10 +- .../src/client/conversation-nodes/command.ts | 20 +- .../client/conversation-nodes/compaction.ts | 12 +- .../ui-conversation/src/client/index.ts | 2 +- .../src/client/input/blocks.ts | 2 +- .../src/client/input/contract.ts | 8 +- .../src/client/input/facade.ts | 38 +- .../ui-conversation/src/client/input/hub.ts | 28 +- .../src/client/input/machine.ts | 2 +- .../ui-conversation/src/client/service.ts | 16 +- .../src/client/skeleton/ConversationRoot.tsx | 2 +- .../src/client/skeleton/PermissionSelect.tsx | 2 +- .../tests/apply-inject.client.spec.tsx | 4 +- .../tests/assembly-surfaces.client.spec.tsx | 10 +- .../tests/chat-apply.client.spec.tsx | 4 +- .../tests/chat-branch-tails.client.spec.tsx | 28 +- .../tests/chat-view.client.spec.tsx | 2 +- ...nversation-node-definitions.client.spec.ts | 30 +- .../tests/enter-behavior-row.client.spec.tsx | 2 +- .../tests/gate-branch-tails.client.spec.tsx | 4 +- .../ui-conversation/tests/host.client.spec.ts | 4 +- .../tests/input-bar.client.spec.tsx | 6 +- .../tests/input-machine.client.spec.ts | 2 +- .../tests/input-matrix.client.spec.tsx | 4 +- .../tests/input-scenarios.client.spec.tsx | 44 +- .../tests/selection-survival.client.spec.tsx | 4 +- .../service-orchestration.client.spec.ts | 20 +- .../tests/skeleton.client.spec.tsx | 2 +- .../tests/views-type-chain.client.spec.tsx | 6 +- packages/client/ui-conversation/tsconfig.json | 6 +- .../tests/produced-files.client.spec.tsx | 6 +- packages/client/ui-deliverables/tsconfig.json | 2 +- .../README.i18n.yaml | 6 + .../README.md | 2 +- .../README.zh.md | 2 +- .../package.json | 4 +- .../src/client/DirectoryBrowser.module.css | 0 .../src/client/DirectoryBrowser.tsx | 0 .../src/client/flow.ts | 0 .../src/client/index.ts | 0 .../src/css-modules.d.ts | 0 .../src/index.ts | 0 .../src/invariant.ts | 8 +- .../tests/client-flow.client.spec.tsx | 12 +- .../tests/directory-browser.client.spec.tsx | 0 .../tsconfig.json | 2 +- .../tsdown.config.ts | 2 +- .../tests/client-flow.client.spec.tsx | 6 +- .../ui-directory-picker-native/tsconfig.json | 2 +- packages/client/ui-feedback/tsdown.config.ts | 3 - packages/client/ui-goal/src/client/slots.ts | 2 +- .../tests/browser-plugin.client.spec.tsx | 8 +- packages/client/ui-goal/tsconfig.json | 2 +- .../README.i18n.yaml | 6 +- packages/client/ui-input-trigger/README.md | 25 + packages/client/ui-input-trigger/README.zh.md | 25 + .../package.json | 4 +- .../src/client/MenuView.module.css | 0 .../src/client/MenuView.tsx | 2 +- .../src/client/contract.ts | 14 +- .../src/client/controller.ts | 34 +- .../src/client/index.ts | 30 +- .../src/client/locales.ts | 0 .../src/client/service.ts | 30 +- .../src/client/slots.ts | 4 +- .../src/core/contract.ts | 8 +- .../src/core/detect.ts | 0 .../src/core/menu.ts | 4 +- .../src/css-modules.d.ts | 0 .../src/index.ts | 0 .../src/invariant.ts | 8 +- .../src/types.ts | 16 +- .../tests/apply.client.spec.ts | 30 +- .../tests/core-detect.client.spec.ts | 0 .../tests/core-menu.client.spec.ts | 0 .../tests/menu-view.client.spec.tsx | 4 +- .../tests/service.client.spec.ts | 126 +- .../tsconfig.json | 2 +- .../client/ui-input-trigger/tsdown.config.ts | 3 + .../{ui-task => ui-jobs}/README.i18n.yaml | 6 +- packages/client/ui-jobs/README.md | 24 + .../client/{ui-task => ui-jobs}/README.zh.md | 8 +- .../client/{ui-task => ui-jobs}/package.json | 6 +- .../src/client/JobListAction.module.css} | 2 +- .../src/client/JobListAction.tsx} | 76 +- .../{ui-task => ui-jobs}/src/client/index.ts | 22 +- .../src/client/locales.ts | 20 +- .../src/css-modules.d.ts | 0 .../client/{ui-task => ui-jobs}/src/index.ts | 2 +- .../{ui-task => ui-jobs}/src/invariant.ts | 10 +- .../tests/browser-plugin.client.spec.ts | 30 +- .../tests/job-list-action.client.spec.tsx} | 118 +- .../client/{ui-task => ui-jobs}/tsconfig.json | 2 +- .../{ui-task => ui-jobs}/tsdown.config.ts | 2 +- packages/client/ui-layout/README.i18n.yaml | 4 +- packages/client/ui-layout/README.md | 2 +- packages/client/ui-layout/README.zh.md | 2 +- packages/client/ui-layout/src/client/index.ts | 6 +- .../client/ui-layout/src/client/service.ts | 4 +- .../ui-layout/tests/apply.client.spec.ts | 20 +- .../ui-layout/tests/service.client.spec.ts | 12 +- packages/client/ui-layout/tsconfig.json | 2 +- .../README.i18n.yaml | 6 +- .../README.md | 6 +- .../README.zh.md | 6 +- .../package.json | 8 +- .../client/MessageFeedbackActions.module.css} | 0 .../src/client/MessageFeedbackActions.tsx} | 8 +- .../src/client/controller.ts | 60 +- .../src/client/index.ts | 28 +- .../src/client/locales.ts | 6 +- .../src/client/slots.ts | 22 +- .../src/css-modules.d.ts | 0 .../src/index.ts | 0 .../src/invariant.ts | 6 +- .../tests/browser-plugin.client.spec.tsx | 16 +- .../tests/controller.client.spec.ts | 82 +- .../message-feedback-actions.client.spec.tsx} | 40 +- .../tsconfig.json | 4 +- .../ui-message-feedback/tsdown.config.ts | 3 + .../README.i18n.yaml | 6 +- .../README.md | 8 +- .../README.zh.md | 8 +- .../package.json | 14 +- .../src/client/ModelSelect.module.css | 0 .../src/client/ModelSelect.tsx | 0 .../src/client/directory.ts | 0 .../src/client/index.ts | 28 +- .../src/client/locales.ts | 0 .../src/client/service.ts | 24 +- .../src/client/slots.ts | 0 .../src/css-modules.d.ts | 0 .../src/index.ts | 0 .../src/invariant.ts | 8 +- .../tests/browser-plugin.client.spec.ts | 20 +- .../tests/model-select.client.spec.tsx | 0 .../tsconfig.json | 6 +- .../ui-model-selection/tsdown.config.ts | 3 + packages/client/ui-model/tsdown.config.ts | 3 - packages/client/ui-models/tsdown.config.ts | 3 - .../ui-permission-presets/README.i18n.yaml | 6 + .../README.md | 4 +- .../README.zh.md | 4 +- .../package.json | 18 +- .../src/client/PermissionRow.module.css | 0 .../src/client/PermissionRow.tsx | 0 .../src/client/index.ts | 14 +- .../src/client/locales.ts | 0 .../src/client/presentation.ts | 0 .../src/client/settings-store.ts | 4 +- .../src/css-modules.d.ts | 0 .../src/index.ts | 0 .../src/invariant.ts | 8 +- .../tests/browser-plugin.client.spec.ts | 14 +- .../permission-presets-row.client.spec.tsx} | 14 +- .../tests/settings-store.client.spec.ts | 22 +- .../tsconfig.json | 8 +- .../tsdown.config.ts | 2 +- .../client/ui-permission/README.i18n.yaml | 6 - .../tests/browser-plugin.client.spec.ts | 16 +- packages/client/ui-plan/tsconfig.json | 2 +- .../client/ui-plugin-config/README.i18n.yaml | 6 - .../client/ui-plugin-config/tsdown.config.ts | 3 - packages/client/ui-plugins/README.i18n.yaml | 6 - packages/client/ui-plugins/tsdown.config.ts | 3 - .../tests/code-block.client.spec.tsx | 2 +- .../tests/invariant.client.spec.ts | 4 +- .../tests/markdown-plain-text.client.spec.ts | 6 +- packages/client/ui-primitives/tsconfig.json | 2 +- packages/client/ui-question/README.i18n.yaml | 6 - .../tests/apply.client.spec.ts | 14 +- .../tests/host.client.spec.ts | 4 +- .../tests/invariant.client.spec.ts | 4 +- .../tests/shell.client.spec.ts | 10 +- .../client/ui-settings-general/tsconfig.json | 2 +- .../README.i18n.yaml | 6 +- .../README.md | 2 +- .../README.zh.md | 2 +- .../package.json | 4 +- .../src/client/CustomProviderCard.tsx | 0 .../src/client/DeepSeekModelsEditor.tsx | 0 .../DeepSeekOnboardingDialog.module.css | 0 .../src/client/DeepSeekOnboardingDialog.tsx | 0 .../src/client/EditorFooter.tsx | 2 +- .../src/client/ModelListEditor.tsx | 0 .../src/client/ModelsSection.module.css | 0 .../src/client/ModelsSection.tsx | 0 .../src/client/ProviderEditor.tsx | 0 .../src/client/apiKey.ts | 2 +- .../src/client/index.ts | 4 +- .../src/client/locales.ts | 0 .../src/client/store.ts | 0 .../src/css-modules.d.ts | 0 .../src/index.ts | 0 .../src/invariant.ts | 8 +- .../tests/apply.client.spec.ts | 16 +- .../tests/components.client.spec.tsx | 0 .../tests/invariant.client.spec.ts | 8 +- .../tests/onboarding-dialog.client.spec.tsx | 0 .../tests/provider-form.client.spec.tsx | 0 .../tests/readiness.client.spec.ts | 0 .../tests/store.client.spec.ts | 0 .../tests/styles.client.spec.ts | 0 .../tsconfig.json | 2 +- .../ui-settings-models/tsdown.config.ts | 3 + .../README.i18n.yaml | 6 + .../README.md | 2 +- .../README.zh.md | 2 +- .../package.json | 4 +- .../PluginInventorySettingsTab.module.css} | 0 .../client/PluginInventorySettingsTab.tsx} | 18 +- .../src/client/index.ts | 20 +- .../src/client/locales.ts | 4 +- .../src/css-modules.d.ts | 0 .../src/index.ts | 0 .../src/invariant.ts | 6 +- .../tests/browser-plugin.client.spec.tsx | 24 +- .../tests/components.client.spec.tsx | 36 +- .../tests/invariant.client.spec.ts | 6 +- .../tsconfig.json | 2 +- .../tsdown.config.ts | 3 + .../ui-settings-plugins/README.i18n.yaml | 6 + .../README.md | 2 +- .../README.zh.md | 2 +- .../package.json | 4 +- .../src/client/AgentLoopCard.tsx | 4 +- .../src/client/BashCard.tsx | 4 +- .../src/client/ConfigurablePluginsTab.tsx | 4 +- .../src/client/PluginCard.module.css | 0 .../src/client/PluginCard.tsx | 10 +- .../client/PluginsSettingsSection.module.css} | 0 .../src/client/PluginsSettingsSection.tsx} | 22 +- .../src/client/WebSearchCard.tsx | 4 +- .../src/client/agent-loop-card-controller.ts} | 2 +- .../src/client/bash-card-controller.ts} | 4 +- .../src/client/card-form.ts} | 0 .../src/client/fields.module.css | 0 .../src/client/fields.tsx | 0 .../src/client/index.ts | 34 +- .../src/client/locales.ts | 6 +- .../src/client/slot-contract.ts | 0 .../src/client/web-search-card-controller.ts} | 2 +- .../src/css-modules.d.ts | 0 .../src/index.ts | 0 .../src/invariant.ts | 8 +- .../tests/apply.client.spec.ts | 26 +- .../tests/fields.client.spec.tsx | 0 .../tests/invariant.client.spec.ts | 8 +- .../tests/section.client.spec.tsx | 22 +- .../tests/stores.client.spec.ts | 8 +- .../tsconfig.json | 4 +- .../ui-settings-plugins/tsdown.config.ts | 3 + .../client/ui-settings/src/client/index.ts | 6 +- .../ui-settings/src/client/settings-scope.ts | 4 +- .../tests/invariant.client.spec.ts | 4 +- .../ui-settings/tests/plugin.client.spec.ts | 4 +- .../tests/settings-scope.client.spec.ts | 8 +- packages/client/ui-settings/tsconfig.json | 2 +- .../ui-sidebar/tests/apply.client.spec.tsx | 10 +- .../ui-sidebar/tests/invariant.client.spec.ts | 4 +- .../tests/sidebar-snapshot.client.spec.tsx | 4 +- packages/client/ui-sidebar/tsconfig.json | 2 +- packages/client/ui-skill/README.i18n.yaml | 4 +- packages/client/ui-skill/README.md | 2 +- packages/client/ui-skill/README.zh.md | 2 +- packages/client/ui-skill/package.json | 6 +- packages/client/ui-skill/src/client/index.ts | 12 +- .../tests/browser-plugin.client.spec.ts | 30 +- packages/client/ui-skill/tsconfig.json | 4 +- packages/client/ui-slash/README.i18n.yaml | 6 - packages/client/ui-slash/README.md | 25 - packages/client/ui-slash/README.zh.md | 25 - packages/client/ui-slash/tsdown.config.ts | 3 - packages/client/ui-slots/src/invariant.ts | 2 +- packages/client/ui-slots/src/renderer.ts | 4 +- .../ui-slots/tests/invariant.client.spec.ts | 4 +- packages/client/ui-slots/tsconfig.json | 2 +- packages/client/ui-subagent/README.i18n.yaml | 4 +- packages/client/ui-subagent/README.md | 4 +- packages/client/ui-subagent/README.zh.md | 4 +- packages/client/ui-subagent/package.json | 6 +- .../client/ui-subagent/src/client/index.ts | 10 +- .../tests/browser-plugin.client.spec.ts | 26 +- .../tests/conversation-ui.client.spec.tsx | 2 +- packages/client/ui-subagent/tsconfig.json | 4 +- packages/client/ui-task/README.md | 24 - packages/client/ui-theme/README.i18n.yaml | 4 +- packages/client/ui-theme/README.md | 4 +- packages/client/ui-theme/README.zh.md | 4 +- packages/client/ui-theme/package.json | 2 +- packages/client/ui-theme/src/client/index.ts | 6 +- packages/client/ui-theme/src/index.ts | 4 +- .../tests/appearance-row.client.spec.tsx | 2 +- .../ui-theme/tests/apply.client.spec.ts | 30 +- .../client/ui-theme/tests/host.client.spec.ts | 14 +- .../ui-theme/tests/invariant.client.spec.ts | 12 +- .../ui-theme/tests/theme.client.spec.ts | 8 +- packages/client/ui-theme/tsconfig.json | 2 +- packages/client/ui-tool/README.i18n.yaml | 4 +- packages/client/ui-tool/README.md | 2 +- packages/client/ui-tool/README.zh.md | 2 +- .../tool/toolviews/ask-question-row.tsx | 2 +- .../tests/ask-question-row.client.spec.tsx | 4 +- .../tests/assembly-surfaces.client.spec.tsx | 4 +- .../tests/chat-code-subcalls.client.spec.tsx | 16 +- .../tests/coverage-tails.client.spec.tsx | 2 +- .../ui-tool/tests/diff-card.client.spec.tsx | 6 +- .../ui-tool/tests/read-card.client.spec.tsx | 6 +- .../ui-tool/tests/search-card.client.spec.tsx | 2 +- .../tests/terminal-card.client.spec.tsx | 8 +- .../tests/toolview-slot.client.spec.tsx | 8 +- .../tests/toolview-type-chain.client.spec.tsx | 4 +- .../ui-tool/tests/web-card.client.spec.tsx | 2 +- packages/client/ui-tool/tsconfig.json | 2 +- packages/client/ui-trajectory/package.json | 4 +- .../trajectory-compaction-definition.ts | 28 +- .../tests/client-bundle.client.spec.ts | 6 +- .../conversation-definitions.client.spec.ts | 8 +- .../ui-trajectory/tests/table.client.spec.tsx | 6 +- .../ui-trajectory/tests/views.client.spec.tsx | 16 +- packages/client/ui-trajectory/tsconfig.json | 4 +- .../README.i18n.yaml | 6 +- .../README.md | 2 +- .../README.zh.md | 2 +- .../package.json | 6 +- .../src/client/PlanReviewPanel.module.css | 0 .../src/client/PlanReviewPanel.tsx | 0 .../src/client/QuestionComposer.module.css | 0 .../src/client/QuestionComposer.tsx | 0 .../src/client/contract/slots.ts | 0 .../src/client/index.ts | 2 +- .../src/client/locales.ts | 0 .../src/css-modules.d.ts | 0 .../src/index.ts | 0 .../src/invariant.ts | 8 +- .../tests/browser-plugin.client.spec.ts | 16 +- .../tests/node-plugin.client.spec.ts | 10 +- .../tests/plan-review-panel.client.spec.tsx | 0 .../user-questions-composer.client.spec.tsx} | 0 .../tsconfig.json | 2 +- .../client/ui-user-questions/tsdown.config.ts | 3 + .../client/ui-workflow-run/README.i18n.yaml | 4 +- packages/client/ui-workflow-run/README.md | 2 +- packages/client/ui-workflow-run/README.zh.md | 2 +- .../src/client/WorkflowRunPanel.tsx | 2 +- .../tests/workflow-run.client.spec.tsx | 6 +- packages/client/ui-workflow-run/tsconfig.json | 2 +- .../src/client/WorkspaceBrowser.tsx | 130 +- .../client/ui-workspace/src/client/stores.ts | 64 +- .../client/ui-workspace/src/client/tree.ts | 10 +- .../ui-workspace/tests/apply.client.spec.ts | 12 +- .../tests/invariant.client.spec.ts | 4 +- .../tests/rename-assembly.client.spec.tsx | 4 +- .../ui-workspace/tests/tree.client.spec.ts | 52 +- .../tests/workspace-browser.client.spec.tsx | 50 +- .../tests/workspace-picker.client.spec.tsx | 2 +- packages/client/ui-workspace/tsconfig.json | 2 +- packages/client/web-react/README.i18n.yaml | 4 +- packages/client/web-react/README.md | 2 +- packages/client/web-react/README.zh.md | 2 +- .../client/web-react/src/scoped-slots.tsx | 2 +- .../tests/scoped-slots.client.spec.tsx | 2 +- packages/client/web-react/tsconfig.json | 2 +- .../web/tests/app-shell.client.spec.tsx | 8 +- packages/client/web/tsconfig.json | 2 +- packages/code-runtime/README.i18n.yaml | 4 +- packages/code-runtime/README.md | 2 +- packages/code-runtime/README.zh.md | 2 +- .../README.i18n.yaml | 6 +- .../README.md | 8 +- .../README.zh.md | 8 +- .../package.json | 4 +- .../src/bootstrap.ts | 2 +- .../src/index.ts | 28 +- .../src/invariant.ts | 8 +- .../src/output-json.ts | 2 +- .../src/protocol.ts | 2 +- .../src/worker-json.ts | 5 +- .../src/worker.ts | 4 +- .../tests/bootstrap.spec.ts | 0 .../tests/built-lib.e2e.ts | 4 +- .../tests/output-json.spec.ts | 0 .../tests/runtime.spec.ts | 34 +- .../tests/source-worker.compat.spec.ts | 0 .../tests/worker-json.spec.ts | 0 .../tsconfig.json | 2 +- .../tsdown.config.ts | 0 .../code-runtime/code-runtime/tsconfig.json | 2 +- packages/compact/README.md | 16 - .../{compact => compaction}/README.i18n.yaml | 6 +- packages/compaction/README.md | 16 + packages/{compact => compaction}/README.zh.md | 10 +- .../command-compact/README.i18n.yaml | 6 +- .../command-compact/README.md | 14 +- .../command-compact/README.zh.md | 14 +- .../command-compact/package.json | 6 +- .../command-compact/src/index.ts | 6 +- .../command-compact/src/invariant.ts | 0 .../tests/command-compact.spec.ts | 32 +- .../command-compact/tests/invariant.spec.ts | 0 .../tests/loader-composition.spec.ts | 30 +- .../command-compact/tsconfig.json | 4 +- .../compaction-basic}/README.i18n.yaml | 6 +- .../compaction-basic}/README.md | 40 +- .../compaction-basic}/README.zh.md | 40 +- .../compaction-basic}/package.json | 14 +- .../compaction-basic}/src/config.ts | 36 +- .../compaction-basic}/src/index.ts | 32 +- .../compaction-basic}/src/invariant.ts | 8 +- .../compaction-basic}/src/region.ts | 38 +- .../compaction-basic}/src/summarizer.ts | 8 +- .../compaction-basic}/src/types.ts | 8 +- .../tests/compaction-basic.spec.ts} | 194 +- .../tests/compaction-loop-repro.spec.ts} | 44 +- .../tests/loader-composition.spec.ts | 52 +- .../tests/manual-compaction.spec.ts} | 142 +- .../compaction-basic}/tsconfig.json | 6 +- .../README.i18n.yaml | 6 +- .../compaction-tool-result-pruner}/README.md | 12 +- .../README.zh.md | 12 +- .../package.json | 8 +- .../src/config.ts | 0 .../src/index.ts | 18 +- .../src/invariant.ts | 8 +- .../src/types.ts | 0 .../tests/loader-composition.spec.ts | 20 +- .../tests/tool-result-pruner.spec.ts} | 30 +- .../tsconfig.json | 4 +- .../compaction}/README.i18n.yaml | 6 +- .../compaction}/README.md | 34 +- .../compaction}/README.zh.md | 34 +- .../compaction}/package.json | 6 +- .../compaction}/src/brand.ts | 0 .../compaction}/src/checkpoint.ts | 8 +- .../compaction}/src/index.ts | 32 +- .../compaction}/src/invariant.ts | 72 +- .../compaction}/src/tool-pairing.ts | 2 +- .../compaction}/src/types.ts | 26 +- .../compaction/tests/compaction.spec.ts} | 62 +- .../compaction}/tests/invariant.spec.ts | 190 +- .../compaction}/tests/tool-pairing.spec.ts | 2 +- .../compaction}/tsconfig.json | 2 +- .../compaction}/tsdown.config.ts | 0 packages/context/README.i18n.yaml | 4 +- packages/context/README.md | 8 +- packages/context/README.zh.md | 8 +- .../README.i18n.yaml | 6 +- .../README.md | 4 +- .../README.zh.md | 4 +- .../package.json | 8 +- .../src/config.ts | 4 +- .../src/digest.ts | 2 +- .../src/files.ts | 4 +- .../src/index.ts | 28 +- .../src/invariant.ts | 6 +- .../src/render.ts | 8 +- .../src/state.ts | 46 +- .../tests/agent-instructions.e2e.ts} | 12 +- .../tests/agent-instructions.spec.ts} | 144 +- .../tsconfig.json | 4 +- .../session-reference/README.i18n.yaml | 4 +- packages/context/session-reference/README.md | 6 +- .../context/session-reference/README.zh.md | 6 +- .../context/session-reference/package.json | 10 +- .../context/session-reference/src/index.ts | 8 +- .../session-reference/src/projection.ts | 4 +- .../tests/session-reference.spec.ts | 86 +- .../context/session-reference/tsconfig.json | 6 +- .../time-context/tests/invariant.spec.ts | 8 +- .../time-context/tests/time-context.spec.ts | 2 +- packages/context/time-context/tsconfig.json | 4 +- .../context/tmux-context/README.i18n.yaml | 4 +- packages/context/tmux-context/README.md | 4 +- packages/context/tmux-context/README.zh.md | 4 +- packages/context/tmux-context/package.json | 4 +- packages/context/tmux-context/src/index.ts | 12 +- .../tmux-context/tests/tmux-context.spec.ts | 22 +- packages/context/tmux-context/tsconfig.json | 4 +- .../core/agent-default-model/README.i18n.yaml | 4 +- packages/core/agent-default-model/README.md | 2 +- .../core/agent-default-model/README.zh.md | 2 +- .../core/agent-default-model/src/index.ts | 6 +- .../tests/agent-default-model.spec.ts | 14 +- .../core/agent-default-model/tsconfig.json | 2 +- packages/core/agent-loop/README.i18n.yaml | 4 +- packages/core/agent-loop/README.md | 2 +- packages/core/agent-loop/README.zh.md | 2 +- packages/core/agent-loop/src/agent.ts | 4 +- packages/core/agent-loop/src/index.ts | 16 +- packages/core/agent-loop/src/tool-calls.ts | 10 +- .../agent-loop/tests/agent-initiator.spec.ts | 16 +- packages/core/agent-loop/tests/agent.spec.ts | 8 +- packages/core/agent-loop/tests/cancel.spec.ts | 12 +- .../tests/config-session-id.spec.ts | 60 +- .../tests/contract-regressions.spec.ts | 42 +- .../agent-loop/tests/coverage-edges.spec.ts | 8 +- .../agent-loop/tests/interception.spec.ts | 8 +- .../core/agent-loop/tests/invariant.spec.ts | 6 +- packages/core/agent-loop/tests/loop.spec.ts | 28 +- .../core/agent-loop/tests/properties.spec.ts | 8 +- .../agent-loop/tests/request-cache.e2e.ts | 8 +- .../agent-loop/tests/request-error.spec.ts | 8 +- .../tests/request-reconstruction.spec.ts | 16 +- packages/core/agent-loop/tests/resume.spec.ts | 76 +- .../agent-loop/tests/scope-lifecycle.spec.ts | 10 +- .../core/agent-loop/tests/settings.spec.ts | 12 +- .../core/agent-loop/tests/tool-calls.spec.ts | 22 +- .../core/agent-loop/tests/tool-order.spec.ts | 8 +- packages/core/agent-loop/tsconfig.json | 2 +- .../README.i18n.yaml | 6 +- .../README.md | 4 +- .../README.zh.md | 4 +- .../package.json | 4 +- .../src/index.ts | 6 +- .../src/invariant.ts | 8 +- .../tests/agent-tool-presentation.spec.ts} | 8 +- .../tsconfig.json | 2 +- packages/core/agent/package.json | 4 +- packages/core/agent/src/index.ts | 12 +- packages/core/agent/tests/agent.spec.ts | 2 +- packages/core/agent/tests/invariant.spec.ts | 4 +- packages/core/agent/tsconfig.json | 4 +- packages/core/scope/tests/invariant.spec.ts | 4 +- packages/core/scope/tsconfig.json | 2 +- packages/core/session/README.i18n.yaml | 4 +- packages/core/session/README.md | 4 +- packages/core/session/README.zh.md | 4 +- packages/core/session/package.json | 4 +- packages/core/session/src/index.ts | 10 +- .../core/session/src/known-event-types.ts | 8 +- packages/core/session/src/surface.ts | 2 +- packages/core/session/src/types.ts | 4 +- packages/core/session/tests/fork.spec.ts | 2 +- packages/core/session/tests/invariant.spec.ts | 6 +- packages/core/session/tests/session.spec.ts | 4 +- packages/core/session/tests/typert.spec.ts | 2 +- packages/core/session/tsconfig.json | 4 +- packages/core/system-prompt/README.i18n.yaml | 4 +- packages/core/system-prompt/README.md | 8 +- packages/core/system-prompt/README.zh.md | 8 +- packages/core/system-prompt/src/index.ts | 2 +- .../system-prompt/tests/invariant.spec.ts | 4 +- .../system-prompt/tests/system-prompt.spec.ts | 12 +- packages/core/system-prompt/tsconfig.json | 2 +- packages/core/tools/README.i18n.yaml | 4 +- packages/core/tools/README.md | 8 +- packages/core/tools/README.zh.md | 8 +- packages/core/tools/src/code-mode.ts | 8 +- packages/core/tools/src/index.ts | 38 +- packages/core/tools/src/presentation.ts | 2 +- packages/core/tools/tests/code-mode.spec.ts | 20 +- .../core/tools/tests/execution-mode.spec.ts | 6 +- .../core/tools/tests/gen-tool-catalog.spec.ts | 4 +- packages/core/tools/tests/invariant.spec.ts | 8 +- packages/core/tools/tests/scoped.spec.ts | 4 +- packages/core/tools/tests/tools.spec.ts | 8 +- packages/core/tools/tsconfig.json | 2 +- .../credentials-local/README.i18n.yaml | 4 +- .../credentials/credentials-local/README.md | 2 +- .../credentials-local/README.zh.md | 2 +- .../credentials-local/package.json | 8 +- .../credentials-local/src/index.ts | 26 +- .../credentials-local/tests/drain.spec.ts | 4 +- .../credentials-local/tests/local.spec.ts | 30 +- .../tests/review-fixes.spec.ts | 6 +- .../credentials-local/tests/watcher.spec.ts | 8 +- .../credentials-local/tsconfig.json | 6 +- packages/credentials/credentials/src/index.ts | 10 +- .../credentials/tests/invariant.spec.ts | 8 +- .../credentials/credentials/tests/memory.ts | 4 +- .../credentials/credentials/tsconfig.json | 2 +- packages/e2b/README.i18n.yaml | 4 +- packages/e2b/README.md | 2 +- packages/e2b/README.zh.md | 2 +- packages/e2b/e2b/src/index.ts | 6 +- packages/e2b/e2b/tests/composition.e2e.ts | 14 +- packages/e2b/e2b/tests/e2b.spec.ts | 28 +- packages/e2b/e2b/tsconfig.json | 2 +- packages/e2b/fs-e2b/tests/filesystem.spec.ts | 8 +- packages/e2b/fs-e2b/tsconfig.json | 2 +- packages/e2b/subprocess-e2b/README.i18n.yaml | 4 +- packages/e2b/subprocess-e2b/README.md | 2 +- packages/e2b/subprocess-e2b/README.zh.md | 2 +- packages/e2b/subprocess-e2b/src/index.ts | 6 +- packages/e2b/subprocess-e2b/src/process.ts | 4 +- packages/e2b/subprocess-e2b/src/terminal.ts | 4 +- .../subprocess-e2b/tests/subprocess.spec.ts | 20 +- .../e2b/subprocess-e2b/tests/terminal.spec.ts | 14 +- packages/e2b/subprocess-e2b/tsconfig.json | 2 +- packages/examples/README.i18n.yaml | 4 +- packages/examples/README.md | 2 +- packages/examples/README.zh.md | 2 +- packages/examples/acp-demo/README.i18n.yaml | 4 +- packages/examples/acp-demo/README.md | 4 +- packages/examples/acp-demo/README.zh.md | 4 +- packages/examples/acp-demo/package.json | 4 +- packages/examples/acp-demo/src/index.ts | 26 +- .../examples/acp-demo/tests/acp-agent.spec.ts | 26 +- .../examples/acp-demo/tests/built-bin.e2e.ts | 6 +- .../examples/acp-demo/tests/load-path.e2e.ts | 2 +- packages/examples/acp-demo/tsconfig.json | 4 +- .../agent-spine-demo/README.i18n.yaml | 4 +- packages/examples/agent-spine-demo/README.md | 20 +- .../examples/agent-spine-demo/README.zh.md | 18 +- .../examples/agent-spine-demo/package.json | 32 +- .../examples/agent-spine-demo/src/index.ts | 84 +- .../agent-spine-demo/tests/agent-core.spec.ts | 60 +- .../tests/multi-project-sandbox.e2e.ts | 8 +- .../examples/agent-spine-demo/tsconfig.json | 20 +- .../examples/jsonrpc-demo/README.i18n.yaml | 4 +- packages/examples/jsonrpc-demo/README.md | 6 +- packages/examples/jsonrpc-demo/README.zh.md | 6 +- packages/examples/jsonrpc-demo/package.json | 2 +- packages/examples/jsonrpc-demo/src/bin.ts | 2 +- packages/examples/jsonrpc-demo/src/index.ts | 4 +- .../examples/jsonrpc-demo/src/invariant.ts | 8 +- .../examples/jsonrpc-demo/src/packaged-bin.ts | 2 +- packages/examples/jsonrpc-demo/src/runner.ts | 2 +- packages/examples/jsonrpc-demo/tsconfig.json | 2 +- .../README.i18n.yaml | 2 +- .../README.md | 0 .../README.zh.md | 0 .../extensions/tool-cordis/README.i18n.yaml | 6 + .../tool-cordis/README.md | 4 +- .../tool-cordis/README.zh.md | 2 +- .../tool-cordis/package.json | 2 +- .../tool-cordis/src/api-catalog.ts | 682 ++++--- .../tool-cordis/src/fiber-state.ts | 0 .../tool-cordis/src/guard.ts | 2 +- .../tool-cordis/src/index.ts | 10 +- .../tool-cordis/src/inspect.ts | 0 .../tool-cordis/src/invariant.ts | 0 .../tool-cordis/src/mount.ts | 0 .../tool-cordis/src/present.ts | 0 .../tool-cordis/src/sandbox.ts | 2 +- .../tests/cordis-lifecycle.spec.ts | 0 .../tool-cordis/tests/cross-mount.spec.ts | 0 .../tool-cordis/tests/helpers.ts | 8 +- .../tool-cordis/tests/inspect.spec.ts | 10 +- .../tool-cordis/tests/integration.spec.ts | 0 .../tool-cordis/tests/mount.spec.ts | 0 .../tool-cordis/tests/present.spec.ts | 0 .../tool-cordis/tests/sandbox-context.spec.ts | 2 +- .../tool-cordis/tests/tool-cordis.spec.ts | 0 .../tool-cordis/tests/unmount-hmr.spec.ts | 4 +- .../tool-cordis/tsconfig.json | 2 +- .../command-feedback/README.i18n.yaml | 4 +- packages/feedback/command-feedback/README.md | 6 +- .../feedback/command-feedback/README.zh.md | 6 +- .../feedback/command-feedback/package.json | 4 +- .../feedback/command-feedback/src/index.ts | 12 +- .../tests/command-feedback.spec.ts | 28 +- .../tests/loader-composition.spec.ts | 8 +- .../feedback/command-feedback/tsconfig.json | 4 +- .../message-feedback/README.i18n.yaml | 4 +- packages/feedback/message-feedback/README.md | 2 +- .../feedback/message-feedback/README.zh.md | 2 +- .../feedback/message-feedback/package.json | 4 +- .../feedback/message-feedback/src/index.ts | 4 +- .../message-feedback/tests/invariant.spec.ts | 4 +- .../tests/loader-composition.spec.ts | 8 +- .../tests/message-feedback.spec.ts | 4 +- .../feedback/message-feedback/tsconfig.json | 4 +- packages/fs/README.i18n.yaml | 4 +- packages/fs/README.md | 4 +- packages/fs/README.zh.md | 4 +- packages/fs/fs-local/README.i18n.yaml | 4 +- packages/fs/fs-local/README.md | 2 +- packages/fs/fs-local/README.zh.md | 2 +- packages/fs/fs-local/tests/filesystem.spec.ts | 2 +- packages/fs/fs-local/tests/fsio.spec.ts | 2 +- packages/fs/fs-local/tsconfig.json | 2 +- .../fs/fs-observation-policy/README.i18n.yaml | 6 + .../README.md | 8 +- .../README.zh.md | 8 +- .../package.json | 4 +- .../src/index.ts | 14 +- .../src/invariant.ts | 8 +- .../src/types.ts | 8 +- .../tests/policy.spec.ts | 12 +- .../tsconfig.json | 2 +- packages/fs/fs-sandbox/README.i18n.yaml | 4 +- packages/fs/fs-sandbox/README.md | 4 +- packages/fs/fs-sandbox/README.zh.md | 4 +- packages/fs/fs-sandbox/src/index.ts | 2 +- packages/fs/fs-sandbox/tsconfig.json | 2 +- packages/fs/fs/README.i18n.yaml | 4 +- packages/fs/fs/README.md | 6 +- packages/fs/fs/README.zh.md | 6 +- packages/fs/fs/src/index.ts | 2 +- packages/fs/fs/tests/invariant.spec.ts | 4 +- packages/fs/fs/tests/service.spec.ts | 2 +- packages/fs/fs/tsconfig.json | 2 +- packages/fs/tool-fs-search/README.i18n.yaml | 4 +- packages/fs/tool-fs-search/README.md | 6 +- packages/fs/tool-fs-search/README.zh.md | 4 +- packages/fs/tool-fs-search/package.json | 4 +- packages/fs/tool-fs-search/src/grep.ts | 2 +- packages/fs/tool-fs-search/src/index.ts | 7 +- .../fs/tool-fs-search/src/presentation.ts | 2 +- packages/fs/tool-fs-search/src/search-core.ts | 10 +- .../tool-fs-search/tests/integration.spec.ts | 8 +- .../fs/tool-fs-search/tests/load-path.spec.ts | 8 +- .../fs/tool-fs-search/tests/tools.spec.ts | 18 +- packages/fs/tool-fs-search/tsconfig.json | 6 +- packages/fs/tool-fs/README.i18n.yaml | 4 +- packages/fs/tool-fs/README.md | 14 +- packages/fs/tool-fs/README.zh.md | 14 +- packages/fs/tool-fs/package.json | 2 +- packages/fs/tool-fs/src/edit.ts | 2 +- packages/fs/tool-fs/src/sandbox.ts | 2 +- packages/fs/tool-fs/src/write.ts | 2 +- packages/fs/tool-fs/tests/harness.ts | 2 +- packages/fs/tool-fs/tests/integration.spec.ts | 16 +- packages/fs/tool-fs/tests/read-image.spec.ts | 12 +- packages/fs/tool-fs/tests/tools.spec.ts | 18 +- packages/fs/tool-fs/tsconfig.json | 4 +- .../fs/tool-str-replace-editor/package.json | 2 +- .../tests/tools.spec.ts | 10 +- .../fs/tool-str-replace-editor/tsconfig.json | 2 +- packages/goal/README.i18n.yaml | 4 +- packages/goal/README.md | 2 +- packages/goal/README.zh.md | 2 +- .../command-goal/tests/command-goal.spec.ts | 6 +- packages/goal/command-goal/tsconfig.json | 2 +- .../README.i18n.yaml | 6 +- .../README.md | 6 +- .../README.zh.md | 6 +- .../package.json | 4 +- .../src/index.ts | 24 +- .../src/invariant.ts | 8 +- .../src/prompt.ts | 0 .../tests/goal-round-driver.spec.ts} | 6 +- .../tests/invariant.spec.ts | 24 +- .../tsconfig.json | 2 +- .../tsdown.config.ts | 0 packages/goal/goal/package.json | 4 +- packages/goal/goal/src/index.ts | 4 +- packages/goal/goal/tests/invariant.spec.ts | 6 +- packages/goal/goal/tsconfig.json | 4 +- .../goal/tool-goal/tests/tool-goal.spec.ts | 8 +- packages/goal/tool-goal/tsconfig.json | 2 +- packages/guard/README.i18n.yaml | 4 +- packages/guard/README.md | 2 +- packages/guard/README.zh.md | 2 +- .../README.i18n.yaml | 6 +- .../README.md | 10 +- .../README.zh.md | 10 +- .../package.json | 4 +- .../src/index.ts | 18 +- .../src/invariant.ts | 8 +- .../tests/repeat-tool-reminder.spec.ts} | 6 +- .../tsconfig.json | 2 +- .../guard/timeout-policy/README.i18n.yaml | 4 +- packages/guard/timeout-policy/README.md | 4 +- packages/guard/timeout-policy/README.zh.md | 4 +- packages/guard/timeout-policy/package.json | 2 +- packages/guard/timeout-policy/src/index.ts | 2 +- .../guard/timeout-policy/src/invariant.ts | 6 +- .../tests/timeout-policy.spec.ts | 16 +- packages/guard/timeout-policy/tsconfig.json | 2 +- packages/hooks/README.i18n.yaml | 4 +- packages/hooks/README.md | 2 +- packages/hooks/README.zh.md | 2 +- packages/hooks/hook-protocol/README.i18n.yaml | 4 +- packages/hooks/hook-protocol/README.md | 12 +- packages/hooks/hook-protocol/README.zh.md | 12 +- packages/hooks/hook-protocol/package.json | 4 +- packages/hooks/hook-protocol/src/invariant.ts | 2 +- packages/hooks/hook-protocol/src/matcher.ts | 4 +- packages/hooks/hook-protocol/src/runner.ts | 8 +- packages/hooks/hook-protocol/src/types.ts | 8 +- .../hooks/hook-protocol/tests/events.spec.ts | 6 +- .../hook-protocol/tests/invariant.spec.ts | 10 +- .../hooks/hook-protocol/tests/matcher.spec.ts | 34 +- .../hooks/hook-protocol/tests/runner.spec.ts | 20 +- packages/hooks/hook-protocol/tsconfig.json | 4 +- .../README.i18n.yaml | 6 +- .../README.md | 10 +- .../README.zh.md | 10 +- .../package.json | 6 +- .../src/config.ts | 12 +- .../src/index.ts | 38 +- .../src/invariant.ts | 8 +- .../tests/bridge.spec.ts | 42 +- .../tests/config.spec.ts | 36 +- .../tests/coverage-cases.ts | 54 +- .../tests/coverage-config.spec.ts | 0 .../tests/coverage-context.spec.ts | 0 .../tests/coverage-edge-paths.spec.ts | 0 .../tests/coverage-stop.spec.ts | 0 .../tsconfig.json | 4 +- packages/hooks/hooks-codex/package.json | 2 +- packages/hooks/hooks-codex/src/index.ts | 4 +- .../hooks/hooks-codex/tests/bridge.spec.ts | 12 +- .../hooks/hooks-codex/tests/coverage-cases.ts | 14 +- packages/hooks/hooks-codex/tsconfig.json | 4 +- packages/host/README.i18n.yaml | 4 +- packages/host/README.md | 6 +- packages/host/README.zh.md | 6 +- packages/host/apiproxy/README.i18n.yaml | 4 +- packages/host/apiproxy/README.md | 6 +- packages/host/apiproxy/README.zh.md | 6 +- packages/host/apiproxy/package.json | 6 +- packages/host/apiproxy/src/api-proxy.ts | 106 +- .../host/apiproxy/src/api/events.schema.ts | 10 +- packages/host/apiproxy/src/api/events.ts | 8 +- packages/host/apiproxy/src/api/index.ts | 2 +- .../api/{tasks.schema.ts => jobs.schema.ts} | 14 +- .../apiproxy/src/api/{tasks.ts => jobs.ts} | 10 +- .../host/apiproxy/src/api/questions.schema.ts | 4 +- packages/host/apiproxy/src/api/questions.ts | 4 +- packages/host/apiproxy/src/api/sessions.ts | 2 +- packages/host/apiproxy/src/index.ts | 2 +- packages/host/apiproxy/src/session-export.ts | 6 +- .../tests/api-proxy-agent-preset.spec.ts | 6 +- .../apiproxy/tests/api-proxy-approval.spec.ts | 8 +- .../apiproxy/tests/api-proxy-blank.spec.ts | 6 +- .../apiproxy/tests/api-proxy-cold.spec.ts | 36 +- .../apiproxy/tests/api-proxy-config.spec.ts | 34 +- .../apiproxy/tests/api-proxy-fork.spec.ts | 6 +- ...y-tasks.spec.ts => api-proxy-jobs.spec.ts} | 90 +- .../apiproxy/tests/api-proxy-models.spec.ts | 8 +- .../tests/api-proxy-projections.spec.ts | 4 +- .../apiproxy/tests/api-proxy-question.spec.ts | 8 +- .../apiproxy/tests/api-proxy-rename.spec.ts | 4 +- .../apiproxy/tests/api-proxy-search.spec.ts | 4 +- .../tests/api-proxy-subagents.spec.ts | 2 +- .../apiproxy/tests/api-proxy-view.spec.ts | 10 +- .../tests/api-proxy-workspace.spec.ts | 10 +- .../host/apiproxy/tests/rpc-schemas.spec.ts | 16 +- .../apiproxy/tests/session-export.spec.ts | 4 +- packages/host/apiproxy/tsconfig.json | 6 +- .../directory-picker-auto/README.i18n.yaml | 4 +- packages/host/directory-picker-auto/README.md | 2 +- .../host/directory-picker-auto/README.zh.md | 2 +- .../host/directory-picker-auto/package.json | 4 +- .../host/directory-picker-auto/src/index.ts | 12 +- .../tests/loader-composition.spec.ts | 2 +- .../host/directory-picker-auto/tsconfig.json | 2 +- .../directory-picker-browse/tsconfig.json | 2 +- .../tests/built-worker.e2e.ts | 2 +- .../directory-picker-native/tsconfig.json | 2 +- .../directory-picker-native/tsdown.config.ts | 2 +- packages/host/directory-picker/tsconfig.json | 2 +- .../host/frontend-static/README.i18n.yaml | 4 +- packages/host/frontend-static/README.md | 2 +- packages/host/frontend-static/README.zh.md | 2 +- packages/host/frontend-static/package.json | 2 +- packages/host/frontend-static/src/index.ts | 12 +- .../host/frontend-static/src/invariant.ts | 8 +- .../tests/frontend-static.spec.ts | 6 +- packages/host/frontend-static/tsconfig.json | 2 +- .../host/plugin-inventory/README.i18n.yaml | 4 +- packages/host/plugin-inventory/README.md | 4 +- packages/host/plugin-inventory/README.zh.md | 4 +- packages/host/plugin-inventory/package.json | 4 +- packages/host/plugin-inventory/src/index.ts | 8 +- .../plugin-inventory/tests/invariant.spec.ts | 4 +- .../plugin-inventory/tests/inventory.spec.ts | 14 +- packages/host/plugin-inventory/tsconfig.json | 4 +- packages/host/webserver/README.i18n.yaml | 4 +- packages/host/webserver/README.md | 2 +- packages/host/webserver/README.zh.md | 2 +- packages/host/webserver/src/index.ts | 12 +- packages/host/webserver/src/invariant.ts | 4 +- .../host/webserver/tests/webserver.spec.ts | 4 +- packages/host/webserver/tsconfig.json | 2 +- packages/{pty => identity}/README.i18n.yaml | 6 +- packages/identity/README.md | 9 + packages/identity/README.zh.md | 9 + .../anonymous-user-id/README.i18n.yaml | 6 + .../anonymous-user-id}/README.md | 6 +- .../anonymous-user-id}/README.zh.md | 6 +- .../anonymous-user-id}/package.json | 8 +- .../anonymous-user-id}/src/index.ts | 10 +- .../anonymous-user-id}/src/invariant.ts | 8 +- .../tests/anonymous-user-id.spec.ts} | 20 +- .../tests/invariant.spec.ts | 6 +- .../anonymous-user-id}/tsconfig.json | 4 +- packages/interaction/README.i18n.yaml | 4 +- packages/interaction/README.md | 6 +- packages/interaction/README.zh.md | 6 +- packages/interaction/commands/package.json | 4 +- packages/interaction/commands/src/index.ts | 8 +- .../commands/tests/commands.spec.ts | 6 +- .../commands/tests/invariant.spec.ts | 4 +- packages/interaction/commands/tsconfig.json | 4 +- .../README.i18n.yaml | 6 +- .../interaction/permission-presets/README.md | 28 + .../permission-presets/README.zh.md | 28 + .../package.json | 10 +- .../src/client.ts | 2 +- .../src/index.ts | 28 +- .../src/invariant.ts | 10 +- .../src/types.ts | 2 +- .../tests/invariant.spec.ts | 10 +- .../tests/permission-presets.spec.ts} | 88 +- .../tests/projection.spec.ts | 18 +- .../tsconfig.json | 4 +- packages/interaction/permission/README.md | 28 - packages/interaction/permission/README.zh.md | 28 - .../tool-ask-user/README.i18n.yaml | 4 +- packages/interaction/tool-ask-user/README.md | 6 +- .../interaction/tool-ask-user/README.zh.md | 4 +- .../interaction/tool-ask-user/package.json | 6 +- .../interaction/tool-ask-user/src/index.ts | 8 +- .../tool-ask-user/tests/tool-ask-user.spec.ts | 38 +- .../interaction/tool-ask-user/tsconfig.json | 4 +- .../user-approval/tests/invariant.spec.ts | 8 +- .../interaction/user-approval/tsconfig.json | 2 +- .../README.i18n.yaml | 6 +- .../README.md | 16 +- .../README.zh.md | 16 +- .../package.json | 6 +- .../src/index.ts | 46 +- .../src/invariant.ts | 8 +- .../src/types.ts | 4 +- .../tests/user-questions.spec.ts} | 104 +- .../tsconfig.json | 2 +- packages/{bash => jobs}/README.i18n.yaml | 6 +- packages/jobs/README.md | 15 + packages/jobs/README.zh.md | 15 + .../jobs-local}/README.i18n.yaml | 6 +- packages/jobs/jobs-local/README.md | 34 + .../jobs-local}/README.zh.md | 12 +- .../jobs-local}/package.json | 10 +- .../jobs-local}/src/index.ts | 316 +-- .../jobs-local}/src/invariant.ts | 12 +- .../jobs-local/tests/jobs.spec.ts} | 642 +++---- .../tests/loader-composition.spec.ts | 22 +- .../jobs-local}/tsconfig.json | 4 +- .../{bash/bash => jobs/jobs}/README.i18n.yaml | 6 +- packages/jobs/jobs/README.md | 40 + .../{tasks/tasks => jobs/jobs}/README.zh.md | 16 +- .../{tasks/tasks => jobs/jobs}/package.json | 6 +- .../{tasks/tasks => jobs/jobs}/src/brand.ts | 16 +- .../{tasks/tasks => jobs/jobs}/src/index.ts | 112 +- .../tasks => jobs/jobs}/src/invariant.ts | 30 +- .../{tasks/tasks => jobs/jobs}/src/types.ts | 92 +- .../jobs}/tests/invariant.spec.ts | 52 +- packages/jobs/jobs/tests/service.spec.ts | 96 + .../{tasks/tasks => jobs/jobs}/tsconfig.json | 2 +- .../tool-jobs}/README.i18n.yaml | 6 +- packages/jobs/tool-jobs/README.md | 96 + .../tool-jobs}/README.zh.md | 24 +- .../tool-jobs}/package.json | 16 +- .../tool-jobs}/src/index.ts | 156 +- .../tool-jobs}/src/invariant.ts | 8 +- .../tool-jobs/tests/tool-jobs.spec.ts} | 346 ++-- .../tool-jobs}/tsconfig.json | 6 +- packages/llm/llm-deepseek/README.i18n.yaml | 4 +- packages/llm/llm-deepseek/README.md | 6 +- packages/llm/llm-deepseek/README.zh.md | 6 +- packages/llm/llm-deepseek/package.json | 8 +- packages/llm/llm-deepseek/src/adapter.ts | 2 +- packages/llm/llm-deepseek/src/index.ts | 10 +- .../llm/llm-deepseek/tests/adapter.e2e.ts | 10 +- .../llm/llm-deepseek/tests/adapter.spec.ts | 58 +- .../llm-deepseek/tests/dynamic-config.spec.ts | 18 +- .../tests/loader-composition.spec.ts | 20 +- packages/llm/llm-deepseek/tsconfig.json | 6 +- packages/llm/llm-pi-ai/README.i18n.yaml | 4 +- packages/llm/llm-pi-ai/README.md | 2 +- packages/llm/llm-pi-ai/README.zh.md | 2 +- packages/llm/llm-pi-ai/package.json | 4 +- packages/llm/llm-pi-ai/src/index.ts | 4 +- packages/llm/llm-pi-ai/tests/adapter.e2e.ts | 6 +- packages/llm/llm-pi-ai/tests/adapter.spec.ts | 42 +- packages/llm/llm-pi-ai/tests/catalog.spec.ts | 10 +- .../llm/llm-pi-ai/tests/discovery.spec.ts | 10 +- .../llm-pi-ai/tests/dynamic-config.spec.ts | 12 +- .../tests/loader-composition.spec.ts | 18 +- .../llm/llm-pi-ai/tests/provider-apis.e2e.ts | 4 +- packages/llm/llm-pi-ai/tsconfig.json | 4 +- .../llm/llm-retry/tests/invariant.spec.ts | 10 +- .../tests/loader-composition.spec.ts | 8 +- .../llm/llm-retry/tests/persistence.spec.ts | 8 +- packages/llm/llm-retry/tests/retry.spec.ts | 8 +- packages/llm/llm-retry/tsconfig.json | 2 +- packages/llm/llm/README.i18n.yaml | 4 +- packages/llm/llm/README.md | 8 +- packages/llm/llm/README.zh.md | 8 +- packages/llm/llm/src/index.ts | 20 +- packages/llm/llm/src/message.ts | 2 +- packages/llm/llm/src/types.ts | 2 +- packages/llm/llm/tests/invariant.spec.ts | 10 +- packages/llm/llm/tests/service.spec.ts | 94 +- packages/llm/llm/tests/topology.spec.ts | 4 +- packages/llm/llm/tsconfig.json | 2 +- packages/llm/token-meter/README.i18n.yaml | 4 +- packages/llm/token-meter/README.md | 8 +- packages/llm/token-meter/README.zh.md | 8 +- packages/llm/token-meter/package.json | 4 +- packages/llm/token-meter/src/index.ts | 6 +- .../llm/token-meter/src/surface-projection.ts | 8 +- .../context-breakdown-projection.spec.ts | 16 +- .../llm/token-meter/tests/token-meter.spec.ts | 22 +- .../tests/token-usage-projection.spec.ts | 14 +- packages/llm/token-meter/tsconfig.json | 4 +- packages/lsp/README.i18n.yaml | 4 +- packages/lsp/README.md | 2 +- packages/lsp/README.zh.md | 2 +- .../{lsp-local => lsp-stdio}/README.i18n.yaml | 6 +- .../lsp/{lsp-local => lsp-stdio}/README.md | 2 +- .../lsp/{lsp-local => lsp-stdio}/README.zh.md | 2 +- .../lsp/{lsp-local => lsp-stdio}/package.json | 4 +- .../lsp/{lsp-local => lsp-stdio}/src/abort.ts | 2 +- .../src/connection.ts | 4 +- .../{lsp-local => lsp-stdio}/src/framing.ts | 2 +- .../lsp/{lsp-local => lsp-stdio}/src/host.ts | 0 .../lsp/{lsp-local => lsp-stdio}/src/index.ts | 24 +- .../{lsp-local => lsp-stdio}/src/instance.ts | 2 +- .../{lsp-local => lsp-stdio}/src/invariant.ts | 8 +- .../{lsp-local => lsp-stdio}/src/protocol.ts | 2 +- .../{lsp-local => lsp-stdio}/src/translate.ts | 2 +- .../tests/built-lib.e2e.ts | 8 +- .../tests/connection.spec.ts | 4 +- .../tests/fixture-server.ts | 2 +- .../tests/framing.spec.ts | 2 +- .../tests/host.spec.ts | 2 +- .../tests/instance.spec.ts | 8 +- .../tests/lifecycle.spec.ts | 24 +- .../tests/provider.spec.ts | 38 +- .../tests/translate.spec.ts | 4 +- .../tests/typescript-server.e2e.ts | 8 +- .../{lsp-local => lsp-stdio}/tsconfig.json | 2 +- packages/lsp/lsp/README.i18n.yaml | 4 +- packages/lsp/lsp/README.md | 2 +- packages/lsp/lsp/README.zh.md | 2 +- packages/lsp/lsp/tsconfig.json | 2 +- packages/lsp/tool-lsp/README.i18n.yaml | 4 +- packages/lsp/tool-lsp/README.md | 2 +- packages/lsp/tool-lsp/README.zh.md | 2 +- packages/lsp/tool-lsp/package.json | 4 +- packages/lsp/tool-lsp/src/index.ts | 2 +- .../lsp/tool-lsp/tests/integration.spec.ts | 12 +- packages/lsp/tool-lsp/tests/tool-lsp.spec.ts | 4 +- packages/lsp/tool-lsp/tsconfig.json | 2 +- packages/mcp/mcp-client/src/tools.ts | 10 +- packages/mcp/mcp-client/tests/apply.spec.ts | 8 +- .../mcp/mcp-client/tests/mcp-client.e2e.ts | 4 +- .../mcp/mcp-client/tests/mcp-client.spec.ts | 4 +- .../mcp/mcp-client/tests/reconnect.spec.ts | 4 +- packages/mcp/mcp-client/tsconfig.json | 2 +- packages/plan/plan-mode/README.i18n.yaml | 4 +- packages/plan/plan-mode/README.md | 4 +- packages/plan/plan-mode/README.zh.md | 4 +- packages/plan/plan-mode/package.json | 4 +- packages/plan/plan-mode/src/index.ts | 14 +- .../plan/plan-mode/tests/integration.spec.ts | 12 +- .../plan/plan-mode/tests/invariant.spec.ts | 10 +- .../plan/plan-mode/tests/plan-mode.spec.ts | 102 +- .../plan/plan-mode/tests/projection.spec.ts | 14 +- packages/plan/plan-mode/tsconfig.json | 4 +- .../preset/agent-presets/README.i18n.yaml | 4 +- packages/preset/agent-presets/README.md | 4 +- packages/preset/agent-presets/README.zh.md | 4 +- packages/preset/agent-presets/package.json | 6 +- .../preset/agent-presets/src/authoring.ts | 2 +- .../preset/agent-presets/src/discovery.ts | 4 +- packages/preset/agent-presets/src/index.ts | 6 +- .../agent-presets/tests/invariant.spec.ts | 12 +- .../preset/agent-presets/tests/mount.spec.ts | 16 +- .../agent-presets/tests/settings.spec.ts | 12 +- .../agent-presets/tests/user-root.spec.ts | 2 +- packages/preset/agent-presets/tsconfig.json | 4 +- packages/preset/persona/tsconfig.json | 2 +- packages/pty/README.md | 15 - packages/pty/README.zh.md | 15 - packages/pty/pty-local/README.i18n.yaml | 6 - .../pty/tool-bash-persistent/README.i18n.yaml | 6 - packages/pty/tool-pty/README.i18n.yaml | 6 - packages/pty/tool-pty/README.md | 71 - packages/pty/tool-pty/README.zh.md | 71 - .../invariants/README.i18n.yaml | 6 + .../invariants/README.md | 14 +- .../invariants/README.zh.md | 14 +- .../invariants/package.json | 2 +- .../invariants/src/index.ts | 6 +- .../invariants/src/invariant.ts | 0 .../invariants/tests/service.spec.ts | 14 +- .../invariants/tsconfig.json | 0 .../sandbox/sandbox-local/README.i18n.yaml | 4 +- packages/sandbox/sandbox-local/README.md | 4 +- packages/sandbox/sandbox-local/README.zh.md | 4 +- .../sandbox-local/tests/packed-install.e2e.ts | 4 +- packages/sandbox/sandbox-local/tsconfig.json | 2 +- .../sandbox-policy/tests/invariant.spec.ts | 6 +- packages/sandbox/sandbox-policy/tsconfig.json | 2 +- .../sandbox-windows-acl/README.i18n.yaml | 4 +- .../sandbox/sandbox-windows-acl/README.md | 2 +- .../sandbox/sandbox-windows-acl/README.zh.md | 2 +- .../sandbox/sandbox-windows-acl/tsconfig.json | 2 +- packages/sandbox/sandbox/README.i18n.yaml | 4 +- packages/sandbox/sandbox/README.md | 6 +- packages/sandbox/sandbox/README.zh.md | 6 +- packages/sandbox/sandbox/tsconfig.json | 2 +- packages/schedule/README.i18n.yaml | 4 +- packages/schedule/README.md | 2 +- packages/schedule/README.zh.md | 2 +- .../README.i18n.yaml | 6 +- .../{tool-schedule => schedule}/README.md | 4 +- .../{tool-schedule => schedule}/README.zh.md | 4 +- .../{tool-schedule => schedule}/package.json | 4 +- .../{tool-schedule => schedule}/src/domain.ts | 2 +- .../{tool-schedule => schedule}/src/index.ts | 32 +- .../src/invariant.ts | 4 +- .../src/persistence.ts | 0 .../src/runtime.ts | 34 +- .../{tool-schedule => schedule}/src/tools.ts | 6 +- .../src/transaction.ts | 0 .../{tool-schedule => schedule}/src/types.ts | 2 +- .../tests/domain.spec.ts | 0 .../tests/invariant.spec.ts | 8 +- .../tests/jsonl-restart.spec.ts | 6 +- .../tests/plugin.spec.ts | 2 +- .../tests/recurrence.spec.ts | 0 .../tests/runtime.spec.ts | 258 +-- .../tests/tools.spec.ts | 4 +- .../{tool-schedule => schedule}/tsconfig.json | 2 +- .../tsdown.config.ts | 0 packages/sdk/client/tsconfig.json | 2 +- packages/sdk/protocol/README.i18n.yaml | 4 +- packages/sdk/protocol/README.md | 8 +- packages/sdk/protocol/README.zh.md | 8 +- packages/sdk/protocol/src/index.ts | 2 +- packages/sdk/protocol/src/types.ts | 4 +- packages/sdk/protocol/tsconfig.json | 2 +- packages/sdk/server/README.i18n.yaml | 4 +- packages/sdk/server/README.md | 4 +- packages/sdk/server/README.zh.md | 4 +- packages/sdk/server/package.json | 2 +- packages/sdk/server/src/index.ts | 12 +- packages/sdk/server/src/invariant.ts | 8 +- packages/sdk/server/src/server.ts | 16 +- .../server/tests/built-scope-carrier.e2e.ts | 16 +- .../sdk/server/tests/plugin-apply.spec.ts | 6 +- .../sdk/server/tests/plugin-shape.spec.ts | 4 +- packages/sdk/server/tests/server.spec.ts | 54 +- packages/sdk/server/tsconfig.json | 2 +- .../tool-cordis/README.i18n.yaml | 6 - packages/session-query/README.i18n.yaml | 4 +- packages/session-query/README.md | 2 +- packages/session-query/README.zh.md | 2 +- .../session-export/tsdown.config.ts | 3 - .../README.i18n.yaml | 6 +- .../README.md | 8 +- .../README.zh.md | 8 +- .../package.json | 10 +- .../src/client/Dialog.tsx | 18 +- .../src/client/HeaderAction.module.css | 0 .../src/client/HeaderAction.tsx | 10 +- .../src/client/controller.ts | 18 +- .../src/client/index.ts | 34 +- .../src/client/locales.ts | 4 +- .../src/css-modules.d.ts | 0 .../src/index.ts | 4 +- .../src/invariant.ts | 4 +- .../tests/client-apply.client.spec.tsx | 34 +- .../tests/command.client.spec.ts | 4 +- .../tests/controller.client.spec.ts | 22 +- .../tests/dialog.client.spec.tsx | 18 +- .../tests/header-action.client.spec.tsx | 30 +- .../tests/invariant.client.spec.ts | 4 +- .../tests/loader-composition.client.spec.ts | 12 +- .../tsconfig.json | 4 +- .../session-log-download/tsdown.config.ts | 3 + .../session-query-sqlite/README.i18n.yaml | 4 +- .../session-query-sqlite/README.md | 2 +- .../session-query-sqlite/README.zh.md | 2 +- .../session-query-sqlite/src/index.ts | 6 +- .../tests/load-path.e2e.ts | 8 +- .../session-query-sqlite/tests/sqlite.spec.ts | 94 +- .../session-query-sqlite/tsconfig.json | 2 +- .../session-query/README.i18n.yaml | 4 +- .../session-query/session-query/README.md | 4 +- .../session-query/session-query/README.zh.md | 4 +- .../session-query/session-query/src/index.ts | 6 +- .../tests/search-helpers.spec.ts | 6 +- .../session-query/tests/session-query.spec.ts | 18 +- .../session-query/tests/test-service.ts | 4 +- .../session-query/tests/tracing.spec.ts | 4 +- .../session-query/session-query/tsconfig.json | 2 +- .../tool-session-query/package.json | 2 +- .../tests/sqlite-integration.spec.ts | 18 +- .../tests/tool-session-query.spec.ts | 10 +- .../tool-session-query/tsconfig.json | 2 +- packages/session/README.i18n.yaml | 4 +- packages/session/README.md | 8 +- packages/session/README.zh.md | 6 +- .../tests/crash-recovery.e2e.ts | 4 +- .../tests/fixtures/crash-child.ts | 4 +- .../tests/session-checkpoint-policy.spec.ts | 12 +- .../session-checkpoint-policy/tsconfig.json | 2 +- .../session-persistence-jsonl/src/index.ts | 4 +- .../tests/jsonl.spec.ts | 62 +- .../tests/zstd.spec.ts | 18 +- .../session-persistence-jsonl/tsconfig.json | 2 +- .../session-persistence-sqlite/src/index.ts | 4 +- .../tests/sqlite.spec.ts | 44 +- .../session-persistence-sqlite/tsconfig.json | 2 +- .../session-persistence/README.i18n.yaml | 4 +- .../session/session-persistence/README.md | 2 +- .../session/session-persistence/README.zh.md | 2 +- .../session/session-persistence/tsconfig.json | 2 +- .../session-projection-cache/tsconfig.json | 2 +- .../session/session-projection/tsconfig.json | 2 +- packages/session/session-stats/tsconfig.json | 2 +- .../session-telemetry-otel/README.i18n.yaml | 4 +- .../session/session-telemetry-otel/README.md | 16 +- .../session-telemetry-otel/README.zh.md | 16 +- .../session-telemetry-otel/package.json | 4 +- .../session-telemetry-otel/src/index.ts | 66 +- .../tests/loader-composition.e2e.ts | 4 +- .../session-telemetry-otel/tests/otel.spec.ts | 114 +- .../session-telemetry-otel/tsconfig.json | 4 +- .../session-telemetry/README.i18n.yaml | 4 +- packages/session/session-telemetry/README.md | 12 +- .../session/session-telemetry/README.zh.md | 12 +- .../session/session-telemetry/package.json | 2 +- .../session-telemetry/src/coordinator.ts | 24 +- .../session/session-telemetry/src/index.ts | 48 +- .../session-telemetry/tests/redact.spec.ts | 32 +- .../session-telemetry/tests/telemetry.spec.ts | 56 +- .../session/session-telemetry/tsconfig.json | 2 +- .../README.i18n.yaml | 6 +- .../README.md | 4 +- .../README.zh.md | 4 +- .../package.json | 4 +- .../src/index.ts | 8 +- .../src/invariant.ts | 8 +- .../tests/provider.spec.ts | 6 +- .../tsconfig.json | 2 +- .../README.i18n.yaml | 6 - .../README.i18n.yaml | 6 +- .../README.md | 4 +- .../README.zh.md | 4 +- .../package.json | 4 +- .../src/index.ts | 8 +- .../src/invariant.ts | 8 +- .../tests/loader-composition.spec.ts | 12 +- .../tests/provider.e2e.ts | 10 +- .../tests/provider.spec.ts | 10 +- .../tsconfig.json | 2 +- .../session-title-llm/tests/llm.spec.ts | 12 +- .../session/session-title-llm/tsconfig.json | 2 +- .../session/session-title/README.i18n.yaml | 4 +- packages/session/session-title/README.md | 4 +- packages/session/session-title/README.zh.md | 2 +- packages/session/session-title/src/index.ts | 6 +- .../session/session-title/src/normalize.ts | 2 +- .../session-title/tests/invariant.spec.ts | 4 +- .../session-title/tests/persistence.spec.ts | 12 +- .../session-title/tests/provider.spec.ts | 30 +- .../session-title/tests/rename.spec.ts | 4 +- .../tests/service-contracts.spec.ts | 22 +- packages/session/session-title/tsconfig.json | 2 +- packages/settings/README.i18n.yaml | 4 +- packages/settings/README.md | 2 +- packages/settings/README.zh.md | 2 +- .../README.i18n.yaml | 6 +- .../README.md | 2 +- .../README.zh.md | 2 +- .../package.json | 8 +- .../src/index.ts | 24 +- .../src/invariant.ts | 8 +- .../tests/concurrency.spec.ts | 6 +- .../tests/loader-composition.spec.ts | 8 +- .../tests/local.spec.ts | 6 +- .../tests/lock-race.spec.ts | 8 +- .../tests/watcher.spec.ts | 8 +- .../tsconfig.json | 4 +- packages/settings/settings/src/index.ts | 8 +- .../settings/settings/tests/invariant.spec.ts | 4 +- packages/settings/settings/tests/memory.ts | 6 +- .../settings/settings/tests/settings.spec.ts | 6 +- packages/settings/settings/tsconfig.json | 2 +- packages/{tasks => shell}/README.i18n.yaml | 6 +- packages/{bash => shell}/README.md | 16 +- packages/{bash => shell}/README.zh.md | 14 +- packages/shell/bash-local/README.i18n.yaml | 6 + packages/{bash => shell}/bash-local/README.md | 8 +- .../{bash => shell}/bash-local/README.zh.md | 8 +- .../{bash => shell}/bash-local/package.json | 6 +- .../{bash => shell}/bash-local/src/index.ts | 28 +- .../bash-local/src/invariant.ts | 0 .../bash-local/tests/executor.spec.ts | 26 +- .../bash-local/tests/settings.spec.ts | 30 +- .../{bash => shell}/bash-local/tsconfig.json | 4 +- packages/shell/bash-sandbox/README.i18n.yaml | 6 + .../{bash => shell}/bash-sandbox/README.md | 10 +- .../{bash => shell}/bash-sandbox/README.zh.md | 10 +- .../{bash => shell}/bash-sandbox/package.json | 6 +- .../bash-sandbox/src/helpers.ts | 4 +- .../{bash => shell}/bash-sandbox/src/index.ts | 20 +- .../bash-sandbox/src/invariant.ts | 0 .../bash-sandbox/tests/bwrap.e2e.ts | 8 +- .../bash-sandbox/tests/landlock.e2e.ts | 8 +- .../tests/partial-landlock.spec.ts | 10 +- .../bash-sandbox/tests/sandbox.spec.ts | 24 +- .../bash-sandbox/tests/seatbelt.e2e.ts | 8 +- .../bash-sandbox/tsconfig.json | 6 +- packages/shell/pwsh-local/README.i18n.yaml | 6 + packages/{bash => shell}/pwsh-local/README.md | 10 +- .../{bash => shell}/pwsh-local/README.zh.md | 8 +- .../{bash => shell}/pwsh-local/package.json | 6 +- .../{bash => shell}/pwsh-local/src/index.ts | 28 +- .../pwsh-local/src/invariant.ts | 0 .../{bash => shell}/pwsh-local/src/resolve.ts | 0 .../pwsh-local/tests/executor.spec.ts | 34 +- .../pwsh-local/tests/settings.spec.ts | 26 +- .../{bash => shell}/pwsh-local/tsconfig.json | 4 +- packages/shell/pwsh-sandbox/README.i18n.yaml | 6 + .../{bash => shell}/pwsh-sandbox/README.md | 2 +- .../{bash => shell}/pwsh-sandbox/README.zh.md | 2 +- .../{bash => shell}/pwsh-sandbox/package.json | 6 +- .../pwsh-sandbox/src/helpers.ts | 4 +- .../{bash => shell}/pwsh-sandbox/src/index.ts | 20 +- .../pwsh-sandbox/src/invariant.ts | 0 .../pwsh-sandbox/tests/acl.e2e.ts | 6 +- .../pwsh-sandbox/tests/sandbox.spec.ts | 18 +- .../pwsh-sandbox/tsconfig.json | 6 +- packages/shell/shell-env/README.i18n.yaml | 6 + packages/shell/shell-env/README.md | 51 + packages/shell/shell-env/README.zh.md | 51 + .../bash-env => shell/shell-env}/package.json | 12 +- .../bash-env => shell/shell-env}/src/index.ts | 32 +- .../shell-env}/src/invariant.ts | 8 +- .../shell-env/tests/shell-env.spec.ts} | 42 +- .../shell-env}/tsconfig.json | 6 +- packages/shell/shell/README.i18n.yaml | 6 + packages/{bash/bash => shell/shell}/README.md | 28 +- packages/shell/shell/README.zh.md | 53 + .../{bash/bash => shell/shell}/package.json | 6 +- .../{bash/bash => shell/shell}/src/index.ts | 48 +- .../bash => shell/shell}/src/invariant.ts | 6 +- .../{bash/bash => shell/shell}/src/render.ts | 4 +- .../{bash/bash => shell/shell}/src/types.ts | 44 +- .../bash => shell/shell}/tests/render.spec.ts | 0 .../shell}/tests/service.spec.ts | 34 +- .../{bash/bash => shell/shell}/tsconfig.json | 2 +- .../tool-bash-persistent/README.i18n.yaml | 6 + .../tool-bash-persistent/README.md | 2 +- .../tool-bash-persistent/README.zh.md | 2 +- .../tool-bash-persistent/package.json | 8 +- .../tool-bash-persistent/src/index.ts | 38 +- .../tool-bash-persistent/src/invariant.ts | 0 .../tests/loader-composition.spec.ts | 20 +- .../tool-bash-persistent/tests/tools.spec.ts | 56 +- .../tool-bash-persistent/tsconfig.json | 4 +- packages/shell/tool-bash/README.i18n.yaml | 6 + packages/{bash => shell}/tool-bash/README.md | 30 +- .../{bash => shell}/tool-bash/README.zh.md | 28 +- .../{bash => shell}/tool-bash/package.json | 20 +- .../tool-bash/src/background.ts | 8 +- .../{bash => shell}/tool-bash/src/index.ts | 48 +- .../tool-bash/src/invariant.ts | 0 .../{bash => shell}/tool-bash/src/render.ts | 16 +- .../tool-bash/tests/integration.spec.ts | 34 +- .../tool-bash/tests/tools.spec.ts | 176 +- .../{bash => shell}/tool-bash/tsconfig.json | 8 +- packages/shell/tool-pwsh/README.i18n.yaml | 6 + packages/{bash => shell}/tool-pwsh/README.md | 26 +- .../{bash => shell}/tool-pwsh/README.zh.md | 22 +- .../{bash => shell}/tool-pwsh/package.json | 18 +- .../tool-pwsh/src/background.ts | 8 +- .../{bash => shell}/tool-pwsh/src/index.ts | 56 +- .../tool-pwsh/src/invariant.ts | 0 .../{bash => shell}/tool-pwsh/src/render.ts | 10 +- .../tool-pwsh/tests/integration.spec.ts | 26 +- .../tool-pwsh/tests/loader.spec.ts | 4 +- .../tool-pwsh/tests/tools.spec.ts | 138 +- .../{bash => shell}/tool-pwsh/tsconfig.json | 10 +- packages/skill/README.i18n.yaml | 4 +- packages/skill/README.md | 2 +- packages/skill/README.zh.md | 2 +- .../skill-badge/tests/skill-badge.spec.ts | 4 +- packages/skill/skill-badge/tsconfig.json | 2 +- .../README.i18n.yaml | 6 +- .../README.md | 6 +- .../README.zh.md | 6 +- .../package.json | 8 +- .../src/index.ts | 28 +- .../src/invariant.ts | 8 +- .../tests/skill-filesystem-watcher.spec.ts} | 56 +- .../tests/skill-filesystem.spec.ts} | 84 +- .../tsconfig.json | 4 +- packages/skill/skill/README.i18n.yaml | 4 +- packages/skill/skill/README.md | 4 +- packages/skill/skill/README.zh.md | 4 +- packages/skill/skill/src/index.ts | 8 +- packages/skill/skill/tests/skill.spec.ts | 86 +- packages/skill/skill/tsconfig.json | 2 +- packages/skill/tool-skill/package.json | 2 +- .../skill/tool-skill/tests/tool-skill.spec.ts | 26 +- packages/skill/tool-skill/tsconfig.json | 2 +- packages/spill/spill-local/tsconfig.json | 2 +- packages/spill/spill-policy/README.i18n.yaml | 4 +- packages/spill/spill-policy/README.md | 4 +- packages/spill/spill-policy/README.zh.md | 4 +- packages/spill/spill-policy/package.json | 6 +- packages/spill/spill-policy/src/index.ts | 6 +- .../spill-policy/tests/spill-policy.spec.ts | 26 +- packages/spill/spill-policy/tsconfig.json | 4 +- packages/spill/spill/README.i18n.yaml | 4 +- packages/spill/spill/README.md | 4 +- packages/spill/spill/README.zh.md | 4 +- packages/spill/spill/src/index.ts | 2 +- packages/spill/spill/tsconfig.json | 2 +- .../storage-domain/tests/invariant.spec.ts | 4 +- packages/storage/storage-domain/tsconfig.json | 2 +- .../storage-json/tests/json-backend.spec.ts | 4 +- packages/storage/storage-json/tsconfig.json | 2 +- .../storage-sqlite/tests/invariant.spec.ts | 4 +- packages/storage/storage-sqlite/tsconfig.json | 2 +- packages/storage/storage/tsconfig.json | 2 +- packages/subagent/README.i18n.yaml | 4 +- packages/subagent/README.md | 6 +- packages/subagent/README.zh.md | 6 +- .../subagent-acp/tests/subagent-acp.e2e.ts | 12 +- .../subagent-acp/tests/subagent-acp.spec.ts | 52 +- packages/subagent/subagent-acp/tsconfig.json | 4 +- .../tests/real-deepseek.e2e.ts | 8 +- .../tests/real-product.spec.ts | 8 +- .../tests/subagent-claude-code.spec.ts | 12 +- .../subagent-claude-code/tsconfig.json | 2 +- .../subagent-codex/tests/real-deepseek.e2e.ts | 8 +- .../subagent-codex/tests/real-product.spec.ts | 8 +- .../tests/subagent-codex.spec.ts | 16 +- .../subagent/subagent-codex/tsconfig.json | 2 +- .../subagent/subagent-dsh-sdk/src/index.ts | 4 +- .../tests/subagent-dsh-sdk.spec.ts | 14 +- .../subagent/subagent-dsh-sdk/tsconfig.json | 4 +- .../README.i18n.yaml | 6 +- .../README.md | 6 +- .../README.zh.md | 6 +- .../package.json | 10 +- .../src/index.ts | 12 +- .../src/invariant.ts | 8 +- .../tests/multi-subagent.spec.ts | 10 +- .../tests/subagent-fork-in-process.spec.ts} | 18 +- .../tsconfig.json | 4 +- .../README.i18n.yaml | 6 +- .../README.md | 2 +- .../README.zh.md | 2 +- .../package.json | 4 +- .../src/index.ts | 2 +- .../src/invariant.ts | 8 +- .../src/structured.ts | 4 +- .../tests/fixtures/plugins/preset-tool.js | 0 .../fixtures/presets/coding/agent.cordis.yml | 0 .../presets/reviewing/agent.cordis.yml | 0 .../tests/inheritance.spec.ts | 0 .../tests/preset-inheritance.spec.ts | 0 .../tests/structured.spec.ts | 8 +- .../tests/subagent-in-process-driver.spec.ts} | 8 +- .../tsconfig.json | 2 +- .../README.i18n.yaml | 6 +- .../README.md | 4 +- .../README.zh.md | 4 +- .../package.json | 8 +- .../src/index.ts | 10 +- .../src/invariant.ts | 8 +- .../tests/harness.ts | 10 +- .../tests/spawn-in-process.e2e.ts} | 0 .../tests/subagent-spawn-in-process.spec.ts} | 24 +- .../tsconfig.json | 4 +- packages/subagent/subagent/README.i18n.yaml | 4 +- packages/subagent/subagent/README.md | 6 +- packages/subagent/subagent/README.zh.md | 4 +- packages/subagent/subagent/package.json | 6 +- .../subagent/subagent/src/continuation.ts | 2 +- packages/subagent/subagent/src/index.ts | 16 +- .../subagent/subagent/src/list-children.ts | 4 +- .../subagent/subagent/src/run-settlement.ts | 12 +- packages/subagent/subagent/src/types.ts | 2 +- .../tests/continuation-inheritance.spec.ts | 8 +- .../subagent/tests/continuation.spec.ts | 22 +- .../subagent/subagent/tests/invariant.spec.ts | 8 +- .../subagent/tests/list-children.spec.ts | 18 +- .../subagent/subagent/tests/service.spec.ts | 10 +- .../subagent/tests/timing-projection.spec.ts | 4 +- packages/subagent/subagent/tsconfig.json | 4 +- .../tool-subagent-control/package.json | 2 +- .../tests/list-agents.spec.ts | 8 +- .../tests/tool-subagent-control.spec.ts | 12 +- .../tool-subagent-control/tsconfig.json | 2 +- .../tool-subagent-report/package.json | 2 +- .../tests/tool-subagent-report.spec.ts | 10 +- .../tool-subagent-report/tsconfig.json | 2 +- .../subagent/tool-subagent/README.i18n.yaml | 4 +- packages/subagent/tool-subagent/README.md | 8 +- packages/subagent/tool-subagent/README.zh.md | 8 +- packages/subagent/tool-subagent/package.json | 10 +- packages/subagent/tool-subagent/src/index.ts | 24 +- .../tests/scripted-provider.spec.ts | 6 +- .../tool-subagent/tests/tool-subagent.spec.ts | 170 +- packages/subagent/tool-subagent/tsconfig.json | 4 +- packages/subprocess/README.i18n.yaml | 4 +- packages/subprocess/README.md | 2 +- packages/subprocess/README.zh.md | 2 +- .../subprocess-local/README.i18n.yaml | 4 +- .../subprocess/subprocess-local/README.md | 2 +- .../subprocess/subprocess-local/README.zh.md | 2 +- .../subprocess/subprocess-local/src/index.ts | 6 +- .../tests/fixtures/process-exit-host.ts | 4 +- .../subprocess-local/tests/local.spec.ts | 52 +- .../subprocess-local/tests/spawn.spec.ts | 6 +- .../subprocess/subprocess-local/tsconfig.json | 2 +- .../subprocess/subprocess/README.i18n.yaml | 4 +- packages/subprocess/subprocess/README.md | 4 +- packages/subprocess/subprocess/README.zh.md | 4 +- packages/subprocess/subprocess/src/index.ts | 6 +- packages/subprocess/subprocess/src/types.ts | 4 +- .../subprocess/tests/service.spec.ts | 12 +- packages/subprocess/subprocess/tsconfig.json | 2 +- packages/support/README.i18n.yaml | 6 - packages/support/invariants/README.i18n.yaml | 6 - packages/support/llm-replay/README.i18n.yaml | 6 - packages/tasks/README.md | 15 - packages/tasks/README.zh.md | 15 - packages/tasks/tasks-local/README.i18n.yaml | 6 - packages/tasks/tasks-local/README.md | 34 - packages/tasks/tasks/README.i18n.yaml | 6 - packages/tasks/tasks/README.md | 40 - packages/tasks/tasks/tests/service.spec.ts | 96 - packages/tasks/tool-tasks/README.i18n.yaml | 6 - packages/tasks/tool-tasks/README.md | 96 - .../{pty/pty => terminal}/README.i18n.yaml | 6 +- packages/terminal/README.md | 15 + packages/terminal/README.zh.md | 15 + .../terminal/terminal-bash/README.i18n.yaml | 6 + .../terminal-bash}/README.md | 10 +- .../terminal-bash}/README.zh.md | 10 +- .../terminal-bash}/package.json | 8 +- .../terminal-bash}/src/config.ts | 10 +- .../terminal-bash}/src/index.ts | 32 +- .../terminal-bash}/src/invariant.ts | 8 +- .../terminal-bash}/src/sanitize.ts | 0 .../terminal-bash}/src/session.ts | 58 +- .../terminal-bash}/tests/config.spec.ts | 6 +- .../terminal-bash}/tests/index.spec.ts | 90 +- .../terminal-bash}/tests/local.spec.ts | 70 +- .../terminal-bash}/tests/sanitize.spec.ts | 2 +- .../terminal-bash}/tests/session.spec.ts | 60 +- .../terminal-bash}/tsconfig.json | 4 +- .../terminal}/README.i18n.yaml | 6 +- .../{pty/pty => terminal/terminal}/README.md | 14 +- .../pty => terminal/terminal}/README.zh.md | 12 +- .../pty => terminal/terminal}/package.json | 4 +- .../pty => terminal/terminal}/src/index.ts | 158 +- .../terminal}/src/invariant.ts | 8 +- .../pty => terminal/terminal}/src/types.ts | 74 +- .../terminal}/tests/service.spec.ts | 246 +-- .../pty => terminal/terminal}/tsconfig.json | 2 +- .../terminal/tool-terminal/README.i18n.yaml | 6 + packages/terminal/tool-terminal/README.md | 71 + packages/terminal/tool-terminal/README.zh.md | 71 + .../tool-terminal}/package.json | 24 +- .../tool-terminal}/src/index.ts | 64 +- .../tool-terminal}/src/invariant.ts | 10 +- .../tool-terminal}/src/render.ts | 4 +- .../tests/loader-composition.spec.ts | 30 +- .../tool-terminal}/tests/render.spec.ts | 22 +- .../tool-terminal}/tests/tools.spec.ts | 78 +- .../tool-terminal}/tsconfig.json | 8 +- .../README.i18n.yaml | 6 +- packages/{support => test-support}/README.md | 6 +- .../{support => test-support}/README.zh.md | 6 +- .../acp-snapshot/README.i18n.yaml | 2 +- .../acp-snapshot/README.md | 0 .../acp-snapshot/README.zh.md | 0 .../acp-snapshot/package.json | 2 +- .../acp-snapshot/src/harness.ts | 0 .../acp-snapshot/src/index.ts | 0 .../acp-snapshot/src/invariant.ts | 0 .../acp-snapshot/src/launcher.ts | 0 .../acp-snapshot/src/normalize.ts | 0 .../acp-snapshot/src/suite.ts | 0 .../tests/fixtures/fake-acp-agent.ts | 0 .../record-suite/rec-child/behavior.json | 0 .../record-suite/rec-child/input.json | 0 .../record-suite/rec-child/session.1.jsonl | 0 .../record-suite/rec-child/session.jsonl | 0 .../rec-child/stdout.expected.jsonl | 0 .../record-suite/rec-pin/behavior.json | 0 .../fixtures/record-suite/rec-pin/input.json | 0 .../record-suite/rec-pin/session.1.jsonl | 0 .../record-suite/rec-pin/session.jsonl | 0 .../rec-pin/stdout.expected.jsonl | 0 .../rec-pin/system-prompt.expected.md | 0 .../rec-pin/tool-schemas.expected.json | 0 .../record-suite/rec-skip/behavior.json | 0 .../fixtures/record-suite/rec-skip/input.json | 0 .../rec-skip/replay.override.json | 0 .../record-suite/rec-skip/session.jsonl | 0 .../rec-skip/stdout.expected.jsonl | 0 .../suite/authored-error/behavior.json | 0 .../fixtures/suite/authored-error/input.json | 0 .../suite/authored-error/replay.override.json | 0 .../suite/authored-error/session.jsonl | 0 .../authored-error/stdout.expected.jsonl | 0 .../fixtures/suite/blocked-log/behavior.json | 0 .../fixtures/suite/blocked-log/input.json | 0 .../fixtures/suite/blocked-log/session.jsonl | 0 .../suite/blocked-log/stdout.expected.jsonl | 0 .../fixtures/suite/no-model/behavior.json | 0 .../tests/fixtures/suite/no-model/input.json | 0 .../fixtures/suite/no-model/session.jsonl | 0 .../suite/no-model/stdout.expected.jsonl | 0 .../fixtures/suite/pin-turn/behavior.json | 0 .../tests/fixtures/suite/pin-turn/input.json | 0 .../fixtures/suite/pin-turn/session.jsonl | 0 .../suite/pin-turn/stdout.expected.jsonl | 0 .../suite/pin-turn/system-prompt.expected.md | 0 .../suite/pin-turn/tool-schemas.expected.json | 0 .../fixtures/suite/plain-turn/behavior.json | 0 .../fixtures/suite/plain-turn/input.json | 0 .../fixtures/suite/plain-turn/session.1.jsonl | 0 .../fixtures/suite/plain-turn/session.jsonl | 0 .../suite/plain-turn/stdout.expected.jsonl | 0 .../plain-turn/system-prompt.1.expected.md | 0 .../plain-turn/tool-schemas.1.expected.json | 0 .../suite/plain-turn/workspace/seed.txt | 0 .../fixtures/suite/shared-pin/behavior.json | 0 .../fixtures/suite/shared-pin/input.json | 0 .../fixtures/suite/shared-pin/session.jsonl | 0 .../suite/shared-pin/stdout.expected.jsonl | 0 .../acp-snapshot/tests/harness.spec.ts | 0 .../acp-snapshot/tests/normalize.spec.ts | 0 .../acp-snapshot/tests/suite.spec.ts | 0 .../acp-snapshot/tsconfig.json | 2 +- .../agent-loop-testkit/README.i18n.yaml | 2 +- .../agent-loop-testkit/README.md | 0 .../agent-loop-testkit/README.zh.md | 0 .../agent-loop-testkit/package.json | 2 +- .../agent-loop-testkit/src/index.ts | 12 +- .../agent-loop-testkit/src/invariant.ts | 0 .../tests/agent-loop-testkit.spec.ts | 0 .../agent-loop-testkit/tsconfig.json | 2 +- .../client-runtime/README.i18n.yaml | 6 + .../client-runtime}/README.md | 4 +- .../client-runtime}/README.zh.md | 4 +- .../client-runtime}/package.json | 4 +- .../client-runtime}/src/fixtures.ts | 2 +- .../client-runtime}/src/index.ts | 22 +- .../client-runtime}/src/invariant.ts | 2 +- .../client-runtime}/src/locale-env.ts | 2 +- .../client-runtime}/src/remote.ts | 0 .../client-runtime}/src/sessions.ts | 10 +- .../client-runtime}/src/settings-scope.ts | 0 .../client-runtime}/src/snapshot.ts | 0 .../client-runtime}/src/translate.ts | 2 +- .../client-runtime}/src/workspaces.ts | 0 .../runtime.client.spec.tsx.snap | 0 .../tests/invariant.client.spec.ts | 4 +- .../tests/remote.client.spec.ts | 0 .../tests/runtime.client.spec.tsx | 2 +- .../client-runtime}/tsconfig.json | 8 +- .../client-runtime}/tsdown.config.ts | 2 +- .../llm-mock-server/README.i18n.yaml | 2 +- .../llm-mock-server/README.md | 0 .../llm-mock-server/README.zh.md | 0 .../llm-mock-server/package.json | 2 +- .../llm-mock-server/src/bin.ts | 0 .../llm-mock-server/src/cli.ts | 0 .../llm-mock-server/src/index.ts | 0 .../llm-mock-server/src/invariant.ts | 0 .../llm-mock-server/tests/cli.spec.ts | 0 .../llm-mock-server/tests/invariant.spec.ts | 4 +- .../llm-mock-server/tests/server.spec.ts | 0 .../llm-mock-server}/tsconfig.json | 2 +- .../llm-mock-server/tsdown.config.ts | 0 .../test-support/llm-replay/README.i18n.yaml | 6 + .../llm-replay/README.md | 4 +- .../llm-replay/README.zh.md | 4 +- .../llm-replay/package.json | 6 +- .../llm-replay/src/index.ts | 16 +- .../llm-replay/src/invariant.ts | 0 .../llm-replay/tests/llm-replay.spec.ts | 100 +- .../llm-replay/tsconfig.json | 4 +- .../loader-smoke/README.i18n.yaml | 2 +- .../loader-smoke/README.md | 0 .../loader-smoke/README.zh.md | 0 .../loader-smoke/package.json | 2 +- .../loader-smoke/src/agent-turn.ts | 0 .../loader-smoke/src/index.ts | 0 .../loader-smoke/src/invariant.ts | 0 .../loader-smoke/tests/agent-turn.spec.ts | 0 .../loader-smoke/tests/example-launch.spec.ts | 0 .../loader-smoke/tests/fixtures/fail.ts | 0 .../loader-smoke/tests/fixtures/hang.ts | 0 .../loader-smoke/tests/fixtures/success.ts | 0 .../loader-smoke/tests/loader-smoke.spec.ts | 0 .../loader-smoke/tsconfig.json | 2 +- packages/todo/tool-todo/package.json | 2 +- .../todo/tool-todo/tests/invariant.spec.ts | 12 +- .../tests/loader-composition.spec.ts | 4 +- .../todo/tool-todo/tests/projection.spec.ts | 8 +- .../todo/tool-todo/tests/tool-todo.spec.ts | 8 +- packages/todo/tool-todo/tsconfig.json | 2 +- packages/typert/generator/README.i18n.yaml | 4 +- packages/typert/generator/README.md | 4 +- packages/typert/generator/README.zh.md | 4 +- packages/typert/generator/src/analyzer.ts | 82 +- .../typert/generator/src/cordis-catalog.ts | 4 +- packages/typert/generator/src/emitter.ts | 18 +- packages/typert/generator/src/model.ts | 2 +- .../tests/cordis-catalog-contract.spec.ts | 4 +- .../generator/tests/cordis-catalog.spec.ts | 2 +- .../remote-model/packages/domain/src/index.ts | 12 +- .../remote-model/packages/remote/src/index.ts | 4 +- .../fixtures/remote-model/tsconfig.base.json | 2 +- .../{type-meta.d.ts => typert-protocol.d.ts} | 34 +- .../generator/tests/remote-model.spec.ts | 72 +- .../typert/generator/tests/type-model.spec.ts | 2 +- packages/typert/generator/tsconfig.json | 2 +- packages/typert/loader/tests/loader.spec.ts | 2 +- packages/typert/loader/tsconfig.json | 2 +- .../{type-meta => protocol}/README.i18n.yaml | 6 +- .../typert/{type-meta => protocol}/README.md | 20 +- .../{type-meta => protocol}/README.zh.md | 20 +- .../{type-meta => protocol}/package.json | 6 +- .../{type-meta => protocol}/src/index.ts | 120 +- .../{type-meta => protocol}/src/invariant.ts | 8 +- .../{type-meta => protocol}/src/types.ts | 180 +- .../tests/fixtures/source-launch.ts | 6 +- .../tests/protocol.spec.ts} | 56 +- .../{type-meta => protocol}/tsconfig.json | 2 +- packages/typert/registry/package.json | 2 +- packages/typert/registry/src/client/index.ts | 2 +- packages/typert/registry/src/index.ts | 10 +- packages/typert/registry/src/service.ts | 136 +- packages/typert/registry/src/types.ts | 2 +- packages/typert/registry/tests/typert.spec.ts | 22 +- packages/typert/registry/tsconfig.json | 4 +- packages/util/README.i18n.yaml | 4 +- packages/util/README.md | 4 +- packages/util/README.zh.md | 4 +- packages/util/atomic-write/README.i18n.yaml | 4 +- packages/util/atomic-write/README.md | 2 +- packages/util/atomic-write/README.zh.md | 2 +- .../util/atomic-write/tests/invariant.spec.ts | 4 +- packages/util/atomic-write/tsconfig.json | 2 +- packages/util/brand/README.i18n.yaml | 4 +- packages/util/brand/README.md | 4 +- packages/util/brand/README.zh.md | 4 +- packages/util/brand/src/index.ts | 2 +- packages/util/brand/tsconfig.json | 2 +- packages/util/environment/README.i18n.yaml | 6 - packages/util/home-paths/README.i18n.yaml | 6 + packages/util/{paths => home-paths}/README.md | 2 +- .../util/{paths => home-paths}/README.zh.md | 2 +- .../util/{paths => home-paths}/package.json | 4 +- .../util/{paths => home-paths}/src/index.ts | 2 +- .../src/invariant.ts | 8 +- .../tests/home-paths.spec.ts} | 2 +- .../{retention => home-paths}/tsconfig.json | 2 +- .../util/launch-environment/README.i18n.yaml | 6 + .../README.md | 8 +- .../README.zh.md | 8 +- .../package.json | 4 +- .../src/index.ts | 40 +- .../src/invariant.ts | 8 +- .../tests/launch-environment.spec.ts} | 20 +- .../tsconfig.json | 2 +- packages/util/native-command/README.i18n.yaml | 4 +- packages/util/native-command/README.md | 2 +- packages/util/native-command/README.zh.md | 2 +- packages/util/native-command/tsconfig.json | 2 +- .../util/output-retention/README.i18n.yaml | 6 + .../{retention => output-retention}/README.md | 8 +- .../README.zh.md | 6 +- .../package.json | 4 +- .../src/index.ts | 2 +- .../src/invariant.ts | 8 +- .../tests/output-retention.spec.ts} | 2 +- .../output-retention}/tsconfig.json | 2 +- packages/util/paths/README.i18n.yaml | 6 - packages/util/retention/README.i18n.yaml | 6 - packages/util/timeout/README.i18n.yaml | 4 +- packages/util/timeout/README.md | 2 +- packages/util/timeout/README.zh.md | 2 +- packages/util/timeout/tsconfig.json | 2 +- packages/web/README.i18n.yaml | 4 +- packages/web/README.md | 2 +- packages/web/README.zh.md | 2 +- packages/web/tool-web/README.i18n.yaml | 4 +- packages/web/tool-web/README.md | 8 +- packages/web/tool-web/README.zh.md | 8 +- packages/web/tool-web/package.json | 4 +- packages/web/tool-web/src/fetch.ts | 4 +- packages/web/tool-web/src/index.ts | 2 +- packages/web/tool-web/src/search.ts | 2 +- .../web/tool-web/tests/integration.spec.ts | 24 +- packages/web/tool-web/tests/load-path.spec.ts | 8 +- packages/web/tool-web/tests/spill.spec.ts | 10 +- packages/web/tool-web/tests/tool-web.spec.ts | 24 +- packages/web/tool-web/tsconfig.json | 2 +- .../README.i18n.yaml | 6 +- .../README.md | 6 +- .../README.zh.md | 6 +- .../package.json | 4 +- .../src/index.ts | 24 +- .../src/invariant.ts | 8 +- .../src/policy.ts | 2 +- .../src/provider.ts | 10 +- .../tests/fetch-http.spec.ts} | 40 +- .../tsconfig.json | 2 +- packages/web/web-search-deepseek/package.json | 4 +- packages/web/web-search-deepseek/src/index.ts | 6 +- .../web-search-deepseek/tests/deepseek.e2e.ts | 2 +- .../tests/deepseek.spec.ts | 22 +- .../tests/settings.spec.ts | 8 +- .../web/web-search-deepseek/tsconfig.json | 4 +- packages/web/web-search-exa/package.json | 4 +- packages/web/web-search-exa/src/index.ts | 4 +- packages/web/web-search-exa/tests/exa.e2e.ts | 2 +- packages/web/web-search-exa/tests/exa.spec.ts | 10 +- packages/web/web-search-exa/tsconfig.json | 4 +- .../web/web-search-perplexity/package.json | 4 +- .../web/web-search-perplexity/src/index.ts | 4 +- .../tests/perplexity.e2e.ts | 2 +- .../tests/perplexity.spec.ts | 10 +- .../web/web-search-perplexity/tsconfig.json | 4 +- packages/web/web/README.i18n.yaml | 4 +- packages/web/web/README.md | 6 +- packages/web/web/README.zh.md | 6 +- packages/web/web/src/index.ts | 12 +- packages/web/web/tests/web.spec.ts | 20 +- packages/web/web/tsconfig.json | 2 +- packages/workflow/README.i18n.yaml | 4 +- packages/workflow/README.md | 4 +- packages/workflow/README.zh.md | 4 +- packages/workflow/tool-ralph/README.i18n.yaml | 4 +- packages/workflow/tool-ralph/README.md | 6 +- packages/workflow/tool-ralph/README.zh.md | 6 +- packages/workflow/tool-ralph/package.json | 6 +- packages/workflow/tool-ralph/src/index.ts | 4 +- .../tool-ralph/tests/integration.spec.ts | 16 +- .../tool-ralph/tests/tool-ralph.spec.ts | 16 +- packages/workflow/tool-ralph/tsconfig.json | 2 +- .../workflow/tool-workflow/README.i18n.yaml | 4 +- packages/workflow/tool-workflow/README.md | 2 +- packages/workflow/tool-workflow/README.zh.md | 2 +- packages/workflow/tool-workflow/package.json | 4 +- packages/workflow/tool-workflow/src/index.ts | 6 +- .../tool-workflow/tests/invariant.spec.ts | 8 +- .../tool-workflow/tests/tool-workflow.spec.ts | 26 +- packages/workflow/tool-workflow/tsconfig.json | 2 +- .../README.i18n.yaml | 6 +- .../README.md | 6 +- .../README.zh.md | 6 +- .../package.json | 6 +- .../src/host.ts | 14 +- .../src/index.ts | 12 +- .../src/invariant.ts | 8 +- .../src/meta.ts | 2 +- .../src/protocol.ts | 2 +- .../src/realm.ts | 2 +- .../src/runtime.ts | 2 +- .../src/session.ts | 2 +- .../src/types.ts | 2 +- .../src/worker.ts | 2 +- .../tests/built-worker.e2e.ts | 10 +- .../tests/integration.spec.ts | 22 +- .../tests/meta.spec.ts | 0 .../tests/realm.spec.ts | 0 .../tests/session.spec.ts | 0 .../tests/source-worker.compat.spec.ts | 10 +- .../tests/workflow-worker-thread.e2e.ts} | 20 +- .../tests/workflow-worker-thread.spec.ts} | 150 +- .../tsconfig.json | 2 +- .../tsdown.config.ts | 0 packages/workflow/workflow/README.i18n.yaml | 4 +- packages/workflow/workflow/README.md | 6 +- packages/workflow/workflow/README.zh.md | 6 +- packages/workflow/workflow/package.json | 2 +- packages/workflow/workflow/src/index.ts | 10 +- .../workflow/workflow/tests/invariant.spec.ts | 4 +- .../workflow/workflow/tests/workflow.spec.ts | 24 +- packages/workflow/workflow/tsconfig.json | 2 +- packages/workspace/README.i18n.yaml | 4 +- packages/workspace/README.md | 2 +- packages/workspace/README.zh.md | 2 +- packages/workspace/workspace/README.i18n.yaml | 4 +- packages/workspace/workspace/README.md | 14 +- packages/workspace/workspace/README.zh.md | 14 +- packages/workspace/workspace/package.json | 2 +- packages/workspace/workspace/src/index.ts | 6 +- packages/workspace/workspace/src/invariant.ts | 8 +- .../workspace/tests/invariant.spec.ts | 6 +- .../workspace/tests/workspace.spec.ts | 8 +- packages/workspace/workspace/tsconfig.json | 2 +- pnpm-lock.yaml | 1688 ++++++++--------- python/sdk-runtime/README.i18n.yaml | 4 +- python/sdk-runtime/README.md | 4 +- python/sdk-runtime/README.zh.md | 4 +- python/sdk-runtime/package.json | 64 +- .../src/deepseek_harness_runtime/__init__.py | 4 +- .../runtime/cordis.yml | 4 +- python/sdk/README.i18n.yaml | 4 +- python/sdk/README.md | 2 +- python/sdk/README.zh.md | 2 +- python/sdk/tests/test_bundled_runtime.py | 4 +- scripts/build-exe-for-python-sdk.ts | 2 +- scripts/check-workspace-constraints.ts | 10 +- scripts/ci-workflow.spec.ts | 6 +- scripts/doc-typecheck-paths.spec.ts | 6 +- scripts/gen-cordis-catalog.ts | 140 +- scripts/gen-doc-graphs.ts | 102 +- scripts/gen-third-party-notices.spec.ts | 4 +- scripts/gen-third-party-notices.ts | 4 +- scripts/gen-tool-catalog.ts | 134 +- scripts/oxlint-contract.spec.ts | 4 +- scripts/package-invariants.spec.ts | 4 +- scripts/package-invariants.ts | 4 +- scripts/publication-payload.spec.ts | 6 +- scripts/publication-payload.ts | 2 +- scripts/release/families.spec.ts | 2 +- scripts/rescope-vendor.ts | 4 +- scripts/run-gates.spec.ts | 2 +- scripts/run-gates.ts | 10 +- scripts/smoke-python-runtime.py | 12 +- .../advanced/result.json | 48 +- .../advanced/session.1.jsonl | 2 +- .../advanced/session.2.jsonl | 2 +- .../advanced/session.jsonl | 6 +- .../request-response.expected.json | 10 +- scripts/test-invariants.spec.ts | 8 +- scripts/test-invariants.ts | 6 +- scripts/type-equiv.manifest.json | 262 +-- scripts/verify-cordis-config.ts | 4 +- .../verify-package-readme-model-experience.ts | 76 +- tsconfig.base.json | 72 +- tsconfig.client.json | 32 +- tsconfig.host.json | 104 +- tsdown.config.ts | 2 +- vitest.config.ts | 56 +- website/docs.ts | 16 +- 3281 files changed, 21730 insertions(+), 21592 deletions(-) rename .agents/notes/implemented/architecture/{2026-07-26-task-registry-seam.i18n.yaml => 2026-07-26-job-registry-seam.i18n.yaml} (59%) create mode 100644 .agents/notes/implemented/architecture/2026-07-26-job-registry-seam.md create mode 100644 .agents/notes/implemented/architecture/2026-07-26-job-registry-seam.zh.md delete mode 100644 .agents/notes/implemented/architecture/2026-07-26-task-registry-seam.md delete mode 100644 .agents/notes/implemented/architecture/2026-07-26-task-registry-seam.zh.md rename .agents/notes/implemented/bug-fix/{2026-08-11-bounded-background-task-admission.i18n.yaml => 2026-08-11-bounded-background-job-admission.i18n.yaml} (55%) rename .agents/notes/implemented/bug-fix/{2026-08-11-bounded-background-task-admission.md => 2026-08-11-bounded-background-job-admission.md} (64%) rename .agents/notes/implemented/bug-fix/{2026-08-11-bounded-background-task-admission.zh.md => 2026-08-11-bounded-background-job-admission.zh.md} (69%) rename .agents/notes/implemented/feature/{2026-08-08-web-background-task-display.i18n.yaml => 2026-08-08-web-background-job-display.i18n.yaml} (57%) create mode 100644 .agents/notes/implemented/feature/2026-08-08-web-background-job-display.md create mode 100644 .agents/notes/implemented/feature/2026-08-08-web-background-job-display.zh.md delete mode 100644 .agents/notes/implemented/feature/2026-08-08-web-background-task-display.md delete mode 100644 .agents/notes/implemented/feature/2026-08-08-web-background-task-display.zh.md rename .agents/notes/implemented/feature/{2026-08-11-background-task-completion-wakes-an-idle-owner.i18n.yaml => 2026-08-11-background-job-completion-wakes-an-idle-owner.i18n.yaml} (51%) rename .agents/notes/implemented/feature/{2026-08-11-background-task-completion-wakes-an-idle-owner.md => 2026-08-11-background-job-completion-wakes-an-idle-owner.md} (65%) rename .agents/notes/implemented/feature/{2026-08-11-background-task-completion-wakes-an-idle-owner.zh.md => 2026-08-11-background-job-completion-wakes-an-idle-owner.zh.md} (73%) rename apps/web/tests/{background-task-list.e2e.ts => background-job-list.e2e.ts} (83%) rename apps/web/tests/snapshots/{background-task-list => background-job-list}/running.expected.md (64%) rename apps/web/tests/snapshots/{background-task-list => background-job-list}/settled.expected.md (68%) rename docs/subsystems/{pty.i18n.yaml => jobs.i18n.yaml} (73%) rename docs/subsystems/{tasks.md => jobs.md} (56%) rename docs/subsystems/{tasks.zh.md => jobs.zh.md} (56%) create mode 100644 docs/subsystems/permission-presets.i18n.yaml rename docs/subsystems/{permission.md => permission-presets.md} (75%) rename docs/subsystems/{permission.zh.md => permission-presets.zh.md} (75%) create mode 100644 docs/subsystems/session-telemetry.i18n.yaml rename docs/subsystems/{telemetry.md => session-telemetry.md} (70%) rename docs/subsystems/{telemetry.zh.md => session-telemetry.zh.md} (69%) rename docs/subsystems/{bash.i18n.yaml => shell.i18n.yaml} (72%) rename docs/subsystems/{bash.md => shell.md} (69%) rename docs/subsystems/{bash.zh.md => shell.zh.md} (71%) delete mode 100644 docs/subsystems/telemetry.i18n.yaml rename docs/subsystems/{tasks.i18n.yaml => terminal.i18n.yaml} (71%) rename docs/subsystems/{pty.md => terminal.md} (58%) rename docs/subsystems/{pty.zh.md => terminal.zh.md} (59%) delete mode 100644 docs/subsystems/user-interaction.i18n.yaml rename docs/subsystems/{http-server.i18n.yaml => user-questions.i18n.yaml} (68%) rename docs/subsystems/{user-interaction.md => user-questions.md} (82%) rename docs/subsystems/{user-interaction.zh.md => user-questions.zh.md} (82%) rename docs/subsystems/{permission.i18n.yaml => web-server.i18n.yaml} (70%) rename docs/subsystems/{http-server.md => web-server.md} (74%) rename docs/subsystems/{http-server.zh.md => web-server.zh.md} (73%) rename examples/acp-agent/{workspace-context.cordis.snapshot.yml => agent-instructions.cordis.snapshot.yml} (93%) rename examples/acp-agent/{workspace-context.cordis.yml => agent-instructions.cordis.yml} (100%) rename examples/acp-agent/{background-task-admission.cordis.snapshot.yml => background-job-admission.cordis.snapshot.yml} (91%) rename examples/acp-agent/{background-task-admission.cordis.yml => background-job-admission.cordis.yml} (96%) rename examples/acp-agent/tests/fixtures/{bash => shell}/tool-pwsh/cordis.yml (76%) rename examples/acp-agent/tests/fixtures/{bash => shell}/tool-pwsh/driver.ts (95%) rename examples/acp-agent/tests/goal-snapshots/{goal-session => goal-round-driver}/input.json (100%) rename examples/acp-agent/tests/goal-snapshots/{goal-session => goal-round-driver}/replay.override.json (91%) rename examples/acp-agent/tests/goal-snapshots/{goal-session => goal-round-driver}/session.expected.jsonl (69%) rename examples/acp-agent/tests/goal-snapshots/{goal-session => goal-round-driver}/session.jsonl (100%) rename examples/acp-agent/tests/goal-snapshots/{goal-session => goal-round-driver}/stdout.expected.jsonl (100%) rename examples/acp-agent/tests/snapshots/{workspace-context => agent-instructions}/input.json (100%) rename examples/acp-agent/tests/snapshots/{workspace-context => agent-instructions}/replay.override.json (100%) rename examples/acp-agent/tests/snapshots/{workspace-context => agent-instructions}/session.jsonl (82%) rename examples/acp-agent/tests/snapshots/{workspace-context => agent-instructions}/stdout.expected.jsonl (100%) rename examples/acp-agent/tests/snapshots/{workspace-context => agent-instructions}/system-prompt.expected.md (80%) rename examples/acp-agent/tests/snapshots/{workspace-context => agent-instructions}/workspace/.dsh-project (100%) rename examples/acp-agent/tests/snapshots/{workspace-context => agent-instructions}/workspace/AGENTS.canonical.md (100%) rename examples/acp-agent/tests/snapshots/{workspace-context => agent-instructions}/workspace/AGENTS.md (100%) rename examples/acp-agent/tests/snapshots/{workspace-context => agent-instructions}/workspace/nested/AGENTS.canonical.md (100%) rename examples/acp-agent/tests/snapshots/{workspace-context => agent-instructions}/workspace/nested/AGENTS.md (100%) rename examples/acp-agent/tests/snapshots/{workspace-context => agent-instructions}/workspace/nested/task.txt (100%) rename examples/acp-agent/tests/snapshots/{background-task-admission => background-job-admission}/input.json (58%) rename examples/acp-agent/tests/snapshots/{background-task-admission => background-job-admission}/replay.override.json (84%) rename examples/acp-agent/tests/snapshots/{background-task-admission => background-job-admission}/session.jsonl (78%) rename examples/acp-agent/tests/snapshots/{background-task-admission => background-job-admission}/stdout.expected.jsonl (100%) rename examples/acp-agent/tests/snapshots/{repeat-tool-guard => repeat-tool-reminder}/input.json (100%) rename examples/acp-agent/tests/snapshots/{repeat-tool-guard => repeat-tool-reminder}/session.jsonl (96%) rename examples/acp-agent/tests/snapshots/{repeat-tool-guard => repeat-tool-reminder}/stdout.expected.jsonl (100%) rename examples/acp-agent/tests/snapshots/{subagent-fork => subagent-fork-in-process}/input.json (100%) rename examples/acp-agent/tests/snapshots/{subagent-fork => subagent-fork-in-process}/session.1.jsonl (100%) rename examples/acp-agent/tests/snapshots/{subagent-fork => subagent-fork-in-process}/session.jsonl (100%) rename examples/acp-agent/tests/snapshots/{subagent-fork => subagent-fork-in-process}/stdout.expected.jsonl (100%) rename examples/acp-agent/tests/snapshots/{subagent-spawn => subagent-spawn-in-process}/input.json (100%) rename examples/acp-agent/tests/snapshots/{subagent-spawn => subagent-spawn-in-process}/session.1.jsonl (100%) rename examples/acp-agent/tests/snapshots/{subagent-spawn => subagent-spawn-in-process}/session.jsonl (100%) rename examples/acp-agent/tests/snapshots/{subagent-spawn => subagent-spawn-in-process}/stdout.expected.jsonl (100%) rename examples/headless-agent/tests/fixtures/{telemetry-otel-driver.ts => session-telemetry-otel-driver.ts} (91%) rename examples/headless-agent/tests/fixtures/{telemetry-otel.cordis.yml => session-telemetry-otel.cordis.yml} (92%) rename examples/jsonrpc-agent/tests/snapshots/{subagent-spawn => subagent-spawn-in-process}/notifications.expected.jsonl (100%) rename examples/jsonrpc-agent/tests/snapshots/{subagent-spawn => subagent-spawn-in-process}/result.expected.json (100%) rename examples/jsonrpc-agent/tests/snapshots/{subagent-spawn => subagent-spawn-in-process}/session.1.jsonl (100%) rename examples/jsonrpc-agent/tests/snapshots/{subagent-spawn => subagent-spawn-in-process}/session.jsonl (100%) delete mode 100644 packages/bash/bash-env/README.md delete mode 100644 packages/bash/bash-env/README.zh.md delete mode 100644 packages/bash/bash/README.zh.md delete mode 100644 packages/bash/pwsh-local/README.i18n.yaml delete mode 100644 packages/bash/pwsh-sandbox/README.i18n.yaml delete mode 100644 packages/bash/tool-bash/README.i18n.yaml delete mode 100644 packages/bash/tool-pwsh/README.i18n.yaml delete mode 100644 packages/client/ui-command/tsdown.config.ts rename packages/client/{ui-models => ui-commands}/README.i18n.yaml (70%) rename packages/client/{ui-command => ui-commands}/README.md (73%) rename packages/client/{ui-command => ui-commands}/README.zh.md (73%) rename packages/client/{ui-command => ui-commands}/package.json (90%) rename packages/client/{ui-command => ui-commands}/src/client/PopupSelectView.module.css (100%) rename packages/client/{ui-command => ui-commands}/src/client/PopupSelectView.tsx (100%) rename packages/client/{ui-command => ui-commands}/src/client/contract.ts (92%) rename packages/client/{ui-command => ui-commands}/src/client/directory.ts (100%) rename packages/client/{ui-command => ui-commands}/src/client/index.ts (78%) rename packages/client/{ui-command => ui-commands}/src/client/locales.ts (100%) rename packages/client/{ui-command => ui-commands}/src/client/popup.ts (97%) rename packages/client/{ui-command => ui-commands}/src/client/service.ts (93%) rename packages/client/{ui-command => ui-commands}/src/css-modules.d.ts (100%) rename packages/client/{ui-command => ui-commands}/src/index.ts (92%) rename packages/client/{ui-command => ui-commands}/src/invariant.ts (84%) rename packages/client/{ui-command => ui-commands}/tests/browser-plugin.client.spec.ts (71%) rename packages/client/{ui-command => ui-commands}/tests/directory.client.spec.ts (100%) rename packages/client/{ui-command => ui-commands}/tests/popup-view.client.spec.tsx (100%) rename packages/client/{ui-command => ui-commands}/tests/popup.client.spec.ts (100%) rename packages/client/{ui-command => ui-commands}/tests/service.client.spec.ts (97%) rename packages/client/{ui-command => ui-commands}/tsconfig.json (88%) rename packages/client/{ui-question => ui-commands}/tsdown.config.ts (62%) create mode 100644 packages/client/ui-directory-picker-browse/README.i18n.yaml rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/README.md (98%) rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/README.zh.md (98%) rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/package.json (94%) rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/src/client/DirectoryBrowser.module.css (100%) rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/src/client/DirectoryBrowser.tsx (100%) rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/src/client/flow.ts (100%) rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/src/client/index.ts (100%) rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/src/css-modules.d.ts (100%) rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/src/index.ts (100%) rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/src/invariant.ts (85%) rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/tests/client-flow.client.spec.tsx (96%) rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/tests/directory-browser.client.spec.tsx (100%) rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/tsconfig.json (88%) rename packages/client/{ui-directory-picker => ui-directory-picker-browse}/tsdown.config.ts (63%) delete mode 100644 packages/client/ui-feedback/tsdown.config.ts rename packages/client/{test-runtime => ui-input-trigger}/README.i18n.yaml (69%) create mode 100644 packages/client/ui-input-trigger/README.md create mode 100644 packages/client/ui-input-trigger/README.zh.md rename packages/client/{ui-slash => ui-input-trigger}/package.json (95%) rename packages/client/{ui-slash => ui-input-trigger}/src/client/MenuView.module.css (100%) rename packages/client/{ui-slash => ui-input-trigger}/src/client/MenuView.tsx (98%) rename packages/client/{ui-slash => ui-input-trigger}/src/client/contract.ts (52%) rename packages/client/{ui-slash => ui-input-trigger}/src/client/controller.ts (92%) rename packages/client/{ui-slash => ui-input-trigger}/src/client/index.ts (71%) rename packages/client/{ui-slash => ui-input-trigger}/src/client/locales.ts (100%) rename packages/client/{ui-slash => ui-input-trigger}/src/client/service.ts (76%) rename packages/client/{ui-slash => ui-input-trigger}/src/client/slots.ts (91%) rename packages/client/{ui-slash => ui-input-trigger}/src/core/contract.ts (88%) rename packages/client/{ui-slash => ui-input-trigger}/src/core/detect.ts (100%) rename packages/client/{ui-slash => ui-input-trigger}/src/core/menu.ts (97%) rename packages/client/{ui-feedback => ui-input-trigger}/src/css-modules.d.ts (100%) rename packages/client/{ui-slash => ui-input-trigger}/src/index.ts (100%) rename packages/client/{ui-slash => ui-input-trigger}/src/invariant.ts (83%) rename packages/client/{ui-slash => ui-input-trigger}/src/types.ts (96%) rename packages/client/{ui-slash => ui-input-trigger}/tests/apply.client.spec.ts (79%) rename packages/client/{ui-slash => ui-input-trigger}/tests/core-detect.client.spec.ts (100%) rename packages/client/{ui-slash => ui-input-trigger}/tests/core-menu.client.spec.ts (100%) rename packages/client/{ui-slash => ui-input-trigger}/tests/menu-view.client.spec.tsx (98%) rename packages/client/{ui-slash => ui-input-trigger}/tests/service.client.spec.ts (88%) rename packages/client/{ui-slash => ui-input-trigger}/tsconfig.json (88%) create mode 100644 packages/client/ui-input-trigger/tsdown.config.ts rename packages/client/{ui-task => ui-jobs}/README.i18n.yaml (71%) create mode 100644 packages/client/ui-jobs/README.md rename packages/client/{ui-task => ui-jobs}/README.zh.md (82%) rename packages/client/{ui-task => ui-jobs}/package.json (91%) rename packages/client/{ui-task/src/client/TaskListAction.module.css => ui-jobs/src/client/JobListAction.module.css} (96%) rename packages/client/{ui-task/src/client/TaskListAction.tsx => ui-jobs/src/client/JobListAction.tsx} (69%) rename packages/client/{ui-task => ui-jobs}/src/client/index.ts (59%) rename packages/client/{ui-task => ui-jobs}/src/client/locales.ts (73%) rename packages/client/{ui-model => ui-jobs}/src/css-modules.d.ts (100%) rename packages/client/{ui-task => ui-jobs}/src/index.ts (79%) rename packages/client/{ui-task => ui-jobs}/src/invariant.ts (80%) rename packages/client/{ui-task => ui-jobs}/tests/browser-plugin.client.spec.ts (79%) rename packages/client/{ui-task/tests/task-list-action.client.spec.tsx => ui-jobs/tests/job-list-action.client.spec.tsx} (55%) rename packages/client/{ui-task => ui-jobs}/tsconfig.json (89%) rename packages/client/{ui-task => ui-jobs}/tsdown.config.ts (61%) rename packages/client/{ui-directory-picker => ui-message-feedback}/README.i18n.yaml (69%) rename packages/client/{ui-feedback => ui-message-feedback}/README.md (83%) rename packages/client/{ui-feedback => ui-message-feedback}/README.zh.md (80%) rename packages/client/{ui-feedback => ui-message-feedback}/package.json (92%) rename packages/client/{ui-feedback/src/client/FeedbackActions.module.css => ui-message-feedback/src/client/MessageFeedbackActions.module.css} (100%) rename packages/client/{ui-feedback/src/client/FeedbackActions.tsx => ui-message-feedback/src/client/MessageFeedbackActions.tsx} (94%) rename packages/client/{ui-feedback => ui-message-feedback}/src/client/controller.ts (88%) rename packages/client/{ui-feedback => ui-message-feedback}/src/client/index.ts (72%) rename packages/client/{ui-feedback => ui-message-feedback}/src/client/locales.ts (91%) rename packages/client/{ui-feedback => ui-message-feedback}/src/client/slots.ts (77%) rename packages/client/{ui-models => ui-message-feedback}/src/css-modules.d.ts (100%) rename packages/client/{ui-feedback => ui-message-feedback}/src/index.ts (100%) rename packages/client/{ui-feedback => ui-message-feedback}/src/invariant.ts (88%) rename packages/client/{ui-feedback => ui-message-feedback}/tests/browser-plugin.client.spec.tsx (94%) rename packages/client/{ui-feedback => ui-message-feedback}/tests/controller.client.spec.ts (89%) rename packages/client/{ui-feedback/tests/feedback-actions.client.spec.tsx => ui-message-feedback/tests/message-feedback-actions.client.spec.tsx} (86%) rename packages/client/{ui-feedback => ui-message-feedback}/tsconfig.json (86%) create mode 100644 packages/client/ui-message-feedback/tsdown.config.ts rename packages/client/{ui-model => ui-model-selection}/README.i18n.yaml (69%) rename packages/client/{ui-model => ui-model-selection}/README.md (72%) rename packages/client/{ui-model => ui-model-selection}/README.zh.md (72%) rename packages/client/{ui-model => ui-model-selection}/package.json (85%) rename packages/client/{ui-model => ui-model-selection}/src/client/ModelSelect.module.css (100%) rename packages/client/{ui-model => ui-model-selection}/src/client/ModelSelect.tsx (100%) rename packages/client/{ui-model => ui-model-selection}/src/client/directory.ts (100%) rename packages/client/{ui-model => ui-model-selection}/src/client/index.ts (88%) rename packages/client/{ui-model => ui-model-selection}/src/client/locales.ts (100%) rename packages/client/{ui-model => ui-model-selection}/src/client/service.ts (83%) rename packages/client/{ui-model => ui-model-selection}/src/client/slots.ts (100%) rename packages/client/{ui-plugins => ui-model-selection}/src/css-modules.d.ts (100%) rename packages/client/{ui-model => ui-model-selection}/src/index.ts (100%) rename packages/client/{ui-model => ui-model-selection}/src/invariant.ts (81%) rename packages/client/{ui-model => ui-model-selection}/tests/browser-plugin.client.spec.ts (95%) rename packages/client/{ui-model => ui-model-selection}/tests/model-select.client.spec.tsx (100%) rename packages/client/{ui-model => ui-model-selection}/tsconfig.json (83%) create mode 100644 packages/client/ui-model-selection/tsdown.config.ts delete mode 100644 packages/client/ui-model/tsdown.config.ts delete mode 100644 packages/client/ui-models/tsdown.config.ts create mode 100644 packages/client/ui-permission-presets/README.i18n.yaml rename packages/client/{ui-permission => ui-permission-presets}/README.md (59%) rename packages/client/{ui-permission => ui-permission-presets}/README.zh.md (61%) rename packages/client/{ui-permission => ui-permission-presets}/package.json (83%) rename packages/client/{ui-permission => ui-permission-presets}/src/client/PermissionRow.module.css (100%) rename packages/client/{ui-permission => ui-permission-presets}/src/client/PermissionRow.tsx (100%) rename packages/client/{ui-permission => ui-permission-presets}/src/client/index.ts (93%) rename packages/client/{ui-permission => ui-permission-presets}/src/client/locales.ts (100%) rename packages/client/{ui-permission => ui-permission-presets}/src/client/presentation.ts (100%) rename packages/client/{ui-permission => ui-permission-presets}/src/client/settings-store.ts (98%) rename packages/client/{ui-permission => ui-permission-presets}/src/css-modules.d.ts (100%) rename packages/client/{ui-permission => ui-permission-presets}/src/index.ts (100%) rename packages/client/{ui-permission => ui-permission-presets}/src/invariant.ts (81%) rename packages/client/{ui-permission => ui-permission-presets}/tests/browser-plugin.client.spec.ts (96%) rename packages/client/{ui-permission/tests/permission-row.client.spec.tsx => ui-permission-presets/tests/permission-presets-row.client.spec.tsx} (92%) rename packages/client/{ui-permission => ui-permission-presets}/tests/settings-store.client.spec.ts (92%) rename packages/client/{ui-permission => ui-permission-presets}/tsconfig.json (78%) rename packages/client/{ui-permission => ui-permission-presets}/tsdown.config.ts (64%) delete mode 100644 packages/client/ui-permission/README.i18n.yaml delete mode 100644 packages/client/ui-plugin-config/README.i18n.yaml delete mode 100644 packages/client/ui-plugin-config/tsdown.config.ts delete mode 100644 packages/client/ui-plugins/README.i18n.yaml delete mode 100644 packages/client/ui-plugins/tsdown.config.ts delete mode 100644 packages/client/ui-question/README.i18n.yaml rename packages/client/{ui-feedback => ui-settings-models}/README.i18n.yaml (69%) rename packages/client/{ui-models => ui-settings-models}/README.md (99%) rename packages/client/{ui-models => ui-settings-models}/README.zh.md (99%) rename packages/client/{ui-models => ui-settings-models}/package.json (95%) rename packages/client/{ui-models => ui-settings-models}/src/client/CustomProviderCard.tsx (100%) rename packages/client/{ui-models => ui-settings-models}/src/client/DeepSeekModelsEditor.tsx (100%) rename packages/client/{ui-models => ui-settings-models}/src/client/DeepSeekOnboardingDialog.module.css (100%) rename packages/client/{ui-models => ui-settings-models}/src/client/DeepSeekOnboardingDialog.tsx (100%) rename packages/client/{ui-models => ui-settings-models}/src/client/EditorFooter.tsx (97%) rename packages/client/{ui-models => ui-settings-models}/src/client/ModelListEditor.tsx (100%) rename packages/client/{ui-models => ui-settings-models}/src/client/ModelsSection.module.css (100%) rename packages/client/{ui-models => ui-settings-models}/src/client/ModelsSection.tsx (100%) rename packages/client/{ui-models => ui-settings-models}/src/client/ProviderEditor.tsx (100%) rename packages/client/{ui-models => ui-settings-models}/src/client/apiKey.ts (97%) rename packages/client/{ui-models => ui-settings-models}/src/client/index.ts (96%) rename packages/client/{ui-models => ui-settings-models}/src/client/locales.ts (100%) rename packages/client/{ui-models => ui-settings-models}/src/client/store.ts (100%) rename packages/client/{ui-slash => ui-settings-models}/src/css-modules.d.ts (100%) rename packages/client/{ui-models => ui-settings-models}/src/index.ts (100%) rename packages/client/{ui-models => ui-settings-models}/src/invariant.ts (81%) rename packages/client/{ui-models => ui-settings-models}/tests/apply.client.spec.ts (95%) rename packages/client/{ui-models => ui-settings-models}/tests/components.client.spec.tsx (100%) rename packages/client/{ui-models => ui-settings-models}/tests/invariant.client.spec.ts (70%) rename packages/client/{ui-models => ui-settings-models}/tests/onboarding-dialog.client.spec.tsx (100%) rename packages/client/{ui-models => ui-settings-models}/tests/provider-form.client.spec.tsx (100%) rename packages/client/{ui-models => ui-settings-models}/tests/readiness.client.spec.ts (100%) rename packages/client/{ui-models => ui-settings-models}/tests/store.client.spec.ts (100%) rename packages/client/{ui-models => ui-settings-models}/tests/styles.client.spec.ts (100%) rename packages/client/{ui-models => ui-settings-models}/tsconfig.json (92%) create mode 100644 packages/client/ui-settings-models/tsdown.config.ts create mode 100644 packages/client/ui-settings-plugin-inventory/README.i18n.yaml rename packages/client/{ui-plugins => ui-settings-plugin-inventory}/README.md (97%) rename packages/client/{ui-plugins => ui-settings-plugin-inventory}/README.zh.md (97%) rename packages/client/{ui-plugins => ui-settings-plugin-inventory}/package.json (94%) rename packages/client/{ui-plugins/src/client/PluginSettingsSection.module.css => ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.module.css} (100%) rename packages/client/{ui-plugins/src/client/PluginSettingsSection.tsx => ui-settings-plugin-inventory/src/client/PluginInventorySettingsTab.tsx} (93%) rename packages/client/{ui-plugins => ui-settings-plugin-inventory}/src/client/index.ts (61%) rename packages/client/{ui-plugins => ui-settings-plugin-inventory}/src/client/locales.ts (92%) rename packages/client/{ui-task => ui-settings-plugin-inventory}/src/css-modules.d.ts (100%) rename packages/client/{ui-plugins => ui-settings-plugin-inventory}/src/index.ts (100%) rename packages/client/{ui-plugins => ui-settings-plugin-inventory}/src/invariant.ts (78%) rename packages/client/{ui-plugins => ui-settings-plugin-inventory}/tests/browser-plugin.client.spec.tsx (80%) rename packages/client/{ui-plugins => ui-settings-plugin-inventory}/tests/components.client.spec.tsx (79%) rename packages/client/{ui-plugins => ui-settings-plugin-inventory}/tests/invariant.client.spec.ts (70%) rename packages/client/{ui-plugins => ui-settings-plugin-inventory}/tsconfig.json (90%) create mode 100644 packages/client/ui-settings-plugin-inventory/tsdown.config.ts create mode 100644 packages/client/ui-settings-plugins/README.i18n.yaml rename packages/client/{ui-plugin-config => ui-settings-plugins}/README.md (99%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/README.zh.md (99%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/package.json (95%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/client/AgentLoopCard.tsx (93%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/client/BashCard.tsx (95%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/client/ConfigurablePluginsTab.tsx (91%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/client/PluginCard.module.css (100%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/client/PluginCard.tsx (93%) rename packages/client/{ui-plugin-config/src/client/PluginConfigSection.module.css => ui-settings-plugins/src/client/PluginsSettingsSection.module.css} (100%) rename packages/client/{ui-plugin-config/src/client/PluginConfigSection.tsx => ui-settings-plugins/src/client/PluginsSettingsSection.tsx} (86%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/client/WebSearchCard.tsx (96%) rename packages/client/{ui-plugin-config/src/client/agent-loop-store.ts => ui-settings-plugins/src/client/agent-loop-card-controller.ts} (97%) rename packages/client/{ui-plugin-config/src/client/bash-store.ts => ui-settings-plugins/src/client/bash-card-controller.ts} (96%) rename packages/client/{ui-plugin-config/src/client/card-store.ts => ui-settings-plugins/src/client/card-form.ts} (100%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/client/fields.module.css (100%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/client/fields.tsx (100%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/client/index.ts (86%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/client/locales.ts (96%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/client/slot-contract.ts (100%) rename packages/client/{ui-plugin-config/src/client/web-search-store.ts => ui-settings-plugins/src/client/web-search-card-controller.ts} (99%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/css-modules.d.ts (100%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/index.ts (100%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/src/invariant.ts (82%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/tests/apply.client.spec.ts (89%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/tests/fields.client.spec.tsx (100%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/tests/invariant.client.spec.ts (76%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/tests/section.client.spec.tsx (95%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/tests/stores.client.spec.ts (99%) rename packages/client/{ui-plugin-config => ui-settings-plugins}/tsconfig.json (84%) create mode 100644 packages/client/ui-settings-plugins/tsdown.config.ts delete mode 100644 packages/client/ui-slash/README.i18n.yaml delete mode 100644 packages/client/ui-slash/README.md delete mode 100644 packages/client/ui-slash/README.zh.md delete mode 100644 packages/client/ui-slash/tsdown.config.ts delete mode 100644 packages/client/ui-task/README.md rename packages/client/{ui-command => ui-user-questions}/README.i18n.yaml (69%) rename packages/client/{ui-question => ui-user-questions}/README.md (98%) rename packages/client/{ui-question => ui-user-questions}/README.zh.md (98%) rename packages/client/{ui-question => ui-user-questions}/package.json (92%) rename packages/client/{ui-question => ui-user-questions}/src/client/PlanReviewPanel.module.css (100%) rename packages/client/{ui-question => ui-user-questions}/src/client/PlanReviewPanel.tsx (100%) rename packages/client/{ui-question => ui-user-questions}/src/client/QuestionComposer.module.css (100%) rename packages/client/{ui-question => ui-user-questions}/src/client/QuestionComposer.tsx (100%) rename packages/client/{ui-question => ui-user-questions}/src/client/contract/slots.ts (100%) rename packages/client/{ui-question => ui-user-questions}/src/client/index.ts (96%) rename packages/client/{ui-question => ui-user-questions}/src/client/locales.ts (100%) rename packages/client/{ui-question => ui-user-questions}/src/css-modules.d.ts (100%) rename packages/client/{ui-question => ui-user-questions}/src/index.ts (100%) rename packages/client/{ui-question => ui-user-questions}/src/invariant.ts (82%) rename packages/client/{ui-question => ui-user-questions}/tests/browser-plugin.client.spec.ts (87%) rename packages/client/{ui-question => ui-user-questions}/tests/node-plugin.client.spec.ts (78%) rename packages/client/{ui-question => ui-user-questions}/tests/plan-review-panel.client.spec.tsx (100%) rename packages/client/{ui-question/tests/question-composer.client.spec.tsx => ui-user-questions/tests/user-questions-composer.client.spec.tsx} (100%) rename packages/client/{ui-question => ui-user-questions}/tsconfig.json (90%) create mode 100644 packages/client/ui-user-questions/tsdown.config.ts rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/README.i18n.yaml (67%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/README.md (90%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/README.zh.md (90%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/package.json (92%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/src/bootstrap.ts (99%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/src/index.ts (93%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/src/invariant.ts (81%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/src/output-json.ts (99%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/src/protocol.ts (97%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/src/worker-json.ts (99%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/src/worker.ts (74%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/tests/bootstrap.spec.ts (100%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/tests/built-lib.e2e.ts (94%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/tests/output-json.spec.ts (100%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/tests/runtime.spec.ts (96%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/tests/source-worker.compat.spec.ts (100%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/tests/worker-json.spec.ts (100%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/tsconfig.json (90%) rename packages/code-runtime/{code-runtime-worker => code-runtime-worker-thread}/tsdown.config.ts (100%) delete mode 100644 packages/compact/README.md rename packages/{compact => compaction}/README.i18n.yaml (71%) create mode 100644 packages/compaction/README.md rename packages/{compact => compaction}/README.zh.md (50%) rename packages/{compact => compaction}/command-compact/README.i18n.yaml (69%) rename packages/{compact => compaction}/command-compact/README.md (78%) rename packages/{compact => compaction}/command-compact/README.zh.md (77%) rename packages/{compact => compaction}/command-compact/package.json (89%) rename packages/{compact => compaction}/command-compact/src/index.ts (94%) rename packages/{compact => compaction}/command-compact/src/invariant.ts (100%) rename packages/{compact => compaction}/command-compact/tests/command-compact.spec.ts (92%) rename packages/{compact => compaction}/command-compact/tests/invariant.spec.ts (100%) rename packages/{compact => compaction}/command-compact/tests/loader-composition.spec.ts (88%) rename packages/{compact => compaction}/command-compact/tsconfig.json (81%) rename packages/{compact/compact => compaction/compaction-basic}/README.i18n.yaml (69%) rename packages/{compact/compact-basic => compaction/compaction-basic}/README.md (73%) rename packages/{compact/compact-basic => compaction/compaction-basic}/README.zh.md (73%) rename packages/{compact/compact-basic => compaction/compaction-basic}/package.json (83%) rename packages/{compact/compact-basic => compaction/compaction-basic}/src/config.ts (91%) rename packages/{compact/compact-basic => compaction/compaction-basic}/src/index.ts (94%) rename packages/{compact/compact-basic => compaction/compaction-basic}/src/invariant.ts (83%) rename packages/{compact/compact-basic => compaction/compaction-basic}/src/region.ts (94%) rename packages/{compact/compact-basic => compaction/compaction-basic}/src/summarizer.ts (97%) rename packages/{compact/compact-basic => compaction/compaction-basic}/src/types.ts (92%) rename packages/{compact/compact-basic/tests/compact-basic.spec.ts => compaction/compaction-basic/tests/compaction-basic.spec.ts} (92%) rename packages/{compact/compact-basic/tests/compact-loop-repro.spec.ts => compaction/compaction-basic/tests/compaction-loop-repro.spec.ts} (94%) rename packages/{compact/compact-basic => compaction/compaction-basic}/tests/loader-composition.spec.ts (69%) rename packages/{compact/compact-basic/tests/manual-compact.spec.ts => compaction/compaction-basic/tests/manual-compaction.spec.ts} (89%) rename packages/{compact/compact-basic => compaction/compaction-basic}/tsconfig.json (82%) rename packages/{compact/compact-tool-result-prune => compaction/compaction-tool-result-pruner}/README.i18n.yaml (67%) rename packages/{compact/compact-tool-result-prune => compaction/compaction-tool-result-pruner}/README.md (80%) rename packages/{compact/compact-tool-result-prune => compaction/compaction-tool-result-pruner}/README.zh.md (80%) rename packages/{compact/compact-tool-result-prune => compaction/compaction-tool-result-pruner}/package.json (86%) rename packages/{compact/compact-tool-result-prune => compaction/compaction-tool-result-pruner}/src/config.ts (100%) rename packages/{compact/compact-tool-result-prune => compaction/compaction-tool-result-pruner}/src/index.ts (93%) rename packages/{compact/compact-tool-result-prune => compaction/compaction-tool-result-pruner}/src/invariant.ts (79%) rename packages/{compact/compact-tool-result-prune => compaction/compaction-tool-result-pruner}/src/types.ts (100%) rename packages/{compact/compact-tool-result-prune => compaction/compaction-tool-result-pruner}/tests/loader-composition.spec.ts (76%) rename packages/{compact/compact-tool-result-prune/tests/tool-result-prune.spec.ts => compaction/compaction-tool-result-pruner/tests/tool-result-pruner.spec.ts} (93%) rename packages/{compact/compact-tool-result-prune => compaction/compaction-tool-result-pruner}/tsconfig.json (82%) rename packages/{compact/compact-basic => compaction/compaction}/README.i18n.yaml (70%) rename packages/{compact/compact => compaction/compaction}/README.md (60%) rename packages/{compact/compact => compaction/compaction}/README.zh.md (60%) rename packages/{compact/compact => compaction/compaction}/package.json (89%) rename packages/{compact/compact => compaction/compaction}/src/brand.ts (100%) rename packages/{compact/compact => compaction/compaction}/src/checkpoint.ts (88%) rename packages/{compact/compact => compaction/compaction}/src/index.ts (88%) rename packages/{compact/compact => compaction/compaction}/src/invariant.ts (79%) rename packages/{compact/compact => compaction/compaction}/src/tool-pairing.ts (99%) rename packages/{compact/compact => compaction/compaction}/src/types.ts (85%) rename packages/{compact/compact/tests/compact.spec.ts => compaction/compaction/tests/compaction.spec.ts} (76%) rename packages/{compact/compact => compaction/compaction}/tests/invariant.spec.ts (62%) rename packages/{compact/compact => compaction/compaction}/tests/tool-pairing.spec.ts (99%) rename packages/{compact/compact => compaction/compaction}/tsconfig.json (90%) rename packages/{compact/compact => compaction/compaction}/tsdown.config.ts (100%) rename packages/context/{workspace-context => agent-instructions}/README.i18n.yaml (69%) rename packages/context/{workspace-context => agent-instructions}/README.md (92%) rename packages/context/{workspace-context => agent-instructions}/README.zh.md (92%) rename packages/context/{workspace-context => agent-instructions}/package.json (89%) rename packages/context/{workspace-context => agent-instructions}/src/config.ts (97%) rename packages/context/{workspace-context => agent-instructions}/src/digest.ts (94%) rename packages/context/{workspace-context => agent-instructions}/src/files.ts (99%) rename packages/context/{workspace-context => agent-instructions}/src/index.ts (94%) rename packages/context/{workspace-context => agent-instructions}/src/invariant.ts (82%) rename packages/context/{workspace-context => agent-instructions}/src/render.ts (98%) rename packages/context/{workspace-context => agent-instructions}/src/state.ts (92%) rename packages/context/{workspace-context/tests/workspace-context.e2e.ts => agent-instructions/tests/agent-instructions.e2e.ts} (95%) rename packages/context/{workspace-context/tests/workspace-context.spec.ts => agent-instructions/tests/agent-instructions.spec.ts} (97%) rename packages/context/{workspace-context => agent-instructions}/tsconfig.json (85%) rename packages/core/{agent-tool-mode => agent-tool-presentation}/README.i18n.yaml (68%) rename packages/core/{agent-tool-mode => agent-tool-presentation}/README.md (85%) rename packages/core/{agent-tool-mode => agent-tool-presentation}/README.zh.md (86%) rename packages/core/{agent-tool-mode => agent-tool-presentation}/package.json (92%) rename packages/core/{agent-tool-mode => agent-tool-presentation}/src/index.ts (94%) rename packages/core/{agent-tool-mode => agent-tool-presentation}/src/invariant.ts (84%) rename packages/core/{agent-tool-mode/tests/agent-tool-mode.spec.ts => agent-tool-presentation/tests/agent-tool-presentation.spec.ts} (96%) rename packages/core/{agent-tool-mode => agent-tool-presentation}/tsconfig.json (88%) rename packages/{self-modification => extensions}/README.i18n.yaml (80%) rename packages/{self-modification => extensions}/README.md (100%) rename packages/{self-modification => extensions}/README.zh.md (100%) create mode 100644 packages/extensions/tool-cordis/README.i18n.yaml rename packages/{self-modification => extensions}/tool-cordis/README.md (89%) rename packages/{self-modification => extensions}/tool-cordis/README.zh.md (90%) rename packages/{self-modification => extensions}/tool-cordis/package.json (96%) rename packages/{self-modification => extensions}/tool-cordis/src/api-catalog.ts (93%) rename packages/{self-modification => extensions}/tool-cordis/src/fiber-state.ts (100%) rename packages/{self-modification => extensions}/tool-cordis/src/guard.ts (99%) rename packages/{self-modification => extensions}/tool-cordis/src/index.ts (97%) rename packages/{self-modification => extensions}/tool-cordis/src/inspect.ts (100%) rename packages/{self-modification => extensions}/tool-cordis/src/invariant.ts (100%) rename packages/{self-modification => extensions}/tool-cordis/src/mount.ts (100%) rename packages/{self-modification => extensions}/tool-cordis/src/present.ts (100%) rename packages/{self-modification => extensions}/tool-cordis/src/sandbox.ts (98%) rename packages/{self-modification => extensions}/tool-cordis/tests/cordis-lifecycle.spec.ts (100%) rename packages/{self-modification => extensions}/tool-cordis/tests/cross-mount.spec.ts (100%) rename packages/{self-modification => extensions}/tool-cordis/tests/helpers.ts (93%) rename packages/{self-modification => extensions}/tool-cordis/tests/inspect.spec.ts (96%) rename packages/{self-modification => extensions}/tool-cordis/tests/integration.spec.ts (100%) rename packages/{self-modification => extensions}/tool-cordis/tests/mount.spec.ts (100%) rename packages/{self-modification => extensions}/tool-cordis/tests/present.spec.ts (100%) rename packages/{self-modification => extensions}/tool-cordis/tests/sandbox-context.spec.ts (99%) rename packages/{self-modification => extensions}/tool-cordis/tests/tool-cordis.spec.ts (100%) rename packages/{self-modification => extensions}/tool-cordis/tests/unmount-hmr.spec.ts (97%) rename packages/{self-modification => extensions}/tool-cordis/tsconfig.json (90%) create mode 100644 packages/fs/fs-observation-policy/README.i18n.yaml rename packages/fs/{fs-policy => fs-observation-policy}/README.md (87%) rename packages/fs/{fs-policy => fs-observation-policy}/README.zh.md (87%) rename packages/fs/{fs-policy => fs-observation-policy}/package.json (92%) rename packages/fs/{fs-policy => fs-observation-policy}/src/index.ts (92%) rename packages/fs/{fs-policy => fs-observation-policy}/src/invariant.ts (76%) rename packages/fs/{fs-policy => fs-observation-policy}/src/types.ts (79%) rename packages/fs/{fs-policy => fs-observation-policy}/tests/policy.spec.ts (94%) rename packages/fs/{fs-policy => fs-observation-policy}/tsconfig.json (87%) rename packages/goal/{goal-session => goal-round-driver}/README.i18n.yaml (69%) rename packages/goal/{goal-session => goal-round-driver}/README.md (98%) rename packages/goal/{goal-session => goal-round-driver}/README.zh.md (98%) rename packages/goal/{goal-session => goal-round-driver}/package.json (93%) rename packages/goal/{goal-session => goal-round-driver}/src/index.ts (93%) rename packages/goal/{goal-session => goal-round-driver}/src/invariant.ts (94%) rename packages/goal/{goal-session => goal-round-driver}/src/prompt.ts (100%) rename packages/goal/{goal-session/tests/goal-session.spec.ts => goal-round-driver/tests/goal-round-driver.spec.ts} (99%) rename packages/goal/{goal-session => goal-round-driver}/tests/invariant.spec.ts (83%) rename packages/goal/{goal-session => goal-round-driver}/tsconfig.json (89%) rename packages/goal/{goal-session => goal-round-driver}/tsdown.config.ts (100%) rename packages/guard/{repeat-tool-guard => repeat-tool-reminder}/README.i18n.yaml (69%) rename packages/guard/{repeat-tool-guard => repeat-tool-reminder}/README.md (87%) rename packages/guard/{repeat-tool-guard => repeat-tool-reminder}/README.zh.md (87%) rename packages/guard/{repeat-tool-guard => repeat-tool-reminder}/package.json (93%) rename packages/guard/{repeat-tool-guard => repeat-tool-reminder}/src/index.ts (93%) rename packages/guard/{repeat-tool-guard => repeat-tool-reminder}/src/invariant.ts (83%) rename packages/guard/{repeat-tool-guard/tests/repeat-tool-guard.spec.ts => repeat-tool-reminder/tests/repeat-tool-reminder.spec.ts} (99%) rename packages/guard/{repeat-tool-guard => repeat-tool-reminder}/tsconfig.json (90%) rename packages/hooks/{hooks-claude => hooks-claude-code}/README.i18n.yaml (69%) rename packages/hooks/{hooks-claude => hooks-claude-code}/README.md (96%) rename packages/hooks/{hooks-claude => hooks-claude-code}/README.zh.md (96%) rename packages/hooks/{hooks-claude => hooks-claude-code}/package.json (93%) rename packages/hooks/{hooks-claude => hooks-claude-code}/src/config.ts (92%) rename packages/hooks/{hooks-claude => hooks-claude-code}/src/index.ts (92%) rename packages/hooks/{hooks-claude => hooks-claude-code}/src/invariant.ts (83%) rename packages/hooks/{hooks-claude => hooks-claude-code}/tests/bridge.spec.ts (95%) rename packages/hooks/{hooks-claude => hooks-claude-code}/tests/config.spec.ts (72%) rename packages/hooks/{hooks-claude => hooks-claude-code}/tests/coverage-cases.ts (95%) rename packages/hooks/{hooks-claude => hooks-claude-code}/tests/coverage-config.spec.ts (100%) rename packages/hooks/{hooks-claude => hooks-claude-code}/tests/coverage-context.spec.ts (100%) rename packages/hooks/{hooks-claude => hooks-claude-code}/tests/coverage-edge-paths.spec.ts (100%) rename packages/hooks/{hooks-claude => hooks-claude-code}/tests/coverage-stop.spec.ts (100%) rename packages/hooks/{hooks-claude => hooks-claude-code}/tsconfig.json (89%) rename packages/host/apiproxy/src/api/{tasks.schema.ts => jobs.schema.ts} (69%) rename packages/host/apiproxy/src/api/{tasks.ts => jobs.ts} (84%) rename packages/host/apiproxy/tests/{api-proxy-tasks.spec.ts => api-proxy-jobs.spec.ts} (74%) rename packages/{pty => identity}/README.i18n.yaml (57%) create mode 100644 packages/identity/README.md create mode 100644 packages/identity/README.zh.md create mode 100644 packages/identity/anonymous-user-id/README.i18n.yaml rename packages/{session/user-id => identity/anonymous-user-id}/README.md (78%) rename packages/{session/user-id => identity/anonymous-user-id}/README.zh.md (79%) rename packages/{session/user-id => identity/anonymous-user-id}/package.json (84%) rename packages/{session/user-id => identity/anonymous-user-id}/src/index.ts (93%) rename packages/{session/user-id => identity/anonymous-user-id}/src/invariant.ts (79%) rename packages/{session/user-id/tests/user-id.spec.ts => identity/anonymous-user-id/tests/anonymous-user-id.spec.ts} (81%) rename packages/{session/user-id => identity/anonymous-user-id}/tests/invariant.spec.ts (62%) rename packages/{session/user-id => identity/anonymous-user-id}/tsconfig.json (73%) rename packages/interaction/{user-interaction => permission-presets}/README.i18n.yaml (68%) create mode 100644 packages/interaction/permission-presets/README.md create mode 100644 packages/interaction/permission-presets/README.zh.md rename packages/interaction/{permission => permission-presets}/package.json (84%) rename packages/interaction/{permission => permission-presets}/src/client.ts (86%) rename packages/interaction/{permission => permission-presets}/src/index.ts (95%) rename packages/interaction/{permission => permission-presets}/src/invariant.ts (83%) rename packages/interaction/{permission => permission-presets}/src/types.ts (96%) rename packages/interaction/{permission => permission-presets}/tests/invariant.spec.ts (88%) rename packages/interaction/{permission/tests/permission.spec.ts => permission-presets/tests/permission-presets.spec.ts} (77%) rename packages/interaction/{permission => permission-presets}/tests/projection.spec.ts (92%) rename packages/interaction/{permission => permission-presets}/tsconfig.json (89%) delete mode 100644 packages/interaction/permission/README.md delete mode 100644 packages/interaction/permission/README.zh.md rename packages/interaction/{permission => user-questions}/README.i18n.yaml (69%) rename packages/interaction/{user-interaction => user-questions}/README.md (78%) rename packages/interaction/{user-interaction => user-questions}/README.zh.md (78%) rename packages/interaction/{user-interaction => user-questions}/package.json (85%) rename packages/interaction/{user-interaction => user-questions}/src/index.ts (74%) rename packages/interaction/{user-interaction => user-questions}/src/invariant.ts (84%) rename packages/interaction/{user-interaction => user-questions}/src/types.ts (95%) rename packages/interaction/{user-interaction/tests/user-interaction.spec.ts => user-questions/tests/user-questions.spec.ts} (66%) rename packages/interaction/{user-interaction => user-questions}/tsconfig.json (87%) rename packages/{bash => jobs}/README.i18n.yaml (59%) create mode 100644 packages/jobs/README.md create mode 100644 packages/jobs/README.zh.md rename packages/{bash/bash-local => jobs/jobs-local}/README.i18n.yaml (56%) create mode 100644 packages/jobs/jobs-local/README.md rename packages/{tasks/tasks-local => jobs/jobs-local}/README.zh.md (69%) rename packages/{tasks/tasks-local => jobs/jobs-local}/package.json (87%) rename packages/{tasks/tasks-local => jobs/jobs-local}/src/index.ts (61%) rename packages/{tasks/tasks-local => jobs/jobs-local}/src/invariant.ts (72%) rename packages/{tasks/tasks-local/tests/tasks.spec.ts => jobs/jobs-local/tests/jobs.spec.ts} (58%) rename packages/{tasks/tasks-local => jobs/jobs-local}/tests/loader-composition.spec.ts (75%) rename packages/{tasks/tasks-local => jobs/jobs-local}/tsconfig.json (86%) rename packages/{bash/bash => jobs/jobs}/README.i18n.yaml (57%) create mode 100644 packages/jobs/jobs/README.md rename packages/{tasks/tasks => jobs/jobs}/README.zh.md (52%) rename packages/{tasks/tasks => jobs/jobs}/package.json (83%) rename packages/{tasks/tasks => jobs/jobs}/src/brand.ts (57%) rename packages/{tasks/tasks => jobs/jobs}/src/index.ts (62%) rename packages/{tasks/tasks => jobs/jobs}/src/invariant.ts (57%) rename packages/{tasks/tasks => jobs/jobs}/src/types.ts (62%) rename packages/{tasks/tasks => jobs/jobs}/tests/invariant.spec.ts (57%) create mode 100644 packages/jobs/jobs/tests/service.spec.ts rename packages/{tasks/tasks => jobs/jobs}/tsconfig.json (89%) rename packages/{bash/bash-env => jobs/tool-jobs}/README.i18n.yaml (56%) create mode 100644 packages/jobs/tool-jobs/README.md rename packages/{tasks/tool-tasks => jobs/tool-jobs}/README.zh.md (57%) rename packages/{tasks/tool-tasks => jobs/tool-jobs}/package.json (75%) rename packages/{tasks/tool-tasks => jobs/tool-jobs}/src/index.ts (68%) rename packages/{tasks/tool-tasks => jobs/tool-jobs}/src/invariant.ts (85%) rename packages/{tasks/tool-tasks/tests/tool-tasks.spec.ts => jobs/tool-jobs/tests/tool-jobs.spec.ts} (71%) rename packages/{tasks/tool-tasks => jobs/tool-jobs}/tsconfig.json (81%) rename packages/lsp/{lsp-local => lsp-stdio}/README.i18n.yaml (71%) rename packages/lsp/{lsp-local => lsp-stdio}/README.md (99%) rename packages/lsp/{lsp-local => lsp-stdio}/README.zh.md (99%) rename packages/lsp/{lsp-local => lsp-stdio}/package.json (96%) rename packages/lsp/{lsp-local => lsp-stdio}/src/abort.ts (97%) rename packages/lsp/{lsp-local => lsp-stdio}/src/connection.ts (99%) rename packages/lsp/{lsp-local => lsp-stdio}/src/framing.ts (98%) rename packages/lsp/{lsp-local => lsp-stdio}/src/host.ts (100%) rename packages/lsp/{lsp-local => lsp-stdio}/src/index.ts (95%) rename packages/lsp/{lsp-local => lsp-stdio}/src/instance.ts (99%) rename packages/lsp/{lsp-local => lsp-stdio}/src/invariant.ts (80%) rename packages/lsp/{lsp-local => lsp-stdio}/src/protocol.ts (98%) rename packages/lsp/{lsp-local => lsp-stdio}/src/translate.ts (99%) rename packages/lsp/{lsp-local => lsp-stdio}/tests/built-lib.e2e.ts (93%) rename packages/lsp/{lsp-local => lsp-stdio}/tests/connection.spec.ts (98%) rename packages/lsp/{lsp-local => lsp-stdio}/tests/fixture-server.ts (99%) rename packages/lsp/{lsp-local => lsp-stdio}/tests/framing.spec.ts (99%) rename packages/lsp/{lsp-local => lsp-stdio}/tests/host.spec.ts (99%) rename packages/lsp/{lsp-local => lsp-stdio}/tests/instance.spec.ts (98%) rename packages/lsp/{lsp-local => lsp-stdio}/tests/lifecycle.spec.ts (97%) rename packages/lsp/{lsp-local => lsp-stdio}/tests/provider.spec.ts (92%) rename packages/lsp/{lsp-local => lsp-stdio}/tests/translate.spec.ts (99%) rename packages/lsp/{lsp-local => lsp-stdio}/tests/typescript-server.e2e.ts (95%) rename packages/lsp/{lsp-local => lsp-stdio}/tsconfig.json (92%) delete mode 100644 packages/pty/README.md delete mode 100644 packages/pty/README.zh.md delete mode 100644 packages/pty/pty-local/README.i18n.yaml delete mode 100644 packages/pty/tool-bash-persistent/README.i18n.yaml delete mode 100644 packages/pty/tool-pty/README.i18n.yaml delete mode 100644 packages/pty/tool-pty/README.md delete mode 100644 packages/pty/tool-pty/README.zh.md create mode 100644 packages/runtime-diagnostics/invariants/README.i18n.yaml rename packages/{support => runtime-diagnostics}/invariants/README.md (90%) rename packages/{support => runtime-diagnostics}/invariants/README.zh.md (90%) rename packages/{support => runtime-diagnostics}/invariants/package.json (94%) rename packages/{support => runtime-diagnostics}/invariants/src/index.ts (98%) rename packages/{support => runtime-diagnostics}/invariants/src/invariant.ts (100%) rename packages/{support => runtime-diagnostics}/invariants/tests/service.spec.ts (97%) rename packages/{support => runtime-diagnostics}/invariants/tsconfig.json (100%) rename packages/schedule/{tool-schedule => schedule}/README.i18n.yaml (69%) rename packages/schedule/{tool-schedule => schedule}/README.md (96%) rename packages/schedule/{tool-schedule => schedule}/README.zh.md (96%) rename packages/schedule/{tool-schedule => schedule}/package.json (95%) rename packages/schedule/{tool-schedule => schedule}/src/domain.ts (99%) rename packages/schedule/{tool-schedule => schedule}/src/index.ts (73%) rename packages/schedule/{tool-schedule => schedule}/src/invariant.ts (95%) rename packages/schedule/{tool-schedule => schedule}/src/persistence.ts (100%) rename packages/schedule/{tool-schedule => schedule}/src/runtime.ts (87%) rename packages/schedule/{tool-schedule => schedule}/src/tools.ts (98%) rename packages/schedule/{tool-schedule => schedule}/src/transaction.ts (100%) rename packages/schedule/{tool-schedule => schedule}/src/types.ts (99%) rename packages/schedule/{tool-schedule => schedule}/tests/domain.spec.ts (100%) rename packages/schedule/{tool-schedule => schedule}/tests/invariant.spec.ts (95%) rename packages/schedule/{tool-schedule => schedule}/tests/jsonl-restart.spec.ts (96%) rename packages/schedule/{tool-schedule => schedule}/tests/plugin.spec.ts (98%) rename packages/schedule/{tool-schedule => schedule}/tests/recurrence.spec.ts (100%) rename packages/schedule/{tool-schedule => schedule}/tests/runtime.spec.ts (84%) rename packages/schedule/{tool-schedule => schedule}/tests/tools.spec.ts (99%) rename packages/schedule/{tool-schedule => schedule}/tsconfig.json (92%) rename packages/schedule/{tool-schedule => schedule}/tsdown.config.ts (100%) delete mode 100644 packages/self-modification/tool-cordis/README.i18n.yaml delete mode 100644 packages/session-query/session-export/tsdown.config.ts rename packages/session-query/{session-export => session-log-download}/README.i18n.yaml (67%) rename packages/session-query/{session-export => session-log-download}/README.md (87%) rename packages/session-query/{session-export => session-log-download}/README.zh.md (86%) rename packages/session-query/{session-export => session-log-download}/package.json (88%) rename packages/session-query/{session-export => session-log-download}/src/client/Dialog.tsx (75%) rename packages/session-query/{session-export => session-log-download}/src/client/HeaderAction.module.css (100%) rename packages/session-query/{session-export => session-log-download}/src/client/HeaderAction.tsx (66%) rename packages/session-query/{session-export => session-log-download}/src/client/controller.ts (89%) rename packages/session-query/{session-export => session-log-download}/src/client/index.ts (57%) rename packages/session-query/{session-export => session-log-download}/src/client/locales.ts (92%) rename packages/session-query/{session-export => session-log-download}/src/css-modules.d.ts (100%) rename packages/session-query/{session-export => session-log-download}/src/index.ts (91%) rename packages/session-query/{session-export => session-log-download}/src/invariant.ts (83%) rename packages/session-query/{session-export => session-log-download}/tests/client-apply.client.spec.tsx (70%) rename packages/session-query/{session-export => session-log-download}/tests/command.client.spec.ts (91%) rename packages/session-query/{session-export => session-log-download}/tests/controller.client.spec.ts (86%) rename packages/session-query/{session-export => session-log-download}/tests/dialog.client.spec.tsx (79%) rename packages/session-query/{session-export => session-log-download}/tests/header-action.client.spec.tsx (69%) rename packages/session-query/{session-export => session-log-download}/tests/invariant.client.spec.ts (83%) rename packages/session-query/{session-export => session-log-download}/tests/loader-composition.client.spec.ts (87%) rename packages/session-query/{session-export => session-log-download}/tsconfig.json (83%) create mode 100644 packages/session-query/session-log-download/tsdown.config.ts rename packages/session/{user-id => session-title-all-prompts-llm}/README.i18n.yaml (67%) rename packages/session/{session-title-all-messages-llm => session-title-all-prompts-llm}/README.md (80%) rename packages/session/{session-title-all-messages-llm => session-title-all-prompts-llm}/README.zh.md (81%) rename packages/session/{session-title-all-messages-llm => session-title-all-prompts-llm}/package.json (91%) rename packages/session/{session-title-all-messages-llm => session-title-all-prompts-llm}/src/index.ts (83%) rename packages/session/{session-title-first-message-llm => session-title-all-prompts-llm}/src/invariant.ts (79%) rename packages/session/{session-title-all-messages-llm => session-title-all-prompts-llm}/tests/provider.spec.ts (96%) rename packages/session/{session-title-first-message-llm => session-title-all-prompts-llm}/tsconfig.json (87%) delete mode 100644 packages/session/session-title-first-message-llm/README.i18n.yaml rename packages/session/{session-title-all-messages-llm => session-title-first-prompt-llm}/README.i18n.yaml (67%) rename packages/session/{session-title-first-message-llm => session-title-first-prompt-llm}/README.md (82%) rename packages/session/{session-title-first-message-llm => session-title-first-prompt-llm}/README.zh.md (80%) rename packages/session/{session-title-first-message-llm => session-title-first-prompt-llm}/package.json (92%) rename packages/session/{session-title-first-message-llm => session-title-first-prompt-llm}/src/index.ts (83%) rename packages/session/{session-title-all-messages-llm => session-title-first-prompt-llm}/src/invariant.ts (80%) rename packages/session/{session-title-first-message-llm => session-title-first-prompt-llm}/tests/loader-composition.spec.ts (92%) rename packages/session/{session-title-first-message-llm => session-title-first-prompt-llm}/tests/provider.e2e.ts (87%) rename packages/session/{session-title-first-message-llm => session-title-first-prompt-llm}/tests/provider.spec.ts (94%) rename packages/session/{session-title-all-messages-llm => session-title-first-prompt-llm}/tsconfig.json (87%) rename packages/settings/{settings-local => settings-file}/README.i18n.yaml (69%) rename packages/settings/{settings-local => settings-file}/README.md (99%) rename packages/settings/{settings-local => settings-file}/README.zh.md (99%) rename packages/settings/{settings-local => settings-file}/package.json (87%) rename packages/settings/{settings-local => settings-file}/src/index.ts (94%) rename packages/settings/{settings-local => settings-file}/src/invariant.ts (85%) rename packages/settings/{settings-local => settings-file}/tests/concurrency.spec.ts (95%) rename packages/settings/{settings-local => settings-file}/tests/loader-composition.spec.ts (95%) rename packages/settings/{settings-local => settings-file}/tests/local.spec.ts (98%) rename packages/settings/{settings-local => settings-file}/tests/lock-race.spec.ts (94%) rename packages/settings/{settings-local => settings-file}/tests/watcher.spec.ts (96%) rename packages/settings/{settings-local => settings-file}/tsconfig.json (83%) rename packages/{tasks => shell}/README.i18n.yaml (59%) rename packages/{bash => shell}/README.md (71%) rename packages/{bash => shell}/README.zh.md (69%) create mode 100644 packages/shell/bash-local/README.i18n.yaml rename packages/{bash => shell}/bash-local/README.md (68%) rename packages/{bash => shell}/bash-local/README.zh.md (68%) rename packages/{bash => shell}/bash-local/package.json (91%) rename packages/{bash => shell}/bash-local/src/index.ts (93%) rename packages/{bash => shell}/bash-local/src/invariant.ts (100%) rename packages/{bash => shell}/bash-local/tests/executor.spec.ts (94%) rename packages/{bash => shell}/bash-local/tests/settings.spec.ts (75%) rename packages/{bash => shell}/bash-local/tsconfig.json (86%) create mode 100644 packages/shell/bash-sandbox/README.i18n.yaml rename packages/{bash => shell}/bash-sandbox/README.md (82%) rename packages/{bash => shell}/bash-sandbox/README.zh.md (82%) rename packages/{bash => shell}/bash-sandbox/package.json (92%) rename packages/{bash => shell}/bash-sandbox/src/helpers.ts (96%) rename packages/{bash => shell}/bash-sandbox/src/index.ts (91%) rename packages/{bash => shell}/bash-sandbox/src/invariant.ts (100%) rename packages/{bash => shell}/bash-sandbox/tests/bwrap.e2e.ts (96%) rename packages/{bash => shell}/bash-sandbox/tests/landlock.e2e.ts (96%) rename packages/{bash => shell}/bash-sandbox/tests/partial-landlock.spec.ts (98%) rename packages/{bash => shell}/bash-sandbox/tests/sandbox.spec.ts (97%) rename packages/{bash => shell}/bash-sandbox/tests/seatbelt.e2e.ts (96%) rename packages/{bash => shell}/bash-sandbox/tsconfig.json (82%) create mode 100644 packages/shell/pwsh-local/README.i18n.yaml rename packages/{bash => shell}/pwsh-local/README.md (75%) rename packages/{bash => shell}/pwsh-local/README.zh.md (77%) rename packages/{bash => shell}/pwsh-local/package.json (91%) rename packages/{bash => shell}/pwsh-local/src/index.ts (93%) rename packages/{bash => shell}/pwsh-local/src/invariant.ts (100%) rename packages/{bash => shell}/pwsh-local/src/resolve.ts (100%) rename packages/{bash => shell}/pwsh-local/tests/executor.spec.ts (95%) rename packages/{bash => shell}/pwsh-local/tests/settings.spec.ts (78%) rename packages/{bash => shell}/pwsh-local/tsconfig.json (86%) create mode 100644 packages/shell/pwsh-sandbox/README.i18n.yaml rename packages/{bash => shell}/pwsh-sandbox/README.md (75%) rename packages/{bash => shell}/pwsh-sandbox/README.zh.md (74%) rename packages/{bash => shell}/pwsh-sandbox/package.json (91%) rename packages/{bash => shell}/pwsh-sandbox/src/helpers.ts (97%) rename packages/{bash => shell}/pwsh-sandbox/src/index.ts (91%) rename packages/{bash => shell}/pwsh-sandbox/src/invariant.ts (100%) rename packages/{bash => shell}/pwsh-sandbox/tests/acl.e2e.ts (97%) rename packages/{bash => shell}/pwsh-sandbox/tests/sandbox.spec.ts (97%) rename packages/{bash => shell}/pwsh-sandbox/tsconfig.json (80%) create mode 100644 packages/shell/shell-env/README.i18n.yaml create mode 100644 packages/shell/shell-env/README.md create mode 100644 packages/shell/shell-env/README.zh.md rename packages/{bash/bash-env => shell/shell-env}/package.json (83%) rename packages/{bash/bash-env => shell/shell-env}/src/index.ts (90%) rename packages/{bash/bash-env => shell/shell-env}/src/invariant.ts (81%) rename packages/{bash/bash-env/tests/bash-env.spec.ts => shell/shell-env/tests/shell-env.spec.ts} (84%) rename packages/{bash/bash-env => shell/shell-env}/tsconfig.json (79%) create mode 100644 packages/shell/shell/README.i18n.yaml rename packages/{bash/bash => shell/shell}/README.md (50%) create mode 100644 packages/shell/shell/README.zh.md rename packages/{bash/bash => shell/shell}/package.json (88%) rename packages/{bash/bash => shell/shell}/src/index.ts (70%) rename packages/{bash/bash => shell/shell}/src/invariant.ts (87%) rename packages/{bash/bash => shell/shell}/src/render.ts (93%) rename packages/{bash/bash => shell/shell}/src/types.ts (85%) rename packages/{bash/bash => shell/shell}/tests/render.spec.ts (100%) rename packages/{bash/bash => shell/shell}/tests/service.spec.ts (66%) rename packages/{bash/bash => shell/shell}/tsconfig.json (89%) create mode 100644 packages/shell/tool-bash-persistent/README.i18n.yaml rename packages/{pty => shell}/tool-bash-persistent/README.md (93%) rename packages/{pty => shell}/tool-bash-persistent/README.zh.md (95%) rename packages/{pty => shell}/tool-bash-persistent/package.json (89%) rename packages/{pty => shell}/tool-bash-persistent/src/index.ts (91%) rename packages/{pty => shell}/tool-bash-persistent/src/invariant.ts (100%) rename packages/{pty => shell}/tool-bash-persistent/tests/loader-composition.spec.ts (91%) rename packages/{pty => shell}/tool-bash-persistent/tests/tools.spec.ts (94%) rename packages/{pty => shell}/tool-bash-persistent/tsconfig.json (76%) create mode 100644 packages/shell/tool-bash/README.i18n.yaml rename packages/{bash => shell}/tool-bash/README.md (60%) rename packages/{bash => shell}/tool-bash/README.zh.md (61%) rename packages/{bash => shell}/tool-bash/package.json (81%) rename packages/{bash => shell}/tool-bash/src/background.ts (75%) rename packages/{bash => shell}/tool-bash/src/index.ts (91%) rename packages/{bash => shell}/tool-bash/src/invariant.ts (100%) rename packages/{bash => shell}/tool-bash/src/render.ts (90%) rename packages/{bash => shell}/tool-bash/tests/integration.spec.ts (91%) rename packages/{bash => shell}/tool-bash/tests/tools.spec.ts (90%) rename packages/{bash => shell}/tool-bash/tsconfig.json (82%) create mode 100644 packages/shell/tool-pwsh/README.i18n.yaml rename packages/{bash => shell}/tool-pwsh/README.md (66%) rename packages/{bash => shell}/tool-pwsh/README.zh.md (69%) rename packages/{bash => shell}/tool-pwsh/package.json (82%) rename packages/{bash => shell}/tool-pwsh/src/background.ts (79%) rename packages/{bash => shell}/tool-pwsh/src/index.ts (92%) rename packages/{bash => shell}/tool-pwsh/src/invariant.ts (100%) rename packages/{bash => shell}/tool-pwsh/src/render.ts (94%) rename packages/{bash => shell}/tool-pwsh/tests/integration.spec.ts (88%) rename packages/{bash => shell}/tool-pwsh/tests/loader.spec.ts (94%) rename packages/{bash => shell}/tool-pwsh/tests/tools.spec.ts (90%) rename packages/{bash => shell}/tool-pwsh/tsconfig.json (79%) rename packages/skill/{skill-local => skill-filesystem}/README.i18n.yaml (69%) rename packages/skill/{skill-local => skill-filesystem}/README.md (97%) rename packages/skill/{skill-local => skill-filesystem}/README.zh.md (96%) rename packages/skill/{skill-local => skill-filesystem}/package.json (86%) rename packages/skill/{skill-local => skill-filesystem}/src/index.ts (97%) rename packages/skill/{skill-local => skill-filesystem}/src/invariant.ts (83%) rename packages/skill/{skill-local/tests/skill-local-watcher.spec.ts => skill-filesystem/tests/skill-filesystem-watcher.spec.ts} (90%) rename packages/skill/{skill-local/tests/skill-local.spec.ts => skill-filesystem/tests/skill-filesystem.spec.ts} (93%) rename packages/skill/{skill-local => skill-filesystem}/tsconfig.json (78%) rename packages/subagent/{subagent-fork => subagent-fork-in-process}/README.i18n.yaml (68%) rename packages/subagent/{subagent-fork => subagent-fork-in-process}/README.md (91%) rename packages/subagent/{subagent-fork => subagent-fork-in-process}/README.zh.md (92%) rename packages/subagent/{subagent-fork => subagent-fork-in-process}/package.json (83%) rename packages/subagent/{subagent-fork => subagent-fork-in-process}/src/index.ts (92%) rename packages/subagent/{subagent-inprocess => subagent-fork-in-process}/src/invariant.ts (81%) rename packages/subagent/{subagent-fork => subagent-fork-in-process}/tests/multi-subagent.spec.ts (95%) rename packages/subagent/{subagent-fork/tests/subagent-fork.spec.ts => subagent-fork-in-process/tests/subagent-fork-in-process.spec.ts} (96%) rename packages/subagent/{subagent-fork => subagent-fork-in-process}/tsconfig.json (83%) rename packages/subagent/{subagent-spawn => subagent-in-process-driver}/README.i18n.yaml (67%) rename packages/subagent/{subagent-inprocess => subagent-in-process-driver}/README.md (99%) rename packages/subagent/{subagent-inprocess => subagent-in-process-driver}/README.zh.md (99%) rename packages/subagent/{subagent-inprocess => subagent-in-process-driver}/package.json (94%) rename packages/subagent/{subagent-inprocess => subagent-in-process-driver}/src/index.ts (99%) rename packages/subagent/{subagent-fork => subagent-in-process-driver}/src/invariant.ts (80%) rename packages/subagent/{subagent-inprocess => subagent-in-process-driver}/src/structured.ts (98%) rename packages/subagent/{subagent-inprocess => subagent-in-process-driver}/tests/fixtures/plugins/preset-tool.js (100%) rename packages/subagent/{subagent-inprocess => subagent-in-process-driver}/tests/fixtures/presets/coding/agent.cordis.yml (100%) rename packages/subagent/{subagent-inprocess => subagent-in-process-driver}/tests/fixtures/presets/reviewing/agent.cordis.yml (100%) rename packages/subagent/{subagent-inprocess => subagent-in-process-driver}/tests/inheritance.spec.ts (100%) rename packages/subagent/{subagent-inprocess => subagent-in-process-driver}/tests/preset-inheritance.spec.ts (100%) rename packages/subagent/{subagent-inprocess => subagent-in-process-driver}/tests/structured.spec.ts (99%) rename packages/subagent/{subagent-inprocess/tests/subagent-inprocess.spec.ts => subagent-in-process-driver/tests/subagent-in-process-driver.spec.ts} (98%) rename packages/subagent/{subagent-inprocess => subagent-in-process-driver}/tsconfig.json (91%) rename packages/subagent/{subagent-inprocess => subagent-spawn-in-process}/README.i18n.yaml (67%) rename packages/subagent/{subagent-spawn => subagent-spawn-in-process}/README.md (90%) rename packages/subagent/{subagent-spawn => subagent-spawn-in-process}/README.zh.md (90%) rename packages/subagent/{subagent-spawn => subagent-spawn-in-process}/package.json (87%) rename packages/subagent/{subagent-spawn => subagent-spawn-in-process}/src/index.ts (87%) rename packages/subagent/{subagent-spawn => subagent-spawn-in-process}/src/invariant.ts (81%) rename packages/subagent/{subagent-spawn => subagent-spawn-in-process}/tests/harness.ts (88%) rename packages/subagent/{subagent-spawn/tests/spawn.e2e.ts => subagent-spawn-in-process/tests/spawn-in-process.e2e.ts} (100%) rename packages/subagent/{subagent-spawn/tests/subagent-spawn.spec.ts => subagent-spawn-in-process/tests/subagent-spawn-in-process.spec.ts} (97%) rename packages/subagent/{subagent-spawn => subagent-spawn-in-process}/tsconfig.json (80%) delete mode 100644 packages/support/README.i18n.yaml delete mode 100644 packages/support/invariants/README.i18n.yaml delete mode 100644 packages/support/llm-replay/README.i18n.yaml delete mode 100644 packages/tasks/README.md delete mode 100644 packages/tasks/README.zh.md delete mode 100644 packages/tasks/tasks-local/README.i18n.yaml delete mode 100644 packages/tasks/tasks-local/README.md delete mode 100644 packages/tasks/tasks/README.i18n.yaml delete mode 100644 packages/tasks/tasks/README.md delete mode 100644 packages/tasks/tasks/tests/service.spec.ts delete mode 100644 packages/tasks/tool-tasks/README.i18n.yaml delete mode 100644 packages/tasks/tool-tasks/README.md rename packages/{pty/pty => terminal}/README.i18n.yaml (57%) create mode 100644 packages/terminal/README.md create mode 100644 packages/terminal/README.zh.md create mode 100644 packages/terminal/terminal-bash/README.i18n.yaml rename packages/{pty/pty-local => terminal/terminal-bash}/README.md (78%) rename packages/{pty/pty-local => terminal/terminal-bash}/README.zh.md (79%) rename packages/{pty/pty-local => terminal/terminal-bash}/package.json (89%) rename packages/{pty/pty-local => terminal/terminal-bash}/src/config.ts (84%) rename packages/{pty/pty-local => terminal/terminal-bash}/src/index.ts (82%) rename packages/{pty/pty-local => terminal/terminal-bash}/src/invariant.ts (79%) rename packages/{pty/pty-local => terminal/terminal-bash}/src/sanitize.ts (100%) rename packages/{pty/pty-local => terminal/terminal-bash}/src/session.ts (93%) rename packages/{pty/pty-local => terminal/terminal-bash}/tests/config.spec.ts (87%) rename packages/{pty/pty-local => terminal/terminal-bash}/tests/index.spec.ts (86%) rename packages/{pty/pty-local => terminal/terminal-bash}/tests/local.spec.ts (75%) rename packages/{pty/pty-local => terminal/terminal-bash}/tests/sanitize.spec.ts (98%) rename packages/{pty/pty-local => terminal/terminal-bash}/tests/session.spec.ts (96%) rename packages/{pty/pty-local => terminal/terminal-bash}/tsconfig.json (88%) rename packages/{bash/bash-sandbox => terminal/terminal}/README.i18n.yaml (55%) rename packages/{pty/pty => terminal/terminal}/README.md (71%) rename packages/{pty/pty => terminal/terminal}/README.zh.md (70%) rename packages/{pty/pty => terminal/terminal}/package.json (93%) rename packages/{pty/pty => terminal/terminal}/src/index.ts (75%) rename packages/{pty/pty => terminal/terminal}/src/invariant.ts (81%) rename packages/{pty/pty => terminal/terminal}/src/types.ts (71%) rename packages/{pty/pty => terminal/terminal}/tests/service.spec.ts (67%) rename packages/{pty/pty => terminal/terminal}/tsconfig.json (87%) create mode 100644 packages/terminal/tool-terminal/README.i18n.yaml create mode 100644 packages/terminal/tool-terminal/README.md create mode 100644 packages/terminal/tool-terminal/README.zh.md rename packages/{pty/tool-pty => terminal/tool-terminal}/package.json (74%) rename packages/{pty/tool-pty => terminal/tool-terminal}/src/index.ts (85%) rename packages/{pty/tool-pty => terminal/tool-terminal}/src/invariant.ts (77%) rename packages/{pty/tool-pty => terminal/tool-terminal}/src/render.ts (97%) rename packages/{pty/tool-pty => terminal/tool-terminal}/tests/loader-composition.spec.ts (84%) rename packages/{pty/tool-pty => terminal/tool-terminal}/tests/render.spec.ts (74%) rename packages/{pty/tool-pty => terminal/tool-terminal}/tests/tools.spec.ts (89%) rename packages/{pty/tool-pty => terminal/tool-terminal}/tsconfig.json (76%) rename packages/{fs/fs-policy => test-support}/README.i18n.yaml (56%) rename packages/{support => test-support}/README.md (75%) rename packages/{support => test-support}/README.zh.md (83%) rename packages/{support => test-support}/acp-snapshot/README.i18n.yaml (79%) rename packages/{support => test-support}/acp-snapshot/README.md (100%) rename packages/{support => test-support}/acp-snapshot/README.zh.md (100%) rename packages/{support => test-support}/acp-snapshot/package.json (96%) rename packages/{support => test-support}/acp-snapshot/src/harness.ts (100%) rename packages/{support => test-support}/acp-snapshot/src/index.ts (100%) rename packages/{support => test-support}/acp-snapshot/src/invariant.ts (100%) rename packages/{support => test-support}/acp-snapshot/src/launcher.ts (100%) rename packages/{support => test-support}/acp-snapshot/src/normalize.ts (100%) rename packages/{support => test-support}/acp-snapshot/src/suite.ts (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/fake-acp-agent.ts (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-child/behavior.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-child/input.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-child/session.1.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-child/session.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-child/stdout.expected.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-pin/behavior.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-pin/input.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-pin/session.1.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-pin/session.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-pin/stdout.expected.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-pin/system-prompt.expected.md (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-pin/tool-schemas.expected.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-skip/behavior.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-skip/input.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-skip/replay.override.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-skip/session.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/record-suite/rec-skip/stdout.expected.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/authored-error/behavior.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/authored-error/input.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/authored-error/replay.override.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/authored-error/session.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/authored-error/stdout.expected.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/blocked-log/behavior.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/blocked-log/input.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/blocked-log/session.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/blocked-log/stdout.expected.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/no-model/behavior.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/no-model/input.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/no-model/session.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/no-model/stdout.expected.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/pin-turn/behavior.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/pin-turn/input.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/pin-turn/session.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/pin-turn/stdout.expected.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/pin-turn/system-prompt.expected.md (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/pin-turn/tool-schemas.expected.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/plain-turn/behavior.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/plain-turn/input.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/plain-turn/session.1.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/plain-turn/session.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/plain-turn/stdout.expected.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/plain-turn/system-prompt.1.expected.md (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/plain-turn/tool-schemas.1.expected.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/plain-turn/workspace/seed.txt (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/shared-pin/behavior.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/shared-pin/input.json (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/shared-pin/session.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/fixtures/suite/shared-pin/stdout.expected.jsonl (100%) rename packages/{support => test-support}/acp-snapshot/tests/harness.spec.ts (100%) rename packages/{support => test-support}/acp-snapshot/tests/normalize.spec.ts (100%) rename packages/{support => test-support}/acp-snapshot/tests/suite.spec.ts (100%) rename packages/{support => test-support}/acp-snapshot/tsconfig.json (84%) rename packages/{support => test-support}/agent-loop-testkit/README.i18n.yaml (78%) rename packages/{support => test-support}/agent-loop-testkit/README.md (100%) rename packages/{support => test-support}/agent-loop-testkit/README.zh.md (100%) rename packages/{support => test-support}/agent-loop-testkit/package.json (96%) rename packages/{support => test-support}/agent-loop-testkit/src/index.ts (85%) rename packages/{support => test-support}/agent-loop-testkit/src/invariant.ts (100%) rename packages/{support => test-support}/agent-loop-testkit/tests/agent-loop-testkit.spec.ts (100%) rename packages/{support => test-support}/agent-loop-testkit/tsconfig.json (90%) create mode 100644 packages/test-support/client-runtime/README.i18n.yaml rename packages/{client/test-runtime => test-support/client-runtime}/README.md (87%) rename packages/{client/test-runtime => test-support/client-runtime}/README.zh.md (84%) rename packages/{client/test-runtime => test-support/client-runtime}/package.json (93%) rename packages/{client/test-runtime => test-support/client-runtime}/src/fixtures.ts (99%) rename packages/{client/test-runtime => test-support/client-runtime}/src/index.ts (96%) rename packages/{client/test-runtime => test-support/client-runtime}/src/invariant.ts (93%) rename packages/{client/test-runtime => test-support/client-runtime}/src/locale-env.ts (95%) rename packages/{client/test-runtime => test-support/client-runtime}/src/remote.ts (100%) rename packages/{client/test-runtime => test-support/client-runtime}/src/sessions.ts (98%) rename packages/{client/test-runtime => test-support/client-runtime}/src/settings-scope.ts (100%) rename packages/{client/test-runtime => test-support/client-runtime}/src/snapshot.ts (100%) rename packages/{client/test-runtime => test-support/client-runtime}/src/translate.ts (94%) rename packages/{client/test-runtime => test-support/client-runtime}/src/workspaces.ts (100%) rename packages/{client/test-runtime => test-support/client-runtime}/tests/__snapshots__/runtime.client.spec.tsx.snap (100%) rename packages/{client/test-runtime => test-support/client-runtime}/tests/invariant.client.spec.ts (77%) rename packages/{client/test-runtime => test-support/client-runtime}/tests/remote.client.spec.ts (100%) rename packages/{client/test-runtime => test-support/client-runtime}/tests/runtime.client.spec.tsx (99%) rename packages/{client/test-runtime => test-support/client-runtime}/tsconfig.json (66%) rename packages/{client/test-runtime => test-support/client-runtime}/tsdown.config.ts (67%) rename packages/{support => test-support}/llm-mock-server/README.i18n.yaml (78%) rename packages/{support => test-support}/llm-mock-server/README.md (100%) rename packages/{support => test-support}/llm-mock-server/README.zh.md (100%) rename packages/{support => test-support}/llm-mock-server/package.json (94%) rename packages/{support => test-support}/llm-mock-server/src/bin.ts (100%) rename packages/{support => test-support}/llm-mock-server/src/cli.ts (100%) rename packages/{support => test-support}/llm-mock-server/src/index.ts (100%) rename packages/{support => test-support}/llm-mock-server/src/invariant.ts (100%) rename packages/{support => test-support}/llm-mock-server/tests/cli.spec.ts (100%) rename packages/{support => test-support}/llm-mock-server/tests/invariant.spec.ts (84%) rename packages/{support => test-support}/llm-mock-server/tests/server.spec.ts (100%) rename packages/{util/paths => test-support/llm-mock-server}/tsconfig.json (78%) rename packages/{support => test-support}/llm-mock-server/tsdown.config.ts (100%) create mode 100644 packages/test-support/llm-replay/README.i18n.yaml rename packages/{support => test-support}/llm-replay/README.md (93%) rename packages/{support => test-support}/llm-replay/README.zh.md (93%) rename packages/{support => test-support}/llm-replay/package.json (89%) rename packages/{support => test-support}/llm-replay/src/index.ts (98%) rename packages/{support => test-support}/llm-replay/src/invariant.ts (100%) rename packages/{support => test-support}/llm-replay/tests/llm-replay.spec.ts (96%) rename packages/{support => test-support}/llm-replay/tsconfig.json (80%) rename packages/{support => test-support}/loader-smoke/README.i18n.yaml (79%) rename packages/{support => test-support}/loader-smoke/README.md (100%) rename packages/{support => test-support}/loader-smoke/README.zh.md (100%) rename packages/{support => test-support}/loader-smoke/package.json (96%) rename packages/{support => test-support}/loader-smoke/src/agent-turn.ts (100%) rename packages/{support => test-support}/loader-smoke/src/index.ts (100%) rename packages/{support => test-support}/loader-smoke/src/invariant.ts (100%) rename packages/{support => test-support}/loader-smoke/tests/agent-turn.spec.ts (100%) rename packages/{support => test-support}/loader-smoke/tests/example-launch.spec.ts (100%) rename packages/{support => test-support}/loader-smoke/tests/fixtures/fail.ts (100%) rename packages/{support => test-support}/loader-smoke/tests/fixtures/hang.ts (100%) rename packages/{support => test-support}/loader-smoke/tests/fixtures/success.ts (100%) rename packages/{support => test-support}/loader-smoke/tests/loader-smoke.spec.ts (100%) rename packages/{support => test-support}/loader-smoke/tsconfig.json (85%) rename packages/typert/generator/tests/fixtures/remote-model/{type-meta.d.ts => typert-protocol.d.ts} (66%) rename packages/typert/{type-meta => protocol}/README.i18n.yaml (70%) rename packages/typert/{type-meta => protocol}/README.md (72%) rename packages/typert/{type-meta => protocol}/README.zh.md (71%) rename packages/typert/{type-meta => protocol}/package.json (85%) rename packages/typert/{type-meta => protocol}/src/index.ts (76%) rename packages/typert/{type-meta => protocol}/src/invariant.ts (78%) rename packages/typert/{type-meta => protocol}/src/types.ts (76%) rename packages/typert/{type-meta => protocol}/tests/fixtures/source-launch.ts (86%) rename packages/typert/{type-meta/tests/type-meta.spec.ts => protocol/tests/protocol.spec.ts} (83%) rename packages/typert/{type-meta => protocol}/tsconfig.json (84%) delete mode 100644 packages/util/environment/README.i18n.yaml create mode 100644 packages/util/home-paths/README.i18n.yaml rename packages/util/{paths => home-paths}/README.md (99%) rename packages/util/{paths => home-paths}/README.zh.md (99%) rename packages/util/{paths => home-paths}/package.json (92%) rename packages/util/{paths => home-paths}/src/index.ts (99%) rename packages/util/{retention => home-paths}/src/invariant.ts (79%) rename packages/util/{paths/tests/paths.spec.ts => home-paths/tests/home-paths.spec.ts} (98%) rename packages/util/{retention => home-paths}/tsconfig.json (78%) create mode 100644 packages/util/launch-environment/README.i18n.yaml rename packages/util/{environment => launch-environment}/README.md (83%) rename packages/util/{environment => launch-environment}/README.zh.md (82%) rename packages/util/{environment => launch-environment}/package.json (91%) rename packages/util/{environment => launch-environment}/src/index.ts (71%) rename packages/util/{environment => launch-environment}/src/invariant.ts (78%) rename packages/util/{environment/tests/environment.spec.ts => launch-environment/tests/launch-environment.spec.ts} (80%) rename packages/util/{environment => launch-environment}/tsconfig.json (78%) create mode 100644 packages/util/output-retention/README.i18n.yaml rename packages/util/{retention => output-retention}/README.md (98%) rename packages/util/{retention => output-retention}/README.zh.md (98%) rename packages/util/{retention => output-retention}/package.json (91%) rename packages/util/{retention => output-retention}/src/index.ts (99%) rename packages/util/{paths => output-retention}/src/invariant.ts (77%) rename packages/util/{retention/tests/retention.spec.ts => output-retention/tests/output-retention.spec.ts} (99%) rename packages/{support/llm-mock-server => util/output-retention}/tsconfig.json (78%) delete mode 100644 packages/util/paths/README.i18n.yaml delete mode 100644 packages/util/retention/README.i18n.yaml rename packages/web/{web-fetch-local => web-fetch-http}/README.i18n.yaml (70%) rename packages/web/{web-fetch-local => web-fetch-http}/README.md (93%) rename packages/web/{web-fetch-local => web-fetch-http}/README.zh.md (92%) rename packages/web/{web-fetch-local => web-fetch-http}/package.json (93%) rename packages/web/{web-fetch-local => web-fetch-http}/src/index.ts (82%) rename packages/web/{web-fetch-local => web-fetch-http}/src/invariant.ts (84%) rename packages/web/{web-fetch-local => web-fetch-http}/src/policy.ts (98%) rename packages/web/{web-fetch-local => web-fetch-http}/src/provider.ts (97%) rename packages/web/{web-fetch-local/tests/fetch-local.spec.ts => web-fetch-http/tests/fetch-http.spec.ts} (94%) rename packages/web/{web-fetch-local => web-fetch-http}/tsconfig.json (89%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/README.i18n.yaml (68%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/README.md (96%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/README.zh.md (96%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/package.json (92%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/src/host.ts (98%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/src/index.ts (96%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/src/invariant.ts (82%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/src/meta.ts (98%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/src/protocol.ts (98%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/src/realm.ts (99%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/src/runtime.ts (99%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/src/session.ts (99%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/src/types.ts (98%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/src/worker.ts (91%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/tests/built-worker.e2e.ts (89%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/tests/integration.spec.ts (88%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/tests/meta.spec.ts (100%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/tests/realm.spec.ts (100%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/tests/session.spec.ts (100%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/tests/source-worker.compat.spec.ts (85%) rename packages/workflow/{workflow-workerthread/tests/workflow-workerthread.e2e.ts => workflow-worker-thread/tests/workflow-worker-thread.e2e.ts} (87%) rename packages/workflow/{workflow-workerthread/tests/workflow-workerthread.spec.ts => workflow-worker-thread/tests/workflow-worker-thread.spec.ts} (92%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/tsconfig.json (92%) rename packages/workflow/{workflow-workerthread => workflow-worker-thread}/tsdown.config.ts (100%) diff --git a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml index 3d34ff3934..760c3fd099 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md -2026-06-11-event-sourced-sessions.md: 1303ee843cd5f9e02eae7a1f9bc51594be4bb407 -2026-06-11-event-sourced-sessions.zh.md: 4308251ddc4564fc0547230d10671b6595389a1a +2026-06-11-event-sourced-sessions.md: b6d17d2db1d9b489f3d4224683b61e62be485014 +2026-06-11-event-sourced-sessions.zh.md: a78349ea2385b3c9d32870757bb607f15996196d diff --git a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md index 1303ee843c..b6d17d2db1 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md +++ b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md @@ -25,4 +25,4 @@ Ordering contract: the loop claims inbox messages before `agent/pre-step`, opens - Replay, trace, and telemetry are structurally guaranteed, not bolted on. - Persistence stays a plugin concern; the in-memory store ships in dsh-session. - The event vocabulary is merge-extensible (plugins add e.g. compaction events); [session persistence](2026-06-14-session-persistence.md) froze its shape once the log became durable. -- Derivation cost grows with log length — compaction (dsh-compact) is the intended mitigation, not log mutation. +- Derivation cost grows with log length — compaction (dsh-compaction) is the intended mitigation, not log mutation. diff --git a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md index 4308251ddc..a78349ea23 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md @@ -25,4 +25,4 @@ MVP 要求严格的基于事件的追踪,以及完全可回放的会话(严 - 回放、追踪与遥测在结构上得到保证,而非事后附加。 - 持久化仍是插件关注点;内存存储随 dsh-session 一起提供。 - 事件词汇可通过合并扩展(插件可添加如压缩(compaction)事件);[会话持久化](2026-06-14-session-persistence.md)在日志具备持久性后固定了其结构。 -- 派生成本随日志长度增长,压缩(dsh-compact)是预期的缓解手段,而不是改写日志。 +- 派生成本随日志长度增长,压缩(dsh-compaction)是预期的缓解手段,而不是改写日志。 diff --git a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml index cc21a54254..f07afccac7 100644 --- a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-13-capability-seams.md -2026-06-13-capability-seams.md: b2dc124f7bf0b56598466e6521e0764af06075ab -2026-06-13-capability-seams.zh.md: 79befde57392888dd6a07236379b0155b68d2f01 +2026-06-13-capability-seams.md: efb000631c6dfe91ab648b102a14a27c30d790b4 +2026-06-13-capability-seams.zh.md: bf6571dfa5c3a90f856480bc22c6560ed65b3e77 diff --git a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.md b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.md index b2dc124f7b..efb000631c 100644 --- a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.md +++ b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.md @@ -8,15 +8,15 @@ English | [中文](2026-06-13-capability-seams.zh.md) The harness has swappable capabilities — bash execution today, sandboxed/remote executors and alternative model providers tomorrow. A capability has three concerns that change at different rates and for different reasons: the *contract* (what the capability is), the *implementation* (how it runs), and the *consumer API* (what the model and other plugins program against). Bundling them in one package couples those rates of change — swapping a local executor for a sandboxed one would churn the tool schemas the model sees, even though the model-facing contract never changed. -This is distinct from "who provides vs. needs a capability at runtime", which Cordis already answers with services + `inject` (a provider registers `ctx.bash`; a consumer declares `inject: ['bash']` and its fiber pends until the service exists). That mechanism is necessary but doesn't dictate package boundaries; this Agent Note does. +This is distinct from "who provides vs. needs a capability at runtime", which Cordis already answers with services + `inject` (a provider registers `ctx.shell`; a consumer declares `inject: ['bash']` and its fiber pends until the service exists). That mechanism is necessary but doesn't dictate package boundaries; this Agent Note does. ## Decision A swappable capability has **three roles**: -1. **Service Definition** — the Cordis `Service` and vocabulary types owning `ctx.` and depending only on the vocabulary the contract needs (e.g. `dsh-bash`: `BashExecutor`, `BashRunResult`, `BashProcess`). A definition may be an abstract class or a concrete registry service; it is never a TypeScript `interface`. +1. **Service Definition** — the Cordis `Service` and vocabulary types owning `ctx.` and depending only on the vocabulary the contract needs (e.g. `dsh-shell`: `ShellExecutor`, `ShellRunResult`, `ShellProcess`). A definition may be an abstract class or a concrete registry service; it is never a TypeScript `interface`. 2. **Service provider** — a plugin that supplies or registers an implementation (e.g. `dsh-bash-local`: subprocesses, process-group kills, spill-file truncation). Sandboxed and remote providers are sibling packages implementing or registering against the same Service Definition. -3. **Consumer** — what the model and plugins program against (e.g. `dsh-tool-bash`: the `bash` schema, with background handles registered into the generic task runtime). Consumers inject the service key and never import provider-specific types. +3. **Consumer** — what the model and plugins program against (e.g. `dsh-tool-bash`: the `bash` schema, with background handles registered into the generic job runtime). Consumers inject the service key and never import provider-specific types. Service providers and Consumers then evolve independently: a sandboxed executor replaces `dsh-bash-local` without touching a tool schema. @@ -24,7 +24,7 @@ Roles normally use separate packages when they evolve independently, but the spl ## Terminology: "seam" names the trio, not the interface -A **seam** is the whole capability — the three roles together: a **Service Definition** (the Cordis `Service` that owns `ctx.` and the vocabulary), one or more **Service providers**, and one or more **Consumers**. `packages/bash` is the canonical example — `dsh-bash` / `dsh-bash-local`+`dsh-bash-sandbox` / `dsh-tool-bash`. A package may own multiple roles, but one role alone is not the seam. The term "seam" is reserved for this complete capability; name a constituent by its role, class, service, contract, or extension point. The [glossary](../../../../docs/glossary.md#capability-seam) is the canonical entry. +A **seam** is the whole capability — the three roles together: a **Service Definition** (the Cordis `Service` that owns `ctx.` and the vocabulary), one or more **Service providers**, and one or more **Consumers**. `packages/shell` is the canonical example — `dsh-shell` / `dsh-bash-local`+`dsh-bash-sandbox` / `dsh-tool-bash`. A package may own multiple roles, but one role alone is not the seam. The term "seam" is reserved for this complete capability; name a constituent by its role, class, service, contract, or extension point. The [glossary](../../../../docs/glossary.md#capability-seam) is the canonical entry. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md index 79befde573..bf6571dfa5 100644 --- a/.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-13-capability-seams.zh.md @@ -8,13 +8,13 @@ Status: implemented harness 具有可替换的能力:当前是 bash 执行,未来会有沙箱化/远程执行器和替代模型提供方。一项能力涉及三个关注点,它们以不同速率、因不同原因变化:*约定*(这项能力是什么)、*实现*(它如何运行)、*消费方 API*(模型和其他插件面向什么编程)。将三者捆绑在一个包中会耦合这些变化速率——把本地执行器换成沙箱化执行器时,模型看到的工具 schema 也会被搅动,尽管面向模型的约定从未改变。 -这与「谁在运行时提供、谁需要一项能力」是不同的问题,后者 Cordis 已通过服务 + `inject` 解决(提供方注册 `ctx.bash`;消费方声明 `inject: ['bash']`,其 fiber 挂起直到服务存在)。该机制是必要的,但不决定包的边界;本 Agent Note 决定的是包的边界。 +这与「谁在运行时提供、谁需要一项能力」是不同的问题,后者 Cordis 已通过服务 + `inject` 解决(提供方注册 `ctx.shell`;消费方声明 `inject: ['bash']`,其 fiber 挂起直到服务存在)。该机制是必要的,但不决定包的边界;本 Agent Note 决定的是包的边界。 ## 决策 一项可替换的能力包含**三个角色**: -1. **Service Definition**——拥有 `ctx.` 的 Cordis `Service` 和词汇类型,仅依赖约定所需的词汇(例如 `dsh-bash`:`BashExecutor`、`BashRunResult`、`BashProcess`)。Service Definition 可以是抽象类,也可以是具体的注册表服务;绝不是 TypeScript `interface`。 +1. **Service Definition**——拥有 `ctx.` 的 Cordis `Service` 和词汇类型,仅依赖约定所需的词汇(例如 `dsh-shell`:`ShellExecutor`、`ShellRunResult`、`ShellProcess`)。Service Definition 可以是抽象类,也可以是具体的注册表服务;绝不是 TypeScript `interface`。 2. **Service provider**——提供或注册实现的插件(例如 `dsh-bash-local`:子进程、进程组 kill、spill 文件截断)。沙箱化和远程 Service provider 是依据同一 Service Definition 实现或注册的兄弟包。 3. **Consumer**——模型和插件编程所面向的内容(例如 `dsh-tool-bash`:`bash` schema,后台句柄注册到通用任务运行时)。Consumer 注入服务键,从不导入 Service provider 特有的类型。 @@ -24,7 +24,7 @@ Service provider 与 Consumer 由此独立演进:沙箱化执行器替换 `dsh ## 术语:seam 指三者组合,而非接口 -一个 **seam** 是完整的能力——三个角色合在一起:**Service Definition**(拥有 `ctx.` 和词汇的 Cordis `Service`)、一个或多个 **Service provider**,以及一个或多个 **Consumer**。`packages/bash` 是规范范例——`dsh-bash` / `dsh-bash-local`+`dsh-bash-sandbox` / `dsh-tool-bash`。一个包可以承担多个角色,但单个角色本身不是 seam。「seam」一词严格保留给这种完整能力;命名其中一个组成部分时,应使用其角色、类、服务、约定或扩展点。[术语表](../../../../docs/glossary.md#capability-seam)是规范条目。 +一个 **seam** 是完整的能力——三个角色合在一起:**Service Definition**(拥有 `ctx.` 和词汇的 Cordis `Service`)、一个或多个 **Service provider**,以及一个或多个 **Consumer**。`packages/shell` 是规范范例——`dsh-shell` / `dsh-bash-local`+`dsh-bash-sandbox` / `dsh-tool-bash`。一个包可以承担多个角色,但单个角色本身不是 seam。「seam」一词严格保留给这种完整能力;命名其中一个组成部分时,应使用其角色、类、服务、约定或扩展点。[术语表](../../../../docs/glossary.md#capability-seam)是规范条目。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml index 880cda00e7..61f9754431 100644 --- a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-14-session-persistence.md -2026-06-14-session-persistence.md: 6961b8f3f4274587c2920fe2b2602aff5efe4afc -2026-06-14-session-persistence.zh.md: a99b236167d8f3229de083c917684d517b50ab9c +2026-06-14-session-persistence.md: 62228bd2f5b25b13880a563818d08f3a2d52d956 +2026-06-14-session-persistence.zh.md: b10ceebd95d4d05ae3f7ea620183ad8fffd074ee diff --git a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md index 6961b8f3f4..62228bd2f5 100644 --- a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md +++ b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.md @@ -12,7 +12,7 @@ The [event-sourced model](2026-06-11-event-sourced-sessions.md) makes the append ## Decision -Persistence is a **capability seam** with an abstract Service Definition ([capability seams](2026-06-13-capability-seams.md), the `dsh-bash` template), not loop or core logic: +Persistence is a **capability seam** with an abstract Service Definition ([capability seams](2026-06-13-capability-seams.md), the `dsh-shell` template), not loop or core logic: 1. **Interface** (`dsh-session-persistence`, `ctx.sessionPersistence`) — an abstract `SessionPersistence` service: `locate`/`create`/`append`/`prepare`/`load`/`inspect`/`readFrom`/`list`/`listSnapshots`. Its persisted unit IS the existing `SessionEvent` (`{ type, seq, time, data }`), reused verbatim — no conversion type. 2. **Implementation** (`dsh-session-persistence-jsonl`) — an append-only logical JSONL log per session: a `SessionHeader` line followed by storage records that losslessly represent the contiguous `SessionEvent` stream. Eligible `assistant/chunk` delta runs use packed rows by default; [checksummed Zstandard frames](2026-07-19-zstandard-jsonl-session-logs.md) are the default physical encoding, with raw lines configurable. diff --git a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md index a99b236167..b10ceebd95 100644 --- a/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -持久化是一个具有抽象 Service Definition 的**能力 seam**([能力 seam](2026-06-13-capability-seams.md),`dsh-bash` 模板),而非循环或核心逻辑: +持久化是一个具有抽象 Service Definition 的**能力 seam**([能力 seam](2026-06-13-capability-seams.md),`dsh-shell` 模板),而非循环或核心逻辑: 1. **接口**(`dsh-session-persistence`,`ctx.sessionPersistence`):一个抽象的 `SessionPersistence` 服务,提供 `locate`/`create`/`append`/`prepare`/`load`/`inspect`/`readFrom`/`list`/`listSnapshots`。其持久化单元就是现有的 `SessionEvent`(`{ type, seq, time, data }`),原样复用,无转换类型。 2. **实现**(`dsh-session-persistence-jsonl`):每个会话一个仅追加的逻辑 JSONL 日志:先是一行 `SessionHeader`,随后是无损表示连续 `SessionEvent` 流的存储记录。符合条件的 `assistant/chunk` 增量连续段默认使用打包行;[带校验和的 Zstandard 帧](2026-07-19-zstandard-jsonl-session-logs.md)是默认物理编码,也可通过配置使用原始行。 diff --git a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml index 3ae4c57d3d..fc070d7c3a 100644 --- a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md -2026-06-17-filesystem-capability-seam.md: 63628d592c17f000ae49f9558597bf8059f04942 -2026-06-17-filesystem-capability-seam.zh.md: d986869bf10f58c8e01873b1d77105accb7bea76 +2026-06-17-filesystem-capability-seam.md: 953ef0eaafe8f654bf55c3e5c4208560243c3d0b +2026-06-17-filesystem-capability-seam.zh.md: 1073226594acbe8c1c596bb4b2dcc959b20c0769 diff --git a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md index 63628d592c..953ef0eaaf 100644 --- a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md +++ b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md @@ -6,7 +6,7 @@ English | [中文](2026-06-17-filesystem-capability-seam.zh.md) ## Problem -The harness has a concrete `bash` capability seam (`dsh-bash` / `dsh-bash-local` / `dsh-tool-bash`), but filesystem operations were about to land as model-facing tools without an equivalent seam. If `read`, `write`, and `edit` directly used `node:fs`, the model-facing tool package would own filesystem execution policy, local path resolution, atomic write behavior, text decoding, symlink behavior, and edit semantics all at once. +The harness has a concrete `bash` capability seam (`dsh-shell` / `dsh-bash-local` / `dsh-tool-bash`), but filesystem operations were about to land as model-facing tools without an equivalent seam. If `read`, `write`, and `edit` directly used `node:fs`, the model-facing tool package would own filesystem execution policy, local path resolution, atomic write behavior, text decoding, symlink behavior, and edit semantics all at once. That couples three concerns that change independently: @@ -28,7 +28,7 @@ Filesystem access is a first-class capability seam following [the capability-sea The Consumer package depends only on the Service Definition package, never on `dsh-fs-local`. A deployment that wants a different backend loads a different provider for `ctx.fs` without changing the tool schemas or model-facing prompt guidance. -The read-before-write/edit and observed-state policy is a fourth package, `@deepseek-ai/dsh-fs-policy` (`packages/fs/fs-policy`), contributed through the `fs/*` event gate rather than living on `ctx.fs`; a deployment loading `dsh-tool-fs` also loads `dsh-fs-policy` to get read-before-write/edit. This decision established the three-package boundary; the split of policy off the provider base class is decided by [the split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md), and its realization as an event-gate plugin (not a method service) by [the event-gate Agent Note](2026-06-26-file-context-as-event-gate.md). +The read-before-write/edit and observed-state policy is a fourth package, `@deepseek-ai/dsh-fs-observation-policy` (`packages/fs/fs-observation-policy`), contributed through the `fs/*` event gate rather than living on `ctx.fs`; a deployment loading `dsh-tool-fs` also loads `dsh-fs-observation-policy` to get read-before-write/edit. This decision established the three-package boundary; the split of policy off the provider base class is decided by [the split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md), and its realization as an event-gate plugin (not a method service) by [the event-gate Agent Note](2026-06-26-file-context-as-event-gate.md). The first backend is deliberately local-only: `dsh-fs-local` implements `ctx.fs` against the host filesystem. Future sibling backends can provide sandboxed, remote, virtual, or project-scoped filesystems behind the same interface. @@ -36,7 +36,7 @@ The first consumer is deliberately text-file-only: `dsh-tool-fs` exposes model-f Filesystem permissions and sandboxing are not implied by this split. The local backend resolves relative paths from its configured base directory, but containment policy is a separate decision: either a stricter `ctx.fs` implementation enforces it, or a permission/sandbox plugin wraps `tools/execute` and vetoes calls before they reach the consumer. -Read-before-write/edit and observed state belong to `dsh-fs-policy`, not `ctx.fs`. Through the `fs/*` event gate, the policy records versions per opaque actor and supplies optional mutation expectations; the provider enforces freshness atomically. `dsh-tool-fs` emits the events without depending on the policy. See the [split-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [event-gate](2026-06-26-file-context-as-event-gate.md) Agent Notes. +Read-before-write/edit and observed state belong to `dsh-fs-observation-policy`, not `ctx.fs`. Through the `fs/*` event gate, the policy records versions per opaque actor and supplies optional mutation expectations; the provider enforces freshness atomically. `dsh-tool-fs` emits the events without depending on the policy. See the [split-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [event-gate](2026-06-26-file-context-as-event-gate.md) Agent Notes. ## Package topology @@ -47,7 +47,7 @@ The filesystem seam uses the same dependency direction as the bash trio: consumer interface implementation ``` -`@deepseek-ai/dsh-fs` depends only on `cordis` plus the repo-wide `HarnessError` base from `@deepseek-ai/dsh-llm`. It declares the `ctx.fs` key, the abstract `FileSystem` service, the vocabulary types shared by backends and consumers, the filesystem error vocabulary, and the `fs/*` policy event vocabulary. It carries no observed-state store and no owner-derivation shape; the events pass an opaque `object` actor that the provider never reads, and the `dsh-fs-policy` plugin owns the owner-derivation shape and the observed-state store on top of those events. +`@deepseek-ai/dsh-fs` depends only on `cordis` plus the repo-wide `HarnessError` base from `@deepseek-ai/dsh-llm`. It declares the `ctx.fs` key, the abstract `FileSystem` service, the vocabulary types shared by backends and consumers, the filesystem error vocabulary, and the `fs/*` policy event vocabulary. It carries no observed-state store and no owner-derivation shape; the events pass an opaque `object` actor that the provider never reads, and the `dsh-fs-observation-policy` plugin owns the owner-derivation shape and the observed-state store on top of those events. `@deepseek-ai/dsh-fs-local` depends on `@deepseek-ai/dsh-fs` and `cordis`. It subclasses `FileSystem`, registers itself as `ctx.fs`, owns local-backend configuration such as the base directory, and contains all direct `node:fs` / `node:path` access. It holds no observed-state store — freshness is a version token the backend mints and the policy plugin records. @@ -68,13 +68,13 @@ The interface covers these semantic operations: - Create or replace a UTF-8 text file. - Edit an existing UTF-8 text file by literal replacement. -The provider contract also carries the freshness hooks that policy builds on — but the observed-state store and owner derivation live in the `dsh-fs-policy` plugin, not on `ctx.fs`: +The provider contract also carries the freshness hooks that policy builds on — but the observed-state store and owner derivation live in the `dsh-fs-observation-policy` plugin, not on `ctx.fs`: - The backend mints an opaque `version` token per target (in `stat` and in every read/mutation outcome). - `writeText`/`editText` take an OPTIONAL version expectation: omit it for an unconditional bare-provider mutation, or supply it to guard the mutation inside the backend's atomic critical section. -- The `dsh-fs-policy` plugin decides that expectation on `fs/write-intent`/`fs/edit-intent` and records observed versions on `fs/observed`, keyed by an owner it derives from the opaque event actor (normally `exec.agent.session`). +- The `dsh-fs-observation-policy` plugin decides that expectation on `fs/write-intent`/`fs/edit-intent` and records observed versions on `fs/observed`, keyed by an owner it derives from the opaque event actor (normally `exec.agent.session`). -Authorization is version freshness, not a full/partial view distinction: any read records the target's version, and a later write/edit is authorized as long as the file is still at that version — so a windowed read of lines 100-150 authorizes an edit of line 120. The observed-state store is a `WeakMap>` inside `dsh-fs-policy`; `dsh-fs` holds none of it and treats the actor as opaque. (This decision first modeled a `FileState` cache with `full`/`partial` views on `ctx.fs`; the split-fs-seam and event-gate notes replaced that with the freshness-based policy plugin described here.) +Authorization is version freshness, not a full/partial view distinction: any read records the target's version, and a later write/edit is authorized as long as the file is still at that version — so a windowed read of lines 100-150 authorizes an edit of line 120. The observed-state store is a `WeakMap>` inside `dsh-fs-observation-policy`; `dsh-fs` holds none of it and treats the actor as opaque. (This decision first modeled a `FileState` cache with `full`/`partial` views on `ctx.fs`; the split-fs-seam and event-gate notes replaced that with the freshness-based policy plugin described here.) Path resolution is explicit and allowed to be async. Local resolution may only normalize a path, but sandboxed/remote/project-scoped backends may need I/O to resolve a user-supplied path into a stable target identity. @@ -86,11 +86,11 @@ Resolved targets must expose at least three concepts: `targetKey` remains opaque even when another capability shares the provider's execution world. Such consumers ask the provider for `processPath(target)`, `fileUrl(target)`, or `contains(parent, child)`; the [portable execution-world decision](2026-07-28-portable-execution-world-consumers.md) owns why these facts sit on the filesystem seam. -Read and mutation results must include an opaque file `version`. The local backend derives its token from bigint stat metadata (`dev`, `ino`, `size`, `mtimeNs`, and `ctimeNs`) so same-size rewrites and inode replacement invalidate consumers reliably; a remote backend can use a revision id or hash-like token. The `dsh-fs-policy` plugin records versions for stale checks; consumers may display related metadata but must not interpret the version token. +Read and mutation results must include an opaque file `version`. The local backend derives its token from bigint stat metadata (`dev`, `ino`, `size`, `mtimeNs`, and `ctimeNs`) so same-size rewrites and inode replacement invalidate consumers reliably; a remote backend can use a revision id or hash-like token. The `dsh-fs-observation-policy` plugin records versions for stale checks; consumers may display related metadata but must not interpret the version token. -The provider hands back decoded text: `readText` returns a whole regular text file and `streamText` streams the same text semantics for large files or consumer-owned retention limits. Line windowing, byte ceilings, numbered-line rendering, and total-line accounting live in consumers such as `dsh-tool-fs` and `dsh-lsp-local`. The provider owns regular-file checks, UTF-8 decoding, and binary/NUL rejection; it does not know about line windows, protocol limits, or views. +The provider hands back decoded text: `readText` returns a whole regular text file and `streamText` streams the same text semantics for large files or consumer-owned retention limits. Line windowing, byte ceilings, numbered-line rendering, and total-line accounting live in consumers such as `dsh-tool-fs` and `dsh-lsp-stdio`. The provider owns regular-file checks, UTF-8 decoding, and binary/NUL rejection; it does not know about line windows, protocol limits, or views. -Observed-state recording is not on `ctx.fs`: after a successful read the executor emits `fs/observed`, and the `dsh-fs-policy` plugin records `{ version }` for the deriving owner. There is no `full`/`partial` view — a read at any window records the version, and freshness (not view completeness) authorizes a later write/edit. +Observed-state recording is not on `ctx.fs`: after a successful read the executor emits `fs/observed`, and the `dsh-fs-observation-policy` plugin records `{ version }` for the deriving owner. There is no `full`/`partial` view — a read at any window records the version, and freshness (not view completeness) authorizes a later write/edit. Full-file writes create or replace UTF-8 text files. Backends may create parent directories when that behavior is supported and documented. Existing non-regular targets are rejected. `writeText` takes an optional expectation: `createIfAbsent` creates a missing target and rejects an existing one with `FS_NOT_OBSERVED` (the path the policy uses for an unobserved owner); `replaceIfVersion` replaces only when the target exists at the observed version, else `FS_STALE_VERSION`; omitting the expectation is the unconditional bare-provider create-or-overwrite. The policy plugin chooses which expectation to supply from the owner's observed state. @@ -115,19 +115,19 @@ Each tool follows the same execution shape: 1. Validate and normalize model arguments. 2. Call the appropriate `ctx.fs` operation. 3. Format the result as `ContentBlock[]` for the model. -4. Let thrown backend/tool errors flow through `ToolRegistry.execute()`, which converts them into `isError` tool results. +4. Let thrown backend/tool errors flow through `ToolRuntime.execute()`, which converts them into `isError` tool results. -The package registers prompt guidance through `ctx.systemPrompt.section(...)` and registers schemas through `ctx.tools.register(...)`. Tool schemas still flow into the normal prompt assembly path via `SystemPrompt.assemble()` and `ToolRegistry.schemas()`; no agent-loop changes are required. +The package registers prompt guidance through `ctx.systemPrompt.section(...)` and registers schemas through `ctx.tools.register(...)`. Tool schemas still flow into the normal prompt assembly path via `SystemPrompt.assemble()` and `ToolRuntime.schemas()`; no agent-loop changes are required. The tool package keeps model-facing contracts stable when backends change: a local backend and a remote backend may resolve paths differently internally, but the `read` / `write` / `edit` schemas do not change solely because the backend changes. -The default deployment requires a prior `read` before updating an existing file with `write` or `edit`. `tool-fs` does not implement this by checking whether a tool named `read` ran: it dispatches the `fs/write-intent`/`fs/edit-intent` events (passing the execution context as the opaque actor), and the `dsh-fs-policy` plugin derives the owner, gates on prior observation, and supplies the version expectation. Any windowed read authorizes a later write/edit as long as the file is unchanged. Creating a new file with `write` does not require prior observation. +The default deployment requires a prior `read` before updating an existing file with `write` or `edit`. `tool-fs` does not implement this by checking whether a tool named `read` ran: it dispatches the `fs/write-intent`/`fs/edit-intent` events (passing the execution context as the opaque actor), and the `dsh-fs-observation-policy` plugin derives the owner, gates on prior observation, and supplies the version expectation. Any windowed read authorizes a later write/edit as long as the file is unchanged. Creating a new file with `write` does not require prior observation. The root plugin registers the full suite by composing the per-tool registration helpers. It injects `fs`, `tools`, and `systemPrompt`. ## Testing -Tests follow the package boundary, not only the user-visible tools: the service contract in `dsh-fs`; real filesystem behavior through the `ctx.fs` interface in `dsh-fs-local` (resolution, symlinks, streaming, binary/UTF-8 rejection, unconditional and version-guarded writes, literal-edit semantics, line-ending preservation, structured `FsError` codes); the consumer surface in `dsh-tool-fs` against the real local provider (mock only the model/clock, never the collaborator); and integration through `ctx.tools.execute()` with and without `dsh-fs-policy`, world-verified by reading files back from disk rather than trusting either the canonical value or rendered content. The observed-state/owner-derivation policy is tested in `dsh-fs-policy`, not here. +Tests follow the package boundary, not only the user-visible tools: the service contract in `dsh-fs`; real filesystem behavior through the `ctx.fs` interface in `dsh-fs-local` (resolution, symlinks, streaming, binary/UTF-8 rejection, unconditional and version-guarded writes, literal-edit semantics, line-ending preservation, structured `FsError` codes); the consumer surface in `dsh-tool-fs` against the real local provider (mock only the model/clock, never the collaborator); and integration through `ctx.tools.execute()` with and without `dsh-fs-observation-policy`, world-verified by reading files back from disk rather than trusting either the canonical value or rendered content. The observed-state/owner-derivation policy is tested in `dsh-fs-observation-policy`, not here. The defensive-pattern classes this repo has been bitten by are pinned directly: @@ -152,11 +152,11 @@ The defensive-pattern classes this repo has been bitten by are pinned directly: **Edit semantics are race-prone by nature.** Literal edit is a read-modify-write operation; the guard is the backend's atomic mutation critical section plus the optional version expectation, so concurrent edits settle deterministically — one wins, the other gets `FS_STALE_VERSION`. -**Observed state does not belong on `ctx.fs`.** Recording what an execution context has seen is workflow policy, not raw filesystem I/O. This decision first placed it inside the filesystem seam; the split-fs-seam note then established that a sandboxed/remote backend should not inherit model-facing observation policy, and moved it into the `dsh-fs-policy` plugin. The provider contract keeps only what write/edit safety genuinely needs at the storage layer — a backend-minted version token and an optional version-guarded mutation — while the policy plugin owns owner derivation, observed-state, and read-before-edit gating over the `fs/*` events. +**Observed state does not belong on `ctx.fs`.** Recording what an execution context has seen is workflow policy, not raw filesystem I/O. This decision first placed it inside the filesystem seam; the split-fs-seam note then established that a sandboxed/remote backend should not inherit model-facing observation policy, and moved it into the `dsh-fs-observation-policy` plugin. The provider contract keeps only what write/edit safety genuinely needs at the storage layer — a backend-minted version token and an optional version-guarded mutation — while the policy plugin owns owner derivation, observed-state, and read-before-edit gating over the `fs/*` events. **The `resolve`-then-operate shape costs an extra round-trip per call.** Each tool may resolve a path to an `FsTarget` and then issue the read/write/edit as a separate `ctx.fs` call. For the local backend this is negligible (resolution is in-memory path normalization), but a remote/sandboxed backend may turn each step into its own request, so a single `read` can become two network round-trips. Backends where the round-trip matters can cache or fold resolution internally while preserving the observable contract. -**Observed-state persistence is deferred.** Observed state lives in memory (the `WeakMap` inside `dsh-fs-policy`), so a resumed session conservatively requires files to be read again before write/edit until a future session-event or persistence mechanism makes observation replayable. +**Observed-state persistence is deferred.** Observed state lives in memory (the `WeakMap` inside `dsh-fs-observation-policy`), so a resumed session conservatively requires files to be read again before write/edit until a future session-event or persistence mechanism makes observation replayable. **Error codes become part of the seam.** `FsError` codes make stale-version and observation failures machine-routable through the existing structured error taxonomy. The cost is that `dsh-fs` imports the shared `HarnessError` base from `dsh-llm`; that dependency is intentional and stays limited to the error vocabulary. diff --git a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md index d986869bf1..1073226594 100644 --- a/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md @@ -6,7 +6,7 @@ Status: implemented ## 问题 -harness 已有一个具体的 `bash` 能力 seam(`dsh-bash` / `dsh-bash-local` / `dsh-tool-bash`),但文件系统操作当时即将作为面向模型的工具落地,却没有等价的 seam。如果 `read`、`write` 和 `edit` 直接使用 `node:fs`,面向模型的工具包就会同时承担文件系统执行策略、本地路径解析、原子写入行为、文本解码、符号链接行为和编辑语义。 +harness 已有一个具体的 `bash` 能力 seam(`dsh-shell` / `dsh-bash-local` / `dsh-tool-bash`),但文件系统操作当时即将作为面向模型的工具落地,却没有等价的 seam。如果 `read`、`write` 和 `edit` 直接使用 `node:fs`,面向模型的工具包就会同时承担文件系统执行策略、本地路径解析、原子写入行为、文本解码、符号链接行为和编辑语义。 这把三个独立变化的关注点耦合在了一起: @@ -28,7 +28,7 @@ harness 已有一个具体的 `bash` 能力 seam(`dsh-bash` / `dsh-bash-local` Consumer 包仅依赖 Service Definition 包,从不依赖 `dsh-fs-local`。需要不同后端的部署只需为 `ctx.fs` 加载不同的提供方,无需改动工具 schema 或面向模型的提示词引导。 -读后写/编辑与观测状态策略是第四个包 `@deepseek-ai/dsh-fs-policy`(`packages/fs/fs-policy`),通过 `fs/*` 事件门控贡献,而非挂在 `ctx.fs` 上;加载 `dsh-tool-fs` 的部署同时加载 `dsh-fs-policy` 以获得读后写/编辑能力。本决策确立了由三个包构成的边界;策略从提供方基类拆出的决策由 [拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) 做出,其以事件门控插件(而非方法服务)实现的方式由 [事件门控 Agent Note](2026-06-26-file-context-as-event-gate.md) 做出。 +读后写/编辑与观测状态策略是第四个包 `@deepseek-ai/dsh-fs-observation-policy`(`packages/fs/fs-observation-policy`),通过 `fs/*` 事件门控贡献,而非挂在 `ctx.fs` 上;加载 `dsh-tool-fs` 的部署同时加载 `dsh-fs-observation-policy` 以获得读后写/编辑能力。本决策确立了由三个包构成的边界;策略从提供方基类拆出的决策由 [拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) 做出,其以事件门控插件(而非方法服务)实现的方式由 [事件门控 Agent Note](2026-06-26-file-context-as-event-gate.md) 做出。 第一个后端有意仅限本地:`dsh-fs-local` 基于宿主文件系统实现 `ctx.fs`。未来的兄弟后端可在同一接口之后提供沙箱、远程、虚拟或项目作用域的文件系统。 @@ -36,7 +36,7 @@ Consumer 包仅依赖 Service Definition 包,从不依赖 `dsh-fs-local`。需 文件系统权限和沙箱并非此拆分所隐含。本地后端从其配置的基目录解析相对路径,但路径包含约束策略是独立的决策:要么由更严格的 `ctx.fs` 实现强制执行,要么由权限/沙箱插件包装 `tools/execute` 并在调用到达消费方之前否决。 -读后写/编辑与观测状态属于 `dsh-fs-policy`,而非 `ctx.fs`。通过 `fs/*` 事件门控,策略按不透明 actor 记录版本,并提供可选的变更期望;提供方原子性地强制新鲜度。`dsh-tool-fs` 发出事件但不依赖策略。见[拆分文件系统 seam](../simplification/2026-06-26-fsspec-style-fs-seam.md)和[事件门控插件](2026-06-26-file-context-as-event-gate.md) Agent Note。 +读后写/编辑与观测状态属于 `dsh-fs-observation-policy`,而非 `ctx.fs`。通过 `fs/*` 事件门控,策略按不透明 actor 记录版本,并提供可选的变更期望;提供方原子性地强制新鲜度。`dsh-tool-fs` 发出事件但不依赖策略。见[拆分文件系统 seam](../simplification/2026-06-26-fsspec-style-fs-seam.md)和[事件门控插件](2026-06-26-file-context-as-event-gate.md) Agent Note。 ## 包拓扑 @@ -47,7 +47,7 @@ Consumer 包仅依赖 Service Definition 包,从不依赖 `dsh-fs-local`。需 consumer interface implementation ``` -`@deepseek-ai/dsh-fs` 仅依赖 `cordis` 加上来自 `@deepseek-ai/dsh-llm` 的仓库级 `HarnessError` 基类。它声明 `ctx.fs` 键、抽象 `FileSystem` 服务、后端和消费方共享的词汇类型、文件系统错误词汇,以及 `fs/*` 策略事件词汇。它不持有观测状态存储,也不持有 owner 推导形态;事件传递一个不透明的 `object` actor,提供方从不读取它,`dsh-fs-policy` 插件在这些事件之上拥有 owner 推导形态和观测状态存储。 +`@deepseek-ai/dsh-fs` 仅依赖 `cordis` 加上来自 `@deepseek-ai/dsh-llm` 的仓库级 `HarnessError` 基类。它声明 `ctx.fs` 键、抽象 `FileSystem` 服务、后端和消费方共享的词汇类型、文件系统错误词汇,以及 `fs/*` 策略事件词汇。它不持有观测状态存储,也不持有 owner 推导形态;事件传递一个不透明的 `object` actor,提供方从不读取它,`dsh-fs-observation-policy` 插件在这些事件之上拥有 owner 推导形态和观测状态存储。 `@deepseek-ai/dsh-fs-local` 依赖 `@deepseek-ai/dsh-fs` 和 `cordis`。它继承 `FileSystem`,将自身注册为 `ctx.fs`,拥有本地后端配置(如基目录),并包含所有直接的 `node:fs` / `node:path` 访问。它不持有观测状态存储——新鲜度是后端铸造、策略插件记录的版本令牌。 @@ -68,13 +68,13 @@ Consumer 包仅依赖 Service Definition 包,从不依赖 `dsh-fs-local`。需 - 创建或替换一个 UTF-8 文本文件。 - 通过字面替换编辑一个已有的 UTF-8 文本文件。 -提供方约定还携带策略所依赖的新鲜度钩子——但观测状态存储和 owner 推导位于 `dsh-fs-policy` 插件中,而非 `ctx.fs` 上: +提供方约定还携带策略所依赖的新鲜度钩子——但观测状态存储和 owner 推导位于 `dsh-fs-observation-policy` 插件中,而非 `ctx.fs` 上: - 后端为每个目标铸造一个不透明的 `version` 令牌(在 `stat` 以及每次读取/变更结果中)。 - `writeText`/`editText` 接受一个可选的版本期望:省略它表示无条件的裸提供方变更;提供它则在后端的原子临界区内守护变更。 -- `dsh-fs-policy` 插件在 `fs/write-intent`/`fs/edit-intent` 上决定该期望,并在 `fs/observed` 上记录观测版本,以它从不透明事件 actor 推导出的 owner 为键(通常是 `exec.agent.session`)。 +- `dsh-fs-observation-policy` 插件在 `fs/write-intent`/`fs/edit-intent` 上决定该期望,并在 `fs/observed` 上记录观测版本,以它从不透明事件 actor 推导出的 owner 为键(通常是 `exec.agent.session`)。 -授权基于版本新鲜度,而非完整/部分视图的区分:任何读取都会记录目标的版本,后续的写入/编辑只要文件仍处于该版本就被授权——因此对第 100-150 行的窗口化读取可以授权对第 120 行的编辑。观测状态存储是 `dsh-fs-policy` 内部的 `WeakMap>`;`dsh-fs` 不持有任何此类数据,并将 actor 视为不透明。(本决策最初建模了一个带 `full`/`partial` 视图的 `FileState` 缓存放在 `ctx.fs` 上;拆分文件系统 seam 与事件门控两份笔记将其替换为此处描述的基于新鲜度的策略插件。) +授权基于版本新鲜度,而非完整/部分视图的区分:任何读取都会记录目标的版本,后续的写入/编辑只要文件仍处于该版本就被授权——因此对第 100-150 行的窗口化读取可以授权对第 120 行的编辑。观测状态存储是 `dsh-fs-observation-policy` 内部的 `WeakMap>`;`dsh-fs` 不持有任何此类数据,并将 actor 视为不透明。(本决策最初建模了一个带 `full`/`partial` 视图的 `FileState` 缓存放在 `ctx.fs` 上;拆分文件系统 seam 与事件门控两份笔记将其替换为此处描述的基于新鲜度的策略插件。) 路径解析是显式的,允许异步。本地解析可能只做路径规范化,但沙箱/远程/项目作用域的后端可能需要 I/O 才能将用户提供的路径解析为稳定的目标标识。 @@ -86,11 +86,11 @@ Consumer 包仅依赖 Service Definition 包,从不依赖 `dsh-fs-local`。需 即使另一项能力共享提供方的执行环境,`targetKey` 仍保持不透明。这类消费方通过提供方的 `processPath(target)`、`fileUrl(target)` 或 `contains(parent, child)` 获取所需事实;[可移植执行环境决策](2026-07-28-portable-execution-world-consumers.md)说明这些事实为何属于文件系统 seam。 -读取和变更结果必须包含不透明的文件 `version`。本地后端从 bigint stat 元数据(`dev`、`ino`、`size`、`mtimeNs` 和 `ctimeNs`)派生令牌,因此同大小重写和 inode 替换都会可靠地使消费方失效;远程后端可以使用 revision id 或类似 hash 的令牌。`dsh-fs-policy` 插件记录版本用于陈旧检查;消费方可以展示相关元数据但禁止解释版本令牌。 +读取和变更结果必须包含不透明的文件 `version`。本地后端从 bigint stat 元数据(`dev`、`ino`、`size`、`mtimeNs` 和 `ctimeNs`)派生令牌,因此同大小重写和 inode 替换都会可靠地使消费方失效;远程后端可以使用 revision id 或类似 hash 的令牌。`dsh-fs-observation-policy` 插件记录版本用于陈旧检查;消费方可以展示相关元数据但禁止解释版本令牌。 -提供方返回已解码的文本:`readText` 返回整个普通文本文件,`streamText` 为大文件或消费方自有的保留上限流式传输相同的文本语义。行窗口化、字节上限、带行号渲染和总行数统计归 `dsh-tool-fs`、`dsh-lsp-local` 等消费方所有。提供方负责普通文件检查、UTF-8 解码和二进制/NUL 拒绝;它不知道行窗口、协议上限或视图。 +提供方返回已解码的文本:`readText` 返回整个普通文本文件,`streamText` 为大文件或消费方自有的保留上限流式传输相同的文本语义。行窗口化、字节上限、带行号渲染和总行数统计归 `dsh-tool-fs`、`dsh-lsp-stdio` 等消费方所有。提供方负责普通文件检查、UTF-8 解码和二进制/NUL 拒绝;它不知道行窗口、协议上限或视图。 -观测状态记录不在 `ctx.fs` 上:成功读取后,执行器发出 `fs/observed`,`dsh-fs-policy` 插件为推导出的 owner 记录 `{ version }`。没有 `full`/`partial` 视图——任何窗口的读取都记录版本,新鲜度(而非视图完整性)授权后续的写入/编辑。 +观测状态记录不在 `ctx.fs` 上:成功读取后,执行器发出 `fs/observed`,`dsh-fs-observation-policy` 插件为推导出的 owner 记录 `{ version }`。没有 `full`/`partial` 视图——任何窗口的读取都记录版本,新鲜度(而非视图完整性)授权后续的写入/编辑。 全文件写入创建或替换 UTF-8 文本文件。后端在支持且有文档说明时可以创建父目录。已有的非常规目标被拒绝。`writeText` 接受一个可选期望:`createIfAbsent` 创建缺失的目标并拒绝已存在的(报 `FS_NOT_OBSERVED`,这是策略为未观测 owner 使用的路径);`replaceIfVersion` 仅在目标处于观测版本时替换,否则报 `FS_STALE_VERSION`;省略期望则为无条件的裸提供方创建或覆盖。策略插件根据 owner 的观测状态选择提供哪个期望。 @@ -115,19 +115,19 @@ Consumer 包仅依赖 Service Definition 包,从不依赖 `dsh-fs-local`。需 1. 校验并规范化模型参数。 2. 调用相应的 `ctx.fs` 操作。 3. 将结果格式化为面向模型的 `ContentBlock[]`。 -4. 让抛出的后端/工具错误流经 `ToolRegistry.execute()`,由其转换为 `isError` 工具结果。 +4. 让抛出的后端/工具错误流经 `ToolRuntime.execute()`,由其转换为 `isError` 工具结果。 -该包通过 `ctx.systemPrompt.section(...)` 注册提示词引导,通过 `ctx.tools.register(...)` 注册 schema。工具 schema 仍通过 `SystemPrompt.assemble()` 和 `ToolRegistry.schemas()` 流入正常的提示词组装路径;无需改动 agent loop(智能体循环)。 +该包通过 `ctx.systemPrompt.section(...)` 注册提示词引导,通过 `ctx.tools.register(...)` 注册 schema。工具 schema 仍通过 `SystemPrompt.assemble()` 和 `ToolRuntime.schemas()` 流入正常的提示词组装路径;无需改动 agent loop(智能体循环)。 工具包在后端变化时保持面向模型的约定稳定:本地后端和远程后端内部可能以不同方式解析路径,但 `read` / `write` / `edit` schema 不会仅因后端变化而改变。 -默认部署要求在用 `write` 或 `edit` 更新已有文件之前先 `read`。`tool-fs` 不通过检查是否运行过名为 `read` 的工具来实现这一点:它分发 `fs/write-intent`/`fs/edit-intent` 事件(将执行上下文作为不透明 actor 传递),`dsh-fs-policy` 插件推导 owner、对先前观测进行门控并提供版本期望。任何窗口化读取都能授权后续的写入/编辑,只要文件未变。用 `write` 创建新文件不要求先前观测。 +默认部署要求在用 `write` 或 `edit` 更新已有文件之前先 `read`。`tool-fs` 不通过检查是否运行过名为 `read` 的工具来实现这一点:它分发 `fs/write-intent`/`fs/edit-intent` 事件(将执行上下文作为不透明 actor 传递),`dsh-fs-observation-policy` 插件推导 owner、对先前观测进行门控并提供版本期望。任何窗口化读取都能授权后续的写入/编辑,只要文件未变。用 `write` 创建新文件不要求先前观测。 根插件通过组合各工具的注册辅助函数来注册完整套件。它注入 `fs`、`tools` 和 `systemPrompt`。 ## 测试 -测试遵循包边界,而不仅是用户可见的工具:`dsh-fs` 中的服务约定;`dsh-fs-local` 中通过 `ctx.fs` 接口测试的真实文件系统行为(解析、符号链接、流式传输、二进制/UTF-8 拒绝、无条件和版本守护的写入、字面编辑语义、行尾保留、结构化 `FsError` 错误码);`dsh-tool-fs` 中基于真实本地提供方的消费方接口(只 mock 模型/时钟,从不 mock 协作者);以及通过 `ctx.tools.execute()` 在有和没有 `dsh-fs-policy` 的情况下进行集成测试,通过从磁盘回读文件来验证世界状态,既不信任规范值,也不信任渲染内容。观测状态/owner 推导策略在 `dsh-fs-policy` 中测试,不在此处。 +测试遵循包边界,而不仅是用户可见的工具:`dsh-fs` 中的服务约定;`dsh-fs-local` 中通过 `ctx.fs` 接口测试的真实文件系统行为(解析、符号链接、流式传输、二进制/UTF-8 拒绝、无条件和版本守护的写入、字面编辑语义、行尾保留、结构化 `FsError` 错误码);`dsh-tool-fs` 中基于真实本地提供方的消费方接口(只 mock 模型/时钟,从不 mock 协作者);以及通过 `ctx.tools.execute()` 在有和没有 `dsh-fs-observation-policy` 的情况下进行集成测试,通过从磁盘回读文件来验证世界状态,既不信任规范值,也不信任渲染内容。观测状态/owner 推导策略在 `dsh-fs-observation-policy` 中测试,不在此处。 本仓库曾踩过的防御性模式类别被直接固定: @@ -152,11 +152,11 @@ Consumer 包仅依赖 Service Definition 包,从不依赖 `dsh-fs-local`。需 **编辑语义天然易受竞争影响。** 字面编辑是读-改-写操作;守护手段是后端的原子变更临界区加上可选的版本期望,因此并发编辑确定性地收敛——一个赢,另一个得到 `FS_STALE_VERSION`。 -**观测状态不属于 `ctx.fs`。** 记录执行上下文看到了什么是工作流策略,而非原始文件系统 I/O。本决策最初将其放在文件系统 seam 内部;拆分文件系统 seam 笔记随后确立了沙箱/远程后端不应继承面向模型的观测策略,并将其移入 `dsh-fs-policy` 插件。提供方约定只保留写入/编辑安全在存储层真正需要的东西——后端铸造的版本令牌和可选的版本守护变更——而策略插件拥有 owner 推导、观测状态和基于 `fs/*` 事件的读后编辑门控。 +**观测状态不属于 `ctx.fs`。** 记录执行上下文看到了什么是工作流策略,而非原始文件系统 I/O。本决策最初将其放在文件系统 seam 内部;拆分文件系统 seam 笔记随后确立了沙箱/远程后端不应继承面向模型的观测策略,并将其移入 `dsh-fs-observation-policy` 插件。提供方约定只保留写入/编辑安全在存储层真正需要的东西——后端铸造的版本令牌和可选的版本守护变更——而策略插件拥有 owner 推导、观测状态和基于 `fs/*` 事件的读后编辑门控。 **`resolve` 然后操作的形态每次调用多一次往返。** 每个工具可能先将路径解析为 `FsTarget`,再以单独的 `ctx.fs` 调用发起读取/写入/编辑。对本地后端来说这可以忽略(解析是内存中的路径规范化),但远程/沙箱后端可能将每步变成独立请求,使单次 `read` 变为两次网络往返。往返开销重要的后端可以在内部缓存或折叠解析,同时保持可观测约定不变。 -**观测状态持久化被推迟。** 观测状态存在于内存中(`dsh-fs-policy` 内部的 `WeakMap`),因此恢复的会话保守地要求文件在写入/编辑前重新读取,直到未来的会话事件或持久化机制使观测可回放。 +**观测状态持久化被推迟。** 观测状态存在于内存中(`dsh-fs-observation-policy` 内部的 `WeakMap`),因此恢复的会话保守地要求文件在写入/编辑前重新读取,直到未来的会话事件或持久化机制使观测可回放。 **错误码成为 seam 的一部分。** `FsError` 错误码使陈旧版本和观测失败可通过既有的结构化错误分类体系进行机器路由。代价是 `dsh-fs` 从 `dsh-llm` 导入共享的 `HarnessError` 基类;该依赖是有意为之且限于错误词汇。 diff --git a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.i18n.yaml index ac99292150..d3c3a774bd 100644 --- a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.md -2026-06-18-agent-lifecycle-and-ownership-contracts.md: c3522d18dad2703664f12364e4d70cc057a3a945 -2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md: 16781c8a5d1a1c67d1fe9296b0f34f2a00b11caf +2026-06-18-agent-lifecycle-and-ownership-contracts.md: f0f9f90b15dee9155cfb1e8c772503642ff34be3 +2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md: a5f6ca10dbe7c71281cccdaf83fff95139ad2d79 diff --git a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.md b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.md index c3522d18da..f0f9f90b15 100644 --- a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.md +++ b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.md @@ -6,7 +6,7 @@ English | [中文](2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md) ## Problem -Several ACP and tool-bash limitations were symptoms of the same missing ownership contract: plugins could create or resume agents through `ctx.agents`, but they could not own and dispose one agent independently, and long-running bash tasks carried no stable owner in the executor itself. ACP aborted and awaited agents on disconnect but could not unregister just that session's agent; `session/cancel` could not cancel queued-but-not-yet-started work; and `tool-bash` kept task ownership in a plugin-local `Map`, so an HMR reload could make an old task look unowned. +Several ACP and tool-bash limitations were symptoms of the same missing ownership contract: plugins could create or resume agents through `ctx.agents`, but they could not own and dispose one agent independently, and long-running bash tasks carried no stable owner in the executor itself. ACP aborted and awaited agents on disconnect but could not unregister just that session's agent; `session/cancel` could not cancel queued-but-not-yet-started work; and `tool-bash` kept job ownership in a plugin-local `Map`, so an HMR reload could make an old task look unowned. ## Decision @@ -24,7 +24,7 @@ A new `cancel()` verb on the `Agent` interface — the single public stop primit ### 3. Bash owner token in the Service Definition -Background-task ownership moved from a `tool-bash` plugin-local `Map` into the executor. `BashExecRequest` gains an optional `owner?: string`; the resolved `BashExecSpec` carries it as required-but-nullable `owner: string | undefined` (a forgotten owner is a visible `undefined`, never a silently-absent property). The executor stores the token on its task and exposes it via a new `BashExecutor.ownerOf(id): string | undefined` method (NOT on the public `BashTask` — one read path, no redundant API). `tool-bash` deletes its `Map` entirely: it stamps `exec.agent?.id` (the shared registry/session id) as the owner at `start`, and `bash_output`/`bash_kill` compare `ctx.bash.ownerOf(id)` to the caller's token with `!== undefined` semantics (an empty-string token is still a real owner). The completion notice finds the live agent by scanning `ctx.get('agents')?.list()` for `agent.id === ownerToken` (read via `ctx.get` — `onTaskDone` runs on the bash fiber, a foreign fiber, where the `ctx.agents` proxy would throw). Because ownership now lives on the task in the executor (disposed with the `dsh-bash` fiber), it SURVIVES a `tool-bash` HMR reload — closing the old `XXX(tool-bash-owner-hmr)` gap. (The `onTaskDone` listener is still effect-scoped to `tool-bash`'s `apply`, so a completion landing during the reload gap still drops its one notice — the pre-existing reload-gap drop — but the ownership fence itself is HMR-proof.) +Background-job ownership moved from a `tool-bash` plugin-local `Map` into the executor. `ShellExecRequest` gains an optional `owner?: string`; the resolved `ShellExecSpec` carries it as required-but-nullable `owner: string | undefined` (a forgotten owner is a visible `undefined`, never a silently-absent property). The executor stores the token on its task and exposes it via a new `ShellExecutor.ownerOf(id): string | undefined` method (NOT on the public `BashTask` — one read path, no redundant API). `tool-bash` deletes its `Map` entirely: it stamps `exec.agent?.id` (the shared registry/session id) as the owner at `start`, and `bash_output`/`bash_kill` compare `ctx.shell.ownerOf(id)` to the caller's token with `!== undefined` semantics (an empty-string token is still a real owner). The completion notice finds the live agent by scanning `ctx.get('agents')?.list()` for `agent.id === ownerToken` (read via `ctx.get` — `onJobDone` runs on the bash fiber, a foreign fiber, where the `ctx.agents` proxy would throw). Because ownership now lives on the task in the executor (disposed with the `dsh-shell` fiber), it SURVIVES a `tool-bash` HMR reload — closing the old `XXX(tool-bash-owner-hmr)` gap. (The `onJobDone` listener is still effect-scoped to `tool-bash`'s `apply`, so a completion landing during the reload gap still drops its one notice — the pre-existing reload-gap drop — but the ownership fence itself is HMR-proof.) ## Verification @@ -32,7 +32,7 @@ These invariants hold and are pinned by tests: - ACP disconnect or plugin teardown leaves no registered agent and no session-store entry for any bridge-owned session, including a create racing connection closure. - `session/cancel` before a queued prompt starts prevents that prompt from running; a later accepted prompt remains an independent queued turn. -- A `tool-bash` HMR reload does NOT make an existing background task readable or killable by a different session (ownership survives on the executor). +- A `tool-bash` HMR reload does NOT make an existing background job readable or killable by a different session (ownership survives on the executor). - Existing non-ACP demos still work without managing handles explicitly; config-created agents remain owned by the `AgentLoop` plugin fiber. ## Session owner tokens are unique among live agents @@ -41,7 +41,7 @@ The bash owner-token comparison relies on the shared `Agent.id`/`SessionId` bein ## Alternatives considered -- **A public `BashTask.owner` field** instead of the `BashExecutor.ownerOf(id)` Service Definition method — rejected: one read path, no redundant API. +- **A public `BashTask.owner` field** instead of the `ShellExecutor.ownerOf(id)` Service Definition method — rejected: one read path, no redundant API. - **Sibling cordis effects for the agent's session lifecycle** — rejected: a fiber unload disposes sibling effects concurrently (`Promise.all`), racing removal of the store-owned append publication hooks against the loop's closing `session/flush`; the single composite effect's ordered LIFO chain is what captures the closing `turn/end` on both disposal paths. - **A separate step-only `abort()` beside `cancel()`** — shipped originally, then removed as unused; `cancel()` is the single public stop primitive ([the public-stop-API Agent Note](../simplification/2026-06-20-public-agent-stop-api.md)). diff --git a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md index 16781c8a5d..a5f6ca10db 100644 --- a/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md @@ -24,7 +24,7 @@ ACP(Agent Client Protocol)与 tool-bash 的若干限制是同一个所有权 ### 3. Service Definition 中的 Bash 所有者令牌 -后台任务所有权从 `tool-bash` 插件本地的 `Map` 移入执行器。`BashExecRequest` 新增可选的 `owner?: string`;解析后的 `BashExecSpec` 将其作为必需但可空的 `owner: string | undefined` 携带(被遗忘的 owner 是可见的 `undefined`,而非静默缺失的属性)。执行器把 token 存在任务上,并通过新的 `BashExecutor.ownerOf(id): string | undefined` 方法暴露它(不放在公开的 `BashTask` 上——只有一条读取路径,没有冗余 API)。`tool-bash` 完全删除其 `Map`:它在 `start` 时将 `exec.agent?.id`(共享的注册表/会话 id)盖章为 owner,`bash_output`/`bash_kill` 则以 `!== undefined` 语义把 `ctx.bash.ownerOf(id)` 与调用方 token 比较(空字符串 token 仍是真实 owner)。完成通知通过扫描 `ctx.get('agents')?.list()` 查找 `agent.id === ownerToken` 的存活 agent(经 `ctx.get` 读取——`onTaskDone` 运行在 bash fiber 这一外部 fiber 上,直接使用 `ctx.agents` proxy 会抛异常)。由于所有权现在保存在执行器的任务上(随 `dsh-bash` fiber dispose),它能跨越 `tool-bash` HMR 重载,关闭旧的 `XXX(tool-bash-owner-hmr)` 缺口。(`onTaskDone` 监听器仍受 `tool-bash` 的 `apply` effect 约束,因此落在重载间隙的完成仍会丢失一条通知——既有的重载间隙丢失——但所有权隔离本身已经不受 HMR 影响。) +后台任务所有权从 `tool-bash` 插件本地的 `Map` 移入执行器。`ShellExecRequest` 新增可选的 `owner?: string`;解析后的 `ShellExecSpec` 将其作为必需但可空的 `owner: string | undefined` 携带(被遗忘的 owner 是可见的 `undefined`,而非静默缺失的属性)。执行器把 token 存在任务上,并通过新的 `ShellExecutor.ownerOf(id): string | undefined` 方法暴露它(不放在公开的 `BashTask` 上——只有一条读取路径,没有冗余 API)。`tool-bash` 完全删除其 `Map`:它在 `start` 时将 `exec.agent?.id`(共享的注册表/会话 id)盖章为 owner,`bash_output`/`bash_kill` 则以 `!== undefined` 语义把 `ctx.shell.ownerOf(id)` 与调用方 token 比较(空字符串 token 仍是真实 owner)。完成通知通过扫描 `ctx.get('agents')?.list()` 查找 `agent.id === ownerToken` 的存活 agent(经 `ctx.get` 读取——`onJobDone` 运行在 bash fiber 这一外部 fiber 上,直接使用 `ctx.agents` proxy 会抛异常)。由于所有权现在保存在执行器的任务上(随 `dsh-shell` fiber dispose),它能跨越 `tool-bash` HMR 重载,关闭旧的 `XXX(tool-bash-owner-hmr)` 缺口。(`onJobDone` 监听器仍受 `tool-bash` 的 `apply` effect 约束,因此落在重载间隙的完成仍会丢失一条通知——既有的重载间隙丢失——但所有权隔离本身已经不受 HMR 影响。) ## 验证 @@ -41,7 +41,7 @@ bash 所有者 token 比较依赖共享的 `Agent.id`/`SessionId` 在存活 agen ## 曾考虑的替代方案 -- **公开的 `BashTask.owner` 字段**而非 `BashExecutor.ownerOf(id)` Service Definition 方法:否决。一条读取路径即可,无需冗余 API。 +- **公开的 `BashTask.owner` 字段**而非 `ShellExecutor.ownerOf(id)` Service Definition 方法:否决。一条读取路径即可,无需冗余 API。 - **为 agent 的会话生命周期使用兄弟 Cordis effect**:否决。fiber 卸载时并发释放兄弟 effect(`Promise.all`),store 拥有的 append 发布钩子的移除与循环的关闭 `session/flush` 产生竞争;单一复合 effect 的有序 LIFO 链才能在两条释放路径上都捕获关闭的 `turn/end`。 - **在 `cancel()` 之外另设一个仅中止步骤的 `abort()`**:最初发布过,后因无人使用而移除;`cancel()` 是唯一的公开停止原语(见[公开停止接口 Agent Note](../simplification/2026-06-20-public-agent-stop-api.md))。 diff --git a/.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml index 8282967ba2..936e601b48 100644 --- a/.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-18-session-surface.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-18-session-surface.md -2026-06-18-session-surface.md: fd48a4ee4e177b84a235b044b054e4ac266df8ba -2026-06-18-session-surface.zh.md: 496624016cd68e2e8b0fe65ed0246c5d469c4585 +2026-06-18-session-surface.md: 3682ae7b8b58b9e5d40695732c3a1531d0651d5e +2026-06-18-session-surface.zh.md: 8cba9645dc6d0c8a4d1ee096668fc0bc38aaa725 diff --git a/.agents/notes/implemented/architecture/2026-06-18-session-surface.md b/.agents/notes/implemented/architecture/2026-06-18-session-surface.md index fd48a4ee4e..3682ae7b8b 100644 --- a/.agents/notes/implemented/architecture/2026-06-18-session-surface.md +++ b/.agents/notes/implemented/architecture/2026-06-18-session-surface.md @@ -68,6 +68,6 @@ Every surface-eligible event must carry `surfaceOp` or it would disappear from d - **`packages/session/session-persistence-jsonl`**: No changes required. - **`packages/session/session-persistence`**: Abstract interface unchanged. -The surface is the foundation history manipulation ships on — dsh-compact's compaction rides it. A compaction or tool-result-prune plugin appends one of the existing message-producing event types (a `user/message` carrying the summary, say) with `surfaceOp: { op: 'replace', start, end }` and `sourceEventSeqs` covering the shadowed entries — the new event takes the range's place on the surface while the plugin's own trace events (e.g. `compaction/start`, `compaction/end`) stay off it. Replay preserves the decision deterministically. +The surface is the foundation history manipulation ships on — dsh-compaction's compaction rides it. A compaction or tool-result-pruner plugin appends one of the existing message-producing event types (a `user/message` carrying the summary, say) with `surfaceOp: { op: 'replace', start, end }` and `sourceEventSeqs` covering the shadowed entries — the new event takes the range's place on the surface while the plugin's own trace events (e.g. `compaction/start`, `compaction/end`) stay off it. Replay preserves the decision deterministically. A `tool/result` replacement may rewrite exactly one current `tool/result` and must preserve every data field except `content`. Session acceptance enforces this rule together with positional range and cited source-event validation, independent of optional diagnostic plugins. diff --git a/.agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md b/.agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md index 496624016c..8cba9645dc 100644 --- a/.agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-18-session-surface.zh.md @@ -68,6 +68,6 @@ export type SurfaceOp = - **`packages/session/session-persistence-jsonl`**:无需改动。 - **`packages/session/session-persistence`**:抽象接口不变。 -surface 是历史操纵赖以落地的基础——dsh-compact 的压缩就搭载于其上。压缩或 tool-result-prune 插件追加一个既有的消息产出事件类型(例如一条携带摘要的 `user/message`),附带 `surfaceOp: { op: 'replace', start, end }` 和覆盖被遮蔽条目的 `sourceEventSeqs`——新事件在 surface 上取代该范围的位置,而插件自身的 trace 事件(如 `compaction/start`、`compaction/end`)不进入 surface。回放以确定性方式保留该决策。 +surface 是历史操纵赖以落地的基础——dsh-compaction 的压缩就搭载于其上。压缩或 tool-result-pruner 插件追加一个既有的消息产出事件类型(例如一条携带摘要的 `user/message`),附带 `surfaceOp: { op: 'replace', start, end }` 和覆盖被遮蔽条目的 `sourceEventSeqs`——新事件在 surface 上取代该范围的位置,而插件自身的 trace 事件(如 `compaction/start`、`compaction/end`)不进入 surface。回放以确定性方式保留该决策。 一次 `tool/result` 替换只能改写当前的一个 `tool/result`,并且必须保留除 `content` 以外的每个数据字段。Session 接纳会与位置范围和引用的源事件校验一起强制这条规则,不依赖可选的诊断插件。 diff --git a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml index e6f65b6879..877a53e0ac 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-branded-ids.md -2026-06-20-branded-ids.md: ded48409bcb3deb19e35029fa795b5ea28c9f6d7 -2026-06-20-branded-ids.zh.md: 824d802b4aaec9f4a6ae530659387126a30fbc2d +2026-06-20-branded-ids.md: 6bf68c9b6b6dea8317992bcb6f694a8ee141f048 +2026-06-20-branded-ids.zh.md: 11a78df609888fdf917246940c12463373ff2de3 diff --git a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md index ded48409bc..6bf68c9b6b 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md +++ b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.md @@ -8,9 +8,9 @@ English | [中文](2026-06-20-branded-ids.zh.md) The harness brands `CallId` (`packages/llm/llm/src/brand.ts`) and the shared agent/session `SessionId` (`packages/core/session/src/types.ts`) using the `Branded = string & { readonly [BRAND]: B }` machinery (owned by the type-only `@deepseek-ai/dsh-brand` package at `packages/util/brand/` — see its [README](../../../../packages/util/brand/README.md)) and a zero-cost cast factory per type. `dsh-brand` also states the governing policy: *"Branding is for ids that cross package boundaries and could plausibly be confused; not every string needs a brand."* That policy is right; the problem is that it is only half-applied. Two gaps let a structurally-identical-but-semantically-wrong string slip through the type checker today. -**Gap 1 — unbranded cross-boundary IDs in the bash seam.** The background-task id is a plain `string`: `BashTask.id: string` (`packages/bash/bash/src/types.ts`), carried as `string` through the whole executor seam (`BashExecutor.get`/`ownerOf`/`readOutput`/`kill(id: string)` in `packages/bash/bash/src/index.ts`) and validated/passed as `string` by the model-facing tools (`validateTaskId`, `assertTaskAccess`, the `task_id` schema arg in `packages/bash/tool-bash/src/index.ts`). It is generated by a per-executor counter — `` `bash-${this.nextTaskId++}` `` in `packages/bash/bash-local/src/index.ts` — which gives it **exactly the same `name-N` shape as `SessionId`'s default** (`` `session-${++counter}` `` in `packages/core/session/src/index.ts`). A bash task id and a session id are trivially swappable at a call site and the compiler says nothing. It is a model-facing id (the model passes `task_id` back to `bash_output`/`bash_kill`), so a confusion here is reachable from untrusted input. +**Gap 1 — unbranded cross-boundary IDs in the bash seam.** The background-job id is a plain `string`: `BashTask.id: string` (`packages/shell/shell/src/types.ts`), carried as `string` through the whole executor seam (`ShellExecutor.get`/`ownerOf`/`readOutput`/`kill(id: string)` in `packages/shell/shell/src/index.ts`) and validated/passed as `string` by the model-facing tools (`validateJobId`, `assertTaskAccess`, the `job_id` schema arg in `packages/shell/tool-bash/src/index.ts`). It is generated by a per-executor counter — `` `bash-${this.nextTaskId++}` `` in `packages/shell/bash-local/src/index.ts` — which gives it **exactly the same `name-N` shape as `SessionId`'s default** (`` `session-${++counter}` `` in `packages/core/session/src/index.ts`). A bash job id and a session id are trivially swappable at a call site and the compiler says nothing. It is a model-facing id (the model passes `job_id` back to `bash_output`/`bash_kill`), so a confusion here is reachable from untrusted input. -The bash **owner token** is the related sub-case: `BashExecRequest.owner?: string` and `BashExecSpec.owner: string | undefined` (`packages/bash/bash/src/types.ts`) are documented as a deliberately *opaque* isolation key, but in every live caller the value IS the owning agent's shared `Agent.id`/`SessionId` (`callerToken = (exec) => exec.agent?.id` in `packages/bash/tool-bash/src/index.ts`) wearing a different seam-local name. It is compared for access control (`owner !== callerToken(exec)`), so a mismatched-but-well-typed string here is a cross-session isolation bug the type system currently cannot catch. This is the shared id alias covered by the [unified agent/session identity decision](../simplification/2026-06-20-unify-agent-and-session-id.md). +The bash **owner token** is the related sub-case: `ShellExecRequest.owner?: string` and `ShellExecSpec.owner: string | undefined` (`packages/shell/shell/src/types.ts`) are documented as a deliberately *opaque* isolation key, but in every live caller the value IS the owning agent's shared `Agent.id`/`SessionId` (`callerToken = (exec) => exec.agent?.id` in `packages/shell/tool-bash/src/index.ts`) wearing a different seam-local name. It is compared for access control (`owner !== callerToken(exec)`), so a mismatched-but-well-typed string here is a cross-session isolation bug the type system currently cannot catch. This is the shared id alias covered by the [unified agent/session identity decision](../simplification/2026-06-20-unify-agent-and-session-id.md). **Gap 2 — brand erosion at the boundaries of the *already-branded* IDs.** Even `CallId` and `SessionId` decay back to bare `string` at exactly the places confusion is most likely: registry/store key types and public method params. Representative sites include the session store, the agent registry (both keyed by the shared `SessionId`), tool-presentation call-id maps, ACP's session records, and the persistence coordinator. A brand that is dropped at a collection key buys nothing on lookups — the value of the existing brands is partly unrealized. @@ -18,9 +18,9 @@ The bash **owner token** is the related sub-case: `BashExecRequest.owner?: strin A type-only change. Brands are zero-cost casts; nothing about runtime behavior, serialization, comparison, or the wire format changes. The decision has three parts, all honoring the existing "not every string" policy. -- **Brand the bash task id.** Add `BashTaskId = Branded<'BashTaskId'>` plus its same-named factory in `packages/bash/bash/src/types.ts` (the package that *owns* the id), importing `Branded` from `@deepseek-ai/dsh-brand` exactly as `SessionId` does. The brand primitive lives in the dependency-free `dsh-brand` utility package precisely so `dsh-bash` can brand its ids by depending on it alone — it never pulls in `dsh-llm` (or `dsh-session`) just to reach `Branded`. Thread it through `BashTask.id`, the `BashExecutor` Service Definition methods (`get`/`ownerOf`/`readOutput`/`kill`), the generation site in `dsh-bash-local` (brand the counter output once, at creation), and the `dsh-tool-bash` validate/access surface (`validateTaskId` returns a `BashTaskId`; `task_id` is branded at the tool boundary where the model's string arrives). +- **Brand the bash job id.** Add `BashTaskId = Branded<'BashTaskId'>` plus its same-named factory in `packages/shell/shell/src/types.ts` (the package that *owns* the id), importing `Branded` from `@deepseek-ai/dsh-brand` exactly as `SessionId` does. The brand primitive lives in the dependency-free `dsh-brand` utility package precisely so `dsh-shell` can brand its ids by depending on it alone — it never pulls in `dsh-llm` (or `dsh-session`) just to reach `Branded`. Thread it through `BashTask.id`, the `ShellExecutor` Service Definition methods (`get`/`ownerOf`/`readOutput`/`kill`), the generation site in `dsh-bash-local` (brand the counter output once, at creation), and the `dsh-tool-bash` validate/access surface (`validateJobId` returns a `BashTaskId`; `job_id` is branded at the tool boundary where the model's string arrives). -- **Mint a distinct `OwnerToken` brand.** Add `OwnerToken = Branded<'OwnerToken'>` in `packages/bash/bash/src/types.ts`; type `BashExecRequest.owner` / `BashExecSpec.owner` / `BashExecutor.ownerOf` as `OwnerToken | undefined`. The `dsh-tool-bash` consumer casts the agent's shared `id` (`SessionId`) into an `OwnerToken` at the boundary — the one place the two vocabularies meet. The bash Service Definition never imports `dsh-session`. (Rationale in the next section.) +- **Mint a distinct `OwnerToken` brand.** Add `OwnerToken = Branded<'OwnerToken'>` in `packages/shell/shell/src/types.ts`; type `ShellExecRequest.owner` / `ShellExecSpec.owner` / `ShellExecutor.ownerOf` as `OwnerToken | undefined`. The `dsh-tool-bash` consumer casts the agent's shared `id` (`SessionId`) into an `OwnerToken` at the boundary — the one place the two vocabularies meet. The bash Service Definition never imports `dsh-session`. (Rationale in the next section.) - **Stop the brand erosion.** Propagate the existing brands to the `Map` key types and public method params listed under Gap 2 — `Map`, `Map`, `get(id: SessionId)`, `Map`, ACP's `SessionId` surface, and the coordinator's `Map`. This is the larger mechanical share of the change and the part that makes the *existing* brands actually load-bearing on lookups, not just on struct fields. @@ -46,21 +46,21 @@ export function OwnerToken(id: string): OwnerToken { ### Why not typing `owner` as `SessionId`? -The obvious shortcut is to type `owner` as `SessionId` directly — it always *is* one. We reject that. The bash executor seam is a capability seam (Service Definition `dsh-bash`, Service provider `dsh-bash-local`, Consumer `dsh-tool-bash`) and its owner token is *documented as deliberately opaque*: the executor "never interprets it (no access policy lives in the seam — that is the consumer's job)" (`packages/bash/bash/src/types.ts`). Typing the Service Definition's field as `SessionId` would import `dsh-session`'s vocabulary into a package that must not know what an owner token *means* — it would couple a generic execution backend to the session model and contradict the opaque-token design. A sandboxed or remote executor that replaces `dsh-bash-local` should not inherit a session dependency. The distinct `OwnerToken` brand keeps the seam decoupled: `dsh-bash` knows only "an owner is some opaque branded token," and the `dsh-tool-bash` consumer — which already decides the access policy — is the single boundary that casts its `SessionId` into an `OwnerToken`. The brand still delivers the safety win (you cannot pass a `BashTaskId` or a raw string where an owner is expected) without the coupling. +The obvious shortcut is to type `owner` as `SessionId` directly — it always *is* one. We reject that. The bash executor seam is a capability seam (Service Definition `dsh-shell`, Service provider `dsh-bash-local`, Consumer `dsh-tool-bash`) and its owner token is *documented as deliberately opaque*: the executor "never interprets it (no access policy lives in the seam — that is the consumer's job)" (`packages/shell/shell/src/types.ts`). Typing the Service Definition's field as `SessionId` would import `dsh-session`'s vocabulary into a package that must not know what an owner token *means* — it would couple a generic execution backend to the session model and contradict the opaque-token design. A sandboxed or remote executor that replaces `dsh-bash-local` should not inherit a session dependency. The distinct `OwnerToken` brand keeps the seam decoupled: `dsh-shell` knows only "an owner is some opaque branded token," and the `dsh-tool-bash` consumer — which already decides the access policy — is the single boundary that casts its `SessionId` into an `OwnerToken`. The brand still delivers the safety win (you cannot pass a `BashTaskId` or a raw string where an owner is expected) without the coupling. ## Out of scope / possible extensions Kept deliberately narrow per the "not every string needs a brand" policy. Each of these is a plausible future brand, deferred with a reason, not a commitment: -- **`ModelId`** (`GenerateOptions.model`, the `LlmService` adapter-registry key) — a real cross-package lookup key (config → agent → llm → adapter); a reasonable next brand, left out only to keep this decision's blast radius focused. -- **`ToolName`** (the `ToolRegistry` key) — author-defined, human-readable, and rarely confused with another id; the weakest candidate, likely not worth a brand. +- **`ModelId`** (`GenerateOptions.model`, the `LlmRuntime` adapter-registry key) — a real cross-package lookup key (config → agent → llm → adapter); a reasonable next brand, left out only to keep this decision's blast radius focused. +- **`ToolName`** (the `ToolRuntime` key) — author-defined, human-readable, and rarely confused with another id; the weakest candidate, likely not worth a brand. - **`ErrorCode`** (`HarnessError.code`) — a closed vocabulary (`ABORTED`, `NO_ADAPTER`, …), not a per-instance id; better served by a string-literal union than a brand, if anything. - **Numeric ordinals** — turn number, step number, and the event `seq` are `number`, not `string`, so `Branded` does not apply; a parallel `number & { readonly [BRAND]: B }` variant could brand them, but they are positional ordinals rarely passed across boundaries, so the payoff is low. - **Validated construction** — the brand factories are pure casts with no runtime check, and every boundary (ACP `sessionId`, provider-issued `call.id`, the empty-string fallback in `dsh-llm-deepseek`) trusts the raw string today. A `SessionId.parse()` / `isValid()` companion that throws on malformed input at boundaries is a genuine gap, but it is a *runtime-behavior* change with its own design (what is "malformed"? what happens on failure?) and belongs in its own decision, not bundled into this type-only change. ## Verification -The landed invariants: `BashTaskId` and `OwnerToken` are defined in `dsh-bash` and threaded end-to-end (Service Definition, the `dsh-bash-local` generation site, the `dsh-tool-bash` model-facing tool) with no `dsh-bash` dependency on `dsh-session`; no collection keyed by an in-scope branded id (`CallId`/`SessionId`/`BashTaskId`) is keyed by bare `string`; public method params and exported signatures keep the brand; and brands are constructed via the cast factory at each boundary where a raw string enters (provider call id, ACP session id, model-supplied `task_id`), never as scattered `as` casts. +The landed invariants: `BashTaskId` and `OwnerToken` are defined in `dsh-shell` and threaded end-to-end (Service Definition, the `dsh-bash-local` generation site, the `dsh-tool-bash` model-facing tool) with no `dsh-shell` dependency on `dsh-session`; no collection keyed by an in-scope branded id (`CallId`/`SessionId`/`BashTaskId`) is keyed by bare `string`; public method params and exported signatures keep the brand; and brands are constructed via the cast factory at each boundary where a raw string enters (provider call id, ACP session id, model-supplied `job_id`), never as scattered `as` casts. ## Consequences diff --git a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md index 824d802b4a..11a78df609 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-20-branded-ids.zh.md @@ -8,9 +8,9 @@ Status: implemented harness 使用 `Branded = string & { readonly [BRAND]: B }` 机制,为 `CallId`(`packages/llm/llm/src/brand.ts`)和 agent(智能体)/会话共享的 `SessionId`(`packages/core/session/src/types.ts`)做 brand 处理;该机制由纯类型包 `@deepseek-ai/dsh-brand` 拥有,位于 `packages/util/brand/`,见其 [README](../../../../packages/util/brand/README.md),并为每个类型提供零开销的 cast 工厂。`dsh-brand` 还声明了治理策略:*「Branding 用于跨包边界且可能被混淆的 id;不是每个 string 都需要 brand。」* 这条策略是正确的;问题在于它只落实了一半。两处缺口使得结构相同但语义错误的 string 今天仍能通过类型检查器。 -**缺口 1:bash seam 中未 brand 的跨边界 ID。** 后台 task id 是普通 `string`:`BashTask.id: string`(`packages/bash/bash/src/types.ts`),作为 `string` 贯穿整个执行器 seam(`packages/bash/bash/src/index.ts` 中的 `BashExecutor.get`/`ownerOf`/`readOutput`/`kill(id: string)`),再由面向模型的工具以 `string` 校验并传递(`validateTaskId`、`assertTaskAccess`、`packages/bash/tool-bash/src/index.ts` 中 `task_id` 的 schema 参数)。它由每执行器计数器生成——`packages/bash/bash-local/src/index.ts` 中的 `` `bash-${this.nextTaskId++}` ``——其形状与 `SessionId` 的默认值**完全相同,都是 `name-N`**(`packages/core/session/src/index.ts` 中的 `` `session-${++counter}` ``)。bash task id 和会话 id 在调用点轻易就能互换,而编译器毫无反应。它是面向模型的 id(模型会把 `task_id` 传回 `bash_output`/`bash_kill`),所以该混淆可由不受信任的输入触达。 +**缺口 1:bash seam 中未 brand 的跨边界 ID。** 后台 job id 是普通 `string`:`BashTask.id: string`(`packages/shell/shell/src/types.ts`),作为 `string` 贯穿整个执行器 seam(`packages/shell/shell/src/index.ts` 中的 `ShellExecutor.get`/`ownerOf`/`readOutput`/`kill(id: string)`),再由面向模型的工具以 `string` 校验并传递(`validateJobId`、`assertTaskAccess`、`packages/shell/tool-bash/src/index.ts` 中 `job_id` 的 schema 参数)。它由每执行器计数器生成——`packages/shell/bash-local/src/index.ts` 中的 `` `bash-${this.nextTaskId++}` ``——其形状与 `SessionId` 的默认值**完全相同,都是 `name-N`**(`packages/core/session/src/index.ts` 中的 `` `session-${++counter}` ``)。bash job id 和会话 id 在调用点轻易就能互换,而编译器毫无反应。它是面向模型的 id(模型会把 `job_id` 传回 `bash_output`/`bash_kill`),所以该混淆可由不受信任的输入触达。 -bash **owner token** 是相关的子情形:`BashExecRequest.owner?: string` 和 `BashExecSpec.owner: string | undefined`(`packages/bash/bash/src/types.ts`)被文档描述为刻意*不透明*的隔离键,但在所有实际调用方中,该值就是所属 agent 共享的 `Agent.id`/`SessionId`(`callerToken = (exec) => exec.agent?.id`,位于 `packages/bash/tool-bash/src/index.ts`),只是披着另一个 seam 本地名称。它被用于访问控制比较(`owner !== callerToken(exec)`),因此一个不匹配但类型正确的 string 在此处就是跨会话隔离 bug,而当前类型系统无法捕获。这正是[统一 agent/session 标识决策](../simplification/2026-06-20-unify-agent-and-session-id.md)覆盖的共享 id 别名。 +bash **owner token** 是相关的子情形:`ShellExecRequest.owner?: string` 和 `ShellExecSpec.owner: string | undefined`(`packages/shell/shell/src/types.ts`)被文档描述为刻意*不透明*的隔离键,但在所有实际调用方中,该值就是所属 agent 共享的 `Agent.id`/`SessionId`(`callerToken = (exec) => exec.agent?.id`,位于 `packages/shell/tool-bash/src/index.ts`),只是披着另一个 seam 本地名称。它被用于访问控制比较(`owner !== callerToken(exec)`),因此一个不匹配但类型正确的 string 在此处就是跨会话隔离 bug,而当前类型系统无法捕获。这正是[统一 agent/session 标识决策](../simplification/2026-06-20-unify-agent-and-session-id.md)覆盖的共享 id 别名。 **缺口 2:*已经 brand* 的 ID 在边界处被侵蚀。** 就连 `CallId` 和 `SessionId` 也恰好在最容易混淆的地方退化为裸 `string`:注册表/store 键类型和公开方法参数。代表性位置包括会话存储、agent 注册表(二者都以共享的 `SessionId` 为键)、工具展示层的 call-id map、ACP(Agent Client Protocol)的会话记录,以及持久化协调器。在集合键处丢弃 brand,会让既有 brand 在查找时毫无价值;它们的价值只实现了一部分。 @@ -18,9 +18,9 @@ bash **owner token** 是相关的子情形:`BashExecRequest.owner?: string` 纯类型变更。Brand 是零开销 cast;运行时行为、序列化、比较和协议格式(wire format)均不变。该决策分三部分,全部遵循既有的「不是每个 string 都需要」策略。 -- **为 bash task id 加 brand。** 在 `packages/bash/bash/src/types.ts`(*拥有*该 id 的包)中添加 `BashTaskId = Branded<'BashTaskId'>` 及其同名工厂,从 `@deepseek-ai/dsh-brand` 导入 `Branded`,方式与 `SessionId` 完全一致。brand 原语位于无依赖的 `dsh-brand` 工具包中,正是为了让 `dsh-bash` 仅依赖它就能为自己的 id 加 brand,而无需引入 `dsh-llm`(或 `dsh-session`)来获取 `Branded`。将其贯穿 `BashTask.id`、`BashExecutor` Service Definition 方法(`get`/`ownerOf`/`readOutput`/`kill`)、`dsh-bash-local` 中的生成点(在创建时对计数器输出做一次 brand),以及 `dsh-tool-bash` 的校验/访问面(`validateTaskId` 返回 `BashTaskId`;`task_id` 在模型 string 到达的工具边界处被 brand)。 +- **为 bash job id 加 brand。** 在 `packages/shell/shell/src/types.ts`(*拥有*该 id 的包)中添加 `BashTaskId = Branded<'BashTaskId'>` 及其同名工厂,从 `@deepseek-ai/dsh-brand` 导入 `Branded`,方式与 `SessionId` 完全一致。brand 原语位于无依赖的 `dsh-brand` 工具包中,正是为了让 `dsh-shell` 仅依赖它就能为自己的 id 加 brand,而无需引入 `dsh-llm`(或 `dsh-session`)来获取 `Branded`。将其贯穿 `BashTask.id`、`ShellExecutor` Service Definition 方法(`get`/`ownerOf`/`readOutput`/`kill`)、`dsh-bash-local` 中的生成点(在创建时对计数器输出做一次 brand),以及 `dsh-tool-bash` 的校验/访问面(`validateJobId` 返回 `BashTaskId`;`job_id` 在模型 string 到达的工具边界处被 brand)。 -- **铸造独立的 `OwnerToken` brand。** 在 `packages/bash/bash/src/types.ts` 中添加 `OwnerToken = Branded<'OwnerToken'>`;将 `BashExecRequest.owner` / `BashExecSpec.owner` / `BashExecutor.ownerOf` 的类型标注为 `OwnerToken | undefined`。`dsh-tool-bash` 消费方在边界处将 agent 共享的 `id`(`SessionId`)cast 为 `OwnerToken`——这是两套词汇唯一交汇的地方。bash Service Definition 从不导入 `dsh-session`。(理由见下一节。) +- **铸造独立的 `OwnerToken` brand。** 在 `packages/shell/shell/src/types.ts` 中添加 `OwnerToken = Branded<'OwnerToken'>`;将 `ShellExecRequest.owner` / `ShellExecSpec.owner` / `ShellExecutor.ownerOf` 的类型标注为 `OwnerToken | undefined`。`dsh-tool-bash` 消费方在边界处将 agent 共享的 `id`(`SessionId`)cast 为 `OwnerToken`——这是两套词汇唯一交汇的地方。bash Service Definition 从不导入 `dsh-session`。(理由见下一节。) - **阻止 brand 侵蚀。** 将既有 brand 传播到缺口 2 列出的 `Map` 键类型和公开方法参数中:`Map`、`Map`、`get(id: SessionId)`、`Map`、ACP 的 `SessionId` surface、协调器的 `Map`。这是变更中机械量最大的部分,也是让*既有* brand 在查找处真正发挥作用(而不仅仅标注在结构体字段上)的关键。 @@ -46,21 +46,21 @@ export function OwnerToken(id: string): OwnerToken { ### 为什么不把 `owner` 类型标注为 `SessionId`? -显而易见的捷径是直接把 `owner` 类型标注为 `SessionId`——它确实*总是*一个会话 id。我们否决这个方案。bash 执行器 seam 是能力 seam(Service Definition `dsh-bash`、Service provider `dsh-bash-local`、Consumer `dsh-tool-bash`),其 owner token 被*明确记录为刻意不透明*:执行器「从不解释它(seam 中没有访问策略——那是消费方的职责)」(`packages/bash/bash/src/types.ts`)。把 Service Definition 的字段类型标注为 `SessionId`,会把 `dsh-session` 的词汇引入一个不应知道 owner token *含义*的包——这会让通用执行后端耦合会话模型,并违背不透明 token 的设计。取代 `dsh-bash-local` 的沙箱化执行器或远程执行器不应继承会话依赖。独立的 `OwnerToken` brand 使 seam 保持解耦:`dsh-bash` 只知道「owner 是某种带 brand 的不透明 token」,而已经决定访问策略的 `dsh-tool-bash` 消费方,是把其 `SessionId` cast 为 `OwnerToken` 的唯一边界。该 brand 仍带来安全收益(不能把 `BashTaskId` 或裸 string 传到 owner 位置),且不引入耦合。 +显而易见的捷径是直接把 `owner` 类型标注为 `SessionId`——它确实*总是*一个会话 id。我们否决这个方案。bash 执行器 seam 是能力 seam(Service Definition `dsh-shell`、Service provider `dsh-bash-local`、Consumer `dsh-tool-bash`),其 owner token 被*明确记录为刻意不透明*:执行器「从不解释它(seam 中没有访问策略——那是消费方的职责)」(`packages/shell/shell/src/types.ts`)。把 Service Definition 的字段类型标注为 `SessionId`,会把 `dsh-session` 的词汇引入一个不应知道 owner token *含义*的包——这会让通用执行后端耦合会话模型,并违背不透明 token 的设计。取代 `dsh-bash-local` 的沙箱化执行器或远程执行器不应继承会话依赖。独立的 `OwnerToken` brand 使 seam 保持解耦:`dsh-shell` 只知道「owner 是某种带 brand 的不透明 token」,而已经决定访问策略的 `dsh-tool-bash` 消费方,是把其 `SessionId` cast 为 `OwnerToken` 的唯一边界。该 brand 仍带来安全收益(不能把 `BashTaskId` 或裸 string 传到 owner 位置),且不引入耦合。 ## 不在范围内 / 可能的扩展 遵循「不是每个 string 都需要 brand」的策略,刻意保持窄范围。以下每项都是合理的未来 brand 候选,附带推迟理由而非承诺: -- **`ModelId`**(`GenerateOptions.model`,`LlmService` 适配器注册表的键):一个真正的跨包查找键(config → agent → llm → 适配器);合理的下一个 brand,仅为控制本决策的影响范围而暂不纳入。 -- **`ToolName`**(`ToolRegistry` 的键):由作者定义、人类可读,且很少与其他 id 混淆;最弱的候选,可能不值得加 brand。 +- **`ModelId`**(`GenerateOptions.model`,`LlmRuntime` 适配器注册表的键):一个真正的跨包查找键(config → agent → llm → 适配器);合理的下一个 brand,仅为控制本决策的影响范围而暂不纳入。 +- **`ToolName`**(`ToolRuntime` 的键):由作者定义、人类可读,且很少与其他 id 混淆;最弱的候选,可能不值得加 brand。 - **`ErrorCode`**(`HarnessError.code`):一个封闭词汇(`ABORTED`、`NO_ADAPTER`……),不是逐实例的 id;如果要做,string 字面量联合类型比 brand 更合适。 - **数值序号**:轮次号、步骤号和事件 `seq` 是 `number` 而非 `string`,`Branded` 不适用;可以用并行的 `number & { readonly [BRAND]: B }` 变体来 brand 它们,但它们是位置序号、很少跨边界传递,收益较低。 - **带校验的构造**:brand 工厂是纯 cast,无运行时检查,且每个边界(ACP `sessionId`、提供方签发的 `call.id`、`dsh-llm-deepseek` 中的空字符串回退)今天都信任裸 string。一个在边界处对格式错误的输入抛异常的 `SessionId.parse()` / `isValid()` 配套工具确实是缺口,但它是*运行时行为*变更,有自己的设计问题(什么算「格式错误」?失败时会怎样?),应在独立决策中处理,不应捆绑进这次纯类型变更。 ## 验证 -已落地的不变式如下:`BashTaskId` 和 `OwnerToken` 定义在 `dsh-bash` 中,并端到端贯穿 Service Definition、`dsh-bash-local` 生成点与 `dsh-tool-bash` 面向模型的工具,且 `dsh-bash` 未添加对 `dsh-session` 的依赖;没有任何以范围内 brand id(`CallId`/`SessionId`/`BashTaskId`)为键的集合使用裸 `string`;公开方法参数和导出签名保留 brand;每个原始 string 进入的边界(提供方 call id、ACP 会话 id、模型提供的 `task_id`)都通过 cast 工厂构造 brand,而不是散落的 `as` cast。 +已落地的不变式如下:`BashTaskId` 和 `OwnerToken` 定义在 `dsh-shell` 中,并端到端贯穿 Service Definition、`dsh-bash-local` 生成点与 `dsh-tool-bash` 面向模型的工具,且 `dsh-shell` 未添加对 `dsh-session` 的依赖;没有任何以范围内 brand id(`CallId`/`SessionId`/`BashTaskId`)为键的集合使用裸 `string`;公开方法参数和导出签名保留 brand;每个原始 string 进入的边界(提供方 call id、ACP 会话 id、模型提供的 `job_id`)都通过 cast 工厂构造 brand,而不是散落的 `as` cast。 ## 后果 diff --git a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml index 088891c738..3480f0b231 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md -2026-06-20-generic-long-running-tool-runtime.md: 1edc3422c253e06178a5c8ebf68dfd4ef1289e31 -2026-06-20-generic-long-running-tool-runtime.zh.md: 1f5349b2aeb02e8db21150012300a6d0a0493ae1 +2026-06-20-generic-long-running-tool-runtime.md: 7db43323dd83f8e99698a7163412c9b33a607dfb +2026-06-20-generic-long-running-tool-runtime.zh.md: fe12ef264d294224fcfaf503ba958fa90c782347 diff --git a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md index 1edc3422c2..7db43323dd 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md +++ b/.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md @@ -1,4 +1,4 @@ -# Agent Note: The background task runtime (`ctx.tasks`) and generic task control tools +# Agent Note: The background job runtime (`ctx.jobs`) and generic task control tools Status: implemented @@ -6,38 +6,38 @@ English | [中文](2026-06-20-generic-long-running-tool-runtime.zh.md) ## Problem -Background bash originally combined two responsibilities: the bash executor ran processes and also managed task ids, ownership, incremental reads, cancellation, completion listeners, and model-facing control tools. Adding background subagents required the same lifecycle and interaction contract. Implementing that contract independently for every long-running capability would duplicate isolation, cleanup, notification, and prompt behavior while teaching the model a different collect-and-stop protocol for each producer. +Background bash originally combined two responsibilities: the bash executor ran processes and also managed job ids, ownership, incremental reads, cancellation, completion listeners, and model-facing control tools. Adding background subagents required the same lifecycle and interaction contract. Implementing that contract independently for every long-running capability would duplicate isolation, cleanup, notification, and prompt behavior while teaching the model a different collect-and-stop protocol for each producer. -The task registry, control tools, and completion notices form one harness capability. Bash and subagents should supply execution-specific hooks without owning generic task behavior. +The job registry, control tools, and completion notices form one harness capability. Bash and subagents should supply execution-specific hooks without owning generic task behavior. ## Decision -The `tasks/` package group owns background-task semantics: +The `jobs/` package group owns background-job semantics: -- `@deepseek-ai/dsh-tasks` registers running work as `ctx.tasks` and owns task ids, authorization, snapshots, reads, cancellation, waiting, completion listeners, and cleanup. -- `@deepseek-ai/dsh-tool-tasks` exposes `task_output`, `task_list`, and `task_kill`, injects completion notices, and supplies the background-task system-prompt guidance. +- `@deepseek-ai/dsh-jobs` registers running work as `ctx.jobs` and owns job ids, authorization, snapshots, reads, cancellation, waiting, completion listeners, and cleanup. +- `@deepseek-ai/dsh-tool-jobs` exposes `job_output`, `job_list`, and `job_kill`, injects completion notices, and supplies the background-job system-prompt guidance. -Long-running tools are producers. `dsh-tool-bash` adapts a `BashProcess` into incremental output and process cancellation; `dsh-tool-subagent` adapts a child run into final output and child disposal. The bash and subagent capability seams remain independent of sessions and the task registry. +Long-running tools are producers. `dsh-tool-bash` adapts a `ShellProcess` into incremental output and process cancellation; `dsh-tool-subagent` adapts a child run into final output and child disposal. The bash and subagent capability seams remain independent of sessions and the job registry. -`TaskService` is the Service Definition in `@deepseek-ai/dsh-tasks`; the process-local provider is `LocalTaskService` in `@deepseek-ai/dsh-tasks-local` (the [task-registry contract Agent Note](2026-07-26-task-registry-seam.md) records that split). +`JobRegistry` is the Service Definition in `@deepseek-ai/dsh-jobs`; the process-local provider is `LocalJobRegistry` in `@deepseek-ai/dsh-jobs-local` (the [task-registry contract Agent Note](2026-07-26-job-registry-seam.md) records that split). ## Runtime contract -The literal types live on the [tasks subsystem page](../../../../docs/subsystems/tasks.md). A producer calls `ctx.tasks.start()` with a kind, label, optional owning `Agent`, optional positive `outputLimitBytes`, and a `run()` function. The runtime completes all failable preflight work before calling `run()` and invokes it once. After `run()` returns hooks, registration commits without another failable step; a producer cannot start work that lacks a collectable task id. +The literal types live on the [tasks subsystem page](../../../../docs/subsystems/jobs.md). A producer calls `ctx.jobs.start()` with a kind, label, optional owning `Agent`, optional positive `outputLimitBytes`, and a `run()` function. The runtime completes all failable preflight work before calling `run()` and invokes it once. After `run()` returns hooks, registration commits without another failable step; a producer cannot start work that lacks a collectable job id. -The process-local provider also owns bounded admission, whose rationale is recorded in the [bounded background task admission decision](../bug-fix/2026-08-11-bounded-background-task-admission.md). Its positive-safe-integer `maxConcurrentTasksPerOwner` config defaults to `10`; `start()` derives each exact `Agent` object's active count from `running` and `stopping` records, while every unowned task shares one service bucket. Capacity rejection occurs before `run()` and id allocation, and producer `done` settlement is the only event that releases a stopping task's place. The provider does not queue, preempt, or retain a second mutable count. +The process-local provider also owns bounded admission, whose rationale is recorded in the [bounded background job admission decision](../bug-fix/2026-08-11-bounded-background-job-admission.md). Its positive-safe-integer `maxConcurrentJobsPerOwner` config defaults to `10`; `start()` derives each exact `Agent` object's active count from `running` and `stopping` records, while every unowned task shares one service bucket. Capacity rejection occurs before `run()` and id allocation, and producer `done` settlement is the only event that releases a stopping task's place. The provider does not queue, preempt, or retain a second mutable count. -`outputLimitBytes` is producer-owned presentation policy, not a registry buffer. The registry validates and projects it unchanged into `TaskSnapshot`; generic control APIs apply the cap to complete model-facing output after adding their own status or notice metadata. Omitting it preserves the existing controller behavior, so the runtime does not impose a hidden default on unrelated producer families. +`outputLimitBytes` is producer-owned presentation policy, not a registry buffer. The registry validates and projects it unchanged into `JobSnapshot`; generic control APIs apply the cap to complete model-facing output after adding their own status or notice metadata. Omitting it preserves the existing controller behavior, so the runtime does not impose a hidden default on unrelated producer families. -A model-facing producer exposes that committed id in its canonical success value, normally `{ kind: 'background', taskId }`; Native rendering may keep human-readable prose. A pre-aborted background call fails rather than returning a no-op because no task exists to satisfy the promised handle. Once registration publishes the id, cancellation belongs to the task's own controller and the task runtime: later cancellation of the producing tool call must not kill the published task. `task_kill`, owner disposal, and service teardown request cancellation; foreground execution remains coupled to the call's `exec.signal`. +A model-facing producer exposes that committed id in its canonical success value, normally `{ kind: 'background', jobId }`; Native rendering may keep human-readable prose. A pre-aborted background call fails rather than returning a no-op because no task exists to satisfy the promised handle. Once registration publishes the id, cancellation belongs to the task's own controller and the job runtime: later cancellation of the producing tool call must not kill the published task. `job_kill`, owner disposal, and service teardown request cancellation; foreground execution remains coupled to the call's `exec.signal`. The producer hooks define three responsibilities: - `cancel(reason?)` synchronously requests termination, is idempotent, and must cause `done` to settle. - `done` never rejects and settles only after the producer has released the task's resources. -- Optional `readOutput()` returns the next consuming output delta. Omitting it declares a final-output task whose terminal result comes from `TaskOutcome.output`. +- Optional `readOutput()` returns the next consuming output delta. Omitting it declares a final-output task whose terminal result comes from `JobOutcome.output`. -Statuses are `running`, `stopping`, `completed`, `killed`, and `failed`. Producer-specific information such as an exit code or stop reason belongs in `detail`; the registry does not interpret it. Task kinds form a merge-extensible string union, and task ids are branded and generated as `-N`, with a counter per kind. +Statuses are `running`, `stopping`, `completed`, `killed`, and `failed`. Producer-specific information such as an exit code or stop reason belongs in `detail`; the registry does not interpret it. Task kinds form a merge-extensible string union, and job ids are branded and generated as `-N`, with a counter per kind. The runtime attaches one continuation to `done`, records the first terminal outcome, resolves waiters, and invokes completion listeners with per-listener error containment. First-wins settlement matters during teardown: if `cancel` throws, the runtime force-fails the record and warns that work may be orphaned rather than waiting forever for a promise that may never settle. A later producer outcome cannot overwrite that diagnosis or notify twice. A `cancel` that returns without eventually settling `done` still blocks teardown because the runtime cannot distinguish it from a slow, valid stop. @@ -45,7 +45,7 @@ Task registrations are not effects of the producer tool fiber. Reloading a tool ## Authorization and owner lifecycle -Task ids are runtime-global and predictable, so every access is authorized by the registry. `get`, `read`, `wait`, and `kill` accept the calling `Agent`; `list` returns only tasks visible to that caller. An owned task is accessible only to the exact owning session. Unowned tasks are open to non-agent callers and die with the task service. +Job ids are runtime-global and predictable, so every access is authorized by the registry. `get`, `read`, `wait`, and `kill` accept the calling `Agent`; `list` returns only tasks visible to that caller. An owned task is accessible only to the exact owning session. Unowned tasks are open to non-agent callers and die with the task service. The snapshot stores the owner's branded `SessionId` for authorization, while lifecycle operations retain the exact live `Agent` instance. These identities serve different purposes: session equality grants access, but exact object identity selects cleanup and completion delivery. Reusing an agent or session id cannot redirect an old scope's cleanup or notices to a replacement. @@ -55,45 +55,45 @@ For contract-compliant producers, `AgentHandle.dispose()` resolves only after ow ## Service API -`TaskService` provides: +`JobRegistry` provides: - `start(spec)` for preflighted, provider-admitted, atomic registration. - `get(id, caller?)` and `list(caller?)` for non-consuming snapshots. - `read(id, caller?)` for a consuming stream delta or an idempotent final result. - `kill(id, caller?, reason?)` for cancellation. - `wait(id, timeoutMs, caller?, signal?)` for bounded terminal waiting. -- `onTaskDone(listener)` for effect-scoped observation with exact-owner delivery and listener containment. +- `onJobDone(listener)` for effect-scoped observation with exact-owner delivery and listener containment. - `attachController(name)` for the task-controller availability fence. `wait` returns the terminal snapshot when the task settles or the live snapshot when its timeout expires. Aborting a wait cancels only that wait. If settlement has already assigned terminal delivery to the waiter, the terminal snapshot still wins. Waiters unregister synchronously on abort so a same-tick settlement cannot suppress a completion notice on behalf of a reader that receives nothing. -A producer loaded without any controller would let callers start work they cannot collect or stop. `dsh-tool-tasks` therefore calls `attachController()` for its lifetime, and `start()` fails before producer execution when no controller is attached. This check occurs at start rather than plugin load because sibling plugins may activate concurrently. Custom non-model controllers can attach themselves without teaching the registry tool names. +A producer loaded without any controller would let callers start work they cannot collect or stop. `dsh-tool-jobs` therefore calls `attachController()` for its lifetime, and `start()` fails before producer execution when no controller is attached. This check occurs at start rather than plugin load because sibling plugins may activate concurrently. Custom non-model controllers can attach themselves without teaching the registry tool names. ## Model-facing control API -`dsh-tool-tasks` registers three kind-independent tools with generic UI cards: +`dsh-tool-jobs` registers three kind-independent tools with generic UI cards: -- `task_output(task_id, wait?, timeout_ms?)` reads output and always appends `[status: ...]`. Stream tasks return only output since the previous read; final-output tasks return their result after settlement. Reads are non-blocking unless `wait: true`, whose timeout is defaulted and capped by plugin config. A wait timeout reports the still-running status and does not stop the task. -- `task_list()` returns caller-visible tasks as ` []