dsh-loop-engine 0.1.5-rc2 → 0.1.5-rc4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +44 -177
  2. package/README.zh.md +48 -100
  3. package/lib/client.js +887 -266
  4. package/lib/index.js +2212 -914
  5. package/lib/invariant.js +43 -45
  6. package/lib/types/agent-preset-ids.d.ts +303 -0
  7. package/lib/types/client/LoopEngineBadge.d.ts +44 -17
  8. package/lib/types/client/LoopEngineComposerSelect.d.ts +79 -13
  9. package/lib/types/client/LoopEngineSection.d.ts +5 -4
  10. package/lib/types/client/locales.d.ts +133 -7
  11. package/lib/types/client/reload.d.ts +135 -0
  12. package/lib/types/client/session-engine.d.ts +474 -0
  13. package/lib/types/client/store.d.ts +1 -1
  14. package/lib/types/client/turn-status.d.ts +112 -10
  15. package/lib/types/client/use-session-engine.d.ts +66 -0
  16. package/lib/types/commands.d.ts +11 -3
  17. package/lib/types/driver-core/host-servers.d.ts +106 -0
  18. package/lib/types/driver-core/hosted-engine-runtime.d.ts +190 -0
  19. package/lib/types/driver-core/hosted-tool-vocabulary.d.ts +72 -0
  20. package/lib/types/driver-core/model-handover.d.ts +116 -0
  21. package/lib/types/driver-core/ownership.d.ts +6 -5
  22. package/lib/types/driver-core/prompt.d.ts +32 -0
  23. package/lib/types/driver-core/session-lifetime.d.ts +62 -0
  24. package/lib/types/driver-core/session-model.d.ts +82 -0
  25. package/lib/types/engine-claude/agent.d.ts +23 -3
  26. package/lib/types/engine-claude/loop.d.ts +16 -15
  27. package/lib/types/engine-codex/agent.d.ts +22 -3
  28. package/lib/types/engine-codex/appserver/client.d.ts +15 -2
  29. package/lib/types/engine-codex/loop.d.ts +13 -15
  30. package/lib/types/engine-codex/model-handover.d.ts +44 -0
  31. package/lib/types/engine-kimi/acp/client.d.ts +10 -0
  32. package/lib/types/engine-kimi/agent.d.ts +19 -2
  33. package/lib/types/engine-kimi/commands.d.ts +18 -14
  34. package/lib/types/engine-kimi/loop.d.ts +14 -16
  35. package/lib/types/engine-kimi/model-handover.d.ts +32 -0
  36. package/lib/types/engine-kimi/process.d.ts +2 -2
  37. package/lib/types/engine-kimi/types.d.ts +1 -1
  38. package/lib/types/engine-of-session.d.ts +97 -0
  39. package/lib/types/engine-pi/agent.d.ts +25 -23
  40. package/lib/types/engine-pi/loop.d.ts +13 -23
  41. package/lib/types/engine-pi/model-handover.d.ts +35 -0
  42. package/lib/types/engine-pi/types.d.ts +2 -2
  43. package/lib/types/engine-remote.d.ts +192 -0
  44. package/lib/types/engine-surface.d.ts +36 -0
  45. package/lib/types/index.d.ts +51 -50
  46. package/lib/types/invariant.d.ts +8 -5
  47. package/lib/types/model-selection-reset.d.ts +271 -0
  48. package/lib/types/patch-manager.d.ts +57 -39
  49. package/lib/types/preset.d.ts +39 -26
  50. package/lib/types/provider-route.d.ts +83 -36
  51. package/lib/types/router-loop.d.ts +406 -0
  52. package/lib/types/session-engine-store.d.ts +138 -0
  53. package/lib/types/settings.d.ts +12 -11
  54. package/package.json +109 -104
@@ -1,53 +1,54 @@
1
1
  /**
2
- * Web-switchable agent loop engine, node half.
2
+ * Per-session agent loop engines, node half.
3
3
  *
4
4
  * Hosts the non-default agent-loop engines (Claude Code, Codex, Pi, Kimi Code)
5
- * and bridges them with the harness's single AgentFactory slot. The engine is
6
- * selected by the `agent-loop-engine` settings section; the selection is
7
- * realized by a managed block in the profile's `cordis.patch.yml` that
8
- * disables the base bundle's `agent-loop` row — exactly one AgentFactory may
9
- * register, so a non-default engine owns the slot by disabling the base loop
10
- * first, and `in-process` leaves the base row active (this plugin does NOT
11
- * register its own factory then).
5
+ * and routes each session to the engine it runs. The engine is the PLUGIN's own
6
+ * per-session fact (`session-engine-store.ts`), read first and over the recorded
7
+ * agent preset; a session the plugin has no record for keeps the preset answer,
8
+ * which is what makes every pre-existing session behave exactly as before.
9
+ * The harness admits exactly one AgentFactory per process, so "session A on
10
+ * Codex while session B runs Kimi" is realized by a single router factory
11
+ * ({@link RouterLoop}, which extends the harness's own `AgentLoop`) dispatching
12
+ * to one driver runtime per engine; `in-process` sessions keep the harness loop
13
+ * through that same router.
12
14
  *
13
- * The managed block is the ground truth the factory decision reads at boot:
14
- * apply() reads the file synchronously, so a committed engine change takes
15
- * effect on the next recomposition (restart); the config-only HMR watcher
16
- * re-applies the patch file but cannot re-register an AgentFactory mid-run.
17
- * The settings section is seeded from the block so the UI mirrors the file,
18
- * and a committed settings change writes the block (only when it differs).
15
+ * Because the router owns the slot, the plugin keeps the base bundle's
16
+ * `agent-loop` row disabled for as long as it is composed. That managed block
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; the block names no
19
+ * engine, because the engine is a per-session decision.
19
20
  *
20
- * A hosted engine also takes over the session's command and skill surface:
21
- * the block disables dsh's `command-goal` row, and the plugin authors a
22
- * stripped copy of the `standard` agent preset into the user preset root
23
- * (see `preset.ts`) and steers the `agent-presets` roster default to it, so
24
- * new sessions get the engine's commands and skills instead of the dsh-native
25
- * ones an external engine cannot honor. Switching back to `in-process`
26
- * restores the previous default.
21
+ * The plugin authors one preset per hosted engine into the user preset root
22
+ * (`$DSH_HOME/.agent-presets/loop-engine-<engine>`, see `preset.ts`), each a
23
+ * copy of `standard` minus the dsh-native command and skill rows an external
24
+ * engine replaces, and it serves every hosted engine's provider route label in
25
+ * the llm registry (`provider-route.ts`): an engine logs its own label into
26
+ * each session's request/header, and the web host refuses a turn whose session
27
+ * selection names a provider no adapter serves. The preset is now only the
28
+ * session's agent-plane composition (and the engine's own default for a session
29
+ * with no record) — it is no longer the thing that decides a running session's
30
+ * engine.
27
31
  *
28
- * While a hosted engine is mounted the plugin also serves its provider route
29
- * label (`claude-code` / `codex` / `pi` / `kimi`) from the llm registry with
30
- * a model-less placeholder adapter (see `provider-route.ts`): the engine logs
31
- * that label into each session's request/header, and the web host refuses a
32
- * turn whose session selection names a provider no adapter serves — without
33
- * the placeholder the second prompt of every hosted session would fail with
34
- * `model-unavailable`.
32
+ * The `agent-loop-engine` settings section carries the DEFAULT engine for new
33
+ * sessions — which preset the roster's default points at — not a process-wide
34
+ * switch: existing sessions keep the engine they run, and nothing is torn down or
35
+ * reloaded by it. (A PER-SESSION switch is a different thing: it is made from the
36
+ * chat composer, and when it involves the harness loop it releases that session's
37
+ * agent and reloads the page — `router-loop.ts` `move`, `client/reload.ts`.)
35
38
  *
36
39
  * @module dsh-loop-engine
37
40
  */
38
- import { Context } from '@deepseek-ai/cordis';
41
+ import type { Context } from '@deepseek-ai/cordis';
39
42
  import z from '@deepseek-ai/schemastery';
40
43
  import { type Config as ClaudeCodeConfig } from './engine-claude/loop.ts';
41
44
  import type { CodexApprovalPolicy, CodexSandboxMode } from './engine-codex/types.ts';
42
- import { type LoopEngineId } from './settings.ts';
43
45
  export declare const name = "loop-engine";
44
46
  /**
45
47
  * Services the plugin's own fiber requires. The plugin declares none of its
46
- * own: the optional host services it reads (`commands`, `skills`) are resolved
47
- * lazily via `ctx.get` and may be absent, and the hosted engine factories
48
- * (Claude Code / Codex) declare their own `inject` when the plugin mounts them
49
- * as children. Empty keeps the plugin from demanding a service that a minimal
50
- * profile does not provide.
48
+ * own: the optional host services it reads (`commands`, `skills`, `agentPresets`,
49
+ * `llm`) are resolved lazily via `ctx.get` and may be absent, and the router
50
+ * declares the harness loop's own dependency set when it mounts. Empty keeps
51
+ * the plugin from demanding a service that a minimal profile does not provide.
51
52
  */
52
53
  export declare const inject: never[];
53
54
  /** Composition entry for the loop engine selection and the hosted engine drivers. */
@@ -77,9 +78,8 @@ export interface Config extends ClaudeCodeConfig {
77
78
  * matching the permissive interface and read path (`resolvePatchPath` defaults
78
79
  * the patch path; each engine driver resolves only the knobs it owns and
79
80
  * omitted deployment tunables fall back to the session). The composition entry
80
- * is an engine-agnostic superset: the selectable knobs belong to whichever
81
- * engine the settings pick at runtime, so both engines' knobs may coexist and
82
- * only the selected one is consumed.
81
+ * is an engine-agnostic superset: every hosted engine's knobs live here at
82
+ * once, because any session may select any engine.
83
83
  */
84
84
  export declare const Config: z<Config>;
85
85
  /** Resolve the managed patch file from configuration, defaulting to the web profile. */
@@ -87,28 +87,29 @@ export declare function resolvePatchPath(config: Config): string;
87
87
  /** Atomically replace the patch file (same-directory temp + rename). */
88
88
  export declare function writePatchFile(path: string, text: string): Promise<void>;
89
89
  /**
90
- * Synchronously atomically replace the patch file. The engine-selection
91
- * onChange is a synchronous hook with no await, and the write MUST land before
92
- * the caller is told the switch committed — otherwise a user who restarts
93
- * `dsh web` immediately reads the stale file and the previous engine boots.
90
+ * Synchronously atomically replace the patch file. The settings onChange that
91
+ * commits a default-engine change is a synchronous hook with no await, and the
92
+ * write MUST land before the caller is told the change committed — otherwise a
93
+ * user who restarts `dsh web` immediately reads the stale file.
94
94
  * @param path - the profile's patch file.
95
95
  * @param text - the next file content.
96
96
  */
97
97
  export declare function writePatchFileSync(path: string, text: string): void;
98
98
  /**
99
- * Rewrite the managed block for a target engine, preserving the rest of the
100
- * file byte for byte. Only writes when the file actually differs.
99
+ * Ensure the profile's patch file carries the plugin's managed block, which
100
+ * disables the base bundle's `agent-loop` row so this plugin's router owns the
101
+ * single AgentFactory slot. Idempotent: a file already carrying a block and no
102
+ * legacy block is left untouched.
101
103
  * @param path - the profile's patch file.
102
- * @param engine - the target engine.
103
104
  * @returns whether a write occurred.
104
105
  */
105
- export declare function syncManagedBlock(path: string, engine: LoopEngineId): Promise<boolean>;
106
+ export declare function syncManagedBlock(path: string): Promise<boolean>;
106
107
  /**
107
- * Apply the plugin: seed the settings section from the managed block, host
108
- * the non-default engine factory when the block says so, and translate
109
- * committed engine changes into managed-block writes.
108
+ * Apply the plugin: own the profile's managed block, mount the routing factory,
109
+ * author the per-engine presets, and serve every hosted engine's provider
110
+ * route. The settings section carries the deployment's default engine.
110
111
  * @param ctx - the composing context.
111
- * @param config - composition entry for the managed patch file.
112
+ * @param config - composition entry for the managed patch file and engine knobs.
112
113
  */
113
114
  export declare function apply(ctx: Context, config: Config): void;
114
115
  //# sourceMappingURL=index.d.ts.map
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * Package-owned invariant companion for the loop engine selection.
3
3
  *
4
- * The plugin's owned relationship is the patch-manager round trip: rendering
5
- * a managed block for an engine and reading it back must produce the same
6
- * engine, and the `in-process` engine must render an absent block (so the base
7
- * bundle's `agent-loop` row stays mounted). The companion asserts both against
8
- * the pure transform, binding the writer's inverse to the reader directly.
4
+ * The plugin's owned relationship is the patch-manager round trip: applying the
5
+ * managed block must be a fixed point, the block must be the one that frees the
6
+ * single AgentFactory slot for the router, a legacy block naming a single engine
7
+ * must be migrated to the engine-agnostic form while still being readable as a
8
+ * legacy pin, and a comment-only layer must be re-seeded to a loadable
9
+ * top-level array rather than left as `null`. The companion asserts those
10
+ * against the pure transform, binding the writer's inverse to the reader
11
+ * directly.
9
12
  *
10
13
  * @module dsh-loop-engine/invariant
11
14
  */
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Keep a session's model selection on the seat the engine driving it owns.
3
+ *
4
+ * A session's model selection FOLLOWS ITS ENGINE, in both directions:
5
+ *
6
+ * - a HOSTED engine owns its model natively, and logs the one shared provider
7
+ * label (`external`, {@link HOSTED_ROUTE_LABEL}) into the session's
8
+ * `request/header`; the host derives the session's selection from the latest
9
+ * header (`packages/api/session-controller/src/agent.ts`, `selectionFor`), so
10
+ * a session a hosted engine drives selects `external`. That label is served
11
+ * only by this plugin's placeholder route (`provider-route.ts`), which
12
+ * advertises exactly one entry under it — {@link HOSTED_DEFAULT_MODEL}. So
13
+ * the seat of a hosted session is `external/default`, and this module is what
14
+ * puts that in the log BEFORE the engine's first request would. (A session
15
+ * whose log still carries an EARLIER build's per-engine label —
16
+ * `claude-code` / `codex` / `pi` / `kimi` — is recognized as hosted too,
17
+ * {@link isHostedProviderRoute}, and rewritten to `external/default` on its
18
+ * next build or switch: no adapter serves those labels any more, so leaving
19
+ * one in place would fail the host's own route check with
20
+ * `model-unavailable`.)
21
+ * - the HARNESS LOOP (`in-process`) DOES call a real model, so it must select
22
+ * something a real adapter serves, and the only selection this plugin can name
23
+ * honestly is the deployment's own default:
24
+ * `agentDefaultModel.currentSelection()`, the value the host's
25
+ * `agentOptions()` gives every session it creates without one
26
+ * (`packages/core/agent-default-model/src/index.ts`,
27
+ * `packages/api/session-controller/src/agent.ts`). That default can name one of
28
+ * these routes ITSELF — `session.selectModel` saves whatever the model menu
29
+ * submitted as the deployment default, and the menu carries the hosted route
30
+ * as an entry — so the deployment's COMPOSED default model (the settings
31
+ * descriptor's `base` layer, what `packages/bundle/base/cordis.patch.yml`
32
+ * configures) is what this plugin names instead, and says so once. See
33
+ * {@link defaultSelection}.
34
+ *
35
+ * The write is the harness's own `model/selection` event: log-only, already a
36
+ * known session event type (`packages/core/session/src/known-event-types.ts`),
37
+ * and exactly what the host's own model picker appends
38
+ * (`ApiSessionAgentController.selectForNextRequest`). That is what makes it the
39
+ * one way a plugin can say "this session's next build selects X" without owning
40
+ * the host's in-memory selection: the event folds into the `modelSelection`
41
+ * projection, and the selection the host installs for the session's next agent
42
+ * reads it back (`selectionFor` prefers the projection's `pending` over the
43
+ * logged header).
44
+ *
45
+ * TWO triggers ask for it, and both answer through the same write:
46
+ *
47
+ * - {@link ModelSelectionReset.resetFor} — a session SWITCHED to another
48
+ * engine. The host reads a session's selection from its log and the change is
49
+ * about to land on that session's next build, so the write has to happen at
50
+ * the switch rather than at the next request.
51
+ * - {@link ModelSelectionReset.guardFor} — a session being BUILT. The router
52
+ * wraps the caller's `setup` for it, which is the one moment the plugin holds
53
+ * the session before the host installs the selection for it
54
+ * (`composeAgent`'s `setup` → `installSelection`), so the write still lands
55
+ * before the first request — and it is what gives a NEW session on a hosted
56
+ * engine its seat. For the harness loop this trigger also settles a defect the
57
+ * build path owns: `session.selectModel` saves the pick it was given as the
58
+ * DEPLOYMENT default (`packages/api/session-controller/src/commands.ts`
59
+ * `selectModel` → `AgentDefaultModelConfig.saveSelection`), that default is
60
+ * the selection of every session with nothing in its log, and the menu now
61
+ * carries the hosted route as an entry — so ONE pick made while a hosted
62
+ * session was open would otherwise hand every session built on the harness
63
+ * loop a route no adapter serves.
64
+ *
65
+ * THREE judgements keep those writes honest, and both triggers share them
66
+ * ({@link ModelSelectionReset.appendSeat}):
67
+ *
68
+ * - a REAL model selection is never overwritten. The test is the provider
69
+ * ({@link isHostedProviderRoute}): only a label some hosted engine logs is a
70
+ * seat this module owns. An explicit pick — the model menu's own event, which
71
+ * also becomes the deployment default — survives every engine change, which is
72
+ * what the browser half's model notice tells the user: under a hosted engine a
73
+ * dsh model is inert, not forbidden.
74
+ * - a session that already selects the target (provider and model) is left
75
+ * alone, so repeated switches never grow the log.
76
+ * - an engine SWITCH has nothing to write for a session whose log names no
77
+ * selection at all: there is no seat to rewrite, and naming a session's first
78
+ * seat is the BUILD trigger's job ({@link ModelSelectionReset.guardFor}), not
79
+ * a switch's. Such a session still selects the deployment default — the host's
80
+ * own fallback — so nothing is broken while the seat shows it.
81
+ *
82
+ * A deployment that composes no `agentDefaultModel` service, or one whose
83
+ * service cannot name a usable selection, is not an error here: the trigger is
84
+ * either the user's own action or a session about to run, and neither must fail
85
+ * over a selection this plugin cannot name. The write is skipped with ONE
86
+ * warning, and the session keeps whatever its log records.
87
+ *
88
+ * @module dsh-loop-engine/model-selection-reset
89
+ */
90
+ import type { Context } from '@deepseek-ai/cordis';
91
+ import type { Session } from '@deepseek-ai/dsh-session';
92
+ import type { SessionModelSelection } from './driver-core/host-servers.ts';
93
+ import type { LoopEngineId } from './settings.ts';
94
+ declare module '@deepseek-ai/dsh-session/types' {
95
+ interface SessionEventMap {
96
+ /**
97
+ * Complete validated model selection requested for subsequent prompt
98
+ * assembly. Log-only: it never enters derived model history.
99
+ */
100
+ 'model/selection': {
101
+ /** Registered provider route. */
102
+ readonly provider: string;
103
+ /** Provider-owned model id. */
104
+ readonly model: string;
105
+ /** Adapter-owned reasoning effort, or provider/default behavior when absent. */
106
+ readonly reasoningEffort?: string;
107
+ };
108
+ }
109
+ }
110
+ /** The host's `agentDefaultModel` service (`packages/core/agent-default-model`), as this read needs it. */
111
+ export interface AgentDefaultModelService {
112
+ /**
113
+ * The selection the host gives an Agent created without one. Read live on
114
+ * every call, because the settings layer it reads may change at any time.
115
+ * @returns the deployment's default provider, model, and optional effort.
116
+ */
117
+ currentSelection(): SessionModelSelection;
118
+ }
119
+ /**
120
+ * Keeps a session's model selection on the seat the engine driving it owns — the
121
+ * shared `external/default` of a hosted engine, or a real model for the harness
122
+ * loop.
123
+ *
124
+ * One instance per router: the "cannot name a default" and "the default itself
125
+ * names a hosted route" warnings are each owed once per process, not once per
126
+ * session.
127
+ */
128
+ export declare class ModelSelectionReset {
129
+ private readonly ctx;
130
+ private readonly warn;
131
+ /** Whether the one warning this deployment is owed has been reported. */
132
+ private warned;
133
+ /** Whether the one report that the deployment default names a hosted route has been made. */
134
+ private reportedHostedDefault;
135
+ /**
136
+ * @param ctx - context carrying the host's default-model service, its settings
137
+ * (for the configured default), and the session-projection registry.
138
+ * @param warn - diagnostic sink, used at most once per problem.
139
+ */
140
+ constructor(ctx: Context, warn: (message: string) => void);
141
+ /**
142
+ * Give one session the selection the engine it is being SWITCHED TO owns,
143
+ * unless its log already names a real model of its own.
144
+ *
145
+ * The switch trigger. A session whose log names no selection at all is left
146
+ * alone: a switch rewrites a seat an engine already put there, and naming a
147
+ * session's first seat belongs to the build of a session being created
148
+ * ({@link guardFor}) rather than to a change of engine.
149
+ * @param session - the live Session the engine switch is being recorded for.
150
+ * @param engine - the engine the session is being moved to.
151
+ * @returns whether a `model/selection` event was appended.
152
+ */
153
+ resetFor(session: Session, engine: LoopEngineId): boolean;
154
+ /**
155
+ * Give one session the selection the engine BUILDING it owns, inside the
156
+ * router's wrapped `setup` — before the caller's own setup installs the
157
+ * session's selection — so a selection written here is the one the host reads
158
+ * back for the agent being built.
159
+ *
160
+ * The harness loop's own build keeps its narrower precondition: it makes real
161
+ * model calls, so it is the one build that must not be left selecting a
162
+ * placeholder route, and a session whose own log already names a real model
163
+ * needs nothing written for it. A hosted engine is being given its seat here,
164
+ * which is what a new session on it starts from.
165
+ * @param session - the session whose agent is being composed.
166
+ * @param engine - the engine composing it.
167
+ * @returns whether a `model/selection` event was appended.
168
+ */
169
+ guardFor(session: Session, engine: LoopEngineId): boolean;
170
+ /**
171
+ * Whether the selection this session's next request would use names one of
172
+ * the hosted engines' provider routes — a label this plugin serves as a
173
+ * placeholder and cannot answer a real model call on.
174
+ *
175
+ * The read is the host's own (`selectionFor`): a pending selection, else the
176
+ * logged header, else the deployment's default. A session with nothing in its
177
+ * log is therefore judged on the deployment default — the value the model menu
178
+ * can write a hosted label into — and that default is read AS IT IS, never as
179
+ * {@link defaultSelection} would substitute it: the substitution only exists
180
+ * once it is written into the session, so judging the substitute would skip
181
+ * the write that makes it real.
182
+ * @param session - the session whose selection is being judged.
183
+ * @returns whether that selection names a hosted engine route.
184
+ */
185
+ private selectsHostedRoute;
186
+ /**
187
+ * Append the selection one engine's seat carries, by the judgements both
188
+ * triggers share.
189
+ *
190
+ * A real model selection is never overwritten — the provider is the test,
191
+ * because only a label some hosted engine logs is a seat this module owns — and
192
+ * a session that already selects the target route is left alone. Provider and
193
+ * model are the comparison: a reasoning effort the target carries is not a
194
+ * reason to log a second event for a session that already selects the same
195
+ * route, and re-appending would only grow the log on every switch.
196
+ * @param session - the session whose selection is being written.
197
+ * @param engine - the engine whose seat the session should select.
198
+ * @returns whether a `model/selection` event was appended.
199
+ */
200
+ private appendSeat;
201
+ /**
202
+ * The selection one engine's seat carries: a hosted engine's shared
203
+ * `external/default`, or — for the harness loop — the deployment's default,
204
+ * with the substitution {@link defaultSelection} documents.
205
+ * @param engine - the engine a session runs, or is being moved to.
206
+ * @returns the selection to write, or undefined when this process cannot name
207
+ * one for that engine.
208
+ */
209
+ private seatOf;
210
+ /**
211
+ * The selection the host will use for this session, by the host's own read —
212
+ * the shared read in `driver-core/session-model.ts` (`currentSelection`),
213
+ * which the four hosted drivers also use to resolve the model they hand over.
214
+ * @param session - the session whose selection is being read.
215
+ * @returns the current selection, or undefined when the log records none.
216
+ */
217
+ private currentSelection;
218
+ /**
219
+ * The selection the deployment's own `agentDefaultModel` service answers, or
220
+ * undefined when this process cannot read one (reported once).
221
+ *
222
+ * Read twice for two different questions — as the selection a session with
223
+ * nothing in its log would use ({@link selectsHostedRoute}), and as the value
224
+ * this plugin restores ({@link defaultSelection}) — so the substitution below
225
+ * never hides the fact that the saved default itself is unusable.
226
+ * @returns the validated default, or undefined when it cannot be named.
227
+ */
228
+ private deploymentDefault;
229
+ /**
230
+ * The selection this plugin restores: the deployment's default, or — when that
231
+ * default names a hosted engine ROUTE — the model the deployment's own
232
+ * composition declares instead.
233
+ *
234
+ * The route case is not hypothetical: `session.selectModel` saves whatever the
235
+ * model menu submitted as the deployment default, and the menu carries the
236
+ * hosted route as an entry, so one pick made while a hosted session was open
237
+ * puts a label there that no adapter serves a real model on — handing it to a
238
+ * session the harness loop drives would fail that session's first request
239
+ * loud. The composed default is a real model the deployment itself asked for,
240
+ * so nothing is guessed on its behalf.
241
+ * @returns the restored selection, or undefined to skip the write.
242
+ */
243
+ private defaultSelection;
244
+ /**
245
+ * The model the DEPLOYMENT's own composition declares as its default, read
246
+ * from the settings descriptor's `base` layer — the value a saved (user-layer)
247
+ * default overrode.
248
+ *
249
+ * This is what replaces a default that names a hosted route, and it is read
250
+ * live because the composition is what the deployment itself configured:
251
+ * `packages/bundle/base/cordis.patch.yml` gives the `agent-default-model` row
252
+ * a real provider and model, and only the user layer can hold a pick made from
253
+ * the model menu.
254
+ * @returns the configured default, or undefined when this deployment composes
255
+ * no default-model section, cannot enumerate settings, configured no usable
256
+ * provider/model, or configured a hosted route there too.
257
+ */
258
+ private configuredDefault;
259
+ /** Read the host session-projection registry, structurally. */
260
+ private projections;
261
+ /**
262
+ * Report the one thing this read could not answer, once per plugin lifetime.
263
+ *
264
+ * Repeated triggers must not repeat it: a deployment without the service would
265
+ * otherwise log a line per session built for as long as it runs.
266
+ * @param problem - what this process cannot read, with the reason.
267
+ * @returns undefined, so callers can `return this.unavailable(…)` to skip.
268
+ */
269
+ private unavailable;
270
+ }
271
+ //# sourceMappingURL=model-selection-reset.d.ts.map
@@ -2,65 +2,83 @@
2
2
  * Managed-block editing for a profile's `cordis.patch.yml`.
3
3
  *
4
4
  * The plugin owns one contiguous block inside the user's patch file, delimited
5
- * by a begin/end marker pair, and rewrites only that span on engine switches
6
- * — everything else the user wrote (other patches, their comments) survives
7
- * byte for byte. The block's content is the loader patch that takes the loop
8
- * engine over: it disables the base bundle's `agent-loop` row so this plugin's
9
- * factory (hosted by dsh-loop-engine) can register without colliding, because
10
- * the harness admits exactly one AgentFactory:
5
+ * by a begin/end marker pair, and rewrites only that span — everything else the
6
+ * user wrote (other patches, their comments) survives byte for byte. The block
7
+ * is what hands the process-wide AgentFactory slot to this plugin's router: it
8
+ * disables the base bundle's `agent-loop` row, because the harness admits
9
+ * exactly one AgentFactory and the router (which extends the harness loop, and
10
+ * therefore serves `in-process` sessions too) registers itself as that one.
11
11
  *
12
- * # -- dsh-loop-engine managed block: claude-code --
12
+ * # -- dsh-loop-engine managed block --
13
13
  * - id: agent-loop
14
14
  * disabled: true
15
15
  * - id: command-goal
16
16
  * disabled: true
17
17
  * # -- /dsh-loop-engine managed block --
18
18
  *
19
- * The `command-goal` row goes down with the loop: a hosted engine owns the
20
- * session's command surface, and dsh's `/goal` would otherwise collide with
21
- * an engine's own goal command (Kimi) or dangle over a goal service nothing
22
- * drives (the other engines). The remaining dsh-native commands (`/export`,
23
- * `/feedback`, `/permission`) are engine-agnostic session/settings controls
24
- * that keep working under a hosted engine, so they stay.
19
+ * The block names no engine. Which engine a session runs is a PER-SESSION
20
+ * decision carried by its agent preset, so it cannot live in a process-wide
21
+ * configuration file; the block's only job is to free the slot for the router.
22
+ * The `command-goal` row goes down with the loop: it is the host-plane copy of
23
+ * dsh's goal command, and a goal service nothing drives has no business being
24
+ * mounted for a profile whose loop is decided per session. The remaining
25
+ * dsh-native commands (`/export`, `/feedback`, `/permission`) are
26
+ * engine-agnostic session and settings controls that keep working under every
27
+ * engine. Per-session command surfaces are the preset's business — a hosted
28
+ * session joins an engine preset that strips `command-goal` itself, which is the
29
+ * only place that reaches the preset-layer registration.
25
30
  *
26
- * `in-process` renders an absent block (the base bundle's `agent-loop` row
27
- * stays active and supplies the factory), so switching back removes the span
28
- * entirely. Any other engine renders the same disable block, and the begin
29
- * marker carries the specific engine id (`# -- dsh-loop-engine managed block:
30
- * claude-code --`) so `currentEngineOf` can read which non-default engine owns
31
- * the slot from the file alone. All functions here are pure string transforms —
32
- * file I/O and durability live in the plugin's apply.
31
+ * Blocks written before the plugin routed per session carried the single
32
+ * engine's id in the begin marker (`# -- dsh-loop-engine managed block:
33
+ * claude-code --`). {@link legacyBlockEngineOf} reads that form so an upgrade
34
+ * can carry the pinned engine into the settings seed before the block is
35
+ * rewritten to the engine-agnostic form; a legacy block is otherwise treated as
36
+ * a present block by {@link hasManagedBlock}.
37
+ *
38
+ * All functions here are pure string transforms — file I/O and durability live
39
+ * in the plugin's apply.
33
40
  *
34
41
  * @module dsh-loop-engine/patch-manager
35
42
  */
36
43
  import type { LoopEngineId } from './settings.ts';
37
44
  /** Begin marker of the plugin-managed span inside a profile patch file. */
38
- export declare const MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block: ";
45
+ export declare const MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block --";
46
+ /**
47
+ * Begin-marker prefix of the pre-routing block form, which named the one engine
48
+ * the profile was pinned to.
49
+ */
50
+ export declare const LEGACY_MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block: ";
39
51
  /** End marker of the plugin-managed span inside a profile patch file. */
40
52
  export declare const MANAGED_BLOCK_END = "# -- /dsh-loop-engine managed block --";
41
- /** Render the managed block for one engine; `in-process` returns the empty span. */
42
- export declare function renderManagedBlock(engine: LoopEngineId): string;
43
- /** Whether a patch-file text contains the managed block span. */
53
+ /** The loader patch that frees the single AgentFactory slot for the router. */
54
+ export declare function renderManagedBlock(): string;
55
+ /**
56
+ * Whether a patch-file text contains the LEGACY managed block span, which
57
+ * named the single engine the profile used to be pinned to.
58
+ */
59
+ export declare function hasLegacyManagedBlock(text: string): boolean;
60
+ /**
61
+ * Whether a patch-file text contains the managed block span, in either the
62
+ * current or the legacy form.
63
+ */
44
64
  export declare function hasManagedBlock(text: string): boolean;
45
65
  /**
46
- * The engine id the managed block's begin marker names, when this build knows
47
- * it. `undefined` covers both "no managed block" and "the block names an engine
48
- * this build does not recognize" — the second being the state where the block
49
- * still disables the base `agent-loop` row while no factory can take the slot.
66
+ * The engine a LEGACY managed block pinned the profile to, when the block has
67
+ * that form and names an engine this build knows. `undefined` covers the
68
+ * current engine-agnostic block, no block at all, and a legacy block naming an
69
+ * engine this build does not recognize.
50
70
  */
51
- export declare function managedBlockEngineOf(text: string): LoopEngineId | undefined;
52
- /** Derive the current engine from a patch-file text by the managed block's begin marker. */
53
- export declare function currentEngineOf(text: string): LoopEngineId;
71
+ export declare function legacyBlockEngineOf(text: string): LoopEngineId | undefined;
54
72
  /**
55
- * Produce the next patch-file text for a target engine, preserving every byte
56
- * outside the managed span. Appends the span when absent; replaces or removes
57
- * it when present. The managed block is a root-level collection, so a leftover
58
- * seed `[]` is dropped when adding it, and a removal that leaves no entries is
59
- * re-seeded back to `[]` — either way the file stays a single valid top-level
60
- * array the harness can boot.
73
+ * Produce the next patch-file text carrying the managed block, preserving every
74
+ * byte outside the managed span. Appends the span when absent and replaces it
75
+ * when present — including a legacy span, which is rewritten to the current
76
+ * form. The managed block is a root-level collection, so a leftover seed `[]`
77
+ * is dropped with it, leaving the file a single valid top-level array the
78
+ * harness can boot: the block's own `- id: agent-loop` is a column-0 entry, so
79
+ * a file carrying it never needs a re-seed.
61
80
  * @param text - current patch-file text.
62
- * @param engine - target engine.
63
81
  * @returns the rewritten patch-file text.
64
82
  */
65
- export declare function applyManagedBlock(text: string, engine: LoopEngineId): string;
83
+ export declare function applyManagedBlock(text: string): string;
66
84
  //# sourceMappingURL=patch-manager.d.ts.map