dsh-loop-engine 0.1.7-rc2 → 0.1.7-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.
- package/README.md +76 -76
- package/README.zh.md +1 -1
- package/lib/index.js +92 -3
- package/lib/types/index.d.ts +13 -9
- package/lib/types/patch-manager.d.ts +20 -0
- package/lib/types/preset.d.ts +84 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
# dsh-loop-engine
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/dsh-loop-engine)
|
|
4
|
-
|
|
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.
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```sh
|
|
10
|
-
dsh plugin --profile web add dsh-loop-engine
|
|
11
|
-
```
|
|
12
|
-
|
|
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.
|
|
14
|
-
|
|
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.
|
|
16
|
-
|
|
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.
|
|
18
|
-
|
|
19
|
-
### Running against a harness source checkout
|
|
20
|
-
|
|
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.
|
|
22
|
-
|
|
23
|
-
## Version compatibility
|
|
24
|
-
|
|
25
|
-
dsh-loop-engine is versioned **in lockstep with the harness it targets**: `<harness version>-rcN`. From `0.1.7-rc1` on, **one release serves two harness generations**: the 0.1.5 line (`>=0.1.5-rc.1 <0.1.6-0`, all sharing the old settings API) and the 0.1.7 line (`>=0.1.7-rc.1 <0.1.8-0`, whose `rc.1` and `rc.2` are both verified). It detects the running generation at load and takes the matching code path, so a single published package installs onto either. Every harness package it consumes declares that union range in `peerDependencies`; a harness outside it fails loudly at boot or session resume. Within a covered line a new `rc` needs no plugin release unless the API surfaces it depends on moved — `docs/compatibility.md` §1 lists the surfaces and the diff command that decides it. `0.1.5-rc3` … `0.1.5-rc5` target 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`.
|
|
26
|
-
|
|
27
|
-
### Requirements
|
|
28
|
-
|
|
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.
|
|
34
|
-
|
|
35
|
-
## Usage
|
|
36
|
-
|
|
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.
|
|
46
|
-
|
|
47
|
-
### What a hosted engine takes over
|
|
48
|
-
|
|
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
|
|
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/compatibility.md](docs/compatibility.md) — **start here when the harness upgrades**: every generation-dependent code site, the four techniques for spanning them, and the checklist for adding the next generation.
|
|
69
|
-
- [docs/driver-core.md](docs/driver-core.md) — the shared driver infrastructure.
|
|
70
|
-
- [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.
|
|
71
|
-
- [docs/optimization-backlog.md](docs/optimization-backlog.md) — known issues and the optimization list.
|
|
72
|
-
- [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`).
|
|
73
|
-
|
|
74
|
-
## License
|
|
75
|
-
|
|
76
|
-
MIT
|
|
1
|
+
# dsh-loop-engine
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-loop-engine)
|
|
4
|
+
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
dsh plugin --profile web add dsh-loop-engine
|
|
11
|
+
```
|
|
12
|
+
|
|
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.
|
|
14
|
+
|
|
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.
|
|
16
|
+
|
|
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.
|
|
18
|
+
|
|
19
|
+
### Running against a harness source checkout
|
|
20
|
+
|
|
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.
|
|
22
|
+
|
|
23
|
+
## Version compatibility
|
|
24
|
+
|
|
25
|
+
dsh-loop-engine is versioned **in lockstep with the harness it targets**: `<harness version>-rcN`. From `0.1.7-rc1` on, **one release serves two harness generations**: the 0.1.5 line (`>=0.1.5-rc.1 <0.1.6-0`, all sharing the old settings API) and the 0.1.7 line (`>=0.1.7-rc.1 <0.1.8-0`, whose `rc.1` and `rc.2` are both verified). It detects the running generation at load and takes the matching code path, so a single published package installs onto either. Every harness package it consumes declares that union range in `peerDependencies`; a harness outside it fails loudly at boot or session resume. Within a covered line a new `rc` needs no plugin release unless the API surfaces it depends on moved — `docs/compatibility.md` §1 lists the surfaces and the diff command that decides it. `0.1.5-rc3` … `0.1.5-rc5` target 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`.
|
|
26
|
+
|
|
27
|
+
### Requirements
|
|
28
|
+
|
|
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.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
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.
|
|
46
|
+
|
|
47
|
+
### What a hosted engine takes over
|
|
48
|
+
|
|
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). The preset is a harness-generation-dependent artifact: a directory under `$DSH_HOME/.agent-presets/loop-engine-<engine>/` on the 0.1.5 line, and an `@deepseek-ai/dsh-agent-preset` row inserted into the profile patch on the 0.1.7 line, which no longer reads that directory. See [docs/architecture.md](docs/architecture.md) §3.5.
|
|
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/compatibility.md](docs/compatibility.md) — **start here when the harness upgrades**: every generation-dependent code site, the four techniques for spanning them, and the checklist for adding the next generation.
|
|
69
|
+
- [docs/driver-core.md](docs/driver-core.md) — the shared driver infrastructure.
|
|
70
|
+
- [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.
|
|
71
|
+
- [docs/optimization-backlog.md](docs/optimization-backlog.md) — known issues and the optimization list.
|
|
72
|
+
- [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`).
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
package/README.zh.md
CHANGED
|
@@ -46,7 +46,7 @@ dsh plugin --profile web add dsh-loop-engine
|
|
|
46
46
|
|
|
47
47
|
### 托管引擎接管什么
|
|
48
48
|
|
|
49
|
-
- 它的预设是 `standard` 的副本,去掉了外部引擎会替代掉的 dsh 原生行——dsh 的 `/plan`、`/compact`(与自动压缩)、模型可见的 goal 工具、人类 `/goal` 命令、以及 dsh skill 行(每引擎一份剥离后的 preset
|
|
49
|
+
- 它的预设是 `standard` 的副本,去掉了外部引擎会替代掉的 dsh 原生行——dsh 的 `/plan`、`/compact`(与自动压缩)、模型可见的 goal 工具、人类 `/goal` 命令、以及 dsh skill 行(每引擎一份剥离后的 preset)。preset 的**载体随 harness 代际而变**:0.1.5 上是 `$DSH_HOME/.agent-presets/loop-engine-<engine>/` 目录,0.1.7 上是插进 profile patch 的一行 `@deepseek-ai/dsh-agent-preset`(0.1.7 已不再读那个目录)。详见 [docs/architecture.md](docs/architecture.md) §3.5。
|
|
50
50
|
- 引擎自己的斜杠命令与技能目录会注册进**这个 agent 自己的 scope**,因此两个跑不同引擎的会话互相看不到对方的菜单,整份表面随 agent 一起回收。
|
|
51
51
|
- 与引擎无关的 dsh 命令(`/export`、`/feedback`、`/permission`)照常可用、保留在菜单里。
|
|
52
52
|
|
package/lib/index.js
CHANGED
|
@@ -5856,6 +5856,64 @@ var MANAGED_HEADER = `# Managed by dsh-loop-engine: the deployment's "${SOURCE_P
|
|
|
5856
5856
|
# from "${SOURCE_PRESET_ID}" on boot \u2014 hand edits are overwritten. The preset id
|
|
5857
5857
|
# names the engine this session runs.
|
|
5858
5858
|
`;
|
|
5859
|
+
var PRESET_ROWS_BEGIN = "# -- dsh-loop-engine presets --";
|
|
5860
|
+
var PRESET_ROWS_END = "# -- /dsh-loop-engine presets --";
|
|
5861
|
+
var ENGINE_PRESET_ORDER = 100;
|
|
5862
|
+
var PLUGIN_LIST_INDENT = " ".repeat(10);
|
|
5863
|
+
function pluginListLines(composition) {
|
|
5864
|
+
const text = composition.replace(/\n+$/, "");
|
|
5865
|
+
const lines = text.split("\n");
|
|
5866
|
+
if (!lines.some((line) => line.startsWith("- "))) return [" plugins: []"];
|
|
5867
|
+
return [" plugins:", ...lines.map((line) => line.trim() === "" ? "" : `${PLUGIN_LIST_INDENT}${line}`)];
|
|
5868
|
+
}
|
|
5869
|
+
function renderEnginePresetRow(engine, composition) {
|
|
5870
|
+
const id = enginePresetId(engine);
|
|
5871
|
+
return [
|
|
5872
|
+
"- insert:",
|
|
5873
|
+
` - id: preset-${id}`,
|
|
5874
|
+
` name: '@deepseek-ai/dsh-agent-preset'`,
|
|
5875
|
+
" config:",
|
|
5876
|
+
` id: ${id}`,
|
|
5877
|
+
` order: ${ENGINE_PRESET_ORDER}`,
|
|
5878
|
+
...pluginListLines(stripPresetRows(composition))
|
|
5879
|
+
].join("\n");
|
|
5880
|
+
}
|
|
5881
|
+
function renderPresetRows(composition) {
|
|
5882
|
+
return [
|
|
5883
|
+
PRESET_ROWS_BEGIN,
|
|
5884
|
+
"# One `@deepseek-ai/dsh-agent-preset` declaration per hosted engine: the",
|
|
5885
|
+
`# deployment's "${SOURCE_PRESET_ID}" preset minus the dsh-native command/skill rows a`,
|
|
5886
|
+
`# hosted loop engine replaces. Regenerated from "${SOURCE_PRESET_ID}" on boot \u2014 hand`,
|
|
5887
|
+
"# edits are overwritten. The preset id names the engine a session runs.",
|
|
5888
|
+
...HOSTED_ENGINE_IDS.map((engine) => renderEnginePresetRow(engine, composition)),
|
|
5889
|
+
`${PRESET_ROWS_END}
|
|
5890
|
+
`
|
|
5891
|
+
].join("\n");
|
|
5892
|
+
}
|
|
5893
|
+
function presetRowsSpan(text) {
|
|
5894
|
+
const begin = text.indexOf(PRESET_ROWS_BEGIN);
|
|
5895
|
+
if (begin === -1) return { head: text, tail: "", present: false, blankBefore: false };
|
|
5896
|
+
const endAt = text.indexOf(PRESET_ROWS_END, begin);
|
|
5897
|
+
const spanEnd = endAt === -1 ? text.length : endAt + PRESET_ROWS_END.length + 1;
|
|
5898
|
+
const before = text.slice(0, begin);
|
|
5899
|
+
const blankBefore = before.endsWith("\n\n");
|
|
5900
|
+
return {
|
|
5901
|
+
head: blankBefore ? before.slice(0, -1) : before,
|
|
5902
|
+
tail: text.slice(spanEnd),
|
|
5903
|
+
present: true,
|
|
5904
|
+
blankBefore
|
|
5905
|
+
};
|
|
5906
|
+
}
|
|
5907
|
+
function applyEnginePresetRows(text, composition) {
|
|
5908
|
+
const region = renderPresetRows(composition);
|
|
5909
|
+
const span = presetRowsSpan(text);
|
|
5910
|
+
if (!span.present) {
|
|
5911
|
+
if (text === "") return region;
|
|
5912
|
+
return dropSeedPlaceholder(`${ensureTrailingNewline(text)}
|
|
5913
|
+
${region}`);
|
|
5914
|
+
}
|
|
5915
|
+
return dropSeedPlaceholder(`${span.head}${span.blankBefore ? "\n" : ""}${region}${span.tail}`);
|
|
5916
|
+
}
|
|
5859
5917
|
function managedMetadata(engine) {
|
|
5860
5918
|
return `name: ${ENGINE_DISPLAY_NAMES[engine]}
|
|
5861
5919
|
description: ${ENGINE_DISPLAY_NAMES[engine]}, with the dsh-native commands and skills it replaces removed.
|
|
@@ -5937,6 +5995,18 @@ ${stripPresetRows(composition)}`;
|
|
|
5937
5995
|
}
|
|
5938
5996
|
return changed;
|
|
5939
5997
|
}
|
|
5998
|
+
async function readTextOrEmpty(path) {
|
|
5999
|
+
try {
|
|
6000
|
+
return await readFile(path, "utf8");
|
|
6001
|
+
} catch {
|
|
6002
|
+
return "";
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
async function ensureEnginePresetRows(patchPath, source) {
|
|
6006
|
+
const composition = await readComposition(source, SOURCE_PRESET_ID);
|
|
6007
|
+
const current = await readTextOrEmpty(patchPath);
|
|
6008
|
+
return writeIfDifferent(patchPath, applyEnginePresetRows(current, composition));
|
|
6009
|
+
}
|
|
5940
6010
|
|
|
5941
6011
|
// src/router-loop.ts
|
|
5942
6012
|
import AgentLoop, { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from "@deepseek-ai/dsh-agent-loop";
|
|
@@ -7444,6 +7514,10 @@ function resolvePatchPath(config) {
|
|
|
7444
7514
|
config.patchFilename ?? "cordis.patch.yml"
|
|
7445
7515
|
);
|
|
7446
7516
|
}
|
|
7517
|
+
function authorHostedPresets(patchPath, presets) {
|
|
7518
|
+
if (LEGACY_HARNESS) return ensureEnginePresets(resolveDshHome2(), presets);
|
|
7519
|
+
return ensureEnginePresetRows(patchPath, presets);
|
|
7520
|
+
}
|
|
7447
7521
|
function isMissing2(error) {
|
|
7448
7522
|
return error?.code === "ENOENT";
|
|
7449
7523
|
}
|
|
@@ -7530,6 +7604,8 @@ function apply(ctx, config) {
|
|
|
7530
7604
|
const AGENT_PRESETS_NS = "agent-presets";
|
|
7531
7605
|
const PRESET_DEFAULT_ATTEMPTS = 30;
|
|
7532
7606
|
const PRESET_DEFAULT_RETRY_MS = 100;
|
|
7607
|
+
const PRESET_SOURCE_ATTEMPTS = 20;
|
|
7608
|
+
const PRESET_SOURCE_RETRY_MS = 100;
|
|
7533
7609
|
const ROUTE_ATTEMPTS = 30;
|
|
7534
7610
|
const ROUTE_RETRY_MS = 100;
|
|
7535
7611
|
const ROUTER_ATTEMPTS = 40;
|
|
@@ -7605,11 +7681,24 @@ function apply(ctx, config) {
|
|
|
7605
7681
|
}
|
|
7606
7682
|
return void 0;
|
|
7607
7683
|
}
|
|
7608
|
-
const settled = authoring ??=
|
|
7609
|
-
|
|
7684
|
+
const settled = authoring ??= (async () => {
|
|
7685
|
+
let lastError;
|
|
7686
|
+
for (let attempt2 = 0; attempt2 <= PRESET_SOURCE_ATTEMPTS; attempt2 += 1) {
|
|
7687
|
+
try {
|
|
7688
|
+
return await authorHostedPresets(patchPath, presets);
|
|
7689
|
+
} catch (error) {
|
|
7690
|
+
lastError = error;
|
|
7691
|
+
if (attempt2 < PRESET_SOURCE_ATTEMPTS) {
|
|
7692
|
+
await new Promise((resolve5) => {
|
|
7693
|
+
setTimeout(resolve5, PRESET_SOURCE_RETRY_MS);
|
|
7694
|
+
});
|
|
7695
|
+
}
|
|
7696
|
+
}
|
|
7697
|
+
}
|
|
7698
|
+
ctx.logger.error(`loop-engine: engine preset authoring failed: ${String(lastError)}`);
|
|
7610
7699
|
authoring = void 0;
|
|
7611
7700
|
return false;
|
|
7612
|
-
});
|
|
7701
|
+
})();
|
|
7613
7702
|
return { presets, settled };
|
|
7614
7703
|
};
|
|
7615
7704
|
let authoring;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -15,16 +15,20 @@
|
|
|
15
15
|
* Because the router owns the slot, the plugin keeps the base bundle's
|
|
16
16
|
* `agent-loop` row disabled for as long as it is composed. That managed block
|
|
17
17
|
* lives in the profile's `cordis.patch.yml` (see `patch-manager.ts`) and is the
|
|
18
|
-
* plugin's only footprint in the harness's configuration
|
|
19
|
-
* engine, because the engine is a per-session
|
|
18
|
+
* plugin's only footprint in the harness's configuration alongside the preset
|
|
19
|
+
* rows below; the block names no engine, because the engine is a per-session
|
|
20
|
+
* decision.
|
|
20
21
|
*
|
|
21
|
-
* The plugin authors one preset per hosted engine
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* engine
|
|
25
|
-
* the
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* The plugin authors one preset per hosted engine — each a copy of `standard`
|
|
23
|
+
* minus the dsh-native command and skill rows an external engine replaces — and
|
|
24
|
+
* it serves every hosted engine's provider route label in the llm registry
|
|
25
|
+
* (`provider-route.ts`): an engine logs its own label into each session's
|
|
26
|
+
* request/header, and the web host refuses a turn whose session selection names
|
|
27
|
+
* a provider no adapter serves. WHERE that copy lives depends on the harness
|
|
28
|
+
* generation — a directory under `$DSH_HOME/.agent-presets/loop-engine-<engine>`
|
|
29
|
+
* on the 0.1.5 line, a `@deepseek-ai/dsh-agent-preset` insert row in the profile
|
|
30
|
+
* patch on the 0.1.7 line, which stopped reading the directory (see
|
|
31
|
+
* `preset.ts`, {@link authorHostedPresets}). The preset is now only the
|
|
28
32
|
* session's agent-plane composition (and the engine's own default for a session
|
|
29
33
|
* with no record) — it is no longer the thing that decides a running session's
|
|
30
34
|
* engine.
|
|
@@ -69,6 +69,26 @@ export declare function hasManagedBlock(text: string): boolean;
|
|
|
69
69
|
* engine this build does not recognize.
|
|
70
70
|
*/
|
|
71
71
|
export declare function legacyBlockEngineOf(text: string): LoopEngineId | undefined;
|
|
72
|
+
/** Normalize a file so a managed span sits on its own lines with a blank separator. */
|
|
73
|
+
export declare function ensureTrailingNewline(text: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* The profile seed template (`cordis.patch.yml` on a fresh profile) is a lone
|
|
76
|
+
* root-level empty flow sequence `[]`. The plugin's managed block is itself a
|
|
77
|
+
* root-level block sequence of loader entries, so a block coexisting with a
|
|
78
|
+
* surviving `[]` is TWO root collections in one document — YAML the harness
|
|
79
|
+
* rejects with "end of the stream or a document separator is expected", and the
|
|
80
|
+
* web app then fails to boot. Remove a whole-line root `[]` placeholder so the
|
|
81
|
+
* managed block is the sole top-level collection. Anchored to column 0 so an
|
|
82
|
+
* indented `[]` that is a real value inside an entry's nested config is never
|
|
83
|
+
* touched.
|
|
84
|
+
*
|
|
85
|
+
* Shared with the preset-rows region (`./preset.ts`): both regions are
|
|
86
|
+
* root-level collections appended to the same file, so either one can be the
|
|
87
|
+
* first thing that meets a surviving seed.
|
|
88
|
+
* @param text - the patch-file text.
|
|
89
|
+
* @returns the text without a whole-line root `[]` placeholder.
|
|
90
|
+
*/
|
|
91
|
+
export declare function dropSeedPlaceholder(text: string): string;
|
|
72
92
|
/**
|
|
73
93
|
* Produce the next patch-file text carrying the managed block, preserving every
|
|
74
94
|
* byte outside the managed span. Appends the span when absent and replaces it
|
package/lib/types/preset.d.ts
CHANGED
|
@@ -10,9 +10,29 @@
|
|
|
10
10
|
* advisory prompt text an external engine never assembles, dsh `/compact`
|
|
11
11
|
* cannot shrink a context the engine's child process holds, and dsh skills
|
|
12
12
|
* would sit next to the engine's own catalog. Those rows live inside the
|
|
13
|
-
* agent-preset composition, which a profile patch cannot reach, so
|
|
14
|
-
* authors a stripped
|
|
15
|
-
*
|
|
13
|
+
* agent-preset composition, which a profile patch cannot otherwise reach, so
|
|
14
|
+
* the plugin authors a stripped copy of that composition per engine.
|
|
15
|
+
*
|
|
16
|
+
* WHICH mechanism carries that copy is the running harness generation's
|
|
17
|
+
* business, and the two share everything but the carrier:
|
|
18
|
+
*
|
|
19
|
+
* - the 0.1.5 line reads presets from a DIRECTORY per preset under the user
|
|
20
|
+
* preset root (`$DSH_HOME/.agent-presets/<id>`), so there it is two files
|
|
21
|
+
* per engine ({@link ensureEnginePresets});
|
|
22
|
+
* - the 0.1.7 line replaced that with composed plugin rows: a preset is one
|
|
23
|
+
* `@deepseek-ai/dsh-agent-preset` row whose `config.plugins` list IS the
|
|
24
|
+
* composition, declared wherever a composition is declared. Nothing reads
|
|
25
|
+
* `.agent-presets` any more, so there it is four `insert` rows in the
|
|
26
|
+
* profile patch the plugin already manages
|
|
27
|
+
* ({@link ensureEnginePresetRows}). The rows mirror the shipped shape
|
|
28
|
+
* (`packages/bundle/web-app/presets/standard.patch.yml` in the harness),
|
|
29
|
+
* with this plugin's own id, order, and stripped composition.
|
|
30
|
+
*
|
|
31
|
+
* Both mechanisms regenerate from the current `standard` composition on every
|
|
32
|
+
* boot: text on disk is never authoritative, so a harness upgrade that changes
|
|
33
|
+
* `standard` flows through. Neither re-parses YAML — the strip is a line
|
|
34
|
+
* transform that preserves everything it does not drop byte for byte, comments
|
|
35
|
+
* included, and the row form only re-indents that text.
|
|
16
36
|
*
|
|
17
37
|
* The preset id is ALSO the per-session engine selector: the harness resolves
|
|
18
38
|
* one preset per session and hands its id to the agent factory at create time
|
|
@@ -21,12 +41,6 @@
|
|
|
21
41
|
* makes "session A on Codex, session B on Kimi, concurrently" expressible in a
|
|
22
42
|
* harness that admits exactly one AgentFactory.
|
|
23
43
|
*
|
|
24
|
-
* The presets are REGENERATED from the current `standard` composition on every
|
|
25
|
-
* boot: text on disk is never authoritative, so a harness upgrade that changes
|
|
26
|
-
* `standard` flows through. The file is plain YAML the loader already accepts —
|
|
27
|
-
* the strip is a line transform that preserves everything it does not drop byte
|
|
28
|
-
* for byte, comments included.
|
|
29
|
-
*
|
|
30
44
|
* @module dsh-loop-engine/preset
|
|
31
45
|
*/
|
|
32
46
|
export { HOSTED_PRESET_PREFIX, SOURCE_PRESET_ID, engineOfPreset, enginePresetId, hostedEngineOf, sessionEngineOf, } from './agent-preset-ids.ts';
|
|
@@ -56,6 +70,47 @@ export declare const HOSTED_PRESET_IDS: readonly string[];
|
|
|
56
70
|
* its context and its own `/compact` (Claude, Kimi).
|
|
57
71
|
*/
|
|
58
72
|
export declare const STRIPPED_ROWS: readonly ["skill-filesystem", "tool-skill", "tool-goal", "command-goal", "planning", "compaction"];
|
|
73
|
+
/**
|
|
74
|
+
* Begin marker of the plugin-managed preset-rows region inside a profile patch
|
|
75
|
+
* file. The region is what registers the hosted engines' presets on the 0.1.7
|
|
76
|
+
* line, where a preset is a composed row rather than a directory.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately NOT the managed block's marker: the two regions are located and
|
|
79
|
+
* rewritten independently, so editing one never rewrites the other's bytes.
|
|
80
|
+
*/
|
|
81
|
+
export declare const PRESET_ROWS_BEGIN = "# -- dsh-loop-engine presets --";
|
|
82
|
+
/** End marker of the plugin-managed preset-rows region inside a profile patch file. */
|
|
83
|
+
export declare const PRESET_ROWS_END = "# -- /dsh-loop-engine presets --";
|
|
84
|
+
/**
|
|
85
|
+
* Roster order of every preset this plugin authors. The shipped presets occupy
|
|
86
|
+
* 1..4 (`standard` 1, `ptc` 2, `minimal` 3, `cordis` 4), and the roster sorts
|
|
87
|
+
* by order and then by id, so one constant past that range puts every managed
|
|
88
|
+
* preset after every shipped one — the deployment's own presets stay at the top
|
|
89
|
+
* of the picker and the engines' copies trail it, which is also where the
|
|
90
|
+
* order they are listed in (selection order) is readable.
|
|
91
|
+
*/
|
|
92
|
+
export declare const ENGINE_PRESET_ORDER = 100;
|
|
93
|
+
/**
|
|
94
|
+
* The managed preset-rows region: one preset row per hosted engine, bracketed
|
|
95
|
+
* by the markers that make it locatable and rewritable.
|
|
96
|
+
* @param composition - the source `standard` composition.
|
|
97
|
+
* @returns the region's text, ending in a newline.
|
|
98
|
+
*/
|
|
99
|
+
export declare function renderPresetRows(composition: string): string;
|
|
100
|
+
/**
|
|
101
|
+
* Produce the next patch-file text carrying the preset-rows region, preserving
|
|
102
|
+
* every byte outside that span — including the managed block, which is located
|
|
103
|
+
* by its own markers. Appends the region when absent and replaces it when
|
|
104
|
+
* present, so an unchanged composition leaves the text byte for byte identical.
|
|
105
|
+
*
|
|
106
|
+
* The region is a root-level collection like the managed block, so a leftover
|
|
107
|
+
* seed `[]` is dropped with it: two root collections in one document is YAML
|
|
108
|
+
* the harness rejects, and the profile would stop booting.
|
|
109
|
+
* @param text - current patch-file text.
|
|
110
|
+
* @param composition - the source `standard` composition.
|
|
111
|
+
* @returns the rewritten patch-file text.
|
|
112
|
+
*/
|
|
113
|
+
export declare function applyEnginePresetRows(text: string, composition: string): string;
|
|
59
114
|
/**
|
|
60
115
|
* Remove top-level entries by id from a preset composition, preserving every
|
|
61
116
|
* other byte. Each entry owns the comment/blank run directly above its opener
|
|
@@ -87,4 +142,24 @@ export interface PresetCompositionSource {
|
|
|
87
142
|
* @throws when the source preset cannot be read or the writes fail.
|
|
88
143
|
*/
|
|
89
144
|
export declare function ensureEnginePresets(dshHome: string, source: PresetCompositionSource): Promise<boolean>;
|
|
145
|
+
/**
|
|
146
|
+
* Regenerate every hosted engine's preset as an `insert` row in the profile
|
|
147
|
+
* patch the plugin already manages, from the roster's `standard` preset.
|
|
148
|
+
*
|
|
149
|
+
* This is the 0.1.7 mechanism: a preset there is a composed plugin row, and the
|
|
150
|
+
* user preset root is not read at all — so a preset written as a directory
|
|
151
|
+
* would simply never register, leaving the roster without the id every hosted
|
|
152
|
+
* session records. The rows go in the profile's own `cordis.patch.yml` because
|
|
153
|
+
* that is the one composition layer the plugin owns: it is already the file the
|
|
154
|
+
* managed block lives in, it is applied after the bundle's own preset rows, and
|
|
155
|
+
* a rewrite of it is what the harness's live patch reload picks up.
|
|
156
|
+
*
|
|
157
|
+
* Idempotent: an up-to-date region is left byte for byte alone, so a rewrite
|
|
158
|
+
* that changes nothing does not touch the file's stamp.
|
|
159
|
+
* @param patchPath - absolute path of the profile's patch file.
|
|
160
|
+
* @param source - the roster's composition reader.
|
|
161
|
+
* @returns whether the patch file was written.
|
|
162
|
+
* @throws when the source preset cannot be read or the write fails.
|
|
163
|
+
*/
|
|
164
|
+
export declare function ensureEnginePresetRows(patchPath: string, source: PresetCompositionSource): Promise<boolean>;
|
|
90
165
|
//# sourceMappingURL=preset.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-loop-engine",
|
|
3
3
|
"description": "Web-switchable agent loop engine selection for the DeepSeek Harness - out-of-tree plugin (Claude Code / Codex / Pi / Kimi Code drivers) maintained by @kuun993, zero main-repo changes",
|
|
4
|
-
"version": "0.1.7-
|
|
4
|
+
"version": "0.1.7-rc4",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|