dsh-loop-engine 0.1.5-rc3 → 0.1.5-rc4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +44 -187
  2. package/README.zh.md +45 -103
  3. package/lib/client.js +887 -266
  4. package/lib/index.js +2212 -914
  5. package/lib/invariant.js +43 -45
  6. package/lib/types/agent-preset-ids.d.ts +303 -0
  7. package/lib/types/client/LoopEngineBadge.d.ts +44 -17
  8. package/lib/types/client/LoopEngineComposerSelect.d.ts +79 -13
  9. package/lib/types/client/LoopEngineSection.d.ts +5 -4
  10. package/lib/types/client/locales.d.ts +133 -7
  11. package/lib/types/client/reload.d.ts +135 -0
  12. package/lib/types/client/session-engine.d.ts +474 -0
  13. package/lib/types/client/store.d.ts +1 -1
  14. package/lib/types/client/turn-status.d.ts +112 -10
  15. package/lib/types/client/use-session-engine.d.ts +66 -0
  16. package/lib/types/commands.d.ts +11 -3
  17. package/lib/types/driver-core/host-servers.d.ts +106 -0
  18. package/lib/types/driver-core/hosted-engine-runtime.d.ts +190 -0
  19. package/lib/types/driver-core/hosted-tool-vocabulary.d.ts +72 -0
  20. package/lib/types/driver-core/model-handover.d.ts +116 -0
  21. package/lib/types/driver-core/ownership.d.ts +6 -5
  22. package/lib/types/driver-core/prompt.d.ts +32 -0
  23. package/lib/types/driver-core/session-lifetime.d.ts +62 -0
  24. package/lib/types/driver-core/session-model.d.ts +82 -0
  25. package/lib/types/engine-claude/agent.d.ts +23 -3
  26. package/lib/types/engine-claude/loop.d.ts +16 -15
  27. package/lib/types/engine-codex/agent.d.ts +22 -3
  28. package/lib/types/engine-codex/appserver/client.d.ts +15 -2
  29. package/lib/types/engine-codex/loop.d.ts +13 -15
  30. package/lib/types/engine-codex/model-handover.d.ts +44 -0
  31. package/lib/types/engine-kimi/acp/client.d.ts +10 -0
  32. package/lib/types/engine-kimi/agent.d.ts +19 -2
  33. package/lib/types/engine-kimi/commands.d.ts +18 -14
  34. package/lib/types/engine-kimi/loop.d.ts +14 -16
  35. package/lib/types/engine-kimi/model-handover.d.ts +32 -0
  36. package/lib/types/engine-kimi/process.d.ts +2 -2
  37. package/lib/types/engine-kimi/types.d.ts +1 -1
  38. package/lib/types/engine-of-session.d.ts +97 -0
  39. package/lib/types/engine-pi/agent.d.ts +25 -23
  40. package/lib/types/engine-pi/loop.d.ts +13 -23
  41. package/lib/types/engine-pi/model-handover.d.ts +35 -0
  42. package/lib/types/engine-pi/types.d.ts +2 -2
  43. package/lib/types/engine-remote.d.ts +192 -0
  44. package/lib/types/engine-surface.d.ts +36 -0
  45. package/lib/types/index.d.ts +51 -50
  46. package/lib/types/invariant.d.ts +8 -5
  47. package/lib/types/model-selection-reset.d.ts +271 -0
  48. package/lib/types/patch-manager.d.ts +57 -39
  49. package/lib/types/preset.d.ts +39 -26
  50. package/lib/types/provider-route.d.ts +83 -36
  51. package/lib/types/router-loop.d.ts +406 -0
  52. package/lib/types/session-engine-store.d.ts +138 -0
  53. package/lib/types/settings.d.ts +12 -11
  54. package/package.json +109 -104
package/README.md CHANGED
@@ -2,10 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/dsh-loop-engine?color=cb3837)](https://www.npmjs.com/package/dsh-loop-engine)
4
4
 
5
- Switch the agent loop engine of **dsh web** the same way you switch a model: a
6
- "Loop engine" dropdown in Settings chooses which driver runs your agents — the
7
- built-in in-process loop, the Claude Code CLI, the Codex CLI, the Pi CLI, or the
8
- Kimi Code CLI — without changing anything in the main repository.
5
+ Pick the agent loop engine of **dsh web** the same way you pick a model — **per session**: the built-in `in-process` loop (the default), or one of the four hosted engines `claude-code`, `codex`, `pi`, `kimi`. The engine is the plugin's own per-session record, so **any open, idle session can be moved to another engine** — between two hosted engines it is an **in-place handover**, and a move involving `in-process` releases that session's agent and reloads the page (the `dsh web` process never restarts). Sessions are independent, so one chat can run Codex while another runs Kimi. None of this changes anything in the main repository.
9
6
 
10
7
  ## Install
11
8
 
@@ -13,205 +10,65 @@ Kimi Code CLI — without changing anything in the main repository.
13
10
  dsh plugin --profile web add dsh-loop-engine
14
11
  ```
15
12
 
16
- Restart `dsh web`, then open **Settings → Loop engine**.
17
-
18
- > Switching engines rewrites a small managed block in `cordis.patch.yml`.
19
- > Everything else you wrote in that file is preserved; only the plugin's own
20
- > span changes.
21
-
22
- > **pnpm users:** pnpm 10+ blocks dependency build scripts by default, so the
23
- > install may fail with `ERR_PNPM_IGNORED_BUILDS` naming `esbuild`,
24
- > `@google/genai`, and `protobufjs` (all reached through the engine SDKs). This
25
- > is expected — allow them and retry, either interactively with
26
- > `pnpm approve-builds`, or by declaring them in the installing project's
27
- > `pnpm-workspace.yaml`:
28
- >
29
- > ```yaml
30
- > allowBuilds:
31
- > esbuild: true
32
- > '@google/genai': true
33
- > protobufjs: true
34
- > ```
35
- >
36
- > Only the installing project can grant this; the plugin cannot pre-approve its
37
- > own dependencies. Note that `allowBuilds` is the pnpm 11 spelling — pnpm 11
38
- > **deletes** the legacy `onlyBuiltDependencies` (and `neverBuiltDependencies`,
39
- > `ignoredBuiltDependencies`) keys from `package.json` and no longer honors
40
- > them, so putting them there silently does nothing.
13
+ Boot `dsh web` so the profile recomposes with the plugin, then open **Settings → Loop engine**. One boot is enough: the router retries for a bounded window while the base bundle's `agent-loop` row still holds the factory slot.
41
14
 
42
- ### Running against a harness source checkout
43
-
44
- The install above assumes a **published** dsh (`npx @deepseek-ai/dsh`) and needs
45
- no extra setup. Booting the harness from its **source checkout**
46
- (`cd deepseek-harness && pnpm dsh web`) takes one more step, because the two
47
- halves then resolve harness packages to different files:
48
-
49
- | Side | `@deepseek-ai/dsh-scope` resolves to |
50
- |---|---|
51
- | Source-launched harness | `packages/core/scope/src/index.ts` (via tsconfig `paths`) |
52
- | Installed plugin (its tarball ships only `lib/`) | `packages/core/scope/lib/index.js` |
53
-
54
- That is one package loaded as two module instances. `dsh-scope` tags a context
55
- with a module-local `Symbol('dsh.scope')`, so a scope minted through one instance
56
- is invisible to the other, and resuming a session fails with:
57
-
58
- ```
59
- agent-presets: refusing to compose an unscoped context;
60
- the scope key is what joins an agent to its preset
61
- ```
62
-
63
- Bridge the profile's peers to the harness source so both halves share one
64
- instance. Set `HARNESS` to the harness checkout **as a `file://` URL**, then run
65
- this from the profile directory:
15
+ > Installing rewrites one **engine-agnostic** managed block in `cordis.patch.yml`: it disables the base bundle's `agent-loop` row so the plugin's own router can own the process's single agent-factory slot, and every other byte you wrote in that file is preserved. Upgrading from an earlier release needs no hand edit — a block that named the one pinned engine is rewritten to this form on the next boot, and seeds the Settings default with that engine.
66
16
 
67
- ```sh
68
- HARNESS=file:///path/to/deepseek-harness # e.g. file:///D:/repos/deepseek-harness
69
- cd "$DSH_HOME/profiles/web" && mkdir -p shims
70
- while IFS='|' read -r name rel; do
71
- mkdir -p "shims/$name"
72
- printf '{"name":"@deepseek-ai/%s","version":"0.0.0","private":true,"type":"module","main":"index.mjs"}\n' \
73
- "$name" > "shims/$name/package.json"
74
- printf "export * from '%s/%s'\nimport * as mod from '%s/%s'\nexport default mod.default\n" \
75
- "$HARNESS" "$rel" "$HARNESS" "$rel" > "shims/$name/index.mjs"
76
- done <<EOF
77
- cordis|vendor/cordis/src/index.ts
78
- schemastery|vendor/schemastery/src/index.ts
79
- dsh-agent|packages/core/agent/src/index.ts
80
- dsh-scope|packages/core/scope/src/index.ts
81
- dsh-session|packages/core/session/src/index.ts
82
- dsh-session-persistence|packages/session/session-persistence/src/index.ts
83
- dsh-settings|packages/settings/settings/src/index.ts
84
- dsh-subprocess|packages/subprocess/subprocess/src/index.ts
85
- dsh-timeout|packages/util/timeout/src/index.ts
86
- dsh-llm|packages/llm/llm/src/index.ts
87
- dsh-invariants|packages/runtime-diagnostics/invariants/src/index.ts
88
- dsh-home-paths|packages/util/home-paths/src/index.ts
89
- EOF
90
- ```
91
-
92
- Then point the profile's `package.json` at them and reinstall:
93
-
94
- ```sh
95
- node -e 'const f="package.json",j=require("./"+f),d=j.dependencies??={}
96
- for(const n of ["cordis","schemastery","dsh-agent","dsh-scope","dsh-session","dsh-session-persistence","dsh-settings","dsh-subprocess","dsh-timeout","dsh-llm","dsh-invariants","dsh-home-paths"])
97
- d["@deepseek-ai/"+n]="file:./shims/"+n
98
- require("fs").writeFileSync(f,JSON.stringify(j,null,2)+"\n")'
99
- pnpm install
100
- ```
17
+ > **pnpm users:** pnpm 10+ blocks dependency build scripts by default, so the install may fail with `ERR_PNPM_IGNORED_BUILDS` naming `esbuild`, `@google/genai`, and `protobufjs` (all reached through the engine SDKs). Allow them and retry — `pnpm approve-builds`, or `allowBuilds` in `pnpm-workspace.yaml`. Only the installing project can grant this; the plugin cannot pre-approve its own dependencies.
101
18
 
102
- Restart `dsh web`. If something loads the `@deepseek-ai/dsh-scope/invariant`
103
- subpath, also give that shim an `invariant.mjs` (`export * from
104
- '$HARNESS/packages/core/scope/src/invariant.ts'`) and add
105
- `"./invariant": "./invariant.mjs"` to its `exports`.
19
+ ### Running against a harness source checkout
106
20
 
107
- > Installing the plugin as a local **`link:`** checkout sidesteps this entirely:
108
- > when the checkout sits beside the harness repo it inherits the harness's own
109
- > `tsconfig.json` and with it the same `paths` mapping. The split only appears
110
- > when a *packed* plugin (npm or tarball) meets a *source* harness.
21
+ A **published** dsh needs no extra setup. Booting the harness from its **source checkout** (`cd deepseek-harness && pnpm dsh web`) takes one more step: bridge the profile's harness peer packages to the checkout source with `file:` shims, so both halves share one module instance (a scope minted through one instance is invisible to the other, and session resume fails with `agent-presets: refusing to compose an unscoped context`). The peer list includes `@deepseek-ai/dsh-agent-loop`. The runnable shim steps are in [docs/source-checkout.md](docs/source-checkout.md); why every `@deepseek-ai/*` package must stay a single instance is in [docs/architecture.md](docs/architecture.md). Installing the plugin as a local **`link:`** checkout sidesteps this entirely.
111
22
 
112
23
  ## Version compatibility
113
24
 
114
- dsh-loop-engine is versioned **in lockstep with the harness it targets**: the
115
- version is the harness version plus a plugin release counter (`0.1.5-rc1` and
116
- `0.1.5-rc2` target harness `0.1.5-rc.1`; `0.1.5-rc3` targets harness
117
- `0.1.5-rc.2`), and every harness package it consumes is pinned exactly in
118
- `peerDependencies`. The two must be matched — a mismatch fails loudly at boot
119
- or session resume:
120
-
121
- | dsh-loop-engine | Requires harness |
122
- |---|---|
123
- | 0.1.5-rc3 | **0.1.5-rc.2** |
124
- | 0.1.5-rc1, 0.1.5-rc2 | 0.1.5-rc.1 |
125
- | 1.0.0-rc8 … 1.0.0-rc15 | 0.1.2-rc.1 |
126
- | 1.0.0-rc7 and earlier | 0.1.1-rc.2 |
127
-
128
- - **Each 0.1.5-rcN release requires the 0.1.5 patch it was built for.**
129
- `0.1.5-rc1`/`0.1.5-rc2` require harness `0.1.5-rc.1`; `0.1.5-rc3` requires
130
- harness `0.1.5-rc.2`. All three use the 0.1.5
131
- assistant-stream contract (`assistant/message` embeds its exact timed
132
- `stream` and rejects `sourceEventSeqs`), the driver-owned `Inbox` interface,
133
- the two-argument `AgentSetup`, and the `SessionPersistence.create` / `open`
134
- handle seam. `0.1.5-rc.2` is a client-UI/docs backport that leaves those
135
- seams untouched, so the driver code is identical across `0.1.5-rc.1` and
136
- `0.1.5-rc.2`.
137
- - Releases up to `1.0.0-rc15` used the plugin's own version series and target
138
- harness `0.1.2-rc.1`; they are not compatible with harness `0.1.5-rc.1`.
139
- - To use the plugin with an older harness, install the release matching it
140
- (e.g. `npm i dsh-loop-engine@1.0.0-rc15` for harness 0.1.2-rc.1).
141
- - The GitHub Release body of each tag states the harness version it targets.
25
+ dsh-loop-engine is versioned **in lockstep with the harness it targets**: `<harness version>-rcN`. `0.1.5-rc3` targets harness `0.1.5-rc.2`; `0.1.5-rc1`/`0.1.5-rc2` target `0.1.5-rc.1`; `1.0.0-rc8` … `1.0.0-rc15` target `0.1.2-rc.1`; `1.0.0-rc7` and earlier target `0.1.1-rc.2`. Every harness package it consumes is pinned exactly in `peerDependencies`, and the two must be matched — a mismatch fails loudly at boot or session resume. To use the plugin with an older harness, install the release matching it (each GitHub Release states the harness version it targets).
142
26
 
143
27
  ### Requirements
144
28
 
145
- - For the Claude Code engine: the Claude Code CLI installed and logged in on
146
- the host.
147
- - For the Codex engine: authenticated either via `codex login` on the host or a
148
- `CODEX_API_KEY` environment entry.
149
- - For the Pi engine: authenticated the way `pi` expects (its own
150
- `~/.pi/agent/auth.json` or the provider's API-key environment variable such as
151
- `ANTHROPIC_API_KEY`).
152
- - For the Kimi Code engine: the `kimi` CLI installed and logged in on the host
153
- (e.g. `kimi login`), and reachable on `PATH` (or pinned to an absolute path
154
- via `kimiBin` in the composition entry).
29
+ - **Claude Code**: the Claude Code CLI installed and logged in on the host.
30
+ - **Codex**: authenticated via `codex login` on the host, or a `CODEX_API_KEY` environment entry.
31
+ - **Pi**: authenticated the way `pi` expects (its own `~/.pi/agent/auth.json`, or the provider's API-key environment variable such as `ANTHROPIC_API_KEY`).
32
+ - **Kimi Code**: the `kimi` CLI installed and logged in (e.g. `kimi login`), and reachable on `PATH` (or pinned to an absolute path via `kimiBin` in the composition entry).
33
+ - Running the harness from a source checkout needs the extra `dsh-agent-loop` `file:` shim above.
155
34
 
156
35
  ## Usage
157
36
 
158
- 1. Pick an engine in **Settings → Loop engine** — `in-process` (default),
159
- `claude-code`, `codex`, `pi`, or `kimi` — then restart `dsh web`.
160
- 2. To return to the default, pick **In-process** and restart again.
161
- 3. To remove the plugin: `dsh plugin --profile web remove dsh-loop-engine`, then
162
- restart `dsh web`.
37
+ **The engine is chosen per session.** Pick `Claude Code`, `Codex`, `Pi`, or `Kimi Code` on the new-session screen's preset chip (beside the workspace picker); keep the deployment's own default (usually `standard`) to run the built-in in-process loop. Different sessions can run different engines at the same time, and a child agent inherits the engine of the agent that spawned it.
38
+
39
+ - **Settings → Loop engine** sets the default for **new** sessions only — it lands immediately, with no restart and no page reload, and sessions already running are unaffected. Choosing **In-process** restores whatever default the plugin replaced.
40
+ - **Moving a session you are in** uses the composer's engine selector (enable *Show the engine selector in the chat page*), which calls the plugin's own `remote.loopEngine.select`. Any open, idle session can move — including one that has already run many turns.
41
+ - **Between two hosted engines the move is an in-place handover**: the session stays open, only the agent is replaced — it applies on the spot, with nothing to confirm.
42
+ - **A move involving `in-process` cannot be handed over in place** (the harness loop neither accepts a session it did not create nor hands a live one over), so the composer asks first: the switch reloads the page, and this page's scroll position and unsent draft go with it (the conversation record does not). On confirm the host RELEASES that session's agent and answers `reload: true`; the page reloads itself and reopens the same session, which is what makes the host build it on the recorded engine. The process never restarts.
43
+ - A session that is **mid-turn is refused** rather than interrupted, and a **subagent's** session cannot be moved at all.
44
+ - **Models**: every hosted engine shares ONE `external` group in the model menu, holding exactly one entry, `default` — meaning "the engine decides". Picking a **real dsh model** hands it to the engine **with its endpoint and credential** (whether the engine can serve it is the engine's business — a refusal is reported, not swallowed). `in-process` sessions use dsh's models as usual.
45
+ - **Uninstall**: `dsh plugin --profile web remove dsh-loop-engine`, then manually delete the plugin's managed block from the profile's `cordis.patch.yml` — the block outlives the plugin, and while it is present the base `agent-loop` row stays disabled, leaving the profile with no agent factory.
163
46
 
164
47
  ### What a hosted engine takes over
165
48
 
166
- While a hosted engine is selected, it owns the session's command and skill
167
- surface: the plugin disables dsh's own `/goal` and points new sessions at a
168
- managed `loop-engine` agent preset — a copy of `standard` minus the dsh-native
169
- `/compact`, `/plan`, goal-tool, and skill rows that an external engine cannot
170
- honor — so the slash menu shows the engine's bridged commands and its own
171
- skill catalog. Engine-agnostic dsh commands (`/export`, `/feedback`,
172
- `/permission`) keep working and stay. Switching back to `in-process` restores
173
- the previous preset default; already-running sessions always keep the preset
174
- they were created with.
175
-
176
- ### Engine notes
177
-
178
- - The Claude Code driver runs one SDK query per step; its slash commands are
179
- bridged into the web menu (built-ins plus user-level `~/.claude/commands/`)
180
- and forwarded to the engine, which expands them natively. Project-level
181
- `.claude/commands/` files stay engine-side and also work typed directly.
182
- - The Codex driver runs `codex app-server`; the thread starts with the
183
- session's `sandboxMode` + `approvalPolicy` stance, and the model's runtime
184
- approval requests (command, file-change, permissions) are answered through
185
- the dsh approval seam — `request_user_input` questions go to the
186
- user-questions seam and MCP elicitations are declined, all fail-closed when
187
- their seam is absent. Its `AGENTS.md` instruction files are surfaced through
188
- the dsh skill-injection seam across every directory from the session cwd up
189
- to the git root, plus `~/.codex/AGENTS.md`.
190
- - The Pi driver runs `pi --mode rpc`; Pi has no permission system, so the whole
191
- child is sandboxed through the dsh subprocess service (default `read-only`).
192
- Its context files (`AGENTS.md`/`CLAUDE.md` with `AGENTS.override.md`
193
- preferred, plus the user-level file under the pi config dir) and its
194
- `skills/` catalogs (`~/.pi/agent/skills/` and `.pi/skills/`) are surfaced
195
- through the dsh skill-injection seam.
196
- - The Kimi Code driver runs a persistent `kimi acp` child (Agent Client
197
- Protocol over stdio) and speaks one stateless `session/new` + `session/prompt`
198
- per dsh step; the durable dsh session log is the sole model context. It streams
199
- assistant text (`agent_message_chunk`) and thinking (`agent_thought_chunk`)
200
- incrementally as live `agent/assistant-stream` frames, and the step's durable
201
- `assistant/message` embeds that exact timed stream; it maps tool calls/streams
202
- (`tool_call` / `tool_call_update`) into `tool/call` + `tool/result`. ACP surfaces tool
203
- approvals as `session/request_permission`, which the driver answers from the
204
- session's dsh approval knobs (an `ask` policy denies, fail-closed). The child
205
- is spawned through the dsh subprocess seam — the only privilege boundary
206
- (default read-only sandbox). Its project `AGENTS.md` chain (cwd→git root) and
207
- `.kimi-code/skills/` catalogs (user and project) are surfaced through the dsh
208
- skill-injection seam, and its slash commands are bridged (built-ins forward the
209
- raw `/name` line back to the engine, which expands it). The prompt is an ACP
210
- request body — not an argv positional — so there is no command-line length
211
- ceiling. Note Kimi's remaining slash-command surface is TUI-only
212
- (`/login`, `/provider`, `/settings`, `/sessions`, …); those are not bridged
213
- because the ACP prompt surface does not expand them, but `skill:` commands are
214
- carried by the skill seam and Kimi's own shorthand.
49
+ - Its preset is a copy of `standard` minus the dsh-native rows an external engine replaces — dsh's `/plan`, `/compact` (and auto-compaction), the model-facing goal tool, the human `/goal` command, and the dsh skill rows (one stripped preset per engine, under `$DSH_HOME/.agent-presets/loop-engine-<engine>/`).
50
+ - The engine's own slash commands and skill catalog are registered into **that agent's own scope**, so two sessions on different engines never see each other's menus, and the whole surface is released with the agent.
51
+ - Engine-agnostic dsh commands (`/export`, `/feedback`, `/permission`) keep working and stay.
52
+
53
+ ## Known limitations
54
+
55
+ - **The engine record lives in the plugin's sidecar, `$DSH_HOME/.loop-engine/engines.json`, not in the session log.** Changing machine or `DSH_HOME` loses it, and the session falls back gracefully to the preset mapping. See [docs/per-session-engine.md](docs/per-session-engine.md) §5.5 and [docs/architecture.md](docs/architecture.md) §3.9.
56
+ - **A move involving `in-process` reloads the page**, because the harness loop neither hands over a live session nor adopts one it did not create. See [docs/per-session-engine.md](docs/per-session-engine.md) §5.2/§5.4.
57
+ - **The engine is settled at session creation / blank period**; once a turn has run, changing engines rebuilds that session's agent (idle only). See [docs/per-session-engine.md](docs/per-session-engine.md) §5.
58
+ - **Old sessions carrying the legacy single preset id (`loop-engine`)** show as "legacy hosted engine" and need one rebuild before the new semantics take over. See [docs/per-session-engine.md](docs/per-session-engine.md) §7.
59
+ - **Under a hosted engine a dsh model pick matters only if you pick a real dsh model** — `default` means "hand it back to the engine". See [docs/per-session-engine.md](docs/per-session-engine.md) §5.2.
60
+ - **Same-engine sessions share that CLI's own auth directory**, with no lock added by the plugin. See [docs/per-session-engine.md](docs/per-session-engine.md) §6.
61
+ - **Switching to `in-process` leaves the shared `external` provider group in the model menu** (the catalog is not scoped per session). See [docs/architecture.md](docs/architecture.md) §3.6.
62
+
63
+ ## Where the details live
64
+
65
+ - [docs/per-session-engine.md](docs/per-session-engine.md) — the full user-visible behavior of per-session engines.
66
+ - [docs/source-checkout.md](docs/source-checkout.md) — the `file:` shims a source-launched harness needs.
67
+ - [docs/architecture.md](docs/architecture.md) — plugin core: the single factory slot, the managed block, routing, per-session engine facts, the provider route.
68
+ - [docs/driver-core.md](docs/driver-core.md) — the shared driver infrastructure.
69
+ - [docs/engine-claude.md](docs/engine-claude.md), [docs/engine-codex.md](docs/engine-codex.md), [docs/engine-kimi.md](docs/engine-kimi.md), [docs/engine-pi.md](docs/engine-pi.md) — per-engine internals.
70
+ - [docs/optimization-backlog.md](docs/optimization-backlog.md) — known issues and the optimization list.
71
+ - [docs/proposals/](docs/proposals/) — main-repo proposals: `append-ignorable-events.md`, `harness-agent-handover.md`, and the two model-selection ones (`dsh-model-into-hosted-engines.md`, `per-session-model-for-hosted-engines.md`).
215
72
 
216
73
  ## License
217
74
 
package/README.zh.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/dsh-loop-engine?color=cb3837)](https://www.npmjs.com/package/dsh-loop-engine)
4
4
 
5
- 像切换模型一样切换 **dsh web** 的 agent 循环引擎:设置页的「Loop engine」下拉选择运行 agent 的驱动——内置 in-process 循环、Claude Code CLI、Codex CLI、Pi CLI,或 Kimi Code CLI——**无需改动主仓库**。
5
+ 像选模型一样选 **dsh web** 的 agent 循环引擎——**按会话**选:内置的 `in-process`(默认),或四个托管引擎 `claude-code`、`codex`、`pi`、`kimi`。引擎本身是本插件的逐会话记录,所以**只要会话处于打开且空闲状态,任何时候都能换成别的引擎**——两个托管引擎之间是**原地换手**,而有一边是 `in-process` 时,宿主会释放这条会话的 agent 并让页面重新载入(`dsh web` 进程本身**不重启**)。会话之间互不影响:一个对话跑 Codex,另一个可以同时跑 Kimi。以上**无需改动主仓库**。
6
6
 
7
7
  ## 安装
8
8
 
@@ -10,123 +10,65 @@
10
10
  dsh plugin --profile web add dsh-loop-engine
11
11
  ```
12
12
 
13
- 重启 `dsh web`,然后打开 **Settings → Loop engine**。
14
-
15
- > 切换引擎会重写 `cordis.patch.yml` 中一小段受管理的内容,文件里你写的其它部分都会保留,只改动插件自己的区间。
16
-
17
- > **pnpm 用户:** pnpm 10+ 默认拦截依赖的 build script,安装可能以
18
- > `ERR_PNPM_IGNORED_BUILDS` 失败,并列出 `esbuild`、`@google/genai`、
19
- > `protobufjs`(都经引擎 SDK 传递而来)。这是预期行为——放行后重试即可:可用
20
- > `pnpm approve-builds` 交互放行,或在安装项目的 `pnpm-workspace.yaml` 里声明:
21
- >
22
- > ```yaml
23
- > allowBuilds:
24
- > esbuild: true
25
- > '@google/genai': true
26
- > protobufjs: true
27
- > ```
28
- >
29
- > 只有安装方能授予该权限,插件无法预先放行自己的依赖。注意 `allowBuilds` 是
30
- > pnpm 11 的写法——pnpm 11 会**删除** `package.json` 里遗留的
31
- > `onlyBuiltDependencies`(以及 `neverBuiltDependencies`、`ignoredBuiltDependencies`)
32
- > 且不再识别它们,写在那里会静默失效。
13
+ 启动 `dsh web` 让 profile 重新组合出这个插件,然后打开 **Settings → Loop engine**。启动一次就够:路由器会在一个有界窗口内重试,等基础 bundle 的 `agent-loop` 行让出 factory 槽位。
33
14
 
34
- ### 源码启动 harness 时的额外步骤
35
-
36
- 上面的安装针对 **发布版** dsh(`npx @deepseek-ai/dsh`),不需要额外操作。若改用**源码**启动 harness(`cd deepseek-harness && pnpm dsh web`),则要多做一步——因为两边会把 harness 的包解析到不同文件:
37
-
38
- | 一侧 | `@deepseek-ai/dsh-scope` 解析到 |
39
- |---|---|
40
- | 源码启动的 harness | `packages/core/scope/src/index.ts`(经 tsconfig `paths`) |
41
- | 安装的插件(包内只有 `lib/`) | `packages/core/scope/lib/index.js` |
42
-
43
- 也就是同一个包被加载成了两个模块实例。`dsh-scope` 用模块私有的 `Symbol('dsh.scope')` 给 context 打标记,一个实例打的标记另一个实例读不到,于是恢复会话时报错:
44
-
45
- ```
46
- agent-presets: refusing to compose an unscoped context;
47
- the scope key is what joins an agent to its preset
48
- ```
49
-
50
- 把 profile 的 peer 桥接到 harness 源码,让两边共用同一个实例。把 `HARNESS` 设为 harness checkout 的 **`file://` URL**,在 profile 目录下执行:
51
-
52
- ```sh
53
- HARNESS=file:///path/to/deepseek-harness # 例如 file:///D:/repos/deepseek-harness
54
- cd "$DSH_HOME/profiles/web" && mkdir -p shims
55
- while IFS='|' read -r name rel; do
56
- mkdir -p "shims/$name"
57
- printf '{"name":"@deepseek-ai/%s","version":"0.0.0","private":true,"type":"module","main":"index.mjs"}\n' \
58
- "$name" > "shims/$name/package.json"
59
- printf "export * from '%s/%s'\nimport * as mod from '%s/%s'\nexport default mod.default\n" \
60
- "$HARNESS" "$rel" "$HARNESS" "$rel" > "shims/$name/index.mjs"
61
- done <<EOF
62
- cordis|vendor/cordis/src/index.ts
63
- schemastery|vendor/schemastery/src/index.ts
64
- dsh-agent|packages/core/agent/src/index.ts
65
- dsh-scope|packages/core/scope/src/index.ts
66
- dsh-session|packages/core/session/src/index.ts
67
- dsh-session-persistence|packages/session/session-persistence/src/index.ts
68
- dsh-settings|packages/settings/settings/src/index.ts
69
- dsh-subprocess|packages/subprocess/subprocess/src/index.ts
70
- dsh-timeout|packages/util/timeout/src/index.ts
71
- dsh-llm|packages/llm/llm/src/index.ts
72
- dsh-invariants|packages/runtime-diagnostics/invariants/src/index.ts
73
- dsh-home-paths|packages/util/home-paths/src/index.ts
74
- EOF
75
- ```
15
+ > 安装会重写 `cordis.patch.yml` 里一小段**不带引擎 id** 的受管理块:它禁用基础 bundle 的 `agent-loop` 行,好让插件自己的路由器占据进程内唯一的 agent factory 槽位;文件里你写的其它部分逐字节保留。从更早版本升级**不需要手工改文件**——点名了那一个钉住引擎的旧块会在下一次启动时被改写成这种形式,并把那个引擎作为设置页默认值的初始 seed。
76
16
 
77
- 再把这些写进 profile 的 `package.json` 并重新安装:
17
+ > **pnpm 用户:** pnpm 10+ 默认拦截依赖的 build script,安装可能以 `ERR_PNPM_IGNORED_BUILDS` 失败,并列出 `esbuild`、`@google/genai`、`protobufjs`(都经引擎 SDK 传递而来)。放行后重试即可——`pnpm approve-builds`,或在 `pnpm-workspace.yaml` 里用 `allowBuilds`。只有安装方能授予该权限,插件无法预先放行自己的依赖。
78
18
 
79
- ```sh
80
- node -e 'const f="package.json",j=require("./"+f),d=j.dependencies??={}
81
- for(const n of ["cordis","schemastery","dsh-agent","dsh-scope","dsh-session","dsh-session-persistence","dsh-settings","dsh-subprocess","dsh-timeout","dsh-llm","dsh-invariants","dsh-home-paths"])
82
- d["@deepseek-ai/"+n]="file:./shims/"+n
83
- require("fs").writeFileSync(f,JSON.stringify(j,null,2)+"\n")'
84
- pnpm install
85
- ```
86
-
87
- 重启 `dsh web`。若有代码加载 `@deepseek-ai/dsh-scope/invariant` 子路径,再给该 shim 补一个 `invariant.mjs`(`export * from '$HARNESS/packages/core/scope/src/invariant.ts'`),并在它的 `exports` 里加上 `"./invariant": "./invariant.mjs"`。
88
-
89
- > 用本地 **`link:`** 方式安装插件可以完全绕开这一步:checkout 与 harness 仓库相邻时,它会继承 harness 自己的 `tsconfig.json`,从而共用同一份 `paths` 映射。这个分裂只在**打包版**插件(npm 或 tarball)遇到**源码版** harness 时出现。
90
-
91
- ### 环境要求
19
+ ### 源码启动 harness 时的额外步骤
92
20
 
93
- - 使用 Claude Code 引擎时需要本机已安装并登录 Claude Code CLI。
94
- - 使用 Codex 引擎时需要完成认证:本机执行过 `codex login`,或配置 `CODEX_API_KEY` 环境变量。
95
- - 使用 Pi 引擎时需要以 `pi` 要求的方式完成认证(其自身的 `~/.pi/agent/auth.json`,或提供方的 API-key 环境变量,如 `ANTHROPIC_API_KEY`)。
96
- - 使用 Kimi Code 引擎时需要本机已安装并登录 `kimi` CLI(例如 `kimi login`),且在 `PATH` 上(或在组合条目里用 `kimiBin` 固定为绝对路径)。
21
+ **发布版** dsh 不需要额外操作。若改用**源码**启动 harness(`cd deepseek-harness && pnpm dsh web`),则要多做一步:用 `file:` shim 把 profile 的 harness peer 包桥接到 checkout 源码,让两边共用同一个模块实例(一个实例打的 scope 标记另一个实例读不到,恢复会话时会报 `agent-presets: refusing to compose an unscoped context`)。清单里包含 `@deepseek-ai/dsh-agent-loop`。可跑的 shim 步骤在 [docs/source-checkout.md](docs/source-checkout.md);每个 `@deepseek-ai/*` 包必须保持单实例的原因见 [docs/architecture.md](docs/architecture.md)。用本地 **`link:`** 方式安装插件可以完全绕开这一步。
97
22
 
98
23
  ## 版本兼容
99
24
 
100
- `dsh-loop-engine` 与它针对的 harness **同版本对齐**:版本号即 harness 版本加插件发布序号(`0.1.5-rc1`、`0.1.5-rc2` 针对 harness `0.1.5-rc.1`,`0.1.5-rc3` 针对 harness `0.1.5-rc.2`),它消费的每个 harness 包都在 `peerDependencies` 里精确钉住。两者必须匹配——不匹配会在启动或会话恢复时响亮地失败:
25
+ `dsh-loop-engine` 与它针对的 harness **同版本对齐**:`<harness version>-rcN`。`0.1.5-rc3` 针对 harness `0.1.5-rc.2`;`0.1.5-rc1`/`0.1.5-rc2` 针对 `0.1.5-rc.1`;`1.0.0-rc8` … `1.0.0-rc15` 针对 `0.1.2-rc.1`;`1.0.0-rc7` 及更早针对 `0.1.1-rc.2`。它消费的每个 harness 包都在 `peerDependencies` 里精确钉住,两者必须匹配——不匹配会在启动或会话恢复时响亮地失败。要在更老的 harness 上使用本插件,请安装与之匹配的版本(每个 GitHub Release 正文会写明它针对的 harness 版本)。
101
26
 
102
- | dsh-loop-engine | 需要 harness |
103
- |---|---|
104
- | 0.1.5-rc3 | **0.1.5-rc.2** |
105
- | 0.1.5-rc1、0.1.5-rc2 | 0.1.5-rc.1 |
106
- | 1.0.0-rc8 … 1.0.0-rc15 | 0.1.2-rc.1 |
107
- | 1.0.0-rc7 及更早 | 0.1.1-rc.2 |
27
+ ### 环境要求
108
28
 
109
- - **每个 0.1.5-rcN 版本需要它为之构建的那个 0.1.5 补丁。** `0.1.5-rc1`/`0.1.5-rc2` 需要 harness `0.1.5-rc.1`;`0.1.5-rc3` 需要 harness `0.1.5-rc.2`。三者都使用 0.1.5 的 assistant-stream 契约(`assistant/message` 内嵌精确计时的 `stream`,并禁止 `sourceEventSeqs`)、由 driver 自己实现的 `Inbox` 接口、双参数 `AgentSetup`,以及 `SessionPersistence.create` / `open` 句柄 seam。`0.1.5-rc.2` 是一次客户端 UI/文档 backport,没有触碰这些 seam,因此 driver 代码在 `0.1.5-rc.1` 与 `0.1.5-rc.2` 之间完全一致。
110
- - `1.0.0-rc15` 及以前的版本沿用插件自己的版本序列,针对 harness `0.1.2-rc.1`,与 harness `0.1.5-rc.1` 不兼容。
111
- - 要在更老的 harness 上使用本插件,请安装与之匹配的版本(例如 harness 0.1.2-rc.1 用 `npm i dsh-loop-engine@1.0.0-rc15`)。
112
- - 每个 tag 的 GitHub Release 正文会写明它针对的 harness 版本。
29
+ - **Claude Code**:本机已安装并登录 Claude Code CLI。
30
+ - **Codex**:本机执行过 `codex login`,或配置 `CODEX_API_KEY` 环境变量。
31
+ - **Pi**:以 `pi` 要求的方式完成认证(其自身的 `~/.pi/agent/auth.json`,或提供方的 API-key 环境变量,如 `ANTHROPIC_API_KEY`)。
32
+ - **Kimi Code**:本机已安装并登录 `kimi` CLI(例如 `kimi login`),且在 `PATH` 上(或在组合条目里用 `kimiBin` 固定为绝对路径)。
33
+ - 用源码 checkout 跑 harness 时还要补上那份 `dsh-agent-loop` 的 `file:` shim。
113
34
 
114
35
  ## 使用方法
115
36
 
116
- 1. 在 **Settings → Loop engine** 选择引擎——`in-process`(默认)、`claude-code`、`codex`、`pi` 或 `kimi`——然后重启 `dsh web`。
117
- 2. 要切回默认,选 **In-process** 再重启即可。
118
- 3. 卸载插件:`dsh plugin --profile web remove dsh-loop-engine`,然后重启 `dsh web`。
37
+ **引擎是按会话选的。** 在**新会话页**的 preset 选择器(工作区选择器旁的 preset chip)上选 `Claude Code`、`Codex`、`Pi` 或 `Kimi Code`;保留部署自己的默认预设(通常是 `standard`)则走内置 in-process 循环。不同会话可以同时跑不同引擎,子 agent 沿用拉起它的那个 agent 的引擎。
119
38
 
120
- ### 托管引擎接管什么
39
+ - **Settings → Loop engine** 只决定**新会话**的默认引擎——改完立即生效,**不需要重启,也不需要刷新页面**,已经在跑的会话不受影响。选 **In-process** 会把插件接手前的默认值恢复回去。
40
+ - **改动你当前这条会话**用对话页 composer 的引擎选择器(勾选 *在对话页显示引擎选择器*),它调用本插件自己的 `remote.loopEngine.select`。只要会话处于打开且空闲状态就能换——包括已经跑过很多轮的会话。
41
+ - **两个托管引擎之间是原地换手**:会话保持打开,只替换 agent——选中即生效,不弹任何确认。
42
+ - **只要有一边是 `in-process` 就无法原地接管**(harness 的 loop 既不接受不是它创建的会话,也不把活会话交出去),所以 composer 会**先弹确认框**说清这次切换要重新载入页面、这一页的滚动位置与还没发出的草稿会跟着丢(会话记录不受影响)。确认后宿主**释放这条会话的 agent** 并回包 `reload: true`;页面自动重载并回到同一条会话,宿主随即按记录构建它。**`dsh web` 进程不重启。**
43
+ - **正在跑一轮时会话会被拒绝**而不是被打断;由 subagent 派生的**子会话**完全不能换。
44
+ - **模型**:模型菜单里所有托管引擎共用一个 `external` 分组,名下只有一条 `default`——含义是「由引擎决定」。选一条**真实 dsh 模型**会把它连同**端点与凭据**一起交给该引擎(能不能用取决于该引擎——不支持会**报错**,而不是静默退回)。`in-process` 会话照常用 dsh 的模型。
45
+ - **卸载**:`dsh plugin --profile web remove dsh-loop-engine`,然后手工删掉 profile 的 `cordis.patch.yml` 里插件的受管理块——这块比插件本身活得更久,只要它还在,基础 `agent-loop` 行就一直被禁用,profile 会完全没有 agent factory。
121
46
 
122
- 选中托管引擎后,它接管该会话的命令与技能面:插件禁用 dsh 自己的 `/goal`,并把新会话指向一个受管理的 `loop-engine` agent 预设——它是 `standard` 的副本,去掉了外部引擎无法履行的 dsh 原生 `/compact`、`/plan`、goal 工具与 skill 行——于是斜杠菜单只显示引擎桥接过来的命令与它自己的技能目录。与引擎无关的 dsh 命令(`/export`、`/feedback`、`/permission`)照常可用、保留在菜单里。切回 `in-process` 会恢复之前的预设默认值;已经在跑的会话始终保留它创建时的预设。
123
-
124
- ### 引擎说明
47
+ ### 托管引擎接管什么
125
48
 
126
- - Claude Code 驱动每步跑一次 SDK query;它的斜杠命令桥接进 web 菜单(内置命令加上用户级 `~/.claude/commands/`),再转发给引擎由它原生展开。项目级 `.claude/commands/` 留在引擎侧,直接手敲同样可用。
127
- - Codex 驱动运行 `codex app-server`;线程以会话的 `sandboxMode` + `approvalPolicy` 姿态启动,模型运行时的工具审批请求(command、file-change、permissions)经 dsh 审批 seam 应答——用户提问走 user-questions seam,MCP elicitation 一律拒绝,seam 缺席时均失败关闭。其 `AGENTS.md` 指令文件经 dsh 技能注入接缝暴露:从会话 cwd 逐级到 git 根,外加 `~/.codex/AGENTS.md`。
128
- - Pi 驱动运行 `pi --mode rpc`;Pi 没有权限系统,所以整个子进程经 dsh subprocess 服务做沙箱化(默认 `read-only`)。它的上下文文件(`AGENTS.md`/`CLAUDE.md`,优先 `AGENTS.override.md`,外加 pi 配置目录下的用户级文件)与 `skills/` 目录(`~/.pi/agent/skills/` 和 `.pi/skills/`)经 dsh 技能注入接缝暴露。
129
- - Kimi Code 驱动运行一个常驻的 `kimi acp` 子进程(Agent Client Protocol over stdio),每步一次无状态的 `session/new` + `session/prompt`;durable 会话日志是唯一模型上下文。它把助手文本(`agent_message_chunk`)与**思考**(`agent_thought_chunk`)**增量**写入日志,并把工具调用/流(`tool_call` / `tool_call_update`)映射为 `tool/call` + `tool/result`。ACP 通过 `session/request_permission` 暴露工具审批,驱动根据会话的 dsh 审批旋钮应答(ask 策略拒绝,失败关闭)。子进程经 dsh subprocess seam 拉起——唯一权限边界(默认只读沙箱)。其项目 `AGENTS.md` 链(cwd→git 根)与 `.kimi-code/skills/` 目录(用户与项目)通过 dsh 技能注入接口暴露,其斜杠命令也已桥接(内置命令把原始 `/name` 行转发回引擎展开)。prompt 是 ACP 请求体而非 argv 位置参数,因此**不存在命令行长度上限**。注意 Kimi 剩余的斜杠命令面是纯 TUI(`/login`、`/provider`、`/settings`、`/sessions`…),这些不桥接(ACP prompt 面不扩展它们);`skill:` 命令由技能接口与 kimi 自身的 shorthand 承载。
49
+ - 它的预设是 `standard` 的副本,去掉了外部引擎会替代掉的 dsh 原生行——dsh 的 `/plan`、`/compact`(与自动压缩)、模型可见的 goal 工具、人类 `/goal` 命令、以及 dsh skill 行(每引擎一份剥离后的 preset,位于 `$DSH_HOME/.agent-presets/loop-engine-<engine>/`)。
50
+ - 引擎自己的斜杠命令与技能目录会注册进**这个 agent 自己的 scope**,因此两个跑不同引擎的会话互相看不到对方的菜单,整份表面随 agent 一起回收。
51
+ - 与引擎无关的 dsh 命令(`/export`、`/feedback`、`/permission`)照常可用、保留在菜单里。
52
+
53
+ ## 已知限制
54
+
55
+ - **引擎记录落在插件自己的侧车文件 `$DSH_HOME/.loop-engine/engines.json`,不在会话日志里。** 换机器或换 `DSH_HOME` 会丢掉它,会话会优雅回退到 preset 映射。详见 [docs/per-session-engine.md](docs/per-session-engine.md) §5.5 与 [docs/architecture.md](docs/architecture.md) §3.9。
56
+ - **涉及 `in-process` 的切换要重新载入页面**,因为 harness 的 loop 既交不出活会话、也不接管别人的。详见 [docs/per-session-engine.md](docs/per-session-engine.md) §5.2/§5.4。
57
+ - **引擎在会话创建/空白期确定**;跑过一轮之后再换引擎,会重建这条会话的 agent(只在空闲时才行)。详见 [docs/per-session-engine.md](docs/per-session-engine.md) §5。
58
+ - **日志里记着旧版单 preset id(`loop-engine`)的老会话**显示为「旧版托管引擎」,需要一次重建才由新语义接管。详见 [docs/per-session-engine.md](docs/per-session-engine.md) §7。
59
+ - **托管引擎下 dsh 的模型选择只有在你真的选了真实 dsh 模型时才有意义**——`default` 意思是「交回引擎自己决定」。详见 [docs/per-session-engine.md](docs/per-session-engine.md) §5.2。
60
+ - **同一引擎上的多条会话共享该 CLI 自己的认证目录**,插件没有加任何锁。详见 [docs/per-session-engine.md](docs/per-session-engine.md) §6。
61
+ - **换成 `in-process` 时,那个共享的 `external` provider 分组仍会留在模型菜单里**(目录不按会话过滤)。详见 [docs/architecture.md](docs/architecture.md) §3.6。
62
+
63
+ ## 细节在哪
64
+
65
+ - [docs/per-session-engine.md](docs/per-session-engine.md)——按会话选引擎的用户可见行为全解。
66
+ - [docs/source-checkout.md](docs/source-checkout.md)——源码启动 harness 时需要的那套 `file:` shim。
67
+ - [docs/architecture.md](docs/architecture.md)——插件核心:唯一 factory 槽位、受管理块、路由、逐会话引擎事实、provider 路由。
68
+ - [docs/driver-core.md](docs/driver-core.md)——共享驱动基础设施。
69
+ - [docs/engine-claude.md](docs/engine-claude.md)、[docs/engine-codex.md](docs/engine-codex.md)、[docs/engine-kimi.md](docs/engine-kimi.md)、[docs/engine-pi.md](docs/engine-pi.md)——逐引擎内部实现。
70
+ - [docs/optimization-backlog.md](docs/optimization-backlog.md)——已知问题与优化清单。
71
+ - [docs/proposals/](docs/proposals/)——主仓提案:`append-ignorable-events.md`、`harness-agent-handover.md`,以及模型选择相关的两篇(`dsh-model-into-hosted-engines.md`、`per-session-model-for-hosted-engines.md`)。
130
72
 
131
73
  ## License
132
74