Merge remote-tracking branch 'origin/master' into worktree/remove-interjection-caption
This commit is contained in:
@@ -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/README.md
|
||||
README.md: 3cfbb5154713046846a3bfcb2ccea62c0e4cb6c0
|
||||
README.md: d3a8943a78238d974d54028e38b773e932429b0e
|
||||
README.zh.md: 4b3a1ee57ea61a8e8ba4d01cf7c719bbf8440e30
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
One kind of design doc lives here. An **Agent Note** records a decision or proposal that shapes this codebase — the *why* and *what we gave up*, the parts code and docs can't carry. This file is the front door and contract: where Agent Notes live, when to write one, and [the in-file format](#the-file-format).
|
||||
One kind of design doc lives here. An **Agent Note** records a decision or proposal that shapes this codebase — the *why* and *what we gave up*, the parts code and docs can't carry. This file is the entry point and contract: where Agent Notes live, when to write one, and [the in-file format](#the-file-format).
|
||||
|
||||
## Layout and naming
|
||||
|
||||
|
||||
@@ -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-07-15-llm-model-catalog-and-acp-selection.md
|
||||
2026-07-15-llm-model-catalog-and-acp-selection.md: 77f8e379e2b07ecf4e67fa7197752543cfedd6dd
|
||||
2026-07-15-llm-model-catalog-and-acp-selection.zh.md: ce4a04a66e345834bc2b16b743be89dc7a0b9424
|
||||
2026-07-15-llm-model-catalog-and-acp-selection.md: bfd17c73b01319c10d5dc03333b3c726db5d6f33
|
||||
2026-07-15-llm-model-catalog-and-acp-selection.zh.md: aeddada5591bb2da2c0861acc368516eff148172
|
||||
|
||||
@@ -26,17 +26,17 @@ Catalog membership is advisory. It drives selectors and diagnostics but never ch
|
||||
|
||||
`dsh-llm-pi-ai` maps the configured provider's installed `getModels(provider)` entries into the neutral catalog. Its existing request-time catalog lookup remains authoritative and still rejects unknown models with `UNKNOWN_MODEL`. `dsh-llm-deepseek` accepts an optional `models` config containing display entries, defaulting to `deepseek-v4-flash` named `DeepSeek-V4-Flash` and `deepseek-v4-pro` named `DeepSeek-V4-Pro`. An explicit list replaces those defaults and an empty list disables discovery. The entries improve selector UX for known public or private models, while every unlisted model id continues to pass through unchanged.
|
||||
|
||||
### Per-session selection in the front door
|
||||
### Per-session selection in the front end
|
||||
|
||||
A selection is owned by the front door that offers it (today the TUI `/model` selector), never by `LlmService` or `AgentOptions`: those are deployment-wide or creation-wide objects, and mutating them would couple concurrent sessions. Each opaque choice carries the full provider/model pair, because the same model id may appear under multiple routes.
|
||||
A selection is owned by the front end that offers it (today the TUI `/model` selector), never by `LlmService` or `AgentOptions`: those are deployment-wide or creation-wide objects, and mutating them would couple concurrent sessions. Each opaque choice carries the full provider/model pair, because the same model id may appear under multiple routes.
|
||||
|
||||
The ACP automation transport is not a catalog consumer. Its deployment config supplies one optional provider/model target for newly created agents, and it advertises no model selector or configuration-option interface.
|
||||
|
||||
### Prompt/request consistency and durability
|
||||
|
||||
`installModelSelection` (in `dsh-agent`) installs scoped `system-prompt/assemble` and `agent/request` listeners for a front-door-owned selection. Prompt assembly snapshots the selected pair once per step, overwrites the assembled `provider` and `model` variables after downstream prompt listeners, and the request listener applies that same snapshot after downstream request listeners. A selection during asynchronous assembly therefore starts on the next step rather than splitting prompt text from routing. Other call-config fields remain untouched.
|
||||
`installModelSelection` (in `dsh-agent`) installs scoped `system-prompt/assemble` and `agent/request` listeners for a front-end-owned selection. Prompt assembly snapshots the selected pair once per step, overwrites the assembled `provider` and `model` variables after downstream prompt listeners, and the request listener applies that same snapshot after downstream request listeners. A selection during asynchronous assembly therefore starts on the next step rather than splitting prompt text from routing. Other call-config fields remain untouched.
|
||||
|
||||
The request header remains the durable source of truth. When a selection is actually used, the existing full `request/header` snapshot records it, and a front door initializes its selection from the folded last request header before falling back to creation options. A selection that is never used by a request is intentionally in-memory only because it never became model-visible state.
|
||||
The request header remains the durable source of truth. When a selection is actually used, the existing full `request/header` snapshot records it, and a front end initializes its selection from the folded last request header before falling back to creation options. A selection that is never used by a request is intentionally in-memory only because it never became model-visible state.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -26,17 +26,17 @@ ACP 选择还必须保留提供方维度。同一个模型 ID 可能存在于多
|
||||
|
||||
`dsh-llm-pi-ai` 将已配置提供方的安装目录 `getModels(provider)` 映射为中立目录。其现有请求时目录查询仍是权威依据,未知模型仍以 `UNKNOWN_MODEL` 失败。`dsh-llm-deepseek` 接受可选的 `models` 配置作为展示条目,默认包含名为 `DeepSeek-V4-Flash` 的 `deepseek-v4-flash` 和名为 `DeepSeek-V4-Pro` 的 `deepseek-v4-pro`。显式列表会替换这些默认值,空列表则关闭发现。这些条目改善已知公开或私有模型的选择体验,而所有未列出的模型 ID 仍会原样透传。
|
||||
|
||||
### 前门内的会话级选择
|
||||
### 前端内的会话级选择
|
||||
|
||||
选择由提供它的前门拥有(今天是 TUI 的 `/model` 选择器),而不由 `LlmService` 或 `AgentOptions` 拥有:它们是部署级或创建级对象,改动它们会把并发会话耦合在一起。每个不透明选项都携带完整的提供方/模型对,因为同一模型 ID 可能出现在多个路由下。
|
||||
选择由提供它的前端拥有(今天是 TUI 的 `/model` 选择器),而不由 `LlmService` 或 `AgentOptions` 拥有:它们是部署级或创建级对象,改动它们会把并发会话耦合在一起。每个不透明选项都携带完整的提供方/模型对,因为同一模型 ID 可能出现在多个路由下。
|
||||
|
||||
ACP 自动化传输层不是目录消费方。它通过部署配置为新创建的 agent 提供一个可选的提供方/模型目标,不展示模型选择器或配置选项接口。
|
||||
|
||||
### 提示词/请求一致性与持久化
|
||||
|
||||
`installModelSelection`(位于 `dsh-agent`)为前门拥有的选择安装 agent 作用域的 `system-prompt/assemble` 与 `agent/request` 监听器。提示词组装在每个步骤对所选组合做一次快照,在下游提示词监听器之后覆写组装出的 `provider` 与 `model` 变量;请求监听器在下游请求监听器之后应用同一快照。因此,发生在异步组装期间的选择会从下一个步骤生效,而不会让提示词文本与路由分裂。其他调用配置字段保持不变。
|
||||
`installModelSelection`(位于 `dsh-agent`)为前端拥有的选择安装 agent 作用域的 `system-prompt/assemble` 与 `agent/request` 监听器。提示词组装在每个步骤对所选组合做一次快照,在下游提示词监听器之后覆写组装出的 `provider` 与 `model` 变量;请求监听器在下游请求监听器之后应用同一快照。因此,发生在异步组装期间的选择会从下一个步骤生效,而不会让提示词文本与路由分裂。其他调用配置字段保持不变。
|
||||
|
||||
请求头仍是持久化的真源。当某个选择真正被使用时,现有的完整 `request/header` 快照会记录它;前门先从折叠后的最后一个请求头初始化其选择,然后才回退到创建选项。从未被请求使用的选择有意只保留在内存中,因为它从未成为模型可见状态。
|
||||
请求头仍是持久化的真源。当某个选择真正被使用时,现有的完整 `request/header` 快照会记录它;前端先从折叠后的最后一个请求头初始化其选择,然后才回退到创建选项。从未被请求使用的选择有意只保留在内存中,因为它从未成为模型可见状态。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
@@ -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-07-19-gui-layering-and-rpc-protocol.md
|
||||
2026-07-19-gui-layering-and-rpc-protocol.md: 7997a682c8745f7b3d0a9721acfa9355603f9b2e
|
||||
2026-07-19-gui-layering-and-rpc-protocol.zh.md: cb36b5cb725128e1c5067e5e69e52aa851668e52
|
||||
2026-07-19-gui-layering-and-rpc-protocol.md: 705b0df5feb5fedaae4d198aed71758b54586e93
|
||||
2026-07-19-gui-layering-and-rpc-protocol.zh.md: b28b08b4b9da058e01af62e610d4e226d794151f
|
||||
|
||||
@@ -31,7 +31,7 @@ Directories layer as follows:
|
||||
- **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dshClient` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
|
||||
- `apps/` holds the externally exported application shapes, assembled from Client / Host mixtures.
|
||||
- `apps/web` (`dsh-frontend`) is the vite application: a thin `main.ts` over the shell surface exported by `dsh-client-web`.
|
||||
- `apps/cli` (`@deepseek-ai/dsh`) dispatches shapes: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh run` = [a direct core Agent/Session front door](2026-08-09-headless-direct-core-front-door.md), with zero Host, HTTP, or browser layer.
|
||||
- `apps/cli` (`@deepseek-ai/dsh`) dispatches shapes: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh run` = [a direct core Agent/Session entry point](2026-08-09-headless-direct-core-entry-point.md), with zero Host, HTTP, or browser layer.
|
||||
- A future Electron shape reuses the same web client packages over an IPC fetch carrier.
|
||||
|
||||
```
|
||||
@@ -79,7 +79,7 @@ Packages under `packages/host/*` and `packages/client/*` **must carry the direct
|
||||
2. **Write an assembly module under `apps/`**: `startHost()` + a client subclass + the shape's private signal/print/exit semantics; a mixture never becomes a package — assembly is written in the app.
|
||||
3. **Import `dsh-host-webserver` only if you need HTTP carriage**, otherwise zero ports.
|
||||
|
||||
The two existing shapes preserve the boundary: the Web shape mounts Host, carrier, and browser composition, while `dsh run` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem, mount via `ctx.plugin(front-door plugin)` directly, and wear no fetch.
|
||||
The two existing shapes preserve the boundary: the Web shape mounts Host, carrier, and browser composition, while `dsh run` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem, mount via `ctx.plugin(entry-point plugin)` directly, and wear no fetch.
|
||||
|
||||
## Message protocol
|
||||
|
||||
@@ -242,7 +242,7 @@ Every client shape consumes one contract: adding a unary method is a five-step m
|
||||
|---|---|
|
||||
| Packaging by "product shape" (a web family, an electron family) | What shapes share is host/client capability, not the shape itself; capability-provider layering means a new shape needs zero new packages |
|
||||
| A package per mixture (e.g. a standalone headless package) | A mixture has exactly one consumer (its own app); packaging it is ownerless abstraction, while assembly in the app is readable and disposable |
|
||||
| Consuming clients connecting to ctx directly (skipping the apiproxy layer) | Client shapes require wire validation, observability, and multi-client consistency. Direct headless is a local front door with no client boundary and uses the public Agent/Session seams rather than a client command plane |
|
||||
| Consuming clients connecting to ctx directly (skipping the apiproxy layer) | Client shapes require wire validation, observability, and multi-client consistency. Direct headless is a local entry point with no client boundary and uses the public Agent/Session seams rather than a client command plane |
|
||||
| webserver depending on runtime (saving the handler injection) | Structural-typing injection keeps webserver reusable by sidecars/tests with zero workspace deps; a package dependency would drag assembly knowledge into the carrier layer |
|
||||
| Package names without the group prefix (continuing dsh-<tail>) | `dsh-runtime`/`dsh-web-ui` lose their belonging in the flat npm namespace; the cost is one explicit paths entry per package |
|
||||
| Reusing the in-repo JSON-RPC 2.0 (dsh-jsonrpc) | Numeric error codes degrade to a single fallback code, contracts get aligned by hand in two copies, and naming drifts without a convention |
|
||||
|
||||
@@ -29,7 +29,7 @@ Status: implemented
|
||||
- **fetch 到达插件包**(`ui-layout`、`ui-sidebar`、`ui-conversation`、`ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dshClient` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布(tsdown 闭包工厂 bundle)。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。
|
||||
- `apps/` 作为对外导出的应用形态入口,可以由 Client / Host 混合组装。
|
||||
- `apps/web`(`dsh-frontend`)是 vite 应用:`dsh-client-web` 导出的壳表面之上的一层薄 `main.ts`。
|
||||
- `apps/cli`(`@deepseek-ai/dsh`)做形态分发:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist;`dsh run` = [直接使用核心 Agent/Session 的前门](2026-08-09-headless-direct-core-front-door.md),不含 Host、HTTP 或浏览器层。
|
||||
- `apps/cli`(`@deepseek-ai/dsh`)做形态分发:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist;`dsh run` = [直接使用核心 Agent/Session 的入口](2026-08-09-headless-direct-core-entry-point.md),不含 Host、HTTP 或浏览器层。
|
||||
- 将来的 Electron 形态经由 IPC fetch 载体复用同一套 web client 包。
|
||||
|
||||
```
|
||||
@@ -77,7 +77,7 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
|
||||
2. **在 `apps/` 下写拼装模块**:`startHost()` + 客户端子类 + 该形态私有的信号/打印/退出语义;混合体不建包,拼装写在 app 里。
|
||||
3. **需要 HTTP 承载才 import `dsh-host-webserver`**,否则零端口。
|
||||
|
||||
现有两种形态保持这一边界:Web 形态挂载 Host、载体与浏览器组合,而 `dsh run` 挂载直接使用核心服务的 runner,不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(前门插件)` 挂载,不套 fetch。
|
||||
现有两种形态保持这一边界:Web 形态挂载 Host、载体与浏览器组合,而 `dsh run` 挂载直接使用核心服务的 runner,不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(入口插件)` 挂载,不套 fetch。
|
||||
|
||||
## 消息协议
|
||||
|
||||
@@ -240,7 +240,7 @@ export type ResponseValue<K> =
|
||||
|---|---|
|
||||
| 按「产品形态」分包(web 一族、electron 一族) | 形态间共享的是 host/client 两侧能力而非形态本身;能力支持方分层让新形态零新包 |
|
||||
| 混合体建包(如 headless 独立包) | 混合体只有一个消费者(它自己的 app),建包是无主抽象;拼装写在 app 里可读可弃 |
|
||||
| 消费型 client 直连 ctx(省 apiproxy 一层) | client 形态需要 wire 校验、观测与多 client 一致性。直接 headless 是没有 client 边界的本地前门,使用公开的 Agent/Session seam,而不是 client 命令面 |
|
||||
| 消费型 client 直连 ctx(省 apiproxy 一层) | client 形态需要 wire 校验、观测与多 client 一致性。直接 headless 是没有 client 边界的本地入口,使用公开的 Agent/Session seam,而不是 client 命令面 |
|
||||
| webserver 依赖 runtime(省 handler 注入) | 结构 typing 注入让 webserver 可被 sidecar/测试复用且零 workspace 依赖;包依赖会把装配知识拖进承载层 |
|
||||
| 包名不带组前缀(沿用 dsh-<尾段>) | `dsh-runtime`/`dsh-web-ui` 在扁平 npm 命名空间里失去归属信息;代价只是每包一条显式 paths |
|
||||
| 复用仓内 JSON-RPC 2.0(dsh-jsonrpc) | 数字错误码退化成单码兜底、约定双份人肉对齐、命名无 convention 自然漂移 |
|
||||
|
||||
@@ -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-07-24-web-config-tree-boot-and-transport-layering.md
|
||||
2026-07-24-web-config-tree-boot-and-transport-layering.md: 496499a691dbca012e5e953cbb6eb1d0bf25b635
|
||||
2026-07-24-web-config-tree-boot-and-transport-layering.zh.md: c4bed7b730cf37e1d90750f5c93fbccb920ced21
|
||||
2026-07-24-web-config-tree-boot-and-transport-layering.md: 92ec665acc745e61f656bd0e57454ad266b722f9
|
||||
2026-07-24-web-config-tree-boot-and-transport-layering.zh.md: 7cd96ad9e52c19a005e6bff356dc5159ad3a31bc
|
||||
|
||||
@@ -16,23 +16,23 @@ English | [中文](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md)
|
||||
|
||||
**Boot glue is a class pair.** `AppCLIEntry` (apps/cli) and `AppWebEntry` (the shell kernel) hold only what must exist independently of cordis: argv facts, the composed patch set, the parsed boot manifest, the module system instance, loading-page handles — everything else lives in plugins. `AppCLIEntry.run()` is three stages: layered env (ambient > cwd `.env` > `$DSH_HOME/.env`, closing the defect above) → patch composition → Loader include boot plus the activation audit. `AppWebEntry.run()` mirrors it browser-side: parse `window.__DSH_BOOT__` into a `BootManifest` (two views: npm-package rows for the module table, cordis-plugin rows for entry composition; malformed wire throws), build the module system, render the loading page, prefetch the `immediately` tier in parallel with Context/Loader setup, **await the prefetch before creating entries** (materialization is `tree.import`'s synchronous require, unprotected by fiber inject waiting; cross-package require edges such as i18n → runtime/client need every immediately-tier factory registered first — an empirically found 10–25% boot race otherwise), adopt the modules entry, create the graph rows, settle, sweep.
|
||||
|
||||
**Config sources have one declaration place each.** Bundle yml values are engineering defaults, Settings sections are writable user preferences, CLI flags address their owning launcher rows, and env values enter through yml `!!js` expressions. Patches replace a row's config wholesale. The resolved frontend `distIndex` uses that patch channel as an assembly fact. The transport-independent provider/model default belongs to `ctx.agentDefaultModel`; the [direct headless front door](2026-08-09-headless-direct-core-front-door.md) and the Web gateway consume the same state.
|
||||
**Config sources have one declaration place each.** Bundle yml values are engineering defaults, Settings sections are writable user preferences, CLI flags address their owning launcher rows, and env values enter through yml `!!js` expressions. Patches replace a row's config wholesale. The resolved frontend `distIndex` uses that patch channel as an assembly fact. The transport-independent provider/model default belongs to `ctx.agentDefaultModel`; the [direct headless entry point](2026-08-09-headless-direct-core-entry-point.md) and the Web gateway consume the same state.
|
||||
|
||||
**The transport splits five ways.** `dsh-host-apiproxy` is the gateway plugin (`api-gateway` row): it default-exports `ApiProxyService`, configures only `{workspaceRoot?}`, consumes the base layer's front-door-neutral `ctx.agentDefaultModel`, provides `ctx.apiProxy`, remains transport-agnostic, and registers no routes. `dsh-host-webserver` is a plain route-registration plugin: `HttpServerService` provides `ctx.httpServer` (`register(route) → disposer` with duplicate-pattern throw, `tapIndex` transforms applied in registration order, `port`), listens on activation, answers per-request failures with 400 and logging, and knows no harness concepts. The connection node half owns the `/api` binding from `ctx.apiProxy` through `toFetchHandler`. The modules node half (`ClientModuleHostService`, providing `ctx.clientModuleHost`) owns incremental package scanning, the bundle route, the index tap, and `onRebuilt`/`onGraphChanged` notification. The hmr node half owns dev reload through `fs.watchFile` membership and the `/plugins/events` SSE route.
|
||||
**The transport splits five ways.** `dsh-host-apiproxy` is the gateway plugin (`api-gateway` row): it default-exports `ApiProxyService`, configures only `{workspaceRoot?}`, consumes the base layer's entry-point-neutral `ctx.agentDefaultModel`, provides `ctx.apiProxy`, remains transport-agnostic, and registers no routes. `dsh-host-webserver` is a plain route-registration plugin: `HttpServerService` provides `ctx.httpServer` (`register(route) → disposer` with duplicate-pattern throw, `tapIndex` transforms applied in registration order, `port`), listens on activation, answers per-request failures with 400 and logging, and knows no harness concepts. The connection node half owns the `/api` binding from `ctx.apiProxy` through `toFetchHandler`. The modules node half (`ClientModuleHostService`, providing `ctx.clientModuleHost`) owns incremental package scanning, the bundle route, the index tap, and `onRebuilt`/`onGraphChanged` notification. The hmr node half owns dev reload through `fs.watchFile` membership and the `/plugins/events` SSE route.
|
||||
|
||||
**Package export discipline.** The modules package exposes exactly `.` (node half) and `./client` (the complete browser half: `ClientModuleSystem`, `parseBootManifest`, the adoption plugin face) — no bespoke subpaths; wire types re-export through the root for host-side consumers. The adoption handshake: the kernel writes the constructed instance to `window.__DSH_MODULES__` before cordis exists; the `./client` apply reads the slot (missing = loud throw) and provides `ctx.modules`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Recomposing a web deployment is a yml/patch edit; the retired pieces (`mountWebPlugins`, `CLIENT_PACKAGES`, `createHostWebPluginRegistry`, `startWebServer`, the webserver's graph/SSE/api knowledge) are deleted.
|
||||
- [Headless is a direct core front door](2026-08-09-headless-direct-core-front-door.md): its shipped profile contains the shared base Agent capabilities and omits Host, HTTP, Web, and browser layers. The transport split in this note is the browser surface's contract.
|
||||
- [Headless is a direct core entry point](2026-08-09-headless-direct-core-entry-point.md): its shipped profile contains the shared base Agent capabilities and omits Host, HTTP, Web, and browser layers. The transport split in this note is the browser surface's contract.
|
||||
- A TypeScript pitfall worth remembering: a `declare module 'cordis'` augmentation in a file with **no cordis import** is demoted to a standalone module declaration and silently shatters the program-wide `Context` merge (`ctx.on`/`ctx.effect` vanish across the program). Anchor with `import type {} from 'cordis'`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
| Rejected | One-line reason |
|
||||
|---|---|
|
||||
| Dedicated `dsh-host-profile` receiver package | User model state belongs to the Settings-backed `ctx.agentDefaultModel`; an extra Host receiver would duplicate ownership and exclude direct front doors |
|
||||
| Dedicated `dsh-host-profile` receiver package | User model state belongs to the Settings-backed `ctx.agentDefaultModel`; an extra Host receiver would duplicate ownership and exclude direct entry points |
|
||||
| Runtime `assembly` shim plugin providing an `apiHandler` service | Existed only because `createApiProxy` lived in runtime; moving it into apiproxy made the gateway self-hosting, and `toFetchHandler` is a pure function the binding side calls |
|
||||
| Full-rescan + incremental scan coexisting | Two implementations, two semantics; the single per-package path covers the activation pass too |
|
||||
| A bespoke `./impl` export on the modules package | Non-uniform export surface; the standard `./client` carries the whole browser half |
|
||||
|
||||
@@ -16,23 +16,23 @@ Status: implemented
|
||||
|
||||
**boot 胶水由两个类组成。** `AppCLIEntry`(apps/cli)与 `AppWebEntry`(壳内核)只持有那些必须独立于 cordis、提前存在的东西:argv 事实、合成的 patch 集、解析出的 boot manifest(元数据清单)、模块系统实例、loading 页句柄——其余一律进插件。`AppCLIEntry.run()` 三段:分层 env(ambient > cwd `.env` > `$DSH_HOME/.env`,顺手关掉上述缺陷)→ patch 合成 → Loader include boot 加 activation audit。`AppWebEntry.run()` 在浏览器侧镜像它:把 `window.__DSH_BOOT__` 解析成 `BootManifest`(双视角:npm 包行给模块表、cordis 插件行给 entry 组合;畸形 wire 大声抛)、建模块系统、渲染 loading 页、immediately 层预取与 Context/Loader 准备并行、**create entry 之前等预取齐**(物化是 `tree.import` 的同步 require,不受 fiber inject 等待保护;i18n → runtime/client 这类跨包 require 边要求 immediately 层工厂全部注册完——否则有实测 10–25% 的 boot 竞态)、收编 modules entry、逐一创建图行、settle、sweep。
|
||||
|
||||
**每个配置源有唯一声明位置。** 组合包 yml 值是工程默认,Settings 分节是可写的用户偏好,CLI(命令行界面)flags 面向其归属的启动器配置行,env 值则通过 yml `!!js` 表达式进入。patch 会整体替换一行的 config。解析后的前端 `distIndex` 通过同一条 patch 通道作为组装事实传递。与传输无关的提供方/模型默认值归 `ctx.agentDefaultModel` 所有;[直接 headless 前门](2026-08-09-headless-direct-core-front-door.md)与 Web 网关消费同一份状态。
|
||||
**每个配置源有唯一声明位置。** 组合包 yml 值是工程默认,Settings 分节是可写的用户偏好,CLI(命令行界面)flags 面向其归属的启动器配置行,env 值则通过 yml `!!js` 表达式进入。patch 会整体替换一行的 config。解析后的前端 `distIndex` 通过同一条 patch 通道作为组装事实传递。与传输无关的提供方/模型默认值归 `ctx.agentDefaultModel` 所有;[直接 headless 入口](2026-08-09-headless-direct-core-entry-point.md)与 Web 网关消费同一份状态。
|
||||
|
||||
**传输五分。** `dsh-host-apiproxy` 是网关插件(`api-gateway` 行):默认导出 `ApiProxyService`,只配置 `{workspaceRoot?}`,消费 base 层不偏向特定前门的 `ctx.agentDefaultModel`,provide `ctx.apiProxy`,保持传输无关且不注册路由。`dsh-host-webserver` 是朴素的路由注册插件:`HttpServerService` provide `ctx.httpServer`(`register(route) → disposer`、重复 pattern 即抛、`tapIndex` 按注册序应用、`port`),激活即 listen,单请求失败时答 400 并记日志,且不认识任何 harness 概念。connection node 半拥有从 `ctx.apiProxy` 经 `toFetchHandler` 绑定到 `/api` 的逻辑。modules node 半(`ClientModuleHostService`,provide `ctx.clientModuleHost`)拥有单包增量扫描、bundle 路由、index tap 与 `onRebuilt`/`onGraphChanged` 通知。HMR node 半通过 `fs.watchFile` membership 与 `/plugins/events` SSE 路由拥有开发期重载。
|
||||
**传输五分。** `dsh-host-apiproxy` 是网关插件(`api-gateway` 行):默认导出 `ApiProxyService`,只配置 `{workspaceRoot?}`,消费 base 层不偏向特定入口的 `ctx.agentDefaultModel`,provide `ctx.apiProxy`,保持传输无关且不注册路由。`dsh-host-webserver` 是朴素的路由注册插件:`HttpServerService` provide `ctx.httpServer`(`register(route) → disposer`、重复 pattern 即抛、`tapIndex` 按注册序应用、`port`),激活即 listen,单请求失败时答 400 并记日志,且不认识任何 harness 概念。connection node 半拥有从 `ctx.apiProxy` 经 `toFetchHandler` 绑定到 `/api` 的逻辑。modules node 半(`ClientModuleHostService`,provide `ctx.clientModuleHost`)拥有单包增量扫描、bundle 路由、index tap 与 `onRebuilt`/`onGraphChanged` 通知。HMR node 半通过 `fs.watchFile` membership 与 `/plugins/events` SSE 路由拥有开发期重载。
|
||||
|
||||
**包出口纪律。** modules 包只暴露 `.`(node 半)与 `./client`(完整浏览器半:`ClientModuleSystem`、`parseBootManifest`、收编插件面)——不设专用子路径;wire 类型经根出口 re-export 给 host 侧消费方。收编握手:内核在 cordis 之前把建好的实例写入 `window.__DSH_MODULES__`;`./client` 的 apply 读取该槽位(缺少时显式抛错)并 provide `ctx.modules`。
|
||||
|
||||
## 后果
|
||||
|
||||
- 重组一个 web 部署 = 改 yml/patch;退役件(`mountWebPlugins`、`CLIENT_PACKAGES`、`createHostWebPluginRegistry`、`startWebServer`、webserver 的图/SSE/api 知识)全部删除。
|
||||
- [Headless 是直接 core 前门](2026-08-09-headless-direct-core-front-door.md):其随附 profile 包含共享的 base Agent 能力,并省去 Host、HTTP、Web 与浏览器层。本笔记的传输划分是浏览器 surface 的约定。
|
||||
- [Headless 是直接 core 入口](2026-08-09-headless-direct-core-entry-point.md):其随附 profile 包含共享的 base Agent 能力,并省去 Host、HTTP、Web 与浏览器层。本笔记的传输划分是浏览器 surface 的约定。
|
||||
- 一个值得记住的 TypeScript 坑:`declare module 'cordis'` augmentation 所在文件若**没有任何 cordis import**,会被降级成独立 module declaration,无声打散全程序的 `Context` merge(`ctx.on`/`ctx.effect` 全程序消失)。用 `import type {} from 'cordis'` 锚定。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
| 弃案 | 一行理由 |
|
||||
|---|---|
|
||||
| 专门的 `dsh-host-profile` 受体包 | 用户模型状态归 Settings 支撑的 `ctx.agentDefaultModel` 所有;额外的 Host 受体会重复归属,并排除直接前门 |
|
||||
| 专门的 `dsh-host-profile` 受体包 | 用户模型状态归 Settings 支撑的 `ctx.agentDefaultModel` 所有;额外的 Host 受体会重复归属,并排除直接入口 |
|
||||
| 运行时里的 `assembly` 垫层插件(provide `apiHandler`) | 它的存在只因 `createApiProxy` 住运行时;本体迁入 apiproxy 后网关自持插件身份,且 `toFetchHandler` 是绑定方自己调的纯函数 |
|
||||
| 全量重扫与增量扫描并存 | 两条实现两份语义;单包路径足以覆盖激活初扫 |
|
||||
| modules 包特设 `./impl` 出口 | 出口面不统一;标准 `./client` 承载完整浏览器半 |
|
||||
|
||||
@@ -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-05-profile-plugin-bundles.md
|
||||
2026-08-05-profile-plugin-bundles.md: 8b5ab0c99282f6868fc3f70781e618af9a317c09
|
||||
2026-08-05-profile-plugin-bundles.zh.md: 2a685d68b3de9210488e26f8e6dd93dfc07f956c
|
||||
2026-08-05-profile-plugin-bundles.md: 2924b3cb445064fd47d82bcc94ec8d77ded5721b
|
||||
2026-08-05-profile-plugin-bundles.zh.md: b2287034010bcac1048bb385b2266f1bc75921da
|
||||
|
||||
@@ -12,7 +12,7 @@ The `dsh` launcher hardcoded its compositions: `base.cordis.yml` + `web.cordis.y
|
||||
|
||||
Everything becomes a **profile**: a directory `$DSH_HOME/profiles/<name>` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the two manifest kinds live under distinct `dsh.profile` / `dsh.bundle` keys so a package.json states which role it plays. The tree composes over an empty root by applying each bundle's patch in `dsh.profile.bundles` order, then the user layer, then `--patch` overlays, then flag patches — one `applyEntryPatches` call, identical for boot, flag derivation, and `--dump-config`.
|
||||
|
||||
The shipped bundles are `@deepseek-ai/dsh-base` (shared core rows), `@deepseek-ai/dsh-web-app` (browser Host rows and Web runtime glue), and `@deepseek-ai/dsh-headless` (a direct one-shot runner over base, without web-app). `dsh web` is the Web-flag alias for `--profile web`; `dsh run [--profile <name>] "task"` owns one-shot execution and defaults to the headless profile; generic `dsh --profile <name>` boots without a task. Patch overlays use `--patch`. `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core front door](2026-08-09-headless-direct-core-front-door.md) owns the headless composition contract.
|
||||
The shipped bundles are `@deepseek-ai/dsh-base` (shared core rows), `@deepseek-ai/dsh-web-app` (browser Host rows and Web runtime glue), and `@deepseek-ai/dsh-headless` (a direct one-shot runner over base, without web-app). `dsh web` is the Web-flag alias for `--profile web`; `dsh run [--profile <name>] "task"` owns one-shot execution and defaults to the headless profile; generic `dsh --profile <name>` boots without a task. Patch overlays use `--patch`. `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract.
|
||||
|
||||
The [`dsh run` command decision](../feature/2026-08-08-dsh-run-headless-command.md) owns the one-shot grammar; this note owns the profile composition it selects.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
一切都变成 **profile**:即目录 `$DSH_HOME/profiles/<name>`,其中包含一个 `package.json`(pnpm 管理的树外插件 `dependencies`,加上 profile manifest(元数据清单)`dsh.profile` 及其有序的 `bundles` 层列表)和一份用户 `cordis.patch.yml`。**组合包**(bundle)是声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;两种 manifest 分别位于互不相同的 `dsh.profile` / `dsh.bundle` 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 `dsh.profile.bundles` 顺序应用每个组合包的 patch,然后是用户层,然后是 `--patch` overlay,最后是 flag patch——全部收敛为一次 `applyEntryPatches` 调用,启动、flag 派生与 `--dump-config` 使用完全相同的路径。
|
||||
|
||||
随附的组合包是 `@deepseek-ai/dsh-base`(共享核心配置行)、`@deepseek-ai/dsh-web-app`(浏览器 Host 配置行与 Web 运行时粘合层)和 `@deepseek-ai/dsh-headless`(直接叠加在 base 上且不含 web-app 的一次性 runner)。`dsh web` 是携带 Web flag 家族的 `--profile web` 别名;`dsh run [--profile <name>] "task"` 负责一次性执行,默认使用 headless profile;通用的 `dsh --profile <name>` 启动 profile 而不携带任务。patch overlay 使用 `--patch`。`dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 前门](2026-08-09-headless-direct-core-front-door.md)负责 headless 组合约定。
|
||||
随附的组合包是 `@deepseek-ai/dsh-base`(共享核心配置行)、`@deepseek-ai/dsh-web-app`(浏览器 Host 配置行与 Web 运行时粘合层)和 `@deepseek-ai/dsh-headless`(直接叠加在 base 上且不含 web-app 的一次性 runner)。`dsh web` 是携带 Web flag 家族的 `--profile web` 别名;`dsh run [--profile <name>] "task"` 负责一次性执行,默认使用 headless profile;通用的 `dsh --profile <name>` 启动 profile 而不携带任务。patch overlay 使用 `--patch`。`dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](2026-08-09-headless-direct-core-entry-point.md)负责 headless 组合约定。
|
||||
|
||||
[`dsh run` 命令决策](../feature/2026-08-08-dsh-run-headless-command.md)负责一次性语法;本 Agent Note 负责该语法所选择的 profile 组合。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-09-headless-direct-core-front-door.md
|
||||
2026-08-09-headless-direct-core-front-door.md: f4604329a9276448a0021bb749b09e8c1b82e3c1
|
||||
2026-08-09-headless-direct-core-front-door.zh.md: aaa1289894bf3c69b39aa863493dffdc3437ad01
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-09-headless-direct-core-entry-point.md
|
||||
2026-08-09-headless-direct-core-entry-point.md: 49afe2993de7302adbedcdf9e8e2347d6424ee2a
|
||||
2026-08-09-headless-direct-core-entry-point.zh.md: 73c1cbe5ac777025f63f46751b1d5ccebbfe9676
|
||||
@@ -1,22 +1,22 @@
|
||||
# Agent Note: headless is a direct core front door
|
||||
# Agent Note: headless is a direct core entry point
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-09-headless-direct-core-front-door.zh.md)
|
||||
English | [中文](2026-08-09-headless-direct-core-entry-point.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The `headless` product contract is one local task with final assistant text on stdout, a success-sensitive exit code, empty stderr on success, and no listening port. A composition containing Workspace Host services, ApiProxy, HTTP, the Web runtime, or browser plugins contradicts that contract and makes local completion depend on an unrelated transport tree.
|
||||
|
||||
The direct front door still needs the same deployment model state as Web-created Agents. A separate provider/model default would give one deployment two answers, while deriving completion before the Agent and Session persistence are quiescent permits stdout and the exit code to observe incomplete state.
|
||||
The direct entry point still needs the same deployment model state as Web-created Agents. A separate provider/model default would give one deployment two answers, while deriving completion before the Agent and Session persistence are quiescent permits stdout and the exit code to observe incomplete state.
|
||||
|
||||
## Decision
|
||||
|
||||
The shipped `headless` profile contains `dsh-base` and `dsh-headless`. The headless bundle supplies its persona and tool mode, disables HMR, mounts the Code Mode worker explicitly, and inserts `headless-runner`. Its tree contains no `@deepseek-ai/dsh-host-*` package, ApiProxy, HTTP server, Web runtime, or browser client. Code Mode and Session persistence are one-shot Agent capabilities independent of Web presentation.
|
||||
|
||||
`headless-runner` is a direct core front door. After Loader settlement, it reads `ctx.agentDefaultModel.currentSelection()`, creates a fresh persisted Agent through `ctx.agents.create`, installs that `ModelSelection` in the Agent scope, waits for startup quiescence, anchors the Session sequence, submits one ordinary user message, and waits for quiescence again. It awaits `ctx.sessions.flush`, folds its durable event interval for the last non-empty assistant text and final `turn/end` reason, writes the text plus one newline to stdout, and requests bounded launcher shutdown with exit 0 exactly when the reason is `completed`. A terminal `error` reason writes its durable code and message to stderr; unexpected driver failures also use stderr and exit 1.
|
||||
`headless-runner` is a direct core entry point. After Loader settlement, it reads `ctx.agentDefaultModel.currentSelection()`, creates a fresh persisted Agent through `ctx.agents.create`, installs that `ModelSelection` in the Agent scope, waits for startup quiescence, anchors the Session sequence, submits one ordinary user message, and waits for quiescence again. It awaits `ctx.sessions.flush`, folds its durable event interval for the last non-empty assistant text and final `turn/end` reason, writes the text plus one newline to stdout, and requests bounded launcher shutdown with exit 0 exactly when the reason is `completed`. A terminal `error` reason writes its durable code and message to stderr; unexpected driver failures also use stderr and exit 1.
|
||||
|
||||
`@deepseek-ai/dsh-agent-default-model` owns the transport-independent default used for an Agent without a session-local selection. `AgentDefaultModelService` provides `ctx.agentDefaultModel` and registers the `agent-default-model` Settings section. Composition config supplies `{provider, model}`; user settings may also supply `reasoningEffort`. `currentSelection()` returns the live complete selection and `saveSelection()` writes it as a complete section, so a selection without an effort clears any stored effort. `dsh-base` supplies the composition entry. Direct and ApiProxy front doors consume this service; ApiProxy alone owns session-local precedence, model validation, and persistence of accepted Web selections.
|
||||
`@deepseek-ai/dsh-agent-default-model` owns the transport-independent default used for an Agent without a session-local selection. `AgentDefaultModelService` provides `ctx.agentDefaultModel` and registers the `agent-default-model` Settings section. Composition config supplies `{provider, model}`; user settings may also supply `reasoningEffort`. `currentSelection()` returns the live complete selection and `saveSelection()` writes it as a complete section, so a selection without an effort clears any stored effort. `dsh-base` supplies the composition entry. Direct and ApiProxy entry points consume this service; ApiProxy alone owns session-local precedence, model validation, and persistence of accepted Web selections.
|
||||
|
||||
`loadProfile` recognizes the exact installation-owned headless tuple (`dsh-base`, `dsh-web-app`, `dsh-headless`) and normalizes it to the shipped headless template while preserving every other manifest field. Extra, missing, or reordered bundle lists are user-owned and remain untouched.
|
||||
|
||||
@@ -31,7 +31,7 @@ Package tests use the real Session store and Agent registry around a scripted Ag
|
||||
| Alternative | Contract mismatch |
|
||||
|---|---|
|
||||
| Keep `dsh-web-app` but suppress its observation line | The process still opens a port and carries the Host, Web, and browser trees. |
|
||||
| Build a Host-only one-shot bundle around ApiProxy | ApiProxy is a client protocol gateway; a local one-shot front door has no client boundary. |
|
||||
| Build a Host-only one-shot bundle around ApiProxy | ApiProxy is a client protocol gateway; a local one-shot entry point has no client boundary. |
|
||||
| Use `InProcessApiClient` for product-level protocol coverage | Product execution would depend on an unrelated protocol solely to exercise that protocol. |
|
||||
| Give headless a separate provider/model config | Direct and Web creation would have independent defaults and persistence. |
|
||||
| Omit Code Mode and Session persistence | Both capabilities belong to one-shot Agent execution rather than Web presentation. |
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-09-headless-direct-core-front-door.md) | 中文
|
||||
[English](2026-08-09-headless-direct-core-entry-point.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
`headless` 的产品约定是一个本地任务:最终 assistant 文本写入 stdout,退出状态反映成功与否,成功时 stderr 为空,并且不打开监听端口。包含 Workspace Host 服务、ApiProxy、HTTP、Web 运行时或浏览器插件的组合违背这一约定,也使本地完成状态依赖无关的传输树。
|
||||
|
||||
直接前门仍需要与 Web 所创建 Agent 相同的部署模型状态。独立的提供方/模型默认值会让同一部署产生两种答案,而在 Agent 与会话持久化完全停稳之前推导完成状态,会让 stdout 与退出状态观察到不完整状态。
|
||||
直接入口仍需要与 Web 所创建 Agent 相同的部署模型状态。独立的提供方/模型默认值会让同一部署产生两种答案,而在 Agent 与会话持久化完全停稳之前推导完成状态,会让 stdout 与退出状态观察到不完整状态。
|
||||
|
||||
## 决策
|
||||
|
||||
@@ -16,7 +16,7 @@ Status: implemented
|
||||
|
||||
`headless-runner` 是直接使用核心服务的入口。Loader 完全加载后,它读取 `ctx.agentDefaultModel.currentSelection()`,通过 `ctx.agents.create` 创建一个新的持久化 Agent,在 Agent 作用域中安装该 `ModelSelection`,等待启动工作完全停稳,锚定会话事件序号,提交一条普通用户消息,再次等待完全停稳。随后,它等待 `ctx.sessions.flush`,折叠自身持有的持久事件区间,以取得最后一条非空 assistant 文本和最终 `turn/end` 结束原因,将文本连同一个换行写入 stdout,并且仅在结束原因为 `completed` 时请求启动器以退出状态 0 有界关闭。结束原因为 `error` 时,其持久化错误码与消息写入 stderr;驱动器的意外失败也写入 stderr 并以 1 退出。
|
||||
|
||||
`@deepseek-ai/dsh-agent-default-model` 拥有与传输无关的默认值,供没有会话级选择的 Agent 使用。`AgentDefaultModelService` 提供 `ctx.agentDefaultModel` 并注册 `agent-default-model` Settings 分节。组合配置提供 `{provider, model}`,用户设置还可以提供 `reasoningEffort`。`currentSelection()` 返回当前的完整选择,`saveSelection()` 则写入完整分节,因此不含强度的选择会清除已存强度。`dsh-base` 提供组合条目。直接前门与 ApiProxy 前门均消费该服务;只有 ApiProxy 负责会话级优先级、模型校验与已接受 Web 选择的持久化。
|
||||
`@deepseek-ai/dsh-agent-default-model` 拥有与传输无关的默认值,供没有会话级选择的 Agent 使用。`AgentDefaultModelService` 提供 `ctx.agentDefaultModel` 并注册 `agent-default-model` Settings 分节。组合配置提供 `{provider, model}`,用户设置还可以提供 `reasoningEffort`。`currentSelection()` 返回当前的完整选择,`saveSelection()` 则写入完整分节,因此不含强度的选择会清除已存强度。`dsh-base` 提供组合条目。直接入口与 ApiProxy 入口均消费该服务;只有 ApiProxy 负责会话级优先级、模型校验与已接受 Web 选择的持久化。
|
||||
|
||||
`loadProfile` 识别安装过程拥有的精确 headless 元组(`dsh-base`、`dsh-web-app`、`dsh-headless`),将其规范化为随附的 headless 模板,并保留 manifest(元数据清单)的其他所有字段。带额外项、缺少项或顺序不同的组合包列表归用户所有,保持不变。
|
||||
|
||||
@@ -31,7 +31,7 @@ Status: implemented
|
||||
| 替代方案 | 约定不匹配之处 |
|
||||
|---|---|
|
||||
| 保留 `dsh-web-app`,但隐藏观察行 | 进程仍会打开端口并携带 Host、Web 与浏览器插件树。 |
|
||||
| 围绕 ApiProxy 构建纯 Host 一次性组合包 | ApiProxy 是客户端协议网关,而本地一次性前门没有客户端边界。 |
|
||||
| 围绕 ApiProxy 构建纯 Host 一次性组合包 | ApiProxy 是客户端协议网关,而本地一次性入口没有客户端边界。 |
|
||||
| 使用 `InProcessApiClient` 实现产品级协议覆盖 | 产品执行会仅为测试无关协议而依赖该协议。 |
|
||||
| 为 headless 单独提供提供方/模型配置 | 直接创建与 Web 创建会拥有彼此独立的默认值和持久化。 |
|
||||
| 省略 Code Mode 与会话持久化 | 两项能力都属于一次性 Agent 执行,而不是 Web 呈现。 |
|
||||
@@ -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-07-20-error-cause-chain-diagnostics.md
|
||||
2026-07-20-error-cause-chain-diagnostics.md: 32716b5a68b3b73bded47633eca95995cdbfc586
|
||||
2026-07-20-error-cause-chain-diagnostics.zh.md: 9911c32b6d68c1f5569a1fceadb65e23c6586594
|
||||
2026-07-20-error-cause-chain-diagnostics.md: b80dd08d79a57738a6eef2f8638b0336ca80d4bf
|
||||
2026-07-20-error-cause-chain-diagnostics.zh.md: 914e983d7ea81eef8bca8fd5aa3791f2f44d4080
|
||||
|
||||
@@ -9,7 +9,7 @@ English | [中文](2026-07-20-error-cause-chain-diagnostics.zh.md)
|
||||
A TUI run against an unreachable DeepSeek endpoint failed with the single notice `fetch failed` and no further detail. Two independent gaps produced that dead end:
|
||||
|
||||
1. undici's `fetch` wraps every transport failure (DNS, refused connection, TLS, proxy) in a bare `TypeError: fetch failed` whose actionable detail — `ECONNREFUSED`, `bad port`, the Happy Eyeballs AggregateError — lives on `error.cause`. Every diagnostic boundary in the harness rendered only `error.message` (or `String(error)`, which is equivalent for Errors), so the wrapper masked the diagnosis in the TUI notice, the durable `turn/end` reason, and every logger line.
|
||||
2. The readline front door (`dsh-stdio`) rendered no failure reason at all: a `turn/end` with `reason.kind === 'error'` printed nothing but the next `> ` prompt, so the same failure in `demo:repl` was pure silence.
|
||||
2. The readline entry point (`dsh-stdio`) rendered no failure reason at all: a `turn/end` with `reason.kind === 'error'` printed nothing but the next `> ` prompt, so the same failure in `demo:repl` was pure silence.
|
||||
|
||||
## Decision
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Status: implemented
|
||||
TUI 连接不可达的 DeepSeek 端点时,失败只显示一条 `fetch failed` 通知,没有任何进一步细节。两个独立缺口共同造成了这个死胡同:
|
||||
|
||||
1. undici 的 `fetch` 把所有传输层失败(DNS、连接被拒、TLS、代理)包装成裸的 `TypeError: fetch failed`,可操作的细节——`ECONNREFUSED`、`bad port`、Happy Eyeballs 的 AggregateError——都在 `error.cause` 上。harness 里的每个诊断边界都只渲染 `error.message`(或对 Error 等价的 `String(error)`),于是包装层在 TUI 通知、持久化的 `turn/end` reason 和所有日志行里都掩盖了诊断信息。
|
||||
2. readline 前门(`dsh-stdio`)完全不渲染失败原因:`reason.kind === 'error'` 的 `turn/end` 只打印下一个 `> ` 提示符,同样的失败在 `demo:repl` 里就是纯粹的沉默。
|
||||
2. readline 入口(`dsh-stdio`)完全不渲染失败原因:`reason.kind === 'error'` 的 `turn/end` 只打印下一个 `> ` 提示符,同样的失败在 `demo:repl` 里就是纯粹的沉默。
|
||||
|
||||
## 决策
|
||||
|
||||
|
||||
@@ -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-10-session-row-identity-covers-the-preset.md
|
||||
2026-08-10-session-row-identity-covers-the-preset.md: 7a89dcb4e4ae292a06a1743842d2e9cf6bd96282
|
||||
2026-08-10-session-row-identity-covers-the-preset.zh.md: 7ffa3423818bcc867c942651540db1975737e073
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note: The session-row identity guard covers the preset
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-10-session-row-identity-covers-the-preset.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`SessionManager.buildListSnapshot` memoizes list rows by value: a wire refresh mints all-new summary objects, so an entry equal to the cached one is replaced by the cached instance, and every `SessionListItem` memo downstream keeps hitting. The stated contract is "reuse the cached object when every field matches"; the comparison enumerated the fields by hand and did not enumerate `agentPreset`.
|
||||
|
||||
A confirmed preset switch moves exactly that one field. `noteAgentPreset` upserts it and `applyMutation` merges it in — the merge deliberately does not take the mutation's `updatedAt`, so a switched row differs from its cached twin in the preset and in nothing else. The guard therefore judged the row unchanged and served the stale instance, permanently: the manager's own summaries said `minimal` while every reader of the projected snapshot went on reading `standard`.
|
||||
|
||||
The hero chip is one of those readers, and it compares the pick against that row before sending anything. Switching back to the preset the session was created under looked to it like "already on that preset", so it dropped the stage and sent no RPC at all — the chip label moved while the composition did not. A session could be switched away from its creation-time preset once and never back.
|
||||
|
||||
## Decision
|
||||
|
||||
The identity guard compares `agentPreset` alongside the other summary fields, which is what "every field matches" already claimed. Nothing else changes: the memoization, the merge, and the chip's no-op check all stay as they are, because each is correct once the row it reads is.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Have the chip re-read the host instead of the list row.** It would route around the stale row, but the row is also what the session header labels itself from, so the staleness would survive in the surface where it is most visible — and any future reader of `SessionSummary.agentPreset` would inherit the same trap.
|
||||
|
||||
**Drop the entry-identity memoization and rebuild rows every snapshot.** It removes the whole class of missing-field bugs, at the cost the memo exists to avoid: a wire refresh mints new objects for every row, so each refresh would re-render the entire session list.
|
||||
|
||||
**Compare summaries structurally rather than field by field.** A generic deep comparison cannot be added blind: the row carries `projectionValues`, whose reference identity is the deliberate signal that the projection store republished, and folding it into a value comparison would either re-render on every projection tick or mask a real one.
|
||||
|
||||
## Consequences
|
||||
|
||||
Every field a session row carries now participates in row identity, so a surface reading `SessionSummary.agentPreset` sees a switch as soon as the host confirms it — the header label included. The guard is still a hand-written enumeration, so a field added to `SessionSummary` later must be added here too; the `sessions-service` projection test names the failure mode for the next such field rather than only pinning this one.
|
||||
|
||||
## Testing
|
||||
|
||||
`sessions-service.spec.ts` feeds a blank row, notes a switch, and asserts the projected snapshot reports the new preset — it fails on the old guard because the row differs in nothing else. The `agent-preset-selection` web e2e switches down and back up, asserting the host honors the second switch and the `/` catalog returns with it; without this fix the second switch never reaches the host at all.
|
||||
|
||||
## Related
|
||||
|
||||
The same e2e covers [the catalog-invalidation fix](2026-08-10-slash-catalog-follows-preset-switch.md), which is what makes the menu follow either switch once the switch itself lands.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note:会话行的标识判定纳入 preset
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-10-session-row-identity-covers-the-preset.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`SessionManager.buildListSnapshot` 按值对列表行做记忆化:一次 wire 刷新会铸造全新的 summary 对象,因此与缓存项相等的行会被替换为缓存实例,下游每一个 `SessionListItem` memo 才能持续命中。它声明的约定是「每个字段都相同就复用缓存对象」,而那段比较是手写枚举字段的,其中没有 `agentPreset`。
|
||||
|
||||
一次已确认的 preset 切换恰好只移动这一个字段。`noteAgentPreset` 把它 upsert 进去,`applyMutation` 合并它——该合并有意不采用 mutation 的 `updatedAt`,因此切换后的行与它的缓存孪生只在 preset 上不同,别处一致。于是标识判定认为这一行没变,永久地提供了过期实例:manager 自己的 summaries 是 `minimal`,而所有读取投影快照的一方继续读到 `standard`。
|
||||
|
||||
hero 上的 chip 正是其中一个读取方,而且它在发出任何请求之前会拿这次选择和那一行比较。切回会话创建时的那个 preset,在它看来就是「已经是这个 preset 了」,于是丢弃 stage、根本不发 RPC——chip 的标签变了,组成没变。一个会话可以从创建时的 preset 切走一次,然后再也切不回来。
|
||||
|
||||
## Decision
|
||||
|
||||
标识判定把 `agentPreset` 与其余 summary 字段一起比较,这本就是「每个字段都相同」所声称的内容。其他一概不动:记忆化、合并、chip 的 no-op 检查各自都是对的——只要它们读到的那一行是对的。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**让 chip 改为直接读宿主,而不是读列表行。** 这样能绕开过期的行,但会话头部的标签同样以这一行为准,过期状态会在最显眼的界面里留下来;而且将来任何 `SessionSummary.agentPreset` 的读取方都会继承同一个陷阱。
|
||||
|
||||
**去掉行标识记忆化,每次快照都重建行。** 这能整类消除「漏字段」缺陷,代价却正是这个 memo 存在的理由:一次 wire 刷新会为每一行铸造新对象,于是每次刷新都要重渲染整个会话列表。
|
||||
|
||||
**改成结构化比较,而不是逐字段枚举。** 通用的深比较不能盲目加:行上带有 `projectionValues`,它的引用标识本身就是「投影 store 重新发布了」这一有意为之的信号,把它折进值比较,要么每个投影 tick 都重渲染,要么把一次真实变化掩盖掉。
|
||||
|
||||
## Consequences
|
||||
|
||||
会话行携带的每个字段现在都参与行标识,因此读取 `SessionSummary.agentPreset` 的界面会在宿主确认后立刻看到切换,会话头部标签也包含在内。该判定仍是手写枚举,所以将来给 `SessionSummary` 新增字段时必须同步加进来;`sessions-service` 的投影测试为下一个这样的字段点明了失效形态,而不只是钉住这一次。
|
||||
|
||||
## Testing
|
||||
|
||||
`sessions-service.spec.ts` 喂入一行空会话、记录一次切换,并断言投影快照报告的是新 preset——在旧判定下它会失败,因为这一行别处都没变。`agent-preset-selection` web e2e 先向下切再向上切,断言宿主认可第二次切换、`/` 目录随之回来;没有这次修复,第二次切换根本到不了宿主。
|
||||
|
||||
## Related
|
||||
|
||||
同一条 e2e 也覆盖[目录失效的修复](2026-08-10-slash-catalog-follows-preset-switch.md)——正是它让菜单在切换真正落地之后跟随任一方向的切换。
|
||||
@@ -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-10-slash-catalog-follows-preset-switch.md
|
||||
2026-08-10-slash-catalog-follows-preset-switch.md: 85bd5b2134fd20c86fdeb13f3ce5b007449105b5
|
||||
2026-08-10-slash-catalog-follows-preset-switch.zh.md: 97c8f08a7b3dfec7c17fbb00bef626e28505c500
|
||||
@@ -0,0 +1,43 @@
|
||||
# Agent Note: The slash catalog follows a blank session's preset switch
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-10-slash-catalog-follows-preset-switch.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Presets moved the rows that decide what a session's `/` menu contains. The Web composition disables host-plane `skill-local`, `tool-skill`, `plan-mode`, and `command-compact`; a preset supplies them, so which commands and skills exist is a property of the session's composition rather than of the deployment.
|
||||
|
||||
Both browser catalogs cache per session — `CommandDirectory` in `dsh-client-ui-command`, the single-flight fetch map in `dsh-client-ui-skill` — and the composer warms both at scope birth, under whatever preset the session was created with. The hero chip then lets the user recompose the still-blank session, and neither cache had an invalidation edge for that: `commands/changed` is registry-wide and `connection/reset` needs a reconnect. `agentPresets.recompose` re-parents the agent's scope onto a standing mount that may already exist, so it registers nothing and the registry-wide signal never fires for it.
|
||||
|
||||
The menu therefore kept serving the composition the session no longer ran. Switching down left `compact`, `plan`, and every project skill listed; switching up left the narrower catalog — the four host-plane rows and the client's own `model` contribution — with no skills at all, which is what the bug report described. The catalog only healed when an unrelated registry change or a reconnect happened to invalidate it.
|
||||
|
||||
## Decision
|
||||
|
||||
The switch's commit point is the logged `agent-preset/selected` event. The host stream frames it as `host/session-preset-changed { sessionId, agentPreset }`, the browser runtime bridges that frame to the typed `session/preset-changed` ctx event beside the registry-invalidation bridges it already owns, and each catalog owner drops its own entry for that session: `ui-command` soft-refreshes the key (the old snapshot keeps serving the open menu until the new one lands), `ui-skill` invalidates it (aborting an in-flight prewarm, so a warm racing the switch cannot publish the stale catalog).
|
||||
|
||||
The frame is per session and carries no catalog, only the preset id — which the manager folds into the session row, because the `agentPresets.select` echo reaches only the client that issued the switch and the row is what the session header labels itself from (and what the hero chip compares the next pick against).
|
||||
|
||||
Deriving the frame from the logged event rather than from the RPC handler's return keeps one authority for "this session's composition changed": every connected client observes the switch, not only the tab that issued it, and a client that is not the switcher never has to infer it from a registry signal that will not come.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Invalidate in the client's own `agentPresets.select` callback.** Smallest change, and the preset is locked after the first turn, so the hero chip is the only place a switch can originate. Rejected because the invalidation would then live in the surface that happens to issue the RPC rather than at the commit point: a second tab on the same blank session keeps a stale menu, and any future host-side recomposition has no signal at all.
|
||||
|
||||
**Derive the client event from the existing `session/event` mux frame.** The logged event already reaches every subscribed client, so no new wire type would be needed. Rejected on face separation: narrowing `event.type` to `agent-preset/selected` requires the `SessionEventMap` augmentation, and the only ways to load it in the Client program are a project reference to `dsh-agent-presets` — which drags the host `ctx.sessions` merge into a program that publishes its own — or a cast that defeats the discriminant.
|
||||
|
||||
**Reuse `host/commands-changed`.** It is the existing catalog-invalidation frame, but it is registry-wide, carries no session, and says nothing about skills; a client would repull every session's commands and still never refresh a skill catalog.
|
||||
|
||||
## Consequences
|
||||
|
||||
The wire gains one frame and the Client one typed event, and every catalog a preset decides now has one place to subscribe: a future per-session surface derived from the composition invalidates on the same signal instead of inventing another. The cost is that the frame is a second reader of a logged fact — the host stream must keep deriving it from `agent-preset/selected`, so a future switch path that recomposes without logging would go unannounced. `ui-command` stays soft (the open menu never blanks) while `ui-skill` drops its entry outright, because a skill catalog has no partial-serve mode; a menu opened inside the refetch window shows no skills for that instant rather than the wrong ones.
|
||||
|
||||
## Testing
|
||||
|
||||
`api-proxy-agent-preset.spec.ts` asserts the committed switch frames once with the session and its new preset; `wire-events.spec.ts` asserts the frame-to-event bridge; the `ui-command` and `ui-skill` specs assert that the event repulls the recomposed session and leaves every other session's cache serving. The `agent-preset-selection` web e2e seeds a project skill and, after the hero chip applies `minimal`, asserts the `/` menu drops `compact`, `plan`, and the skill while keeping the host-plane rows — the assembled-application evidence that the panel follows the composition.
|
||||
|
||||
That e2e also stopped reading its staged-pick assertion off the serialized session list: the seeded session records `minimal` too, so the substring answered before the switch had landed. It now addresses the live session by id.
|
||||
|
||||
## Related
|
||||
|
||||
Reaching the host on a SECOND switch is a separate defect with its own cause and fix: [the session-row identity guard](2026-08-10-session-row-identity-covers-the-preset.md). Until it landed, `agent-preset-selection.e2e.ts` could only exercise the first switch — the invalidation edge here is direction-blind, but the switch it reacts to has to happen.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Agent Note:斜杠目录跟随空会话的 preset 切换
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-10-slash-catalog-follows-preset-switch.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
preset 把决定 `/` 菜单内容的那些行搬走了。Web 组装禁用了宿主面的 `skill-local`、`tool-skill`、`plan-mode` 和 `command-compact`,改由 preset 提供,因此一个会话有哪些命令和技能,是它自身组成的属性,而不是部署的属性。
|
||||
|
||||
浏览器侧两份目录都按会话缓存——`dsh-client-ui-command` 的 `CommandDirectory`,`dsh-client-ui-skill` 的 single-flight 拉取表——并且 composer 在 scope 出生时就按会话创建时的 preset 预热了它们。随后 hero 上的 chip 允许用户重组这个仍为空的会话,而两份缓存都没有对应的失效边:`commands/changed` 是注册表级的,`connection/reset` 需要重连。`agentPresets.recompose` 只是把 agent 的 scope 重新挂接到一个可能已经存在的常驻挂载上,不产生任何注册,注册表级信号因此永远不会为它触发。
|
||||
|
||||
于是菜单继续提供会话已经不再运行的那套组成。向下切换后 `compact`、`plan` 和全部项目技能仍列在菜单里;向上切换后留在原地的是更窄的目录——四条宿主面行加客户端自己的 `model` 贡献——而且完全没有技能,这正是 bug 报告描述的现象。只有当某个无关的注册表变化或一次重连恰好使其失效时,目录才会自愈。
|
||||
|
||||
## Decision
|
||||
|
||||
这次切换的提交点是落账的 `agent-preset/selected` 事件。宿主流把它成帧为 `host/session-preset-changed { sessionId, agentPreset }`,浏览器运行时在它已经拥有的那组注册表失效桥接旁,把该帧桥接为类型化的 `session/preset-changed` ctx 事件,两份目录各自丢弃该会话的那一项:`ui-command` 软刷新该键(新快照落地前,旧快照继续服务已打开的菜单),`ui-skill` 让它失效(并中止在途的预热,使一次与切换赛跑的 warm 无法发布过期目录)。
|
||||
|
||||
该帧按会话粒度,不携带目录,只带 preset id——manager 会把它折进会话行,因为 `agentPresets.select` 的回执只会到达发起切换的那个客户端,而会话头部标签正是以这一行为准(hero chip 比较下一次选择时读的也是它)。
|
||||
|
||||
从落账事件而不是 RPC 处理器的返回值派生该帧,使「这个会话的组成变了」只有一个权威来源:每个已连接的客户端都能观察到这次切换,而不只是发起它的那个标签页;不是发起方的客户端也无需从一个根本不会到来的注册表信号里去推断。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**在客户端自己的 `agentPresets.select` 回调里就地失效。** 改动最小,而且第一轮之后 preset 就锁定,hero 上的 chip 是切换唯一可能的发起处。否决理由是失效逻辑会落在恰好发起 RPC 的那个界面上,而不是提交点:同一个空会话在第二个标签页里仍是过期菜单,将来任何宿主侧的重组也完全没有信号。
|
||||
|
||||
**从既有的 `session/event` mux 帧派生客户端事件。** 落账事件本来就会送达每个已订阅的客户端,不需要新增协议类型。因面(face)分离而否决:把 `event.type` 收窄到 `agent-preset/selected` 需要 `SessionEventMap` 增补,而在 Client 程序里加载它只有两条路——引用 `dsh-agent-presets` 工程,那会把宿主的 `ctx.sessions` 合并拖进一个自己也发布同名服务的程序;或者用一次类型断言绕过判别式。
|
||||
|
||||
**复用 `host/commands-changed`。** 它是既有的目录失效帧,但它是注册表级的、不带会话、也与技能无关;客户端会把每个会话的命令都重拉一遍,却依然永远刷不新技能目录。
|
||||
|
||||
## Consequences
|
||||
|
||||
协议多了一个帧,Client 多了一个类型化事件,而每一份由 preset 决定的目录从此有了统一的订阅点:将来任何从组成派生的按会话界面,都在同一个信号上失效,而不必再发明一个。代价是该帧成为一项落账事实的第二个读者——宿主流必须持续从 `agent-preset/selected` 派生它,因此将来若出现一条不落账就重组的切换路径,它将无人宣告。`ui-command` 保持软失效(已打开的菜单不会变空),而 `ui-skill` 直接丢弃该项,因为技能目录没有「部分可服务」的状态;在重拉窗口内打开的菜单,那一瞬间显示的是没有技能,而不是错误的技能。
|
||||
|
||||
## Testing
|
||||
|
||||
`api-proxy-agent-preset.spec.ts` 断言已提交的切换恰好成帧一次,并带上会话与新 preset;`wire-events.spec.ts` 断言帧到事件的桥接;`ui-command` 与 `ui-skill` 的 spec 断言该事件只重拉被重组的会话,其他会话的缓存继续服务。`agent-preset-selection` web e2e 播种一个项目技能,并在 hero chip 应用 `minimal` 之后断言 `/` 菜单丢掉了 `compact`、`plan` 和该技能,同时保留宿主面的那几行——这是面板跟随组成的整装应用证据。
|
||||
|
||||
同一条 e2e 也不再从序列化后的会话列表里读它的 staged-pick 断言:被播种的会话同样记录着 `minimal`,子串匹配在切换落地之前就会通过。现在它按 id 寻址那个活跃会话。
|
||||
|
||||
## Related
|
||||
|
||||
第二次切换能否到达宿主是另一个缺陷,有各自的成因与修复:[会话行的标识判定](2026-08-10-session-row-identity-covers-the-preset.md)。在它落地之前,`agent-preset-selection.e2e.ts` 只能演练第一次切换——这里的失效边对方向无感,但它所响应的那次切换必须真的发生。
|
||||
@@ -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-24-workspace-context.md
|
||||
2026-06-24-workspace-context.md: 53a580aee50752b7c6daeff5caa42ba6409c8885
|
||||
2026-06-24-workspace-context.md: e7a3724847b9dc8cfad11e87b2c96a3ef442bcba
|
||||
2026-06-24-workspace-context.zh.md: 39c3f52da10b7299301d10bd8b78330cbae8e19c
|
||||
|
||||
@@ -78,7 +78,7 @@ There is intentionally no watcher. Detection occurs at the next successful struc
|
||||
|
||||
## Consequences
|
||||
|
||||
Workspace guidance is isolated per session and shared by the demo front doors, Web Host, and every tool presentation mode. Initial, nested, and changed instructions are durable and replayable. The generic session/agent context contract carries typed source data through inbox-staged and durably entered user messages without flattening entries.
|
||||
Workspace guidance is isolated per session and shared by the demo entry points, Web Host, and every tool presentation mode. Initial, nested, and changed instructions are durable and replayable. The generic session/agent context contract carries typed source data through inbox-staged and durably entered user messages without flattening entries.
|
||||
|
||||
Repository text remains untrusted input. Lower-authority user-role framing, explicit precedence language, and delimiter escaping reduce risk but do not eliminate prompt injection. Following a candidate symlink to its target widens that surface to off-tree content, so the permission and sandbox layers that confine `ctx.fs` to trusted roots are the boundary that treats workspace files as data rather than authority (the [instruction-symlink follow note](2026-07-21-follow-instruction-symlinks.md) owns the residual risk).
|
||||
|
||||
|
||||
@@ -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-16-harness-level-loop.md
|
||||
2026-07-16-harness-level-loop.md: e84a5738a55988d3b968eac829d9daaf10d7e304
|
||||
2026-07-16-harness-level-loop.zh.md: d7cda56d66d4456ac5c3c7d55bb238e85fcacbc5
|
||||
2026-07-16-harness-level-loop.md: e5dd94fbf76e27f0843666f29622969635ef2fc3
|
||||
2026-07-16-harness-level-loop.zh.md: 773b5ff1f4b07206d0d817c29774435ce24407b4
|
||||
|
||||
@@ -70,7 +70,7 @@ The human UX follows the compact Codex shape in the [public OpenAI Codex TUI dis
|
||||
|
||||
The model receives only `get_goal`, `create_goal`, and `update_goal`. It may create a goal when a direct human request clearly asks for substantial multi-round work, and it may infer that intent in any language. It must not turn routine one-turn work into a goal. Code requires a direct human message in the current live root-agent turn; semantic interpretation remains model judgment. An autonomous goal round may report `complete` or `blocked` for the exact current goal round but cannot edit, pause, resume, or replace the human objective.
|
||||
|
||||
TUI mounts the shared command registry and complete goal stack by default and exposes `/goal` through one producer. ACP mounts the goal domain, model tools, and same-session driver but deliberately omits the human command plane. Every effective registered command is discoverable and invocable through every composed command adapter; a plugin incompatible with an application omits its command producer from that composition rather than relying on registry-level surface masks. The UI-less agent spine is opt-in so one-shot callers do not silently become multi-round operations. The headless CLI and JSON-RPC front doors do not consume the command plane; ordinary human text can still authorize model goal tools when that stack is composed.
|
||||
TUI mounts the shared command registry and complete goal stack by default and exposes `/goal` through one producer. ACP mounts the goal domain, model tools, and same-session driver but deliberately omits the human command plane. Every effective registered command is discoverable and invocable through every composed command adapter; a plugin incompatible with an application omits its command producer from that composition rather than relying on registry-level surface masks. The UI-less agent spine is opt-in so one-shot callers do not silently become multi-round operations. The headless CLI and JSON-RPC entry points do not consume the command plane; ordinary human text can still authorize model goal tools when that stack is composed.
|
||||
|
||||
### Fresh-agent Ralph execution
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ Goal Round 驱动器为每个特定的实时 agent 至多拥有一个待定预
|
||||
|
||||
模型只接收 `get_goal`、`create_goal` 和 `update_goal`。当直接人类请求清楚要求大量多 Round 工作时,模型可以创建目标,并且可以从任何语言推断该意图。它不得把日常单 Turn 工作变成目标。代码要求当前实时根 agent Turn 中有一条人类直接发送的消息;语义解释仍是模型判断。自治目标 Round 可以为确切的当前 Goal Round 报告 `complete` 或 `blocked`,但不能编辑、暂停、恢复或替换人类目标。
|
||||
|
||||
TUI 默认挂载共享命令注册表和完整目标栈,并通过一个生产方暴露 `/goal`。ACP(Agent Client Protocol)挂载目标领域、模型工具和同会话驱动器,但有意省略人类命令平面。每条有效已注册命令都能被每个已组合的命令适配器发现和调用;若插件与某应用不兼容,该应用组合会省略其命令生产方,而不是依赖注册表层面的表面掩码。无 UI 的 agent 主干要求显式选择加入,以免单次调用方静默变成多 Round 操作。无头 CLI(命令行界面)与 JSON-RPC 前端不消费命令平面;挂载目标栈后,普通人类文本仍可授权模型目标工具。
|
||||
TUI 默认挂载共享命令注册表和完整目标栈,并通过一个生产方暴露 `/goal`。ACP(Agent Client Protocol)挂载目标领域、模型工具和同会话驱动器,但有意省略人类命令平面。每条有效已注册命令都能被每个已组合的命令适配器发现和调用;若插件与某应用不兼容,该应用组合会省略其命令生产方,而不是依赖注册表层面的表面掩码。无 UI 的 agent 主干要求显式选择加入,以免单次调用方静默变成多 Round 操作。无头 CLI(命令行界面)与 JSON-RPC 运行入口不消费命令平面;挂载目标栈后,普通人类文本仍可授权模型目标工具。
|
||||
|
||||
### 全新 agent Ralph 执行
|
||||
|
||||
|
||||
@@ -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-16-persistent-pty-sessions.md
|
||||
2026-07-16-persistent-pty-sessions.md: f27d799574e61d92123c9ca4e31c5c2a9d3229b4
|
||||
2026-07-16-persistent-pty-sessions.zh.md: 0c23619faf00794c2c4e7b3f85ef961faddd99e0
|
||||
2026-07-16-persistent-pty-sessions.md: fb9cd06bade7bc357baa738f0d9dd03b7f5b7936
|
||||
2026-07-16-persistent-pty-sessions.zh.md: 55a5848c1ab1e8c2cd3b29f2d4748ea4abbe088c
|
||||
|
||||
@@ -152,7 +152,7 @@ The package ships concise tool guidance explaining persistent state, owner isola
|
||||
|
||||
**Include TUI sequences and BEL handling.** Rejected. The source prototype treats those paths as timing-sensitive and still records unresolved alternate-screen and interaction failures. Line-oriented PTY use proves the core value without making those unverified behaviors foundational.
|
||||
|
||||
**Use an out-of-process daemon immediately.** Rejected for the initial in-process capability because current persistent front doors already keep a Cordis context alive. A daemon becomes justified by cross-process restoration or multi-client attachment, both deferred here.
|
||||
**Use an out-of-process daemon immediately.** Rejected for the initial in-process capability because current long-lived entry points already keep a Cordis context alive. A daemon becomes justified by cross-process restoration or multi-client attachment, both deferred here.
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ plugins:
|
||||
|
||||
**包含 TUI sequence 与 BEL 处理。**拒绝。源 prototype 将这些路径视为 timing-sensitive,且仍记录未解决的 alternate-screen 和交互失败。行式 PTY 已能证明核心价值,无需把未经验证的行为放进基础层。
|
||||
|
||||
**立即采用进程外 daemon。**初始的进程内功能不采用,因为当前持久 front door 已能维持 Cordis context。跨进程恢复或多客户端 attach 会让 daemon 变得合理,但两者都已推迟。
|
||||
**立即采用进程外 daemon。**初始的进程内功能不采用,因为当前长驻的运行入口已能维持 Cordis context。跨进程恢复或多客户端 attach 会让 daemon 变得合理,但两者都已推迟。
|
||||
|
||||
## 验证
|
||||
|
||||
|
||||
@@ -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-19-human-goal-command.md
|
||||
2026-07-19-human-goal-command.md: 5fdd80f7423b80e84e58f7379130ee59a2e8a723
|
||||
2026-07-19-human-goal-command.zh.md: 89d29497abfc758ff8373d272aa8620ca1bcfcc2
|
||||
2026-07-19-human-goal-command.md: d68e4025a4d37d07211f15ddfc6069bc7c637124
|
||||
2026-07-19-human-goal-command.zh.md: dbda35c731ccdc7aff2d4213a3df8b78070319df
|
||||
|
||||
@@ -68,5 +68,5 @@ The producer suite uses the real command registry, goal service, agent registry,
|
||||
- The portable command contract has no modal editor or confirmation interaction; inline edit and explicit clear are intentional until a general cross-surface interaction primitive exists.
|
||||
- `/goal` does not accept a per-command round cap. Deployment config owns the default, and the authorized model tool can edit a cap after direct human instruction.
|
||||
- TUI renders portable plain text rather than a continuously updated goal status widget. Reconnectable command output and adapter-specific status indicators are deferred.
|
||||
- The ACP automation server, headless CLI, and JSON-RPC front doors do not consume the command registry.
|
||||
- The ACP automation server, headless CLI, and JSON-RPC entry points do not consume the command registry.
|
||||
- The command observes and mutates state but does not certify completion or blockers. Evaluator-backed certification remains deferred to a separate policy layer with an explicit authority and isolation contract.
|
||||
|
||||
@@ -68,5 +68,5 @@ TUI 应用包作出相反的产品选择。它默认让 `goals` 使用所有者
|
||||
- 可移植命令约定没有模态编辑器或确认交互;在出现通用跨界面交互原语之前,行内编辑与明确清除是有意选择。
|
||||
- `/goal` 不接受逐命令 Round 上限。部署配置拥有默认值;得到直接人类指示后,已授权模型工具可以编辑上限。
|
||||
- TUI 渲染可移植纯文本,而不是持续更新的目标状态组件。可重连命令输出和适配器专用状态指示器予以延期。
|
||||
- ACP 自动化服务器、无头 CLI 与 JSON-RPC 前端不消费命令注册表。
|
||||
- ACP 自动化服务器、无头 CLI 与 JSON-RPC 运行入口不消费命令注册表。
|
||||
- 该命令观察并改变状态,但不认证完成或阻塞。基于评估器的认证延期到具有明确权限与隔离约定的独立策略层。
|
||||
|
||||
@@ -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-19-plugin-command-registration.md
|
||||
2026-07-19-plugin-command-registration.md: c8f0f2772a41948e9eb257a16f40194518c568f9
|
||||
2026-07-19-plugin-command-registration.zh.md: a8f7a3d1e7947aeb41344a72106cee55d249010c
|
||||
2026-07-19-plugin-command-registration.md: 76feba84492bd6b245246a6f1fb1e8f68d555684
|
||||
2026-07-19-plugin-command-registration.zh.md: e89cb79ba1e15dfefa527fe7bc37c09c3449f5b4
|
||||
|
||||
@@ -12,7 +12,7 @@ A shared mechanism must remain a UI concern rather than a model tool or agent-lo
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-commands` in `packages/interaction/commands/` is the product command registry. The TUI app bundle mounts it beside its consuming front door; the [automation-only ACP app](../simplification/2026-07-23-acp-automation-only-protocol.md) and the executor-less, UI-less agent spine omit it. TUI injects the service, while command producers depend only on the registry and any domain they operate.
|
||||
`@deepseek-ai/dsh-commands` in `packages/interaction/commands/` is the product command registry. The TUI app bundle mounts it beside its consuming front end; the [automation-only ACP app](../simplification/2026-07-23-acp-automation-only-protocol.md) and the executor-less, UI-less agent spine omit it. TUI injects the service, while command producers depend only on the registry and any domain they operate.
|
||||
|
||||
### Registry contract
|
||||
|
||||
@@ -50,7 +50,7 @@ TUI tests exercise all migrated built-ins, live plugin discovery, help/autocompl
|
||||
|
||||
- **Keep adapter-local switches** — rejected because optional plugins cannot contribute discovery and behavior without editing the TUI.
|
||||
- **Represent human commands as model tools** — rejected because discovery and direct invocation are human UI behavior; routing through the model adds latency, token cost, and reinterpretation.
|
||||
- **Put the registry in the core agent spine** — rejected because UI-less front doors do not consume it, while TUI can compose it explicitly.
|
||||
- **Put the registry in the core agent spine** — rejected because UI-less entry points do not consume it, while TUI can compose it explicitly.
|
||||
- **Make `dsh-agent-loop` inject commands** — rejected because the loop does not execute or discover human commands. Agent-scoped producers declare the UI dependency in a child plugin instead.
|
||||
- **Attach adapter masks to each definition** — rejected because support is a composition fact, not command-domain state. Every composed adapter exposes a registered command; an incompatible plugin omits registration in that deployment.
|
||||
- **Send unknown slash input to the model** — rejected because typoed or unavailable direct actions must fail predictably rather than change execution planes.
|
||||
@@ -68,4 +68,4 @@ TUI tests exercise all migrated built-ins, live plugin discovery, help/autocompl
|
||||
- Input metadata is limited to an unstructured text hint. Typed forms, argument schemas, and completion providers remain command-owned or require a later registry or consumer extension.
|
||||
- Generic command output is live-only and is not reconstructed after TUI restart.
|
||||
- Registry cancellation stops awaiting immediately, but external work stops only when a handler cooperates with its signal.
|
||||
- The ACP automation server, headless CLI, and JSON-RPC SDK front doors do not expose the command plane; only TUI consumes it.
|
||||
- The ACP automation server, headless CLI, and JSON-RPC SDK entry points do not expose the command plane; only TUI consumes it.
|
||||
|
||||
@@ -12,7 +12,7 @@ TUI 拥有斜杠命令。如果命令名、帮助文本、自动补全、分派
|
||||
|
||||
## 决策
|
||||
|
||||
位于 `packages/interaction/commands/` 的 `@deepseek-ai/dsh-commands` 是产品命令注册表。TUI 应用 bundle(组合包)把它挂载在消费该服务的入口旁;[仅面向自动化的 ACP(Agent Client Protocol)应用](../simplification/2026-07-23-acp-automation-only-protocol.md)和无执行器、无 UI 的智能体 spine(主干)都省略该服务。TUI 注入该服务,命令生产者只依赖注册表及其操作的领域。
|
||||
位于 `packages/interaction/commands/` 的 `@deepseek-ai/dsh-commands` 是产品命令注册表。TUI 应用 bundle(组合包)把它挂载在消费该服务的前端旁;[仅面向自动化的 ACP(Agent Client Protocol)应用](../simplification/2026-07-23-acp-automation-only-protocol.md)和无执行器、无 UI 的智能体 spine(主干)都省略该服务。TUI 注入该服务,命令生产者只依赖注册表及其操作的领域。
|
||||
|
||||
### 注册表约定
|
||||
|
||||
@@ -50,7 +50,7 @@ TUI 测试覆盖全部迁移后的内置命令、实时插件发现、帮助与
|
||||
|
||||
- **保留适配器本地 switch**——不予采纳,因为可选插件无法贡献发现与行为,除非修改 TUI。
|
||||
- **把人类命令表示为模型工具**——不予采纳,因为发现与直接调用属于人类 UI 行为;经由模型路由会增加延迟、token 成本和重新解释。
|
||||
- **把注册表放入核心智能体主干**——不予采纳,因为无 UI 前端不消费它,而 TUI 可以显式组合它。
|
||||
- **把注册表放入核心智能体主干**——不予采纳,因为无 UI 运行入口不消费它,而 TUI 可以显式组合它。
|
||||
- **让 `dsh-agent-loop` 注入 commands**——不予采纳,因为循环不执行也不发现人类命令。智能体作用域生产者改为在子插件中声明 UI 依赖。
|
||||
- **为每个定义附加适配器掩码**——不予采纳,因为支持能力是组合事实,而不是命令领域状态。每个已组合适配器都暴露已注册命令;不兼容插件不会在该部署中注册。
|
||||
- **把未知斜杠输入发送给模型**——不予采纳,因为输入错误或不可用的直接操作必须可预测地失败,而不能改变执行平面。
|
||||
@@ -68,4 +68,4 @@ TUI 测试覆盖全部迁移后的内置命令、实时插件发现、帮助与
|
||||
- 输入元数据仅限非结构化文本提示。类型化表单、参数模式和补全提供器仍由命令拥有,或需要后续注册表或消费方扩展。
|
||||
- 通用命令输出仅实时存在,TUI 重启后不会重建。
|
||||
- 注册表取消会立即停止等待,但外部工作只有在处理器配合信号时才会停止。
|
||||
- ACP 自动化服务器、无头 CLI 与 JSON-RPC SDK 前端不暴露命令平面;只有 TUI 消费它。
|
||||
- ACP 自动化服务器、无头 CLI 与 JSON-RPC SDK 运行入口不暴露命令平面;只有 TUI 消费它。
|
||||
|
||||
@@ -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-21-local-instruction-overlay.md
|
||||
2026-07-21-local-instruction-overlay.md: 3c7b2141b0515b5e667be4add6ad765e26c88cd8
|
||||
2026-07-21-local-instruction-overlay.md: fb5f916d426595a80bbbaa4192e4a3975b922b8a
|
||||
2026-07-21-local-instruction-overlay.zh.md: c97ed04607f497d829da0e904c248836252c73f7
|
||||
|
||||
@@ -26,7 +26,7 @@ The base and local candidates in one directory must stay independent across base
|
||||
|
||||
**Keep it opt-in through `instructionFileCandidates`.** Rejected: one directory has a single winner, so a `.local.` name added to that list shadows the base file rather than supplementing it. The packages guidance to keep opt-ins out of shipped defaults is outweighed here by strong prior art and the user-facing expectation that `.local.` files are always read.
|
||||
|
||||
**Default at the product `cordis.yml` level instead of the plugin schema.** Rejected: it would enable `.local.` only for whichever front door remembered to opt in, splitting behavior across TUI/ACP/headless and duplicating a value that belongs beside the existing candidate default.
|
||||
**Default at the product `cordis.yml` level instead of the plugin schema.** Rejected: it would enable `.local.` only for whichever entry point remembered to opt in, splitting behavior across TUI/ACP/headless and duplicating a value that belongs beside the existing candidate default.
|
||||
|
||||
**Reuse the bare directory as the scope key for base and local files.** Rejected: base and local files in one directory would collide in every scope-keyed map, so a change to one would suppress or overwrite the other. A distinct scope key per candidate keeps them independent without widening the persisted metadata shape.
|
||||
|
||||
|
||||
@@ -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-24-web-session-model-selector.md
|
||||
2026-07-24-web-session-model-selector.md: 78017b14a806f8e609a85e340094cd2a349d47e1
|
||||
2026-07-24-web-session-model-selector.zh.md: 87a342721f5e47f5bcedfafb578eb6916101d2f5
|
||||
2026-07-24-web-session-model-selector.md: e6a96ac62f69a3bd312f61cc920caa259d2dc5b0
|
||||
2026-07-24-web-session-model-selector.zh.md: 5a0245359d69ac6e59a20dc3276b9411c4b25e23
|
||||
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-07-24-web-session-model-selector.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Web conversation needs a visible, mutable session model selection sourced from the Host. Copying TUI presentation or hardcoding DeepSeek models in the browser would split model discovery and step-boundary semantics across front doors. A switch made while a response is running also needs one atomic boundary: prompt variables and request routing cannot observe different selections.
|
||||
The Web conversation needs a visible, mutable session model selection sourced from the Host. Copying TUI presentation or hardcoding DeepSeek models in the browser would split model discovery and step-boundary semantics across front ends. A switch made while a response is running also needs one atomic boundary: prompt variables and request routing cannot observe different selections.
|
||||
|
||||
## Decision
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
Web 对话需要一项由 Host 提供、可见且可更改的会话模型选择。如果照搬 TUI 的呈现方式,或在浏览器中硬编码 DeepSeek 模型,就会让模型发现逻辑和步骤边界语义分散到不同前门中。响应运行期间发生的切换还需要一个原子边界:提示词变量与请求路由不能观测到不同的选择。
|
||||
Web 对话需要一项由 Host 提供、可见且可更改的会话模型选择。如果照搬 TUI 的呈现方式,或在浏览器中硬编码 DeepSeek 模型,就会让模型发现逻辑和步骤边界语义分散到不同前端中。响应运行期间发生的切换还需要一个原子边界:提示词变量与请求路由不能观测到不同的选择。
|
||||
|
||||
## 决策
|
||||
|
||||
Web Host 为每个新建或恢复的 Agent 安装 `ModelSelection`。如果会话已经使用过模型,提供方/模型/推理(reasoning)选择来自最新的 `request/header`;否则来自 `ctx.agentDefaultModel`。`session.selectModel` 会赋值会话级选择,提示词组装则将它与请求路由一并捕获,因此运行中步骤发生的切换会应用于下一个组装步骤。下一个实际采用的选择通过完整的 `request/header` 快照持久化;尚未进入请求的选择则仅保存在当前进程中。
|
||||
|
||||
会话 RPC 领域公开 `session.models` 模型目录与 `session.selectModel`。该目录从 LLM(大语言模型)注册表动态构建,并按提供方分组;每个已列出模型的精确元数据还会加入由适配器持有的推理强度 ID、名称、说明和可选默认值。各提供方的目录与精确元数据会按提供方并发加载,且彼此独立失败,因此成功加载的分组仍可与可重试的失败记录一同使用。模型是否位于目录仅供参考:`session.models.current` 独立返回,即使不在任何分组中也仍然可以路由,但提供方停止公布该模型后,Host 不会合成未列出行。两个前门对这一状态给出不同回答:TUI 把未列出的当前模型渲染为独立一行,Web 则显示未设置状态的触发器标签并要求选择替代模型。Web 是编辑目录的 surface,因此缺席的目录行代表一项待作出的选择;TUI 只从现有行中选择。显示未设置标签的 Web composer 仍可以使用当前可路由选择发送消息。精确解析决定提供方/模型组合与显式推理强度是否可用。选择操作通过 `resolveCallConfig` 拒绝不支持的推理强度 ID,并在赋值该选择前具体化适配器配置的默认值。
|
||||
会话 RPC 领域公开 `session.models` 模型目录与 `session.selectModel`。该目录从 LLM(大语言模型)注册表动态构建,并按提供方分组;每个已列出模型的精确元数据还会加入由适配器持有的推理强度 ID、名称、说明和可选默认值。各提供方的目录与精确元数据会按提供方并发加载,且彼此独立失败,因此成功加载的分组仍可与可重试的失败记录一同使用。模型是否位于目录仅供参考:`session.models.current` 独立返回,即使不在任何分组中也仍然可以路由,但提供方停止公布该模型后,Host 不会合成未列出行。两个前端对这一状态给出不同回答:TUI 把未列出的当前模型渲染为独立一行,Web 则显示未设置状态的触发器标签并要求选择替代模型。Web 是编辑目录所在的前端,因此缺席的目录行代表一项待作出的选择;TUI 只从现有行中选择。显示未设置标签的 Web composer 仍可以使用当前可路由选择发送消息。精确解析决定提供方/模型组合与显式推理强度是否可用。选择操作通过 `resolveCallConfig` 拒绝不支持的推理强度 ID,并在赋值该选择前具体化适配器配置的默认值。
|
||||
|
||||
浏览器中的 `ModelService` 为每个实时会话持有一个 `ModelDirectory`。其快照包含当前完整的 `ModelSelection`、分组目录、提供方失败记录、操作错误,以及 `idle`、`loading`、`ready`、`selecting`、`error` 状态。挂载时会预先填充触发器标签,此后每次打开菜单都会刷新目录。目录与选择调用共用操作代次,防止较早响应覆盖较新结果;连接重置会先丢弃当前进程中的投影,再恢复 Host 选择。失败时保留先前的选择和可用分组。
|
||||
|
||||
|
||||
@@ -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-default-model-follows-the-picker.md
|
||||
2026-08-07-default-model-follows-the-picker.md: ed7e7a424d2cacadea890506fd9150ffdf7a993c
|
||||
2026-08-07-default-model-follows-the-picker.zh.md: 523c6f917dedf21c726ce5631226ba545126c757
|
||||
2026-08-07-default-model-follows-the-picker.md: 2a3ada55486345c0f58f0767bed5a93ecba04b88
|
||||
2026-08-07-default-model-follows-the-picker.zh.md: 08fecc6ec9b177f6424ada3172c67018ac72baea
|
||||
|
||||
@@ -6,13 +6,13 @@ English | [中文](2026-08-07-default-model-follows-the-picker.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A session model picker and a deployment default are two layers of the same preference. If the picker affects only its addressed session, the next blank session can select a different model with no user-facing way to align the default. If the default lives inside a Host gateway, direct Agent front doors cannot share it without depending on Host or duplicating state.
|
||||
A session model picker and a deployment default are two layers of the same preference. If the picker affects only its addressed session, the next blank session can select a different model with no user-facing way to align the default. If the default lives inside a Host gateway, direct Agent entry points cannot share it without depending on Host or duplicating state.
|
||||
|
||||
Reasoning effort makes the persistence shape significant: a model selection without an effort must clear a stored effort, or the next Agent may apply an effort that its selected model does not accept.
|
||||
|
||||
## Decision
|
||||
|
||||
`AgentDefaultModelService` provides `ctx.agentDefaultModel` and registers `{provider, model, reasoningEffort?}` as the `agent-default-model` Settings section. Its `{provider, model}` composition entry is the base layer and `settings.yaml` supplies the user layer. The service is front-door-neutral, so direct creation and ApiProxy-backed creation share one default ([headless direct core front door](../architecture/2026-08-09-headless-direct-core-front-door.md)). `workspaceRoot` remains ApiProxy config because it is a Host launcher fact rather than model state.
|
||||
`AgentDefaultModelService` provides `ctx.agentDefaultModel` and registers `{provider, model, reasoningEffort?}` as the `agent-default-model` Settings section. Its `{provider, model}` composition entry is the base layer and `settings.yaml` supplies the user layer. The service is entry-point-neutral, so direct creation and ApiProxy-backed creation share one default ([headless direct core entry point](../architecture/2026-08-09-headless-direct-core-entry-point.md)). `workspaceRoot` remains ApiProxy config because it is a Host launcher fact rather than model state.
|
||||
|
||||
`reasoningEffort` belongs to the Settings section but not to the plugin config. Settings layers merge by field, so a configured effort would survive a user selection that omits it. `saveSelection()` instead writes the complete user section; absence therefore clears a stored effort. A deployment-wide effort default belongs to the adapter profile, which resolves it per model.
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
会话模型选择器与部署默认值是同一项偏好的两个层次。如果选择器只影响其所在会话,下一个空白会话可能选择不同模型,用户却没有途径使默认值与选择器一致。如果默认值位于 Host 网关内部,直接创建 Agent 的前门只有依赖 Host 或复制状态才能共享它。
|
||||
会话模型选择器与部署默认值是同一项偏好的两个层次。如果选择器只影响其所在会话,下一个空白会话可能选择不同模型,用户却没有途径使默认值与选择器一致。如果默认值位于 Host 网关内部,直接创建 Agent 的入口只有依赖 Host 或复制状态才能共享它。
|
||||
|
||||
推理强度使持久化形态成为约定的一部分:不含强度的模型选择必须清除已存强度,否则下一个 Agent 可能会采用所选模型不接受的强度。
|
||||
|
||||
## 决定
|
||||
|
||||
`AgentDefaultModelService` 提供 `ctx.agentDefaultModel`,并把 `{provider, model, reasoningEffort?}` 注册为 `agent-default-model` Settings 分节。其 `{provider, model}` 组合条目是 base 层,`settings.yaml` 提供用户层。该服务不偏向特定前门,因此直接创建与 ApiProxy 支撑的创建共享同一个默认值([headless 直接 core 前门](../architecture/2026-08-09-headless-direct-core-front-door.md))。`workspaceRoot` 仍是 ApiProxy 配置,因为它是 Host 启动器事实,而不是模型状态。
|
||||
`AgentDefaultModelService` 提供 `ctx.agentDefaultModel`,并把 `{provider, model, reasoningEffort?}` 注册为 `agent-default-model` Settings 分节。其 `{provider, model}` 组合条目是 base 层,`settings.yaml` 提供用户层。该服务不偏向特定入口,因此直接创建与 ApiProxy 支撑的创建共享同一个默认值([headless 直接 core 入口](../architecture/2026-08-09-headless-direct-core-entry-point.md))。`workspaceRoot` 仍是 ApiProxy 配置,因为它是 Host 启动器事实,而不是模型状态。
|
||||
|
||||
`reasoningEffort` 属于 Settings 分节,但不属于插件配置。Settings 层按字段合并,因此已配置的强度会在用户选择省略它时继续存在。`saveSelection()` 写入完整的用户分节;缺席值由此清除已存强度。部署级强度默认值属于适配器 profile,并由它按模型解析。
|
||||
|
||||
|
||||
@@ -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-08-dsh-run-headless-command.md
|
||||
2026-08-08-dsh-run-headless-command.md: 566eeb5b2a09a0d07d72a68e4a5f449d2822e708
|
||||
2026-08-08-dsh-run-headless-command.zh.md: 177410e783a37026940829da5f81343ddc61cb29
|
||||
2026-08-08-dsh-run-headless-command.md: ed095f4077a23e51bffb647d24eed19ba09e11ed
|
||||
2026-08-08-dsh-run-headless-command.zh.md: 89d54e35573f14786e05d648f2b42891ca27a043
|
||||
|
||||
@@ -22,7 +22,7 @@ dsh run [--profile <name>] [--patch <path>...] <task...>
|
||||
|
||||
`RunInvocation` is a distinct `DshInvocation` member. The generic profile invocation carries no task state and accepts no positional arguments. Both dispatch paths use `runProfile`: profile boot omits `task`, while `run` supplies it. A one-shot profile without `headless-runner` fails through the composed-row check, and profile boot containing that row without a task points to `dsh run --profile <name> "<task>"`.
|
||||
|
||||
The [profile plugin bundle decision](../architecture/2026-08-05-profile-plugin-bundles.md) owns composition. [Headless is a direct core front door](../architecture/2026-08-09-headless-direct-core-front-door.md) owns the execution contract: one fresh persisted Session, final assistant text on stdout, completed/non-completed exit mapping, empty stderr on success, no listening port, and bounded signal shutdown after Agent quiescence and Session flush.
|
||||
The [profile plugin bundle decision](../architecture/2026-08-05-profile-plugin-bundles.md) owns composition. [Headless is a direct core entry point](../architecture/2026-08-09-headless-direct-core-entry-point.md) owns the execution contract: one fresh persisted Session, final assistant text on stdout, completed/non-completed exit mapping, empty stderr on success, no listening port, and bounded signal shutdown after Agent quiescence and Session flush.
|
||||
|
||||
The `run` verb belongs only to one-shot task execution. Application-file launch requires a distinct command name.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ dsh run [--profile <name>] [--patch <path>...] <task...>
|
||||
|
||||
`RunInvocation` 是单独的 `DshInvocation` 成员。通用 profile 调用不携带任务状态,也不接受位置参数。两条分派路径都使用 `runProfile`:profile 启动省略 `task`,而 `run` 提供该字段。缺少 `headless-runner` 的一次性 profile 会触发组合行检查;如果启动的 profile 包含该行却未提供任务,错误会指向 `dsh run --profile <name> "<task>"`。
|
||||
|
||||
[profile 插件组合包决策](../architecture/2026-08-05-profile-plugin-bundles.md)负责组合。[Headless 是直接 core 前门](../architecture/2026-08-09-headless-direct-core-front-door.md)负责执行约定:一个新的持久化会话、stdout 上的最终 assistant 文本、completed/非 completed 的退出状态映射、成功时为空的 stderr、无监听端口,以及 Agent 完全停稳且会话 flush 后的有界信号关闭。
|
||||
[profile 插件组合包决策](../architecture/2026-08-05-profile-plugin-bundles.md)负责组合。[Headless 是直接 core 入口](../architecture/2026-08-09-headless-direct-core-entry-point.md)负责执行约定:一个新的持久化会话、stdout 上的最终 assistant 文本、completed/非 completed 的退出状态映射、成功时为空的 stderr、无监听端口,以及 Agent 完全停稳且会话 flush 后的有界信号关闭。
|
||||
|
||||
`run` 动词只负责一次性任务执行。应用文件启动需要不同的命令名。
|
||||
|
||||
|
||||
@@ -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-08-user-explicit-skill-invocation.md
|
||||
2026-08-08-user-explicit-skill-invocation.md: 74d9f01f191005db6d3d283a3c56f5ee664447f8
|
||||
2026-08-08-user-explicit-skill-invocation.zh.md: 0f7c9e1261dda796d988c17ddf7199b74be4133e
|
||||
2026-08-08-user-explicit-skill-invocation.md: a7c2c15703af318cb4112f2d3dfda698bc5e3bc2
|
||||
2026-08-08-user-explicit-skill-invocation.zh.md: a8d685e5eb12766bebddebb7f5993579cf08531e
|
||||
|
||||
@@ -10,14 +10,14 @@ A `disable-model-invocation: true` skill is user-only by design: it never enters
|
||||
|
||||
## Decision
|
||||
|
||||
User-explicit invocation is a host-side pre-step injection, uniform for every user-invocable skill and every front end:
|
||||
User-explicit invocation is a host-side pre-step injection, uniform for every user-invocable skill and every entry point:
|
||||
|
||||
- `dsh-tool-skill` registers a second `agent/pre-step` listener (beside its catalog listener, the same seam `workspace-instructions` and the runtime-context snapshot ride): it scans the step's claimed messages for whitespace-bounded `/name` tokens — anywhere in the text, the same word-boundary shape the transcript chip decoration uses — collects first-seen-deduplicated names, loads each through `ctx.skills.get`, checks `isUserInvocable` on the loaded definition (the single lookup that produces what is injected), renders it with the shared `renderSkillContent`, and appends the injections after every other injection of the step: background first (workspace rules, runtime policy, catalog), the material the model must act on last, closest to its answer. Registration order pins the placement — the gesture listener registers before the catalog listener, so the waterfall hands it the catalog-bearing list to extend.
|
||||
- Precision is closed-set matching, exactly like slash commands: `/goal` resolves against the command registry, `/name` against the workspace's user-invocable skill directory; a miss stays ordinary prose, so nothing is ever guessed. Only `source.kind === 'user'` messages are scanned — external text cannot forge a gesture. Paths (`/usr/bin`), fractions (`5/8`), and prefixed tokens (`foo/name`) all break the boundary.
|
||||
- The client keeps the [plain-text-reference decision](../architecture/2026-07-25-web-input-machine-and-slash-pipeline.md): a menu pick lands the literal `/name ` and the prompt ships it verbatim; ui-skill implements no adjudication hooks and no reference codec. `skill.list` (now the domain's only RPC) serves every user-invocable skill with `modelInvocable` so menus mark user-only entries. A name shared with a host command resolves to the command — adjudication claims the line client-side before it becomes a prompt.
|
||||
- The injection is a `user`-role message carrying the `skill-invocation` source (`{ name, form: 'instructions' }`), so `user/message` logging, the context-injection transcript row (labelled with the skill name), and replay all come free; `renderSkillContent` lives in the `dsh-skill` seam, shared verbatim with the `skill` tool result, and the catalog's closing sentence tells the model to follow an injected block instead of re-loading it.
|
||||
|
||||
Peer-product survey (Pi, OpenCode, Claude Code, Kimi Code, Codex, DeepSeek-Reasonix — local checkouts) was unanimous that user-explicit triggering is programmatic injection with zero model participation; the final shape is closest to Codex's core-side `$name` mention scanning, which likewise frees every front end from implementing recognition.
|
||||
Peer-product survey (Pi, OpenCode, Claude Code, Kimi Code, Codex, DeepSeek-Reasonix — local checkouts) was unanimous that user-explicit triggering is programmatic injection with zero model participation; the final shape is closest to Codex's core-side `$name` mention scanning, which likewise frees every entry point from implementing recognition.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
用户显式调用是一次宿主侧的 pre-step 注入,对每一个用户可调用的 skill 和每一种前端一致:
|
||||
用户显式调用是一次宿主侧的 pre-step 注入,对每一个用户可调用的 skill 和每一种运行入口一致:
|
||||
|
||||
- `dsh-tool-skill` 注册第二个 `agent/pre-step` 监听器(与其目录监听器并列,也是 `workspace-instructions` 与运行时上下文快照搭乘的同一 seam):它在该步骤已认领的消息中扫描以空白为界的 `/name` token——文本中任意位置均可,与 transcript(文本记录)chip 装饰所用的词边界形状相同——收集按首见去重的名称,逐个经 `ctx.skills.get` 加载,在已加载定义上检查 `isUserInvocable`(产生注入内容的正是这同一次查找),用共享的 `renderSkillContent` 渲染,并把注入追加在该步骤所有其他注入之后:背景在前(工作区规则、运行时策略、目录),模型必须着手处理的材料在最后、最贴近它的回答。注册顺序钉住了这一位置——手势监听器先于目录监听器注册,因此 waterfall(瀑布式事件)会把携带目录的列表交给它来扩展。
|
||||
- 精确性来自封闭集合匹配,与斜杠命令完全一致:`/goal` 对照命令注册表解析,`/name` 对照工作区的用户可调用 skill 目录解析;未命中即保持为普通行文,因此绝不猜测。只扫描 `source.kind === 'user'` 的消息——外部文本无法伪造手势。路径(`/usr/bin`)、分数(`5/8`)与带前缀的 token(`foo/name`)都会破坏该边界。
|
||||
- 客户端沿用[纯文本引用决策](../architecture/2026-07-25-web-input-machine-and-slash-pipeline.md):菜单 pick 落下字面文本 `/name `,该文本随提示词原样提交;ui-skill 不实现任何裁决钩子,也没有引用 codec。`skill.list`(现在是该领域唯一的 RPC)提供每一个用户可调用的 skill 并携带 `modelInvocable`,供菜单标出仅限用户的条目。与宿主命令同名的名称解析为命令——客户端会在该行成为提示词之前完成裁决并将其认领。
|
||||
- 注入是一条携带 `skill-invocation` 来源(`{ name, form: 'instructions' }`)的 `user` 角色消息,因此 `user/message` 落账、上下文注入的 transcript 行(以 skill 名称标注)与回放全部免费获得;`renderSkillContent` 位于 `dsh-skill` seam,由注入和 `skill` 工具结果共用,二者内容逐字相同,目录的结尾一句会告诉模型遵循注入块而不是重新加载。
|
||||
|
||||
同类产品调研(Pi、OpenCode、Claude Code、Kimi Code、Codex、DeepSeek-Reasonix——本地检出)一致表明:用户显式触发都是模型零参与的程序化注入;最终形态最接近 Codex 核心侧的 `$name` mention 扫描——它同样让每一种前端免于自行实现识别。
|
||||
同类产品调研(Pi、OpenCode、Claude Code、Kimi Code、Codex、DeepSeek-Reasonix——本地检出)一致表明:用户显式触发都是模型零参与的程序化注入;最终形态最接近 Codex 核心侧的 `$name` mention 扫描——它同样让每一种运行入口免于自行实现识别。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
@@ -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-05-uniform-agent-note-format.md
|
||||
2026-07-05-uniform-agent-note-format.md: 06082251c1b96c90ed470d84224662e00e29791b
|
||||
2026-07-05-uniform-agent-note-format.md: c05d81c700b81f0d619173a261f405b1cc039df1
|
||||
2026-07-05-uniform-agent-note-format.zh.md: 3daa686b64b31ee2638b25dd4c42e5d8172f1d97
|
||||
|
||||
@@ -23,7 +23,7 @@ The whole corpus was normalized in the same change that defined the format — t
|
||||
- **A bare `# <title>` H1** — rejected: the `Agent Note: ` prefix self-describes the genre when a file is read outside its tree, and the format gate prevents it from drifting.
|
||||
- **`## What we give up` as the implemented closer** (the README's own phrase for what an Agent Note records) — rejected: it names only costs, and an honest consequences section records what the trade-off bought as well.
|
||||
- **Convention without a gate** (write the contract down, enforce by review) — rejected: the slop checklist already outlawed spec-speak in `implemented/` by convention, and nineteen files show what convention alone achieves here.
|
||||
- **A standalone `FORMAT.md` contract file** — rejected because one front door carrying layout, classification, and format is easier to discover and maintain than two contract files.
|
||||
- **A standalone `FORMAT.md` contract file** — rejected because one entry point carrying layout, classification, and format is easier to discover and maintain than two contract files.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -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-19-remove-generated-agent-note-index.md
|
||||
2026-07-19-remove-generated-agent-note-index.md: ee85ec0757d5924f5784c43a50003eb96e0a9531
|
||||
2026-07-19-remove-generated-agent-note-index.md: 652ac72afe69284240667e61af1f3fbfcb182ddb
|
||||
2026-07-19-remove-generated-agent-note-index.zh.md: 6e1967fbde0c6ac59bbc3a184dad68c989efadbc
|
||||
|
||||
@@ -12,7 +12,7 @@ The centralized chronological list adds little discovery value beyond browsing t
|
||||
|
||||
## Decision
|
||||
|
||||
The lifecycle/class filesystem tree is the Agent Note inventory. [README.md](../../README.md) remains the curated front door and contract, while ordinary tree navigation and repository search provide discovery.
|
||||
The lifecycle/class filesystem tree is the Agent Note inventory. [README.md](../../README.md) remains the curated entry point and contract, while ordinary tree navigation and repository search provide discovery.
|
||||
|
||||
`scripts/agent-note-tree.ts` owns the closed lifecycle/class sets and structural walker. `verify-agent-note-classification` validates that tree and rejects the legacy homes and a root `INDEX.md`; it does not render or freshness-check a centralized list.
|
||||
|
||||
|
||||
@@ -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-22-product-first-root-readme.md
|
||||
2026-07-22-product-first-root-readme.md: 32542a45019d64ed1826d4eb21e68c67c3c3d52e
|
||||
2026-07-22-product-first-root-readme.md: 00f6084da9e83135c881abfef21b0b249cd4e30a
|
||||
2026-07-22-product-first-root-readme.zh.md: 8ef6f4b99ca2c935183a225b6357d2d128edb3b0
|
||||
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-07-22-product-first-root-readme.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The root README is the repository's product front door. Its product-first structure and established voice remain useful, but concrete entry points and capability claims drift as the runtime grows. Rewriting sections whose facts remain correct increases the review surface and discards language that already works.
|
||||
The root README is the repository's product entry point. Its product-first structure and established voice remain useful, but concrete entry points and capability claims drift as the runtime grows. Rewriting sections whose facts remain correct increases the review surface and discards language that already works.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -26,7 +26,7 @@ Detailed package and service inventories remain at their owning documentation. T
|
||||
|
||||
**Use a long marketing page with screenshots, badges, and duplicated tutorials.** Rich media can demonstrate a stable product journey, but it ages separately from commands and source contracts. The root stays compact and links to runnable examples and owned guides.
|
||||
|
||||
**Project the root README as the documentation website home page.** A single landing page avoids two narratives, but the website's user guide and the repository's product/developer front door have different navigation and maintenance needs.
|
||||
**Project the root README as the documentation website home page.** A single landing page avoids two narratives, but the website's user guide and the repository's product/developer entry point have different navigation and maintenance needs.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -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-08-03-package-anchored-subsystem-pages.md
|
||||
2026-08-03-package-anchored-subsystem-pages.md: f429f3d41c1f152e83faeb12c379d221627e767f
|
||||
2026-08-03-package-anchored-subsystem-pages.zh.md: 3a56fa39357591197606a28b49cf0eac8f58963e
|
||||
2026-08-03-package-anchored-subsystem-pages.md: 2a47f35e9d3755286bed7d42f59fd21e30f9148d
|
||||
2026-08-03-package-anchored-subsystem-pages.zh.md: 53216a430b979f0612f256aaec9774b88fe14bbd
|
||||
|
||||
@@ -14,7 +14,7 @@ Every `docs/subsystems/` page anchors to the package or package group that decla
|
||||
|
||||
Every type a generated signature references must resolve somewhere in the folder: the agent ownership vocabulary moved from the generator's `TYPE_LINK_EXEMPTIONS` into `LINK_MAP → core.md`, so exemptions are reserved for genuinely service-local or vendored shapes. Each pasted declaration has one home (`SessionEvent` lives on [session.md](../../../../docs/subsystems/session.md); core.md summarizes and links).
|
||||
|
||||
Every `packages/<group>/README.md` pair is a thin front door in one shape: a why-first intro paragraph, a package table (Package / Role / ctx key), and a closing pointer to the owning subsystems page. Load-bearing prose that outgrows that shape relocates to the owning subsystems page rather than being deleted.
|
||||
Every `packages/<group>/README.md` pair is a thin entry point in one shape: a why-first intro paragraph, a package table (Package / Role / ctx key), and a closing pointer to the owning subsystems page. Load-bearing prose that outgrows that shape relocates to the owning subsystems page rather than being deleted.
|
||||
|
||||
The [subsystems README](../../../../docs/subsystems/README.md) indexes every page in the folder on both language sides; `scripts/project-doc-site.spec.ts` enforces one table row per page, so a page added by a later PR (or absorbed in a merge) cannot silently miss the index.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
生成签名引用的每个类型都必须能在目录中某处解析:agent 所有权词汇从生成器的 `TYPE_LINK_EXEMPTIONS` 移入 `LINK_MAP → core.md`,因此豁免只留给真正服务本地或 vendored 的形状。每个粘贴的声明只有一个家(`SessionEvent` 位于 [session.md](../../../../docs/subsystems/session.md);core.md 概括并链接)。
|
||||
|
||||
每个 `packages/<group>/README.md` 配对都是统一形状的轻薄门面:一段以「为什么」开头的介绍、一张包表格(包 / 角色 / ctx 键)、一个指向拥有方子系统页面的收尾指针。超出该形状的承重散文迁移到拥有方子系统页面,而非删除。
|
||||
每个 `packages/<group>/README.md` 配对都是统一形状的精简入口:一段以「为什么」开头的介绍、一张包表格(包 / 角色 / ctx 键)、一个指向拥有方子系统页面的收尾指针。超出该形状的承重散文迁移到拥有方子系统页面,而非删除。
|
||||
|
||||
[子系统 README](../../../../docs/subsystems/README.md) 在两个语言侧索引目录中的每一页;`scripts/project-doc-site.spec.ts` 强制每页一行表格,因此后续 PR 新增(或合并吸收)的页面无法悄悄缺席索引。
|
||||
|
||||
|
||||
@@ -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/simplification/2026-07-23-acp-automation-only-protocol.md
|
||||
2026-07-23-acp-automation-only-protocol.md: 29da3025251f9826c5780493bb2725b114a40601
|
||||
2026-07-23-acp-automation-only-protocol.md: 56c433acf59d3a0c4b5c8b6605e422d3c4efede2
|
||||
2026-07-23-acp-automation-only-protocol.zh.md: bf326ce9f6fea8c55b842bc61e3d8521032ea4d1
|
||||
|
||||
@@ -46,7 +46,7 @@ Protocol and lifecycle tests pin stop-reason and prompt codecs, version negotiat
|
||||
|
||||
## Consequences
|
||||
|
||||
ACP has a narrow contract suitable for agents and automation, while TUI and Web own human interaction and presentation. The package has fewer injected services, dependencies, protocol branches, and lifecycle states, and it no longer claims compatibility as a general editor front door.
|
||||
ACP has a narrow contract suitable for agents and automation, while TUI and Web own human interaction and presentation. The package has fewer injected services, dependencies, protocol branches, and lifecycle states, and it no longer claims compatibility as a general editor entry point.
|
||||
|
||||
Automation clients receive complete committed text rather than token deltas or structured tool UI. They inspect durable logs or another API when they need reasoning, tool traces, titles, or richer state. Fresh-session-only operation also means callers that need durable browsing or resume use a host API rather than ACP.
|
||||
|
||||
|
||||
@@ -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/simplification/2026-08-04-remove-tui-package.md
|
||||
2026-08-04-remove-tui-package.md: f048a04db02b582d038ebf549999c0fda50e30d3
|
||||
2026-08-04-remove-tui-package.md: 19cc7d1a89a55bb57a69b9fce301f48f89384acd
|
||||
2026-08-04-remove-tui-package.zh.md: be18cbd33cd4a2bb592de4e7986b2786da272d0f
|
||||
|
||||
@@ -8,7 +8,7 @@ English | [中文](2026-08-04-remove-tui-package.zh.md)
|
||||
|
||||
Removing the implicit `dsh` terminal application left `@deepseek-ai/dsh-tui` without a shipped composition. The package still carried a terminal renderer, interactive command and question adapters, extension overlays, snapshot fixtures, a patched `pi-tui` dependency, and SDK scaffolding that advertised TUI as a supported application interface. Keeping that surface required maintaining a product-sized frontend whose only remaining consumer was the project generator itself.
|
||||
|
||||
The package also made the repository's supported application inventory misleading. Current runnable products use Web, ACP, JSON-RPC, or one-shot CLI front doors, while the SDK continued to offer a terminal choice that no example or product command exercised.
|
||||
The package also made the repository's supported application inventory misleading. Current runnable products use Web, ACP, JSON-RPC, or one-shot CLI entry points, while the SDK continued to offer a terminal choice that no example or product command exercised.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -34,6 +34,6 @@ Repository searches and generated catalogs contain no TUI package, dependency pa
|
||||
|
||||
## Consequences
|
||||
|
||||
DeepSeek Harness has no terminal UI package or generated TUI application. Existing imports, `cordis.yml` rows, SDK `--interface=tui` requests, and projects that depend on the package fail instead of being translated. Web remains the shipped interactive surface; ACP, JSON-RPC, and one-shot CLI remain the non-Web front doors.
|
||||
DeepSeek Harness has no terminal UI package or generated TUI application. Existing imports, `cordis.yml` rows, SDK `--interface=tui` requests, and projects that depend on the package fail instead of being translated. Web remains the shipped interactive surface; ACP, JSON-RPC, and one-shot CLI remain the non-Web entry points.
|
||||
|
||||
The provider-neutral command, user-interaction, approval, tool-presentation, PTY, and session-projection capabilities remain available to other hosts. Reintroducing a terminal frontend requires a named product or deployment, an explicit package boundary, a concrete interaction provider, and assembled lifecycle and transcript acceptance for that frontend.
|
||||
|
||||
@@ -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/simplification/2026-08-08-remove-cli-demo.md
|
||||
2026-08-08-remove-cli-demo.md: c1153f5e9fcc89585e926f8f088829c849d1f6c1
|
||||
2026-08-08-remove-cli-demo.md: 403e01f94c976d2d17eb391830721b31675cd6a9
|
||||
2026-08-08-remove-cli-demo.zh.md: 7f11e0c17a15454b99b32d14ea6eda177f4b01f6
|
||||
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-08-08-remove-cli-demo.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
After [`dsh run`](../feature/2026-08-08-dsh-run-headless-command.md) became the product one-shot command, `@deepseek-ai/dsh-cli-demo` remained a second application package for the same job. It carried another executable, argument grammar, app composition, cancellation lifecycle, text/JSON/stream-JSON output contract, built artifact, documentation surface, and test suite. The two front doors also assembled different trees, so a successful demo did not prove the shipped `headless` profile and users had to choose between overlapping commands.
|
||||
After [`dsh run`](../feature/2026-08-08-dsh-run-headless-command.md) became the product one-shot command, `@deepseek-ai/dsh-cli-demo` remained a second application package for the same job. It carried another executable, argument grammar, app composition, cancellation lifecycle, text/JSON/stream-JSON output contract, built artifact, documentation surface, and test suite. The two entry points also assembled different trees, so a successful demo did not prove the shipped `headless` profile and users had to choose between overlapping commands.
|
||||
|
||||
The replay suites still need canonical session events to pin assembled backend behavior. That testing need does not require a published command or compatibility contract.
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
shutdown.interrupt(code)
|
||||
}
|
||||
// Signals own teardown throughout the startup window, not only after boot()
|
||||
// settles: an inserted front door can publish readiness before sibling rows
|
||||
// settles: an inserted entry point can publish readiness before sibling rows
|
||||
// finish mounting.
|
||||
process.on('SIGTERM', () => { interrupt(options.task === undefined ? 0 : 143) })
|
||||
process.on('SIGINT', () => { interrupt(130) })
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//
|
||||
// Zero model calls: no replay fixture mounts, so a stray stream fails loud.
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
@@ -29,6 +30,30 @@ const HEADER_EXPECTED = join(SNAPSHOT_DIR, 'header.expected.md')
|
||||
const SHIPPED_PRESETS = fileURLToPath(new URL('../../cli/config/agent-presets', import.meta.url))
|
||||
const MODE = webSnapshotMode()
|
||||
const SEED_ID = 'agent-preset-selection-web-e2e'
|
||||
/** A project skill only a preset that mounts `skill-local` can discover. */
|
||||
const SKILL_NAME = 'preset-catalog-demo'
|
||||
|
||||
/**
|
||||
* Seed one project skill under the connected workspace.
|
||||
*
|
||||
* Local skill discovery is a PRESET row, so this file is visible through
|
||||
* `standard` and invisible through `minimal` — which makes the '/' menu's
|
||||
* skill group a statement about the session's composition.
|
||||
* @param workspaceCwd - the scaffold's temp project parent.
|
||||
*/
|
||||
async function seedWorkspaceSkill(workspaceCwd: string): Promise<void> {
|
||||
const directory = join(workspaceCwd, 'workspace', '.agents', 'skills', SKILL_NAME)
|
||||
await mkdir(directory, { recursive: true })
|
||||
await writeFile(join(directory, 'SKILL.md'), [
|
||||
'---',
|
||||
`name: ${SKILL_NAME}`,
|
||||
'description: Prove the slash catalog follows the session composition',
|
||||
'---',
|
||||
'',
|
||||
'Body.',
|
||||
'',
|
||||
].join('\n'))
|
||||
}
|
||||
|
||||
/**
|
||||
* A settled one-turn session with no model content: this lane asserts chrome
|
||||
@@ -53,6 +78,35 @@ function seedLog(): string {
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* The preset the host reports for the blank session the workspace connect
|
||||
* produced. Addressed by id rather than by scanning the serialized list: the
|
||||
* seeded session records `minimal` too, so a substring match over the whole
|
||||
* list answers before the switch has landed.
|
||||
* @param baseUrl - the scaffold's origin.
|
||||
* @returns the live session's preset, or undefined before it is listed.
|
||||
*/
|
||||
async function livePreset(baseUrl: string): Promise<string | undefined> {
|
||||
const response = await fetch(`${baseUrl}/api/session.list`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type: 'client-request', rpcId: 'agent-preset-live', method: 'session.list', payload: {},
|
||||
}),
|
||||
})
|
||||
const body = await response.json() as {
|
||||
result: { value?: { items: { sessionId: string; agentPreset?: string }[] } }
|
||||
}
|
||||
return body.result.value?.items.find(item => item.sessionId !== SEED_ID)?.agentPreset
|
||||
}
|
||||
|
||||
/** Every option label the trigger menu currently lists. */
|
||||
async function menuOptions(page: Page): Promise<string[]> {
|
||||
const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
|
||||
await menu.waitFor({ timeout: 10_000 })
|
||||
return await menu.getByRole('option').allTextContents()
|
||||
}
|
||||
|
||||
describe('web e2e: agent-preset selection', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
@@ -67,6 +121,7 @@ describe('web e2e: agent-preset selection', () => {
|
||||
// records `minimal` is what makes the header label a claim about the
|
||||
// session rather than an echo of the current default.
|
||||
await seedSession(scaffold, seedLog(), SEED_ID, 'minimal')
|
||||
await seedWorkspaceSkill(scaffold.workspaceCwd)
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
@@ -114,21 +169,47 @@ describe('web e2e: agent-preset selection', () => {
|
||||
|
||||
// The chip stages; the blank session the workspace connect produced is
|
||||
// what the stage lands on. The host's own answer is what comes back.
|
||||
await expect.poll(async () => {
|
||||
const response = await fetch(`${scaffold.baseUrl}/api/session.list`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type: 'client-request', rpcId: 'agent-preset-stage', method: 'session.list', payload: {},
|
||||
}),
|
||||
})
|
||||
const body = await response.json() as {
|
||||
result: { value?: { sessions: { blank: boolean; agentPreset?: string }[] } }
|
||||
}
|
||||
return JSON.stringify(body.result.value?.sessions ?? body.result)
|
||||
}, { timeout: 15_000 }).toContain('minimal')
|
||||
await expect.poll(() => livePreset(scaffold.baseUrl), { timeout: 15_000 }).toBe('minimal')
|
||||
})
|
||||
|
||||
it('re-reads the slash catalog through the composition the switch installed', async () => {
|
||||
// Continues the previous case: the chip has already applied `minimal` to
|
||||
// the blank session, and this one reads the menu that switch left behind.
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-slash-catalog'))
|
||||
const composer = page.locator('textarea:enabled').last()
|
||||
|
||||
// `minimal` mounts neither the compaction group nor plan mode nor local
|
||||
// skill discovery, so the catalog the composer warmed under the
|
||||
// deployment default must not survive the switch.
|
||||
await composer.fill('/')
|
||||
await expect.poll(() => menuOptions(page), { timeout: 15_000 })
|
||||
.not.toEqual(expect.arrayContaining([expect.stringContaining(SKILL_NAME)]))
|
||||
const onMinimal = await menuOptions(page)
|
||||
expect(onMinimal.some(option => option.startsWith('compact'))).toBe(false)
|
||||
expect(onMinimal.some(option => option.startsWith('plan'))).toBe(false)
|
||||
// The host-plane commands and the client's own contribution are the
|
||||
// floor: they belong to no preset and never move.
|
||||
expect(onMinimal.some(option => option.startsWith('goal'))).toBe(true)
|
||||
expect(onMinimal.some(option => option.startsWith('model'))).toBe(true)
|
||||
await composer.fill('')
|
||||
|
||||
// Switching back up reaches the host at all — the chip compares the pick
|
||||
// against its list row, so a row that never reprojected the first switch
|
||||
// answers "already standard" and sends nothing — and restores the catalog
|
||||
// instead of leaving the session reading the narrower composition.
|
||||
await page.getByRole('button', { name: '极简模式' }).click()
|
||||
await page.getByRole('menuitem', { name: /^标准模式/ }).first().click()
|
||||
await expect.poll(() => livePreset(scaffold.baseUrl), { timeout: 15_000 }).toBe('standard')
|
||||
|
||||
await composer.fill('/')
|
||||
await expect.poll(() => menuOptions(page), { timeout: 15_000 })
|
||||
.toEqual(expect.arrayContaining([expect.stringContaining(SKILL_NAME)]))
|
||||
const onStandard = await menuOptions(page)
|
||||
expect(onStandard.some(option => option.startsWith('compact'))).toBe(true)
|
||||
expect(onStandard.some(option => option.startsWith('plan'))).toBe(true)
|
||||
await composer.fill('')
|
||||
}, 90_000)
|
||||
|
||||
it('labels a resumed session with the preset it was created under', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-header'))
|
||||
// The seeded session's cwd is the scaffold root rather than the connected
|
||||
|
||||
@@ -314,6 +314,7 @@ describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
await page.getByText('标准模式', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -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: 90d64fb0ac62020e13a7ce995c8e3273a5a9f906
|
||||
architecture.md: ebf05397cb67cea336dd36a4d9416d43b6002d4e
|
||||
architecture.zh.md: fec9a00484c495b0eed4773f262bb44543e304bd
|
||||
|
||||
@@ -17,7 +17,7 @@ Harnesses are [Cordis](cordis-primer.md) contexts; packages contribute services,
|
||||
| `ctx.systemPrompt` | `dsh-system-prompt` | ordered prompt sections, tool schemas, and variables |
|
||||
| `ctx.tools` | `dsh-tools` | tool registry and [execution pipeline](tool-execution-pipeline.md) |
|
||||
| `ctx.agents` | `dsh-agent` | live agents, delegated creation, `agent/*` events, process-local initiator scope |
|
||||
| `ctx.agentDefaultModel` | [`dsh-agent-default-model`](../packages/core/agent-default-model/README.md) | Settings-backed model selection shared by Agent front doors |
|
||||
| `ctx.agentDefaultModel` | [`dsh-agent-default-model`](../packages/core/agent-default-model/README.md) | Settings-backed model selection shared by Agent entry points |
|
||||
| `ctx.agentLoop` | `dsh-agent-loop` | concrete `Agent` driver |
|
||||
|
||||
### Capability Services
|
||||
@@ -164,7 +164,7 @@ Exceptions combine LLM Service Definition/Consumer roles, filesystem policy, web
|
||||
|
||||
### Bundles And Apps
|
||||
|
||||
`dsh-agent-spine-demo` bundles a spine and optional goals. App packages own CLI, ACP automation, and JSON-RPC front doors ([README](../packages/examples/agent-spine-demo/README.md), [acp/](../packages/acp/README.md), [interaction/](../packages/interaction/README.md)). `dsh-jsonrpc-agent` boots external `cordis.yml`; the Python SDK defaults when config is absent ([Python SDK](../python/README.md)). Thin deployments use swappable backends and optional tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)).
|
||||
`dsh-agent-spine-demo` bundles a spine and optional goals. App packages own CLI, ACP automation, and JSON-RPC entry points ([README](../packages/examples/agent-spine-demo/README.md), [acp/](../packages/acp/README.md), [interaction/](../packages/interaction/README.md)). `dsh-jsonrpc-agent` boots external `cordis.yml`; the Python SDK defaults when config is absent ([Python SDK](../python/README.md)). Thin deployments use swappable backends and optional tools ([examples/](../examples/AGENTS.md), [runnable wirings](cookbook/extension-cookbook.md#runnable-wirings), [graph atlas](graph-atlas.md)).
|
||||
|
||||
### Agent Presets
|
||||
|
||||
|
||||
@@ -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/capability-seams.md
|
||||
capability-seams.md: 05788eb8044f91e31c82dc4b78af0421e2b11030
|
||||
capability-seams.zh.md: ae182cfeeb1d7461122d791eedb818160a772e9b
|
||||
capability-seams.md: 345e17c8c28bbe3465abd20639770e6331a70d11
|
||||
capability-seams.zh.md: 472aaabf4c992fdcd54fbe0e9a14cf9a82e6b803
|
||||
|
||||
@@ -399,7 +399,7 @@ flowchart LR
|
||||
| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-message-llm`](../packages/session/session-title-first-message-llm), [`session-title-all-messages-llm`](../packages/session/session-title-all-messages-llm) | - | - | Owns the deterministic fallback, latest-title fold, and sole optional asynchronous provider registration. |
|
||||
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop), [`tools`](../packages/core/tools), [`tool-fs`](../packages/fs/tool-fs), [`tool-pty`](../packages/pty/tool-pty), [`tool-web`](../packages/web/tool-web) | - | Collects prompt sections and model-facing tool schemas for each step. |
|
||||
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/interaction/tool-ask-user), [`tool-bash`](../packages/bash/tool-bash), [`tool-cordis`](../packages/self-modification/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-pty`](../packages/pty/tool-pty), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web) | - | Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. |
|
||||
| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/interaction/user-interaction) | - | [`tool-ask-user`](../packages/interaction/tool-ask-user) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
|
||||
| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/interaction/user-interaction) | - | [`tool-ask-user`](../packages/interaction/tool-ask-user) | - | UI front ends provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
|
||||
| `ctx.planMode` | `core` | [`plan-mode`](../packages/plan/plan-mode) | - | - | - | Folds logged plan/mode state, flushes user selections at turn boundaries, renders deployment-owned guidance, registers /plan, and keeps the plan-exit schema stable across transitions. |
|
||||
| `ctx.agentPresets` | `core` | [`agent-presets`](../packages/preset/agent-presets) | - | - | - | Discovers preset directories over trusted and user-authored roots and mounts one preset cordis.yml under an agent scope during creation, rejecting a row that never activates or that publishes into the root service realm. |
|
||||
| `ctx.commands` | `core` | [`commands`](../packages/interaction/commands) | - | - | - | Plugins register direct human commands without sending invocations to the model. |
|
||||
@@ -407,7 +407,7 @@ flowchart LR
|
||||
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points) and serves the cold-read ladder: cache row + persistence tail replay, so listings never load full logs. |
|
||||
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge), [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
|
||||
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
|
||||
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`headless`](../packages/bundle/headless), [`host-apiproxy`](../packages/host/apiproxy) | - | Layers the default ModelSelection through settings so direct and Host-backed Agent front doors share one state owner. |
|
||||
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`headless`](../packages/bundle/headless), [`host-apiproxy`](../packages/host/apiproxy) | - | Layers the default ModelSelection through settings so direct and Host-backed Agent entry points share one state owner. |
|
||||
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
|
||||
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. |
|
||||
| `ctx.e2b` | `core` | [`e2b`](../packages/e2b/e2b) | - | [`fs-e2b`](../packages/e2b/fs-e2b), [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | - | Owns one shared E2B SDK handle, remote working directory, and final sandbox disposition so both fundamental E2B providers inhabit the same Linux runtime. |
|
||||
|
||||
@@ -401,7 +401,7 @@ flowchart LR
|
||||
| `ctx.sessionTitle` | `seam` | [`session-title`](../packages/session/session-title) | [`session-title-first-message-llm`](../packages/session/session-title-first-message-llm)、[`session-title-all-messages-llm`](../packages/session/session-title-all-messages-llm) | - | - | 负责确定性回退、最新标题折叠区,以及唯一的可选异步提供方注册。 |
|
||||
| `ctx.systemPrompt` | `core` | [`system-prompt`](../packages/core/system-prompt) | - | [`agent-loop`](../packages/core/agent-loop)、[`tools`](../packages/core/tools)、[`tool-fs`](../packages/fs/tool-fs)、[`tool-pty`](../packages/pty/tool-pty)、[`tool-web`](../packages/web/tool-web) | - | 为每个步骤收集提示词各部分和面向模型的工具 schema。 |
|
||||
| `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop)、[`tool-ask-user`](../packages/interaction/tool-ask-user)、[`tool-bash`](../packages/bash/tool-bash)、[`tool-cordis`](../packages/self-modification/tool-cordis)、[`tool-fs`](../packages/fs/tool-fs)、[`tool-pty`](../packages/pty/tool-pty)、[`tool-skill`](../packages/skill/tool-skill)、[`tool-subagent`](../packages/subagent/tool-subagent)、[`tool-todo`](../packages/todo/tool-todo)、[`tool-web`](../packages/web/tool-web) | - | 注册能力,负责 Code Mode 传输,并让调用依次经过策略前处理、单调守卫、环绕分派、策略后处理和最终结果观测。 |
|
||||
| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/interaction/user-interaction) | - | [`tool-ask-user`](../packages/interaction/tool-ask-user) | - | UI 入口提供当前生效的人工回答提供方;tool-ask-user 在提供方无关的 ask() promise 上暂停工具调用。 |
|
||||
| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/interaction/user-interaction) | - | [`tool-ask-user`](../packages/interaction/tool-ask-user) | - | UI 前端提供当前生效的人工回答提供方;tool-ask-user 在提供方无关的 ask() promise 上暂停工具调用。 |
|
||||
| `ctx.planMode` | `core` | [`plan-mode`](../packages/plan/plan-mode) | - | - | - | 折叠已记录的计划/模式状态,在轮次边界刷新用户选择,渲染由部署方拥有的指导信息,注册 /plan,并在状态转换期间保持计划退出 schema 稳定。 |
|
||||
| `ctx.agentPresets` | `core` | [`agent-presets`](../packages/preset/agent-presets) | - | - | - | 在受信任根目录与用户创作根目录上发现 preset 目录,并在创建期把一份 preset cordis.yml 挂载到 agent 作用域之下,拒绝始终未激活或向根服务 realm 发布服务的行。 |
|
||||
| `ctx.commands` | `core` | [`commands`](../packages/interaction/commands) | - | - | - | 插件注册直接面向人的命令,而不会把调用发送给模型。 |
|
||||
@@ -409,7 +409,7 @@ flowchart LR
|
||||
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`host-apiproxy`](../packages/host/apiproxy) | - | 按会话持久保存投影单元状态的检查点(节流检查点,以及轮次/结束/分离时的必选检查点),并提供冷读取阶梯:缓存行加持久化尾部回放,因此列表读取永远不需要加载完整日志。 |
|
||||
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-badge`](../packages/skill/skill-badge)、[`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | 合并提供方的 skill(技能)目录;tool-skill 渲染会话前缀目录,并加载完整的 skill 正文。 |
|
||||
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop)、[`acp`](../packages/acp/acp)、[`subagent-inprocess`](../packages/subagent/subagent-inprocess) | - | 拥有实时 Agent 句柄、创建/恢复工厂 seam,以及进程本地的发起方传播。 |
|
||||
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`headless`](../packages/bundle/headless)、[`host-apiproxy`](../packages/host/apiproxy) | - | 通过 settings 分层默认 `ModelSelection`,让直接前门与 Host 支撑的 Agent 前门共享同一个状态所有者。 |
|
||||
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`headless`](../packages/bundle/headless)、[`host-apiproxy`](../packages/host/apiproxy) | - | 通过 settings 分层默认 `ModelSelection`,让直接入口与 Host 支撑的 Agent 入口共享同一个状态所有者。 |
|
||||
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | 唯一的具体循环插件;扩展包依赖 dsh-agent 的事件和服务,而不依赖此包。 |
|
||||
| `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | 从会话日志折叠带修订版本的目标状态,并将实时延续激活保留在进程本地。 |
|
||||
| `ctx.e2b` | `core` | [`e2b`](../packages/e2b/e2b) | - | [`fs-e2b`](../packages/e2b/fs-e2b)、[`subprocess-e2b`](../packages/e2b/subprocess-e2b) | - | 拥有一个共享的 E2B SDK 句柄、远程工作目录和最终沙箱处置,使两个基础 E2B 提供方处于同一个 Linux 运行时中。 |
|
||||
|
||||
@@ -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/cordis-tutorial/07-into-the-harness.md
|
||||
07-into-the-harness.md: 38483b5c4993a44562970782dca5f676e4cb84f6
|
||||
07-into-the-harness.zh.md: 59ce716bdace894682bc1c8e6e00cf174008c26c
|
||||
07-into-the-harness.md: 69133786f58541b015aed080f4ac8fb2a7e488c0
|
||||
07-into-the-harness.zh.md: bc9c61da984e3eb691eb6bfbe59ae556823e82de
|
||||
|
||||
@@ -95,7 +95,7 @@ The logger fired first: `tools/result` is emitted as part of result materializat
|
||||
|
||||
## From here to a full agent
|
||||
|
||||
A real agent is this composition plus more plugins: an LLM adapter, the agent loop, persistence, a front end. Compare [examples/headless-agent/cordis.yml](../../examples/headless-agent/cordis.yml) — you can read every entry in it now. Add your `greet-tool.ts` to a copy of that file.
|
||||
A real agent is this composition plus more plugins: an LLM adapter, the agent loop, persistence, an entry point. Compare [examples/headless-agent/cordis.yml](../../examples/headless-agent/cordis.yml) — you can read every entry in it now. Add your `greet-tool.ts` to a copy of that file.
|
||||
|
||||
Where to go next:
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ logger 会先触发:`tools/result` 在结果物化过程中发出,发生在
|
||||
|
||||
## 从这里走向完整 agent(智能体)
|
||||
|
||||
真实 agent 就是这套组合再加上更多插件:LLM(大语言模型)适配器、agent loop(智能体循环)、持久化和前端。对照 [examples/headless-agent/cordis.yml](../../examples/headless-agent/cordis.yml),你现在已经可以读懂其中每个配置项。将 `greet-tool.ts` 加入该文件的副本即可。
|
||||
真实 agent 就是这套组合再加上更多插件:LLM(大语言模型)适配器、agent loop(智能体循环)、持久化和运行入口。对照 [examples/headless-agent/cordis.yml](../../examples/headless-agent/cordis.yml),你现在已经可以读懂其中每个配置项。将 `greet-tool.ts` 加入该文件的副本即可。
|
||||
|
||||
后续可以阅读:
|
||||
|
||||
|
||||
@@ -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/event-producer-consumer.md
|
||||
event-producer-consumer.md: 3b8a6b1dd155fd1350b164f1dd2d2bf0ec26a4a5
|
||||
event-producer-consumer.zh.md: 12de167fcd1217f00a8ae719ef3191a4873a2799
|
||||
event-producer-consumer.md: 70de749c328f1d901ff6f9bc0d97cd52a6f3bf63
|
||||
event-producer-consumer.zh.md: 6c49c33a1b1197a7da9bccfc161b7cfa6b6a548f
|
||||
|
||||
@@ -70,6 +70,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `internal/status` | - | [`agent`](../packages/core/agent) |
|
||||
| `locale/change` | `locale` (`emit`) | `locale` |
|
||||
| `models/changed` | `runtime` (`emit`) | `ui-models` |
|
||||
| `session/preset-changed` | `runtime` (`emit`) | `ui-command` |
|
||||
| `settings/changed` | `runtime` (`emit`) | `ui-models`, `ui-permission`, `ui-settings-general` |
|
||||
| `slash/input-begin-command` | - | `ui-conversation` |
|
||||
| `slash/input-consume-token` | - | `ui-conversation` |
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
| `internal/status` | - | [`agent`](../packages/core/agent) |
|
||||
| `locale/change` | `locale` (`emit`) | `locale` |
|
||||
| `models/changed` | `runtime` (`emit`) | `ui-models` |
|
||||
| `session/preset-changed` | `runtime` (`emit`) | `ui-command` |
|
||||
| `settings/changed` | `runtime` (`emit`) | `ui-models`, `ui-permission`, `ui-settings-general` |
|
||||
| `slash/input-begin-command` | - | `ui-conversation` |
|
||||
| `slash/input-consume-token` | - | `ui-conversation` |
|
||||
|
||||
@@ -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/core.md
|
||||
core.md: 9a1fa827a7e0168e662bc4595a3c0fc486be8d49
|
||||
core.zh.md: e51f8b61fb6ed0c7a6e2de377f8f93877f972831
|
||||
core.md: af27484160769156836f377e5b3aba2521280005
|
||||
core.zh.md: 12935f4d881f371cfe2c3c5bed85ef88f57ec71a
|
||||
|
||||
@@ -330,7 +330,7 @@ currentSelection(): ModelSelection
|
||||
/**
|
||||
* Save the complete default model selection. A deployment without a settings
|
||||
* provider keeps its composition entry.
|
||||
* @param next - resolved selection accepted by a front door.
|
||||
* @param next - resolved selection accepted by an entry point.
|
||||
* @returns fulfillment after the optional settings write settles.
|
||||
*/
|
||||
async saveSelection(next: ModelSelection): Promise<void>
|
||||
|
||||
@@ -338,7 +338,7 @@ currentSelection(): ModelSelection
|
||||
/**
|
||||
* Save the complete default model selection. A deployment without a settings
|
||||
* provider keeps its composition entry.
|
||||
* @param next - resolved selection accepted by a front door.
|
||||
* @param next - resolved selection accepted by an entry point.
|
||||
* @returns fulfillment after the optional settings write settles.
|
||||
*/
|
||||
async saveSelection(next: ModelSelection): Promise<void>
|
||||
|
||||
@@ -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/user/guide/providers.md
|
||||
providers.md: 29575fcc860721d35588d82a15d9b97f1b7dea0e
|
||||
providers.zh.md: 4b5fd32224fbfe5b685f886375b590fa3704505d
|
||||
providers.md: 4667e54161e77a62d454f3f78a8164d5c79b78c8
|
||||
providers.zh.md: 15e0ccc826a5c285c71d4776793f8048c11f6254
|
||||
|
||||
@@ -133,7 +133,7 @@ agent-default-model:
|
||||
reasoningEffort: high # optional
|
||||
```
|
||||
|
||||
After a session has run a turn, its own log remains authoritative for its model selection; the default applies only to sessions without a recorded request. The shipped fallback under this section is the base bundle's `agent-default-model` composition entry (`deepseek-official` / `deepseek-v4-flash`). A self-assembled `cordis.yml` mounts and configures `@deepseek-ai/dsh-agent-default-model`; both direct front doors and Host-backed front doors read that same service.
|
||||
After a session has run a turn, its own log remains authoritative for its model selection; the default applies only to sessions without a recorded request. The shipped fallback under this section is the base bundle's `agent-default-model` composition entry (`deepseek-official` / `deepseek-v4-flash`). A self-assembled `cordis.yml` mounts and configures `@deepseek-ai/dsh-agent-default-model`; both direct entry points and Host-backed entry points read that same service.
|
||||
|
||||
If the provider a saved default names is later removed, the composer says **Select model** and refuses input until you pick one, rather than sending to a route nothing serves.
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ agent-default-model:
|
||||
reasoningEffort: high # optional
|
||||
```
|
||||
|
||||
会话跑过一轮后,其自身日志仍是模型选择的权威;默认值只适用于尚无请求记录的会话。这个段落之下的出厂兜底是 base 组合包的 `agent-default-model` 组合条目(`deepseek-official` / `deepseek-v4-flash`)。自行组装的 `cordis.yml` 会挂载并配置 `@deepseek-ai/dsh-agent-default-model`;直接前门与 Host 支撑的前门都读取同一服务。
|
||||
会话跑过一轮后,其自身日志仍是模型选择的权威;默认值只适用于尚无请求记录的会话。这个段落之下的出厂兜底是 base 组合包的 `agent-default-model` 组合条目(`deepseek-official` / `deepseek-v4-flash`)。自行组装的 `cordis.yml` 会挂载并配置 `@deepseek-ai/dsh-agent-default-model`;直接入口与 Host 支撑的入口都读取同一服务。
|
||||
|
||||
如果某个已存默认值指向的提供方后来被删掉了,输入框会显示**选择模型**并拒绝输入,而不是把消息发给一个没人服务的路由。
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ flowchart LR
|
||||
cfg --> plugin_acp_acp_agent
|
||||
plugin_acp_acp_agent --> bundle_agent_core["@deepseek-ai/dsh-agent-spine-demo"]
|
||||
plugin_acp_acp_agent --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"]
|
||||
plugin_acp_acp_agent --> frontdoor_acp["@deepseek-ai/dsh-acp<br/>automation-only JSON-RPC stdio<br/>fresh sessions created by client"]
|
||||
plugin_acp_acp_agent --> entrypoint_acp["@deepseek-ai/dsh-acp<br/>automation-only JSON-RPC stdio<br/>fresh sessions created by client"]
|
||||
bundle_agent_core --> spine_llm["ctx.llm"]
|
||||
bundle_agent_core --> spine_sessions["ctx.sessions"]
|
||||
bundle_agent_core --> spine_tools["ctx.tools + tool-bash"]
|
||||
|
||||
@@ -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/headless-agent/README.md
|
||||
README.md: 6e80e56dec70c2be341ae5dfbad70e13a5109715
|
||||
README.md: f12a56920c79f3a7e257c4e56163f323e4312d11
|
||||
README.zh.md: 9e409735f03afc62cd788fa2a5d1afdef0fa6c2a
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
This directory owns the replay and real-model test composition for a headless coding agent: DeepSeek V4 + local bash and filesystem tools + subagent delegation + workflows and fresh-agent Ralph iteration + `todo_write` + JSONL persistence. It explicitly mounts the shared agent spine, one root agent, persistence, and checkpoint policy; it is not a second product front door.
|
||||
This directory owns the replay and real-model test composition for a headless coding agent: DeepSeek V4 + local bash and filesystem tools + subagent delegation + workflows and fresh-agent Ralph iteration + `todo_write` + JSONL persistence. It explicitly mounts the shared agent spine, one root agent, persistence, and checkpoint policy; it is not a second product entry point.
|
||||
|
||||
## Run it
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
- id: agent
|
||||
name: '@deepseek-ai/dsh-agent'
|
||||
|
||||
# The transport-independent default for Agents created by front doors.
|
||||
# The transport-independent default for Agents created by entry points.
|
||||
# Settings may supply a saved selection; consumers read it at creation time.
|
||||
- id: agent-default-model
|
||||
name: '@deepseek-ai/dsh-agent-default-model'
|
||||
|
||||
@@ -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/runtime/README.md
|
||||
README.md: 0a7d9975093da558af623ee9940f4be398526821
|
||||
README.zh.md: 41388e4ba564baa61cfdaaacb74f4f5ea053d41a
|
||||
README.md: 753d1de796ba8ff20217d423555710429e9b7a75
|
||||
README.zh.md: 9b5b8ba7ce42875afd4b9b83b9c2f64e95298ca5
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Client cordis boot and React-free object services: SlotsService wraps SlotCore and supplies renderer data sources; SessionsService owns Session objects and the Chat-facing list, scope, and event-window state; SessionHistoryService lazily owns independent raw-history ledgers for inspection consumers, loading the current tail first and prepending one older page only when its consumer requests it. Each history snapshot exposes the raw window's absolute base sequence so a consumer detects a prepend even when the page adds no surface-visible node. WorkspacesService depends on SessionsService and owns Workspace objects, list/actions, default-target derivation, and the New Session blank-reuse entry (`connectWorkspace`). The runtime fans the shared Host stream into the Session, Workspace, and activated history owners without routing inspection state through Session or SessionManager, and bridges the registry-invalidation frames to typed ctx events (`commands/changed`, `settings/changed`, `credentials/changed`, `models/changed`) so surface caches refetch without touching the stream. Client sessions are always Host-born (Session+Agent+cwd in one `session.create`); the client holds no pre-entity session state — a session's Agent scope (the client mirror of host dsh-scope, keyed by the shared agent/session id) is born when its row enters the list mirror and dies with the prune. Each `Session` holds a generic `ProjectionValueStore` seeded from the history-tail `projections` block and updated by `session/projection` frames under higher-seq-wins; domain keys (including `todos`) are read via `projections.faceOf` / `useProjection`, not via `ConversationSnapshot`. The store also publishes one reference-stable whole-value map through `SessionSummary.projectionValues`, allowing global list consumers to reuse the same projections without creating per-session subscriptions.
|
||||
Client cordis boot and React-free object services: SlotsService wraps SlotCore and supplies renderer data sources; SessionsService owns Session objects and the Chat-facing list, scope, and event-window state; SessionHistoryService lazily owns independent raw-history ledgers for inspection consumers, loading the current tail first and prepending one older page only when its consumer requests it. Each history snapshot exposes the raw window's absolute base sequence so a consumer detects a prepend even when the page adds no surface-visible node. WorkspacesService depends on SessionsService and owns Workspace objects, list/actions, default-target derivation, and the New Session blank-reuse entry (`connectWorkspace`). The runtime fans the shared Host stream into the Session, Workspace, and activated history owners without routing inspection state through Session or SessionManager, and bridges the registry-invalidation frames to typed ctx events (`commands/changed`, `session/preset-changed`, `settings/changed`, `credentials/changed`, `models/changed`) so surface caches refetch without touching the stream. `host/session-preset-changed` also folds its preset into the session row, because the switch's RPC echo reaches only the client that issued it. Client sessions are always Host-born (Session+Agent+cwd in one `session.create`); the client holds no pre-entity session state — a session's Agent scope (the client mirror of host dsh-scope, keyed by the shared agent/session id) is born when its row enters the list mirror and dies with the prune. Each `Session` holds a generic `ProjectionValueStore` seeded from the history-tail `projections` block and updated by `session/projection` frames under higher-seq-wins; domain keys (including `todos`) are read via `projections.faceOf` / `useProjection`, not via `ConversationSnapshot`. The store also publishes one reference-stable whole-value map through `SessionSummary.projectionValues`, allowing global list consumers to reuse the same projections without creating per-session subscriptions.
|
||||
|
||||
## Slot declaration injection
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
客户端 cordis 启动与不依赖 React 的对象服务:SlotsService 包装 SlotCore 并提供 renderer 数据源;SessionsService 拥有 Session 对象以及 Chat 所需的列表、scope 和事件窗口状态;SessionHistoryService 为检查类消费方惰性拥有彼此独立的原始历史账本,先加载当前尾部,并仅在消费方请求时向前补入一页更早历史。每份历史快照都会公开原始窗口的绝对基准序号,因此即使该页没有新增任何 surface 可见节点,消费方仍能检测到向前补页。WorkspacesService 依赖 SessionsService,拥有 Workspace 对象、列表/操作、默认目标派生,以及 New Session 空会话复用入口(`connectWorkspace`)。运行时把共享 Host 流分发给 Session、Workspace 和已激活的历史数据所有者,不让检查状态经过 Session 或 SessionManager,并把注册表失效帧桥接为类型化 ctx 事件(`commands/changed`、`settings/changed`、`credentials/changed`、`models/changed`),使各表面缓存无需触碰流即可重拉。客户端会话一律由 Host 创建(一次 `session.create` 同时产生 Session、agent(智能体)和 cwd);客户端不持有任何实体化之前的会话状态——agent scope(host dsh-scope 的客户端镜像,以 agent/session 共用 id 为键)在会话行进入列表镜像时创建,并随 prune 销毁。每个 `Session` 持有一个通用的 `ProjectionValueStore`,由历史记录尾部的 `projections` 块播种,并经 `session/projection` 帧按 seq 高者胜更新;领域键(含 `todos`)经 `projections.faceOf`/`useProjection` 读取,不经 `ConversationSnapshot`。该 store 还会通过 `SessionSummary.projectionValues` 发布一份引用稳定的完整值映射,使全局列表消费方无需为每个会话创建订阅,即可复用同一组投影。
|
||||
客户端 cordis 启动与不依赖 React 的对象服务:SlotsService 包装 SlotCore 并提供 renderer 数据源;SessionsService 拥有 Session 对象以及 Chat 所需的列表、scope 和事件窗口状态;SessionHistoryService 为检查类消费方惰性拥有彼此独立的原始历史账本,先加载当前尾部,并仅在消费方请求时向前补入一页更早历史。每份历史快照都会公开原始窗口的绝对基准序号,因此即使该页没有新增任何 surface 可见节点,消费方仍能检测到向前补页。WorkspacesService 依赖 SessionsService,拥有 Workspace 对象、列表/操作、默认目标派生,以及 New Session 空会话复用入口(`connectWorkspace`)。运行时把共享 Host 流分发给 Session、Workspace 和已激活的历史数据所有者,不让检查状态经过 Session 或 SessionManager,并把注册表失效帧桥接为类型化 ctx 事件(`commands/changed`、`session/preset-changed`、`settings/changed`、`credentials/changed`、`models/changed`),使各表面缓存无需触碰流即可重拉。`host/session-preset-changed` 还会把其中的 preset 折进会话行,因为这次切换的 RPC 回执只会到达发起它的那个客户端。客户端会话一律由 Host 创建(一次 `session.create` 同时产生 Session、agent(智能体)和 cwd);客户端不持有任何实体化之前的会话状态——agent scope(host dsh-scope 的客户端镜像,以 agent/session 共用 id 为键)在会话行进入列表镜像时创建,并随 prune 销毁。每个 `Session` 持有一个通用的 `ProjectionValueStore`,由历史记录尾部的 `projections` 块播种,并经 `session/projection` 帧按 seq 高者胜更新;领域键(含 `todos`)经 `projections.faceOf`/`useProjection` 读取,不经 `ConversationSnapshot`。该 store 还会通过 `SessionSummary.projectionValues` 发布一份引用稳定的完整值映射,使全局列表消费方无需为每个会话创建订阅,即可复用同一组投影。
|
||||
|
||||
## Slot 声明注入
|
||||
|
||||
|
||||
@@ -181,6 +181,18 @@ declare module 'cordis' {
|
||||
* @mode emit
|
||||
*/
|
||||
'models/changed'(): void
|
||||
/**
|
||||
* One session's agent preset changed (host/session-preset-changed
|
||||
* passthrough), so everything its composition decides — the command
|
||||
* catalog, the skill catalog — is stale for that session and no other.
|
||||
* Every connected client observes it, not only the one that issued the
|
||||
* switch. Subscribers refetch their own session-keyed caches; the frame
|
||||
* carries no catalog.
|
||||
* @mode emit
|
||||
* @param sessionId - the session whose composition changed.
|
||||
* @param agentPreset - the preset it now runs.
|
||||
*/
|
||||
'session/preset-changed'(sessionId: SessionId, agentPreset: string): void
|
||||
/**
|
||||
* A connection generation was (re-)established. Wire-derived caches must
|
||||
* treat their state as stale and repull (commands directory; the queue
|
||||
@@ -244,6 +256,9 @@ export function apply(ctx: Context): void {
|
||||
// and model surfaces) subscribe on ctx.
|
||||
const frame = envelope.payload
|
||||
if (frame.type === 'host/commands-changed') ctx.emit('commands/changed')
|
||||
else if (frame.type === 'host/session-preset-changed') {
|
||||
ctx.emit('session/preset-changed', frame.sessionId, frame.agentPreset)
|
||||
}
|
||||
else if (frame.type === 'host/settings-changed') ctx.emit('settings/changed', frame.ns)
|
||||
else if (frame.type === 'host/credentials-changed') ctx.emit('credentials/changed', frame.ref)
|
||||
else if (frame.type === 'host/models-changed') ctx.emit('models/changed')
|
||||
|
||||
@@ -780,6 +780,14 @@ export class SessionManager {
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'host/session-preset-changed': {
|
||||
// Every connected client observes the switch here; only the tab that
|
||||
// issued it also gets the RPC echo. The merge keeps the row's own
|
||||
// updatedAt and lowers `blank` only, so re-applying the switching
|
||||
// tab's own frame is a no-op.
|
||||
this.noteAgentPreset(frame.sessionId, frame.agentPreset)
|
||||
return
|
||||
}
|
||||
case 'host/session-removed': {
|
||||
const summary = this.summaries.find(candidate => candidate.sessionId === frame.sessionId)
|
||||
const durableSubagent = summary?.origin === 'subagent' || this.addresses.has(frame.sessionId)
|
||||
@@ -1005,7 +1013,7 @@ export class SessionManager {
|
||||
const prev = this.entryCache.get(entry.sessionId)
|
||||
if (
|
||||
prev !== undefined && prev.updatedAt === entry.updatedAt && prev.running === entry.running
|
||||
&& prev.blank === entry.blank
|
||||
&& prev.blank === entry.blank && prev.agentPreset === entry.agentPreset
|
||||
&& prev.parentSessionId === entry.parentSessionId && prev.cwd === entry.cwd
|
||||
&& prev.origin === entry.origin && prev.title === entry.title && prev.depth === entry.depth
|
||||
&& prev.pendingInteraction === entry.pendingInteraction
|
||||
|
||||
@@ -35,6 +35,7 @@ type FeedRow = {
|
||||
origin?: 'subagent'
|
||||
running?: boolean
|
||||
blank?: boolean
|
||||
agentPreset?: string
|
||||
}
|
||||
|
||||
async function feedList(b: Bench, rows: FeedRow[]): Promise<void> {
|
||||
@@ -44,6 +45,7 @@ async function feedList(b: Bench, rows: FeedRow[]): Promise<void> {
|
||||
...(r.cwd !== undefined ? { cwd: r.cwd } : {}),
|
||||
...(r.parentId !== undefined ? { parentSessionId: sid(r.parentId) } : {}),
|
||||
...(r.origin !== undefined ? { origin: r.origin } : {}),
|
||||
...(r.agentPreset !== undefined ? { agentPreset: r.agentPreset } : {}),
|
||||
})),
|
||||
}) as never)
|
||||
await b.svc.refresh()
|
||||
@@ -70,6 +72,38 @@ describe('list store projection', () => {
|
||||
expect(state.byId[sid('s2')]?.title).toBeUndefined()
|
||||
})
|
||||
|
||||
it('reprojects a blank session whose composition switched and nothing else moved', async () => {
|
||||
const b = bench()
|
||||
await feedList(b, [{ id: 's1', blank: true, agentPreset: 'standard' }])
|
||||
expect(b.svc.list.getSnapshot().byId[sid('s1')]?.agentPreset).toBe('standard')
|
||||
|
||||
// A confirmed switch moves the preset alone: the row keeps its updatedAt,
|
||||
// title, running, and blank bits, so an identity guard blind to the preset
|
||||
// would serve the old row forever — and every reader (the hero chip's own
|
||||
// no-op check, the header label) would keep the composition it replaced.
|
||||
b.svc.noteAgentPreset(sid('s1'), 'minimal')
|
||||
await Promise.resolve()
|
||||
|
||||
expect(b.svc.list.getSnapshot().byId[sid('s1')]?.agentPreset).toBe('minimal')
|
||||
})
|
||||
|
||||
it('learns a preset switch from the host frame, not only from the tab that issued it', async () => {
|
||||
const b = bench()
|
||||
await feedList(b, [{ id: 's1', blank: true, agentPreset: 'standard' }])
|
||||
|
||||
// Every connected client gets this frame; only the switching tab gets the
|
||||
// RPC echo. A client that ignored the payload would keep labelling the
|
||||
// session with the composition it replaced.
|
||||
b.svc.handleHostEnvelope({
|
||||
rpcId: 'r1' as never,
|
||||
payload: { type: 'host/session-preset-changed', sessionId: sid('s1'), agentPreset: 'minimal' } as never,
|
||||
})
|
||||
await Promise.resolve()
|
||||
|
||||
expect(b.svc.list.getSnapshot().byId[sid('s1')]?.agentPreset).toBe('minimal')
|
||||
expect(b.svc.list.getSnapshot().byId[sid('s1')]?.blank).toBe(true)
|
||||
})
|
||||
|
||||
it('reflects live increments (host stream via manager) into the store', async () => {
|
||||
const b = bench()
|
||||
await feedList(b, [{ id: 's1' }])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Wire-to-typed-event bridge: host/commands-changed
|
||||
* → ctx 'commands/changed'; each established connection generation →
|
||||
* → ctx 'commands/changed'; host/session-preset-changed →
|
||||
* ctx 'session/preset-changed'; each established connection generation →
|
||||
* ctx 'connection/reset' (the forced cache-invalidation broadcast).
|
||||
*/
|
||||
import { Context } from 'cordis'
|
||||
@@ -67,6 +68,17 @@ describe('wire event bridge', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('broadcasts session/preset-changed with the recomposed session and its new preset', async () => {
|
||||
const bench = await mount()
|
||||
const seen: Array<[string, string]> = []
|
||||
bench.ctx.on('session/preset-changed', (sessionId, agentPreset) => { seen.push([sessionId, agentPreset]) })
|
||||
bench.sinks?.onHostEnvelope?.({
|
||||
rpcId: 'r1' as never,
|
||||
payload: { type: 'host/session-preset-changed', sessionId: 's1' as never, agentPreset: 'minimal' },
|
||||
})
|
||||
expect(seen).toEqual([['s1', 'minimal']])
|
||||
})
|
||||
|
||||
it('broadcasts connection/reset on every established generation (reconnect invalidation)', async () => {
|
||||
const bench = await mount()
|
||||
let resets = 0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user