Explorar o código

fix(cli): keep the subagent registry host-plane in the cordis preset

The cordis preset moved `bash-env`, the `subagents` registry, and its
spawn/fork backends into entry-local realms. A host row that injects a service
is the criterion for host-plane ownership: `apps/cli/src/web.ts` injects
`bash-env`, and `dsh-host-apiproxy` injects `subagents` to answer the
browser's cross-session queries, so both waited forever for a service only
sessions provided. A provider name also registers once, so the second session
would have collided regardless.

This layer introduces the preset, so it is where the plane belongs — a later
layer already corrected it, which left this layer and the docs one unmountable
on their own. The composition test stops disabling `api-gateway` with it: the
row whose pending injection names the break cannot be the row the test turns
off, so the boot audit now covers the whole host-plane injection graph.
Yichen Jiang hai 2 meses
pai
achega
51e1f48540

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md
-2026-08-03-per-session-agent-presets.md: dbf2f7c3de1447382071ebcfb1d6b9abe9640210
-2026-08-03-per-session-agent-presets.zh.md: e34018f713144c6457c86d5ce4b533106a7e5372
+2026-08-03-per-session-agent-presets.md: 63bbc361b7968c3447cffaf8134422b7b78af98f
+2026-08-03-per-session-agent-presets.zh.md: bee5ba37dd27db22c18c9d1dd6d58965ee66b80e

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md

@@ -19,7 +19,7 @@ Composition splits into two planes, decided by what must be shared rather than b
 | Plane | Instances | Contents |
 |---|---|---|
 | Host | one | The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), cross-session facilities (persistence, query, projections, storage, settings, credentials, telemetry), and the web host |
-| Agent | one per session | What a single agent contributes to those registries: tool plugins, persona and prompt sections, delegation backends, compaction policy |
+| Agent | one per session | What a single agent contributes to those registries: tool plugins, persona and prompt sections, compaction policy |
 
 Model routing stays out of presets. `installAgentLlmTarget` is already the per-agent seam for provider, model, and reasoning effort, and an LLM adapter mounted inside a preset would never be resolved by `agent-loop`, which lives in the host plane.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.zh.md

@@ -19,7 +19,7 @@ Status: implemented
 | 平面 | 实例数 | 内容 |
 |---|---|---|
 | 宿主 | 一份 | 注册表本身(`tools`、`systemPrompt`、`agents`、`agent-loop`、`sessions`)、跨会话设施(持久化、查询、投影、存储、设置、凭据、遥测),以及 web 宿主 |
-| agent | 每会话一份 | 单个 agent 对这些注册表的贡献:工具插件、人设与提示词段落、委派后端、压缩策略 |
+| agent | 每会话一份 | 单个 agent 对这些注册表的贡献:工具插件、人设与提示词段落、压缩策略 |
 
 模型路由不进 preset。`installAgentLlmTarget` 已经是 provider、model 与 reasoning effort 的按 agent 可替换点;而挂在 preset 内部的 LLM 适配器永远不会被 `agent-loop` 解析到,因为后者位于宿主平面。
 

+ 28 - 37
apps/cli/config/agent-presets/cordis/agent.cordis.yml

@@ -22,7 +22,7 @@
 
       You can read and modify the harness you run on. Its composition is Cordis: every capability is a plugin row in a `cordis.yml`, and an agent preset is one such file mounted for a single session.
 
-      Two planes decide where an edit belongs. The HOST composition holds the registries and anything shared across sessions — persistence, the sandbox and approval stack, the model route. An AGENT PRESET holds what one session contributes to those registries: its tools, its persona, its delegation backends. A row that publishes a service belongs in the host composition, or inside an `isolate` realm if the preset genuinely owns that service.
+      Two planes decide where an edit belongs. The HOST composition holds the registries and anything shared across sessions — persistence, the sandbox and approval stack, the model route, the subagent registry and its backends. An AGENT PRESET holds what one session contributes to those registries: its tools, its persona, its prompt sections. A row that publishes a service belongs in the host composition, or inside an `isolate` realm if the preset genuinely owns that service and nothing outside one agent reads it.
 
       Load the `editing-cordis-compositions` skill before writing or changing a composition.
 
@@ -33,22 +33,15 @@
 
 # ── shell ───────────────────────────────────────────────────────────────────
 
-# `tool-bash` reads as a tool but provides the `bashEnv` service, so it needs a
-# realm like any other provider. The executor behind it (`bash-sandbox`) stays
-# in the host composition, where the sandbox policy owns it.
-- id: shell
-  name: cordis:group
-  group: true
-  isolate:
-    bashEnv: true
-  config:
-    # The registry and its consumer share the realm: a consumer left outside
-    # would resolve the host's `bashEnv`, which this plane no longer provides.
-    - id: bash-env
-      name: '@deepseek-ai/dsh-bash-env'
-
-    - id: tool-bash
-      name: '@deepseek-ai/dsh-tool-bash'
+# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
+# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
+# the criterion for host-plane ownership — injection resolves before any session
+# exists, so there is no agent to key by. Behind a preset realm those variables
+# never reached the model's shell at all. `tool-bash` consumes the host registry
+# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
+# sandbox policy owns it.
+- id: tool-bash
+  name: '@deepseek-ai/dsh-tool-bash'
 
 # ── filesystem ──────────────────────────────────────────────────────────────
 
@@ -157,29 +150,27 @@
 
 # ── delegation and workflows ────────────────────────────────────────────────
 
-# Every backend and every tool that reaches `subagents` or `workflows` shares
-# one realm: a consumer left outside it would resolve the host's registry
-# instead, which this preset does not populate.
+# The `subagents` registry and its spawn/fork backends live in the HOST
+# composition: the registry is a process singleton whose cross-session queries
+# the api-proxy serves to the browser, and a provider name may only be
+# registered once. This preset contributes the delegation TOOLS, which resolve
+# that host registry.
+#
+# `workflows` is different — nothing outside an agent reads it — so every row
+# that reaches it shares one entry-local realm here, and a consumer left
+# outside would resolve a host registry this preset does not populate.
+#
+# `tool-subagent-report` is host-plane for the same reason as the registry,
+# not because a preset may not want it: it registers a CONTINUABLE SETUP on
+# that singleton rather than a tool this agent calls, and the setup list is
+# not scope-aware — one copy per mounted preset means every child gets
+# `report` registered once per live session, which throws on the second.
 - id: delegation
   name: cordis:group
   group: true
   isolate:
-    subagents: true
     workflows: true
   config:
-    - id: subagent
-      name: '@deepseek-ai/dsh-subagent'
-
-    - id: subagent-spawn
-      name: '@deepseek-ai/dsh-subagent-spawn'
-      config:
-        providerName: spawn
-
-    - id: subagent-fork
-      name: '@deepseek-ai/dsh-subagent-fork'
-      config:
-        providerName: fork
-
     - id: tool-subagent-control
       name: '@deepseek-ai/dsh-tool-subagent-control'
 
@@ -200,9 +191,6 @@
         toolName: subagent_fork
         backgroundMode: continuable
 
-    - id: tool-subagent-report
-      name: '@deepseek-ai/dsh-tool-subagent-report'
-
     - id: workflow-workerthread
       name: '@deepseek-ai/dsh-workflow-workerthread'
       config:
@@ -219,6 +207,9 @@
 
 # ── remaining model-facing rows ─────────────────────────────────────────────
 
+- id: tool-ask-user
+  name: '@deepseek-ai/dsh-tool-ask-user'
+
 - id: tool-todo
   name: '@deepseek-ai/dsh-tool-todo'
   config:

+ 4 - 2
apps/cli/config/agent-presets/cordis/skills/editing-cordis-compositions/SKILL.md

@@ -11,9 +11,11 @@ Every capability in this harness is a plugin row in a `cordis.yml`. There is no
 
 Two planes, and the choice is not about how "agent-related" something feels — it is about whether the thing must be shared.
 
-**Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, and the model route. One instance for the process.
+**Host composition.** The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), anything crossing sessions (persistence, session query, storage, settings, credentials, telemetry), the sandbox and approval stack, the model route, and the subagent registry with its spawn/fork backends. One instance for the process.
 
-**Agent preset.** What one session contributes to those registries: its tool plugins, its persona, its delegation backends, its compaction policy. One instance per session, mounted under that session's scope and unwound with it.
+**Agent preset.** What one session contributes to those registries: its tool plugins, its persona and prompt sections, its compaction policy. One instance per session, mounted under that session's scope and unwound with it.
+
+**A service with a consumer outside the agent plane cannot move into a preset.** `subagents` is the worked example: the registry answers cross-session queries for the host api-proxy, so a per-session copy both starves that host row — it waits forever for a service nothing provides — and collides on the second session, since a provider name registers once. The preset contributes the delegation *tools*; the registry and its backends stay host-side.
 
 A preset is a directory holding one `agent.cordis.yml`. The shipped ones live beside the deployment's composition; locally authored ones live under `$DSH_HOME/.agent-presets/<name>/`.
 

+ 9 - 6
apps/cli/tests/web-agent-presets.e2e.ts

@@ -43,13 +43,16 @@ async function bootWeb(settingsFile: string): Promise<Context> {
     { id: 'telemetry-otel', disabled: true },
     { id: 'modules', disabled: true },
     { id: 'connection', disabled: true },
-    // NOT a side-effect row: the api-proxy cannot mount in THIS layer at all,
-    // because it injects `subagents` and the subagent registry moved into the
-    // presets here. That is the breakage a later layer returns to the host
-    // plane; when it does, this line comes out and the boot audit covers the
-    // whole host-plane injection graph again.
-    { id: 'api-gateway', disabled: true },
+    // `api-gateway` stays ENABLED on purpose — the api-proxy is the host row
+    // that injects `subagents`, `workspace`, and the rest of the agent plane,
+    // so disabling it would hide exactly the breakage this file exists to
+    // catch: a service moved into the presets that a host row still waits for.
+    // The boot audit is that assertion.
+    // The shipped `-auto` chooser resolves its interaction from a running
+    // host and so waits for the webserver disabled above; the browse variant
+    // supplies `directoryPicker` without one.
     { id: 'directory-picker', disabled: true },
+    { insert: [{ id: 'directory-picker-browse', name: '@deepseek-ai/dsh-host-directory-picker-browse' }] },
     // The roster AppCLIEntry would patch in; only the shipped root, so a
     // developer's own `~/.dsh/.preset` cannot change this test's outcome.
     // `default` here is the COMPOSITION default — the base layer the settings