dsh-loop-engine 0.1.7-rc3 → 0.1.7-rc5
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 +77 -76
- package/README.zh.md +2 -2
- package/lib/index.js +164 -11
- package/lib/types/driver-core/idle-child.d.ts +57 -0
- package/lib/types/engine-codex/agent.d.ts +8 -0
- package/lib/types/engine-codex/loop.d.ts +6 -0
- package/lib/types/engine-codex/types.d.ts +7 -0
- package/lib/types/engine-kimi/agent.d.ts +8 -0
- package/lib/types/engine-kimi/loop.d.ts +6 -0
- package/lib/types/engine-kimi/types.d.ts +7 -0
- package/lib/types/index.d.ts +21 -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,77 @@
|
|
|
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
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- **
|
|
57
|
-
- **
|
|
58
|
-
- **
|
|
59
|
-
- **
|
|
60
|
-
- **
|
|
61
|
-
- **
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- [docs/
|
|
67
|
-
- [docs/
|
|
68
|
-
- [docs/
|
|
69
|
-
- [docs/
|
|
70
|
-
- [docs/
|
|
71
|
-
- [docs/
|
|
72
|
-
- [docs/
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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. Read `rcN` as **this plugin's own release counter for that harness line**, not as the harness's `rc` number (`0.1.7-rc1` … `0.1.7-rc5` all serve `0.1.7-rc.1` and `0.1.7-rc.2`). `0.1.7-rc1` … `0.1.7-rc5` target harness `0.1.7-rc.2`; `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
|
+
- **Kimi Code and Codex keep ONE child process per session** (`kimi acp` / `codex app-server`), reused across steps — that is what saves a spawn per step, and it also means a session that finished a turn and is left alone keeps paying for that process (the harness does not release an agent at turn end). Claude Code and Pi spawn per step and hold nothing. Set `childIdleMs` (milliseconds) in the composition entry — the plugin's row in the profile's `cordis.patch.yml` — to shut an idle session's child down, respawning it on the next step; `0` or omitted keeps the child for the session's whole life, which is the default. It closes the child only: the agent, the session, and the page are untouched, so nothing reloads and `session/disposed` is never emitted. See [docs/driver-core.md](docs/driver-core.md) §7.8.
|
|
52
|
+
- Engine-agnostic dsh commands (`/export`, `/feedback`, `/permission`) keep working and stay.
|
|
53
|
+
|
|
54
|
+
## Known limitations
|
|
55
|
+
|
|
56
|
+
- **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.
|
|
57
|
+
- **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.
|
|
58
|
+
- **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.
|
|
59
|
+
- **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.
|
|
60
|
+
- **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.
|
|
61
|
+
- **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.
|
|
62
|
+
- **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.
|
|
63
|
+
|
|
64
|
+
## Where the details live
|
|
65
|
+
|
|
66
|
+
- [docs/per-session-engine.md](docs/per-session-engine.md) — the full user-visible behavior of per-session engines.
|
|
67
|
+
- [docs/source-checkout.md](docs/source-checkout.md) — the `file:` shims a source-launched harness needs.
|
|
68
|
+
- [docs/architecture.md](docs/architecture.md) — plugin core: the single factory slot, the managed block, routing, per-session engine facts, the provider route.
|
|
69
|
+
- [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.
|
|
70
|
+
- [docs/driver-core.md](docs/driver-core.md) — the shared driver infrastructure.
|
|
71
|
+
- [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.
|
|
72
|
+
- [docs/optimization-backlog.md](docs/optimization-backlog.md) — known issues and the optimization list.
|
|
73
|
+
- [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`).
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
package/README.zh.md
CHANGED
|
@@ -22,7 +22,7 @@ dsh plugin --profile web add dsh-loop-engine
|
|
|
22
22
|
|
|
23
23
|
## 版本兼容
|
|
24
24
|
|
|
25
|
-
`dsh-loop-engine` 与它针对的 harness **同版本对齐**:`<harness version>-rcN`。自 `0.1.7-rc1` 起,**一个发布版本同时服务两代 harness**:0.1.5 线(`>=0.1.5-rc.1 <0.1.6-0`,三段 `rc` 共用同一套旧 settings API)与 0.1.7 线(`>=0.1.7-rc.1 <0.1.8-0`,其中 `rc.1` 与 `rc.2` 都已核对)。它在加载期探测当前是哪一代、走对应分支,所以同一份已发布产物在两代上都能安装运行。它消费的每个 harness 包都在 `peerDependencies` 里声明这个并集范围;超出范围会在启动或会话恢复时响亮地失败。在被覆盖的那条线里,新的 `rc` **不需要**插件跟版——除非它依赖的 API 面动了;`docs/compatibility.md` §1 列出了这些面与"要不要改代码"的那条判定命令。`0.1.5-rc3` … `0.1.5-rc5` 针对 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`。
|
|
25
|
+
`dsh-loop-engine` 与它针对的 harness **同版本对齐**:`<harness version>-rcN`。自 `0.1.7-rc1` 起,**一个发布版本同时服务两代 harness**:0.1.5 线(`>=0.1.5-rc.1 <0.1.6-0`,三段 `rc` 共用同一套旧 settings API)与 0.1.7 线(`>=0.1.7-rc.1 <0.1.8-0`,其中 `rc.1` 与 `rc.2` 都已核对)。它在加载期探测当前是哪一代、走对应分支,所以同一份已发布产物在两代上都能安装运行。它消费的每个 harness 包都在 `peerDependencies` 里声明这个并集范围;超出范围会在启动或会话恢复时响亮地失败。在被覆盖的那条线里,新的 `rc` **不需要**插件跟版——除非它依赖的 API 面动了;`docs/compatibility.md` §1 列出了这些面与"要不要改代码"的那条判定命令。`rcN` 读作**本插件对那条 harness 线的第 N 次发布号**,不是 harness 自己的 `rc` 号(`0.1.7-rc1` … `0.1.7-rc5` 都服务 `0.1.7-rc.1` 与 `0.1.7-rc.2`)。`0.1.7-rc1` … `0.1.7-rc5` 针对 harness `0.1.7-rc.2`;`0.1.5-rc3` … `0.1.5-rc5` 针对 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`。
|
|
26
26
|
|
|
27
27
|
### 环境要求
|
|
28
28
|
|
|
@@ -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
|
@@ -2389,6 +2389,34 @@ import { LlmError as LlmError3, createAssistantMessage as createAssistantMessage
|
|
|
2389
2389
|
import { createScope as createScope2 } from "@deepseek-ai/dsh-scope";
|
|
2390
2390
|
import { canonicalHeader as canonicalHeader2 } from "@deepseek-ai/dsh-session";
|
|
2391
2391
|
|
|
2392
|
+
// src/driver-core/idle-child.ts
|
|
2393
|
+
function createIdleChildCloser(options) {
|
|
2394
|
+
const { idleMs, close, warn } = options;
|
|
2395
|
+
let timer;
|
|
2396
|
+
const cancel = () => {
|
|
2397
|
+
if (timer === void 0) return;
|
|
2398
|
+
clearTimeout(timer);
|
|
2399
|
+
timer = void 0;
|
|
2400
|
+
};
|
|
2401
|
+
return {
|
|
2402
|
+
arm: () => {
|
|
2403
|
+
if (!(idleMs > 0)) return;
|
|
2404
|
+
cancel();
|
|
2405
|
+
timer = setTimeout(() => {
|
|
2406
|
+
timer = void 0;
|
|
2407
|
+
try {
|
|
2408
|
+
close();
|
|
2409
|
+
} catch (error) {
|
|
2410
|
+
warn(`loop-engine: idle shutdown failed to close a child process: ${String(error)}`);
|
|
2411
|
+
}
|
|
2412
|
+
}, idleMs);
|
|
2413
|
+
timer.unref?.();
|
|
2414
|
+
},
|
|
2415
|
+
cancel,
|
|
2416
|
+
dispose: cancel
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2392
2420
|
// src/engine-codex/model-handover.ts
|
|
2393
2421
|
var CODEX_DSH_PROVIDER = "dsh";
|
|
2394
2422
|
var CODEX_DSH_API_KEY_ENV = "DSH_LOOP_ENGINE_API_KEY";
|
|
@@ -2928,9 +2956,18 @@ var CodexAgent = class {
|
|
|
2928
2956
|
this.phase = { kind: "idle", lastTurn };
|
|
2929
2957
|
this.scope = createScope2(loopCtx, this);
|
|
2930
2958
|
this.ctx = this.scope.ctx.extend({ agent: this });
|
|
2959
|
+
this.idleChild = createIdleChildCloser({
|
|
2960
|
+
idleMs: this.config.childIdleMs,
|
|
2961
|
+
close: () => {
|
|
2962
|
+
this.closeChild();
|
|
2963
|
+
},
|
|
2964
|
+
warn: (message) => {
|
|
2965
|
+
loopCtx.logger.warn(message);
|
|
2966
|
+
}
|
|
2967
|
+
});
|
|
2931
2968
|
this.scope.ctx.effect(() => () => {
|
|
2932
|
-
this.
|
|
2933
|
-
this.
|
|
2969
|
+
this.idleChild.dispose();
|
|
2970
|
+
this.closeChild();
|
|
2934
2971
|
}, "codex.appServerClient()");
|
|
2935
2972
|
}
|
|
2936
2973
|
loopCtx;
|
|
@@ -2982,6 +3019,17 @@ var CodexAgent = class {
|
|
|
2982
3019
|
* must respawn the child rather than reuse one configured for another endpoint.
|
|
2983
3020
|
*/
|
|
2984
3021
|
appServerConfig;
|
|
3022
|
+
/**
|
|
3023
|
+
* Countdown that shuts the child down once this agent has been idle past the
|
|
3024
|
+
* configured window, so an idle session stops paying for a process nothing
|
|
3025
|
+
* is driving. Armed and cancelled by {@link setPhase}.
|
|
3026
|
+
*/
|
|
3027
|
+
idleChild;
|
|
3028
|
+
/** Shut the cached child down; the next step's {@link appServerClient} spawns a fresh one. */
|
|
3029
|
+
closeChild() {
|
|
3030
|
+
this.appServer?.dispose();
|
|
3031
|
+
this.appServer = void 0;
|
|
3032
|
+
}
|
|
2985
3033
|
/**
|
|
2986
3034
|
* Return the cached app-server client, spawning one on first use, after a dead
|
|
2987
3035
|
* process, or when the resolved dsh endpoint changed since the last spawn.
|
|
@@ -3075,6 +3123,8 @@ var CodexAgent = class {
|
|
|
3075
3123
|
setPhase(next) {
|
|
3076
3124
|
const previousStatus = this.status;
|
|
3077
3125
|
this.phase = next;
|
|
3126
|
+
if (next.kind === "idle") this.idleChild.arm();
|
|
3127
|
+
else this.idleChild.cancel();
|
|
3078
3128
|
const status = this.status;
|
|
3079
3129
|
if (status !== previousStatus) {
|
|
3080
3130
|
this.dispatch.emit("agent/status", { status });
|
|
@@ -3610,14 +3660,16 @@ var Config2 = z2.object({
|
|
|
3610
3660
|
sandboxMode: z2.union([...CODEX_SANDBOX_MODES]),
|
|
3611
3661
|
approvalPolicy: z2.union([...CODEX_APPROVAL_POLICIES]),
|
|
3612
3662
|
env: z2.dict(z2.string()).default({}),
|
|
3613
|
-
model: z2.string()
|
|
3663
|
+
model: z2.string(),
|
|
3664
|
+
childIdleMs: z2.number()
|
|
3614
3665
|
});
|
|
3615
3666
|
function resolveConfig2(config) {
|
|
3616
3667
|
return {
|
|
3617
3668
|
sandboxMode: config.sandboxMode,
|
|
3618
3669
|
approvalPolicy: config.approvalPolicy,
|
|
3619
3670
|
env: config.env ?? {},
|
|
3620
|
-
model: config.model
|
|
3671
|
+
model: config.model,
|
|
3672
|
+
childIdleMs: config.childIdleMs ?? 0
|
|
3621
3673
|
};
|
|
3622
3674
|
}
|
|
3623
3675
|
var CODEX_ENGINE_LABEL = "agentLoopCodex";
|
|
@@ -5122,9 +5174,18 @@ var KimiAgent = class {
|
|
|
5122
5174
|
this.phase = { kind: "idle", lastTurn };
|
|
5123
5175
|
this.scope = createScope4(loopCtx, this);
|
|
5124
5176
|
this.ctx = this.scope.ctx.extend({ agent: this });
|
|
5177
|
+
this.idleChild = createIdleChildCloser({
|
|
5178
|
+
idleMs: this.config.childIdleMs,
|
|
5179
|
+
close: () => {
|
|
5180
|
+
this.closeChild();
|
|
5181
|
+
},
|
|
5182
|
+
warn: (message) => {
|
|
5183
|
+
loopCtx.logger.warn(message);
|
|
5184
|
+
}
|
|
5185
|
+
});
|
|
5125
5186
|
this.scope.ctx.effect(() => () => {
|
|
5126
|
-
this.
|
|
5127
|
-
this.
|
|
5187
|
+
this.idleChild.dispose();
|
|
5188
|
+
this.closeChild();
|
|
5128
5189
|
}, "kimi.acpClient()");
|
|
5129
5190
|
}
|
|
5130
5191
|
loopCtx;
|
|
@@ -5157,6 +5218,17 @@ var KimiAgent = class {
|
|
|
5157
5218
|
* every step.
|
|
5158
5219
|
*/
|
|
5159
5220
|
handoverEnvCache;
|
|
5221
|
+
/**
|
|
5222
|
+
* Countdown that shuts the child down once this agent has been idle past the
|
|
5223
|
+
* configured window, so an idle session stops paying for a process nothing
|
|
5224
|
+
* is driving. Armed and cancelled by {@link setPhase}.
|
|
5225
|
+
*/
|
|
5226
|
+
idleChild;
|
|
5227
|
+
/** Shut the cached child down; the next step's {@link acpClient} spawns a fresh one. */
|
|
5228
|
+
closeChild() {
|
|
5229
|
+
this.acp?.dispose();
|
|
5230
|
+
this.acp = void 0;
|
|
5231
|
+
}
|
|
5160
5232
|
get status() {
|
|
5161
5233
|
return this.phase.kind === "idle" || this.phase.kind === "maintenance" ? "idle" : "running";
|
|
5162
5234
|
}
|
|
@@ -5164,6 +5236,8 @@ var KimiAgent = class {
|
|
|
5164
5236
|
setPhase(next) {
|
|
5165
5237
|
const previousStatus = this.status;
|
|
5166
5238
|
this.phase = next;
|
|
5239
|
+
if (next.kind === "idle") this.idleChild.arm();
|
|
5240
|
+
else this.idleChild.cancel();
|
|
5167
5241
|
const status = this.status;
|
|
5168
5242
|
if (status !== previousStatus) {
|
|
5169
5243
|
this.dispatch.emit("agent/status", { status });
|
|
@@ -5732,13 +5806,15 @@ var KIMI_DISPOSE_GRACE_MS = 3e3;
|
|
|
5732
5806
|
var Config4 = z4.object({
|
|
5733
5807
|
model: z4.string(),
|
|
5734
5808
|
env: z4.dict(z4.string()).default({}),
|
|
5735
|
-
bin: z4.string()
|
|
5809
|
+
bin: z4.string(),
|
|
5810
|
+
childIdleMs: z4.number()
|
|
5736
5811
|
});
|
|
5737
5812
|
function resolveConfig4(config) {
|
|
5738
5813
|
return {
|
|
5739
5814
|
model: config.model,
|
|
5740
5815
|
env: config.env ?? {},
|
|
5741
|
-
bin: kimiBinResolver(config.bin)
|
|
5816
|
+
bin: kimiBinResolver(config.bin),
|
|
5817
|
+
childIdleMs: config.childIdleMs ?? 0
|
|
5742
5818
|
};
|
|
5743
5819
|
}
|
|
5744
5820
|
var KIMI_ENGINE_LABEL = "agentLoopKimi";
|
|
@@ -5856,6 +5932,64 @@ var MANAGED_HEADER = `# Managed by dsh-loop-engine: the deployment's "${SOURCE_P
|
|
|
5856
5932
|
# from "${SOURCE_PRESET_ID}" on boot \u2014 hand edits are overwritten. The preset id
|
|
5857
5933
|
# names the engine this session runs.
|
|
5858
5934
|
`;
|
|
5935
|
+
var PRESET_ROWS_BEGIN = "# -- dsh-loop-engine presets --";
|
|
5936
|
+
var PRESET_ROWS_END = "# -- /dsh-loop-engine presets --";
|
|
5937
|
+
var ENGINE_PRESET_ORDER = 100;
|
|
5938
|
+
var PLUGIN_LIST_INDENT = " ".repeat(10);
|
|
5939
|
+
function pluginListLines(composition) {
|
|
5940
|
+
const text = composition.replace(/\n+$/, "");
|
|
5941
|
+
const lines = text.split("\n");
|
|
5942
|
+
if (!lines.some((line) => line.startsWith("- "))) return [" plugins: []"];
|
|
5943
|
+
return [" plugins:", ...lines.map((line) => line.trim() === "" ? "" : `${PLUGIN_LIST_INDENT}${line}`)];
|
|
5944
|
+
}
|
|
5945
|
+
function renderEnginePresetRow(engine, composition) {
|
|
5946
|
+
const id = enginePresetId(engine);
|
|
5947
|
+
return [
|
|
5948
|
+
"- insert:",
|
|
5949
|
+
` - id: preset-${id}`,
|
|
5950
|
+
` name: '@deepseek-ai/dsh-agent-preset'`,
|
|
5951
|
+
" config:",
|
|
5952
|
+
` id: ${id}`,
|
|
5953
|
+
` order: ${ENGINE_PRESET_ORDER}`,
|
|
5954
|
+
...pluginListLines(stripPresetRows(composition))
|
|
5955
|
+
].join("\n");
|
|
5956
|
+
}
|
|
5957
|
+
function renderPresetRows(composition) {
|
|
5958
|
+
return [
|
|
5959
|
+
PRESET_ROWS_BEGIN,
|
|
5960
|
+
"# One `@deepseek-ai/dsh-agent-preset` declaration per hosted engine: the",
|
|
5961
|
+
`# deployment's "${SOURCE_PRESET_ID}" preset minus the dsh-native command/skill rows a`,
|
|
5962
|
+
`# hosted loop engine replaces. Regenerated from "${SOURCE_PRESET_ID}" on boot \u2014 hand`,
|
|
5963
|
+
"# edits are overwritten. The preset id names the engine a session runs.",
|
|
5964
|
+
...HOSTED_ENGINE_IDS.map((engine) => renderEnginePresetRow(engine, composition)),
|
|
5965
|
+
`${PRESET_ROWS_END}
|
|
5966
|
+
`
|
|
5967
|
+
].join("\n");
|
|
5968
|
+
}
|
|
5969
|
+
function presetRowsSpan(text) {
|
|
5970
|
+
const begin = text.indexOf(PRESET_ROWS_BEGIN);
|
|
5971
|
+
if (begin === -1) return { head: text, tail: "", present: false, blankBefore: false };
|
|
5972
|
+
const endAt = text.indexOf(PRESET_ROWS_END, begin);
|
|
5973
|
+
const spanEnd = endAt === -1 ? text.length : endAt + PRESET_ROWS_END.length + 1;
|
|
5974
|
+
const before = text.slice(0, begin);
|
|
5975
|
+
const blankBefore = before.endsWith("\n\n");
|
|
5976
|
+
return {
|
|
5977
|
+
head: blankBefore ? before.slice(0, -1) : before,
|
|
5978
|
+
tail: text.slice(spanEnd),
|
|
5979
|
+
present: true,
|
|
5980
|
+
blankBefore
|
|
5981
|
+
};
|
|
5982
|
+
}
|
|
5983
|
+
function applyEnginePresetRows(text, composition) {
|
|
5984
|
+
const region = renderPresetRows(composition);
|
|
5985
|
+
const span = presetRowsSpan(text);
|
|
5986
|
+
if (!span.present) {
|
|
5987
|
+
if (text === "") return region;
|
|
5988
|
+
return dropSeedPlaceholder(`${ensureTrailingNewline(text)}
|
|
5989
|
+
${region}`);
|
|
5990
|
+
}
|
|
5991
|
+
return dropSeedPlaceholder(`${span.head}${span.blankBefore ? "\n" : ""}${region}${span.tail}`);
|
|
5992
|
+
}
|
|
5859
5993
|
function managedMetadata(engine) {
|
|
5860
5994
|
return `name: ${ENGINE_DISPLAY_NAMES[engine]}
|
|
5861
5995
|
description: ${ENGINE_DISPLAY_NAMES[engine]}, with the dsh-native commands and skills it replaces removed.
|
|
@@ -5937,6 +6071,18 @@ ${stripPresetRows(composition)}`;
|
|
|
5937
6071
|
}
|
|
5938
6072
|
return changed;
|
|
5939
6073
|
}
|
|
6074
|
+
async function readTextOrEmpty(path) {
|
|
6075
|
+
try {
|
|
6076
|
+
return await readFile(path, "utf8");
|
|
6077
|
+
} catch {
|
|
6078
|
+
return "";
|
|
6079
|
+
}
|
|
6080
|
+
}
|
|
6081
|
+
async function ensureEnginePresetRows(patchPath, source) {
|
|
6082
|
+
const composition = await readComposition(source, SOURCE_PRESET_ID);
|
|
6083
|
+
const current = await readTextOrEmpty(patchPath);
|
|
6084
|
+
return writeIfDifferent(patchPath, applyEnginePresetRows(current, composition));
|
|
6085
|
+
}
|
|
5940
6086
|
|
|
5941
6087
|
// src/router-loop.ts
|
|
5942
6088
|
import AgentLoop, { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from "@deepseek-ai/dsh-agent-loop";
|
|
@@ -7428,6 +7574,7 @@ var Config5 = z6.object({
|
|
|
7428
7574
|
piProvider: z6.string(),
|
|
7429
7575
|
piThinking: z6.string(),
|
|
7430
7576
|
kimiBin: z6.string(),
|
|
7577
|
+
childIdleMs: z6.number(),
|
|
7431
7578
|
// The live fields exist only on the 0.1.7 line; the 0.1.5 line carries the
|
|
7432
7579
|
// selection in a settings section instead. The empty arm never runs when the
|
|
7433
7580
|
// coverage job is on 0.1.7; the 0.1.5 dep set is exercised by
|
|
@@ -7444,6 +7591,10 @@ function resolvePatchPath(config) {
|
|
|
7444
7591
|
config.patchFilename ?? "cordis.patch.yml"
|
|
7445
7592
|
);
|
|
7446
7593
|
}
|
|
7594
|
+
function authorHostedPresets(patchPath, presets) {
|
|
7595
|
+
if (LEGACY_HARNESS) return ensureEnginePresets(resolveDshHome2(), presets);
|
|
7596
|
+
return ensureEnginePresetRows(patchPath, presets);
|
|
7597
|
+
}
|
|
7447
7598
|
function isMissing2(error) {
|
|
7448
7599
|
return error?.code === "ENOENT";
|
|
7449
7600
|
}
|
|
@@ -7494,7 +7645,8 @@ function codexConfig(config) {
|
|
|
7494
7645
|
...config.sandboxMode === void 0 ? {} : { sandboxMode: config.sandboxMode },
|
|
7495
7646
|
...config.approvalPolicy === void 0 ? {} : { approvalPolicy: config.approvalPolicy },
|
|
7496
7647
|
...config.env === void 0 ? {} : { env: config.env },
|
|
7497
|
-
...config.model === void 0 ? {} : { model: config.model }
|
|
7648
|
+
...config.model === void 0 ? {} : { model: config.model },
|
|
7649
|
+
...config.childIdleMs === void 0 ? {} : { childIdleMs: config.childIdleMs }
|
|
7498
7650
|
};
|
|
7499
7651
|
}
|
|
7500
7652
|
function piConfig(config) {
|
|
@@ -7510,7 +7662,8 @@ function kimiConfig(config) {
|
|
|
7510
7662
|
return {
|
|
7511
7663
|
...config.model === void 0 ? {} : { model: config.model },
|
|
7512
7664
|
...config.env === void 0 ? {} : { env: config.env },
|
|
7513
|
-
...config.kimiBin === void 0 ? {} : { bin: config.kimiBin }
|
|
7665
|
+
...config.kimiBin === void 0 ? {} : { bin: config.kimiBin },
|
|
7666
|
+
...config.childIdleMs === void 0 ? {} : { childIdleMs: config.childIdleMs }
|
|
7514
7667
|
};
|
|
7515
7668
|
}
|
|
7516
7669
|
function apply(ctx, config) {
|
|
@@ -7611,7 +7764,7 @@ function apply(ctx, config) {
|
|
|
7611
7764
|
let lastError;
|
|
7612
7765
|
for (let attempt2 = 0; attempt2 <= PRESET_SOURCE_ATTEMPTS; attempt2 += 1) {
|
|
7613
7766
|
try {
|
|
7614
|
-
return await
|
|
7767
|
+
return await authorHostedPresets(patchPath, presets);
|
|
7615
7768
|
} catch (error) {
|
|
7616
7769
|
lastError = error;
|
|
7617
7770
|
if (attempt2 < PRESET_SOURCE_ATTEMPTS) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idle shutdown for a driver's persistent child process.
|
|
3
|
+
*
|
|
4
|
+
* Two engines keep one child alive per session so a step never pays a spawn:
|
|
5
|
+
* Kimi's `kimi acp` and Codex's app-server. Nothing in the harness releases an
|
|
6
|
+
* agent when a turn ends — an agent lives as long as its session's scope — so
|
|
7
|
+
* that child outlives the work that needed it and an idle session keeps
|
|
8
|
+
* costing what its child costs (tens to hundreds of MB for a Node CLI). The
|
|
9
|
+
* closer arms a countdown whenever its owner goes idle and runs the owner's
|
|
10
|
+
* close when the countdown expires, so the next step's own lazy accessor
|
|
11
|
+
* spawns a fresh child.
|
|
12
|
+
*
|
|
13
|
+
* The closer is inert unless `idleMs` is positive: a deployment that does not
|
|
14
|
+
* configure idle shutdown keeps every child for the process's lifetime, which
|
|
15
|
+
* is the behaviour that predates this reasoner.
|
|
16
|
+
*
|
|
17
|
+
* This is deliberately NOT an agent release: disposing the agent emits
|
|
18
|
+
* `session/disposed`, which the browser half reads as "this session is gone"
|
|
19
|
+
* with no way back in that page's lifetime (`router-loop.ts` `move`). Closing
|
|
20
|
+
* the child leaves the agent, the session, and the page exactly where they are.
|
|
21
|
+
*
|
|
22
|
+
* @module dsh-loop-engine/driver-core/idle-child
|
|
23
|
+
*/
|
|
24
|
+
/** An armed-on-idle countdown that closes one child process. */
|
|
25
|
+
export interface IdleChildCloser {
|
|
26
|
+
/**
|
|
27
|
+
* (Re)start the countdown. Called when the owner settles into an idle phase;
|
|
28
|
+
* a no-op while idle shutdown is disabled.
|
|
29
|
+
*/
|
|
30
|
+
arm(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Cancel a pending countdown, for the moment the child is needed again.
|
|
33
|
+
* Cancelling a countdown that never armed is a no-op.
|
|
34
|
+
*/
|
|
35
|
+
cancel(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Cancel the countdown and drop the timer. Called from the owner's teardown,
|
|
38
|
+
* so a disposed agent never closes a child through a stale callback.
|
|
39
|
+
*/
|
|
40
|
+
dispose(): void;
|
|
41
|
+
}
|
|
42
|
+
/** Input of {@link createIdleChildCloser}. */
|
|
43
|
+
export interface IdleChildCloserOptions {
|
|
44
|
+
/** Idle window in milliseconds; zero or less leaves the child alone forever. */
|
|
45
|
+
readonly idleMs: number;
|
|
46
|
+
/** Close the child; the owner's next accessor call spawns a fresh one. */
|
|
47
|
+
readonly close: () => void;
|
|
48
|
+
/** Report a failing close without letting it escape the countdown. */
|
|
49
|
+
readonly warn: (message: string) => void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build one owner's idle countdown.
|
|
53
|
+
* @param options - the idle window, the close to run, and a diagnostic sink.
|
|
54
|
+
* @returns the closer, inert while the idle window is not positive.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createIdleChildCloser(options: IdleChildCloserOptions): IdleChildCloser;
|
|
57
|
+
//# sourceMappingURL=idle-child.d.ts.map
|
|
@@ -70,7 +70,15 @@ export declare class CodexAgent implements Agent {
|
|
|
70
70
|
* must respawn the child rather than reuse one configured for another endpoint.
|
|
71
71
|
*/
|
|
72
72
|
private appServerConfig;
|
|
73
|
+
/**
|
|
74
|
+
* Countdown that shuts the child down once this agent has been idle past the
|
|
75
|
+
* configured window, so an idle session stops paying for a process nothing
|
|
76
|
+
* is driving. Armed and cancelled by {@link setPhase}.
|
|
77
|
+
*/
|
|
78
|
+
private readonly idleChild;
|
|
73
79
|
constructor(loopCtx: Context, id: SessionId, options: AgentOptions, session: Session, config: ResolvedConfig);
|
|
80
|
+
/** Shut the cached child down; the next step's {@link appServerClient} spawns a fresh one. */
|
|
81
|
+
private closeChild;
|
|
74
82
|
/**
|
|
75
83
|
* Return the cached app-server client, spawning one on first use, after a dead
|
|
76
84
|
* process, or when the resolved dsh endpoint changed since the last spawn.
|
|
@@ -40,6 +40,12 @@ export interface Config {
|
|
|
40
40
|
env?: Record<string, string>;
|
|
41
41
|
/** Fallback model for the app-server thread, used when the session selects none; Codex native settings own the model when omitted. */
|
|
42
42
|
model?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Idle window in milliseconds before a session's `codex app-server` child is
|
|
45
|
+
* shut down, spawned again by the next step. Omitted or non-positive keeps
|
|
46
|
+
* the child for the session's whole life.
|
|
47
|
+
*/
|
|
48
|
+
childIdleMs?: number;
|
|
43
49
|
}
|
|
44
50
|
/** Schema of the Codex loop plugin configuration. */
|
|
45
51
|
export declare const Config: z<Config>;
|
|
@@ -15,5 +15,12 @@ export interface ResolvedConfig {
|
|
|
15
15
|
readonly approvalPolicy: CodexApprovalPolicy | undefined;
|
|
16
16
|
readonly env: Record<string, string>;
|
|
17
17
|
readonly model: string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* How long an idle session keeps its `codex app-server` child, in
|
|
20
|
+
* milliseconds; zero or less keeps it for the session's whole life. See
|
|
21
|
+
* `driver-core/idle-child.ts` for why the child, and not the agent, is what
|
|
22
|
+
* gets released.
|
|
23
|
+
*/
|
|
24
|
+
readonly childIdleMs: number;
|
|
18
25
|
}
|
|
19
26
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -56,7 +56,15 @@ export declare class KimiAgent implements Agent {
|
|
|
56
56
|
* every step.
|
|
57
57
|
*/
|
|
58
58
|
private handoverEnvCache;
|
|
59
|
+
/**
|
|
60
|
+
* Countdown that shuts the child down once this agent has been idle past the
|
|
61
|
+
* configured window, so an idle session stops paying for a process nothing
|
|
62
|
+
* is driving. Armed and cancelled by {@link setPhase}.
|
|
63
|
+
*/
|
|
64
|
+
private readonly idleChild;
|
|
59
65
|
constructor(loopCtx: Context, id: SessionId, options: AgentOptions, session: Session, config: ResolvedConfig, spawn: KimiSpawnCapability, bin: string);
|
|
66
|
+
/** Shut the cached child down; the next step's {@link acpClient} spawns a fresh one. */
|
|
67
|
+
private closeChild;
|
|
60
68
|
get status(): AgentStatus;
|
|
61
69
|
/** Commit a phase and publish its externally visible status transition. */
|
|
62
70
|
private setPhase;
|
|
@@ -32,6 +32,12 @@ export interface Config {
|
|
|
32
32
|
env?: Record<string, string>;
|
|
33
33
|
/** Kimi CLI executable; `'kimi'` resolves through PATH when not pinned to an absolute path. */
|
|
34
34
|
bin?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Idle window in milliseconds before a session's `kimi acp` child is shut
|
|
37
|
+
* down, spawned again by the next step. Omitted or non-positive keeps the
|
|
38
|
+
* child for the session's whole life.
|
|
39
|
+
*/
|
|
40
|
+
childIdleMs?: number;
|
|
35
41
|
}
|
|
36
42
|
/** Schema of the Kimi loop plugin configuration. */
|
|
37
43
|
export declare const Config: z<Config>;
|
|
@@ -19,5 +19,12 @@ export interface ResolvedConfig {
|
|
|
19
19
|
readonly env: Record<string, string>;
|
|
20
20
|
/** Kimi CLI executable; `'kimi'` resolves through PATH when not pinned to an absolute path. */
|
|
21
21
|
readonly bin: string;
|
|
22
|
+
/**
|
|
23
|
+
* How long an idle session keeps its `kimi acp` child, in milliseconds; zero
|
|
24
|
+
* or less keeps it for the session's whole life. See
|
|
25
|
+
* `driver-core/idle-child.ts` for why the child, and not the agent, is what
|
|
26
|
+
* gets released.
|
|
27
|
+
*/
|
|
28
|
+
readonly childIdleMs: number;
|
|
22
29
|
}
|
|
23
30
|
//# sourceMappingURL=types.d.ts.map
|
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.
|
|
@@ -70,6 +74,14 @@ export interface Config extends ClaudeCodeConfig {
|
|
|
70
74
|
piThinking?: string;
|
|
71
75
|
/** Kimi CLI executable; `'kimi'` resolves through PATH when not pinned to an absolute path. */
|
|
72
76
|
kimiBin?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Idle window in milliseconds after which a hosted session's PERSISTENT child
|
|
79
|
+
* process (Kimi's `kimi acp`, Codex's app-server) is shut down and spawned
|
|
80
|
+
* again by the next step. Zero or omitted keeps the child for the session's
|
|
81
|
+
* whole life. Only these two engines hold a child between steps: the Pi and
|
|
82
|
+
* Claude Code drivers spawn per step, so the knob does not reach them.
|
|
83
|
+
*/
|
|
84
|
+
childIdleMs?: number;
|
|
73
85
|
/**
|
|
74
86
|
* Default engine NEW sessions are created on. A live, profile-backed field:
|
|
75
87
|
* the settings shell edits it and the running plugin reads it through this
|
|
@@ -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-rc5",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|