dsh-loop-engine-allinone 0.0.0-stage → 0.4.1

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 (143) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/CONTRIBUTING.md +78 -0
  3. package/LICENSE +23 -0
  4. package/NOTICE +45 -0
  5. package/README.md +223 -2
  6. package/README.zh.md +179 -0
  7. package/SECURITY.md +39 -0
  8. package/cordis.patch.yml +3 -0
  9. package/lib/client.js +2166 -0
  10. package/lib/index.js +11621 -0
  11. package/lib/invariant.js +453 -0
  12. package/lib/types/agent-preset-ids.d.ts +276 -0
  13. package/lib/types/client/LoopEngineBadge.d.ts +71 -0
  14. package/lib/types/client/LoopEngineComposerSelect.d.ts +126 -0
  15. package/lib/types/client/LoopEngineSection.d.ts +35 -0
  16. package/lib/types/client/engine-marks.d.ts +87 -0
  17. package/lib/types/client/index.d.ts +39 -0
  18. package/lib/types/client/locales.d.ts +182 -0
  19. package/lib/types/client/reload.d.ts +135 -0
  20. package/lib/types/client/session-engine.d.ts +508 -0
  21. package/lib/types/client/store.d.ts +103 -0
  22. package/lib/types/client/turn-status.d.ts +234 -0
  23. package/lib/types/client/use-session-engine.d.ts +74 -0
  24. package/lib/types/commands.d.ts +77 -0
  25. package/lib/types/compat.d.ts +25 -0
  26. package/lib/types/driver-acp/agent.d.ts +289 -0
  27. package/lib/types/driver-acp/approval.d.ts +27 -0
  28. package/lib/types/driver-acp/capabilities.d.ts +200 -0
  29. package/lib/types/driver-acp/client.d.ts +297 -0
  30. package/lib/types/driver-acp/executable.d.ts +53 -0
  31. package/lib/types/driver-acp/mapping.d.ts +76 -0
  32. package/lib/types/driver-acp/profile.d.ts +126 -0
  33. package/lib/types/driver-acp/runtime.d.ts +45 -0
  34. package/lib/types/driver-acp/subprocess.d.ts +19 -0
  35. package/lib/types/driver-acp/terminal.d.ts +98 -0
  36. package/lib/types/driver-acp/transport.d.ts +43 -0
  37. package/lib/types/driver-acp/types.d.ts +152 -0
  38. package/lib/types/driver-cli/capability.d.ts +42 -0
  39. package/lib/types/driver-cli/cli-agent.d.ts +258 -0
  40. package/lib/types/driver-cli/cli-loop.d.ts +46 -0
  41. package/lib/types/driver-cli/flatten.d.ts +35 -0
  42. package/lib/types/driver-cli/frames.d.ts +128 -0
  43. package/lib/types/driver-cli/ndjson.d.ts +106 -0
  44. package/lib/types/driver-cli/session-sink.d.ts +333 -0
  45. package/lib/types/driver-cli/stream-json.d.ts +61 -0
  46. package/lib/types/driver-cli/text.d.ts +66 -0
  47. package/lib/types/driver-cli/transport.d.ts +84 -0
  48. package/lib/types/driver-core/agents-md-skill-provider.d.ts +72 -0
  49. package/lib/types/driver-core/assistant-stream.d.ts +68 -0
  50. package/lib/types/driver-core/context-files.d.ts +62 -0
  51. package/lib/types/driver-core/host-servers.d.ts +119 -0
  52. package/lib/types/driver-core/hosted-engine-runtime.d.ts +190 -0
  53. package/lib/types/driver-core/hosted-tool-vocabulary.d.ts +78 -0
  54. package/lib/types/driver-core/idle-child.d.ts +57 -0
  55. package/lib/types/driver-core/image-access.d.ts +25 -0
  56. package/lib/types/driver-core/inbox.d.ts +104 -0
  57. package/lib/types/driver-core/model-handover.d.ts +134 -0
  58. package/lib/types/driver-core/ownership.d.ts +41 -0
  59. package/lib/types/driver-core/permission-knobs.d.ts +26 -0
  60. package/lib/types/driver-core/prompt.d.ts +64 -0
  61. package/lib/types/driver-core/session-lifetime.d.ts +62 -0
  62. package/lib/types/driver-core/session-model.d.ts +128 -0
  63. package/lib/types/driver-core/skill-inject.d.ts +59 -0
  64. package/lib/types/driver-core/step-close.d.ts +49 -0
  65. package/lib/types/driver-core/step-images.d.ts +62 -0
  66. package/lib/types/driver-core/step-tools.d.ts +43 -0
  67. package/lib/types/driver-core/system-head.d.ts +43 -0
  68. package/lib/types/engine-agy/agent.d.ts +67 -0
  69. package/lib/types/engine-agy/frames.d.ts +90 -0
  70. package/lib/types/engine-agy/loop.d.ts +64 -0
  71. package/lib/types/engine-agy/process.d.ts +173 -0
  72. package/lib/types/engine-claude/agent.d.ts +186 -0
  73. package/lib/types/engine-claude/loop.d.ts +73 -0
  74. package/lib/types/engine-claude/mapping.d.ts +108 -0
  75. package/lib/types/engine-claude/permission.d.ts +41 -0
  76. package/lib/types/engine-claude/process.d.ts +59 -0
  77. package/lib/types/engine-claude/sdk.d.ts +67 -0
  78. package/lib/types/engine-claude/types.d.ts +28 -0
  79. package/lib/types/engine-codex/agent.d.ts +216 -0
  80. package/lib/types/engine-codex/appserver/client.d.ts +80 -0
  81. package/lib/types/engine-codex/appserver/mapping.d.ts +67 -0
  82. package/lib/types/engine-codex/appserver/thread.d.ts +66 -0
  83. package/lib/types/engine-codex/appserver/types.d.ts +225 -0
  84. package/lib/types/engine-codex/loop.d.ts +75 -0
  85. package/lib/types/engine-codex/model-handover.d.ts +56 -0
  86. package/lib/types/engine-codex/permission.d.ts +126 -0
  87. package/lib/types/engine-codex/skills.d.ts +28 -0
  88. package/lib/types/engine-codex/types.d.ts +36 -0
  89. package/lib/types/engine-drivers.d.ts +132 -0
  90. package/lib/types/engine-grok/agent.d.ts +24 -0
  91. package/lib/types/engine-grok/loop.d.ts +61 -0
  92. package/lib/types/engine-grok/process.d.ts +46 -0
  93. package/lib/types/engine-grok/profile.d.ts +82 -0
  94. package/lib/types/engine-hermes/agent.d.ts +47 -0
  95. package/lib/types/engine-hermes/loop.d.ts +56 -0
  96. package/lib/types/engine-hermes/process.d.ts +92 -0
  97. package/lib/types/engine-kimi/acp/client.d.ts +123 -0
  98. package/lib/types/engine-kimi/acp/mapping.d.ts +73 -0
  99. package/lib/types/engine-kimi/acp/types.d.ts +143 -0
  100. package/lib/types/engine-kimi/agent.d.ts +24 -0
  101. package/lib/types/engine-kimi/commands.d.ts +44 -0
  102. package/lib/types/engine-kimi/loop.d.ts +62 -0
  103. package/lib/types/engine-kimi/model-handover.d.ts +32 -0
  104. package/lib/types/engine-kimi/permission.d.ts +28 -0
  105. package/lib/types/engine-kimi/process.d.ts +47 -0
  106. package/lib/types/engine-kimi/profile.d.ts +19 -0
  107. package/lib/types/engine-kimi/skills.d.ts +52 -0
  108. package/lib/types/engine-kimi/types.d.ts +25 -0
  109. package/lib/types/engine-minimaxcode/agent.d.ts +24 -0
  110. package/lib/types/engine-minimaxcode/loop.d.ts +83 -0
  111. package/lib/types/engine-minimaxcode/process.d.ts +53 -0
  112. package/lib/types/engine-minimaxcode/profile.d.ts +72 -0
  113. package/lib/types/engine-of-session.d.ts +97 -0
  114. package/lib/types/engine-pi/agent.d.ts +230 -0
  115. package/lib/types/engine-pi/loop.d.ts +72 -0
  116. package/lib/types/engine-pi/model-handover.d.ts +84 -0
  117. package/lib/types/engine-pi/permission.d.ts +47 -0
  118. package/lib/types/engine-pi/rpc/client.d.ts +107 -0
  119. package/lib/types/engine-pi/rpc/mapping.d.ts +37 -0
  120. package/lib/types/engine-pi/rpc/types.d.ts +249 -0
  121. package/lib/types/engine-pi/skills.d.ts +47 -0
  122. package/lib/types/engine-pi/types.d.ts +36 -0
  123. package/lib/types/engine-qoderclicn/agent.d.ts +59 -0
  124. package/lib/types/engine-qoderclicn/loop.d.ts +51 -0
  125. package/lib/types/engine-qoderclicn/process.d.ts +113 -0
  126. package/lib/types/engine-registry.d.ts +584 -0
  127. package/lib/types/engine-remote.d.ts +192 -0
  128. package/lib/types/engine-surface.d.ts +36 -0
  129. package/lib/types/engine-zcode/agent.d.ts +50 -0
  130. package/lib/types/engine-zcode/loop.d.ts +47 -0
  131. package/lib/types/engine-zcode/process.d.ts +111 -0
  132. package/lib/types/index.d.ts +205 -0
  133. package/lib/types/invariant.d.ts +26 -0
  134. package/lib/types/model-selection-reset.d.ts +271 -0
  135. package/lib/types/namespace.d.ts +25 -0
  136. package/lib/types/patch-manager.d.ts +104 -0
  137. package/lib/types/preset.d.ts +254 -0
  138. package/lib/types/provider-route.d.ts +245 -0
  139. package/lib/types/router-loop.d.ts +458 -0
  140. package/lib/types/session-engine-store.d.ts +138 -0
  141. package/lib/types/settings.d.ts +60 -0
  142. package/lib/types/skills.d.ts +85 -0
  143. package/package.json +152 -6
@@ -0,0 +1,458 @@
1
+ /**
2
+ * The process-wide agent factory, dispatching each session to the engine it
3
+ * runs.
4
+ *
5
+ * The harness admits exactly ONE `AgentFactory` per process
6
+ * (`AgentRegistry.setFactory` throws on a second registration) and exposes no
7
+ * per-session way to resolve one, so "session A on Codex while session B runs
8
+ * Kimi" is only expressible if the single factory itself routes. This class is
9
+ * that router: it extends the harness's own `AgentLoop`, inheriting the
10
+ * in-process loop in full (turn boundary projection, prompt variables, the
11
+ * agent-loop settings section, the factory registration), and overrides the two
12
+ * AgentFactory entry points to hand a session to its engine's runtime.
13
+ *
14
+ * Which engine that is comes from {@link engineOfSession} — the plugin's own
15
+ * per-session record first, the recorded agent preset otherwise — with one
16
+ * create-time exception spelled out on {@link RouterLoop.engineFor}:
17
+ * - create: the record, else `CreateAgentOptions.meta.agentPreset` (already
18
+ * resolved by the caller, `packages/api/session-controller/src/agent.ts`
19
+ * `composeAgent`), with a live parent's engine as the fallback for a child;
20
+ * - resume: the record, else the persisted `agentPreset` projection.
21
+ * `ResumeAgentOptions` carries no metadata, so that projection is read
22
+ * through {@link engineOfSession} — the same fold the host itself reads
23
+ * before choosing the composition to mount, and the same one the plugin's
24
+ * own Remote reports to the browser half.
25
+ *
26
+ * The session can also be moved while it is LIVE — that is what
27
+ * {@link RouterLoop.selectEngine} is for, and it is the half of the story the
28
+ * preset channel cannot express (the harness refuses a preset change on a
29
+ * started session, `agent-preset/locked`). Between two hosted engines the move
30
+ * is an IN-PLACE SWAP: the successor is built onto the session's own `Session`
31
+ * object and the outgoing machine is retired around it, so the session — and
32
+ * the page attached to it — never witnesses a lifecycle edge
33
+ * ({@link RouterLoop.hotSwap}). A move that involves the harness loop on either
34
+ * side cannot be done in place at all, so it is made to land the other way:
35
+ * the session's agent is RELEASED, which leaves the session cold with its
36
+ * record already naming the new engine, and the outcome asks the browser half
37
+ * to reload the page ({@link RouterLoop.move}) — the reload is what reopens the
38
+ * session, and the host's next resolve then builds it on the record's engine.
39
+ *
40
+ * `in-process` sessions are served by `super`; every hosted engine gets one
41
+ * {@link HostedEngineRuntime} built on first use, which is also what makes the
42
+ * engines concurrent: their agents, subprocesses, and scopes are all
43
+ * per-session, and each runtime owns only its own live agents.
44
+ *
45
+ * The router also answers what a session is running
46
+ * ({@link RouterLoop.reportEngine}) from the same bookkeeping that routes it,
47
+ * because it is the only thing that knows which engine built a live agent — see
48
+ * {@link SessionEngineReport} for why "what runs now" and "what the record says"
49
+ * are two facts that must travel separately.
50
+ *
51
+ * @module dsh-loop-engine-allinone/router-loop
52
+ */
53
+ import type { Context } from '@deepseek-ai/cordis';
54
+ import AgentLoop from '@deepseek-ai/dsh-agent-loop';
55
+ import type { AgentHandle, CreateAgentOptions, ResumeAgentOptions } from '@deepseek-ai/dsh-agent';
56
+ import type { SessionId } from '@deepseek-ai/dsh-session';
57
+ import type { LoopEngineSelectResult, SessionEngineReport } from './agent-preset-ids.ts';
58
+ import type { HostedAgent, HostedEngineRuntime } from './driver-core/hosted-engine-runtime.ts';
59
+ import type { EngineRecordStore } from './session-engine-store.ts';
60
+ import type { HostedEngineId, LoopEngineId } from './settings.ts';
61
+ /**
62
+ * The preset roster announces a committed per-session preset change on the
63
+ * shared event bus so consumers can invalidate state derived from that
64
+ * session's composition. Declared here rather than imported from
65
+ * `@deepseek-ai/dsh-agent-presets`: the plugin consumes the notification but
66
+ * takes no build-time dependency on the roster, which a minimal profile may not
67
+ * compose at all.
68
+ */
69
+ declare module '@deepseek-ai/cordis' {
70
+ interface Events {
71
+ /**
72
+ * One session committed a different agent preset to its durable log.
73
+ * @mode emit
74
+ * @param sessionId - the session whose composition changed.
75
+ * @param agentPreset - the preset recorded by the committed selection.
76
+ */
77
+ 'agent-preset/selected'(sessionId: SessionId, agentPreset: string): void;
78
+ }
79
+ }
80
+ /**
81
+ * Services the router's context must inject.
82
+ *
83
+ * Taken from the harness loop verbatim rather than re-listed: the router IS the
84
+ * harness loop for `in-process` sessions, and cordis refuses a service
85
+ * PROPERTY READ on a context whose fiber did not inject it
86
+ * (`cannot get property "tools" without inject`). The loop's turn machinery
87
+ * reads `ctx.tools` and `ctx.llm` directly, so a shorter gate here compiles and
88
+ * even creates agents, then dies on the first real turn.
89
+ */
90
+ export declare const ROUTER_SERVICES: readonly string[];
91
+ /**
92
+ * Any hosted engine's runtime, as this router holds it. The engines differ only
93
+ * in configuration and driver; the transaction surface the router calls is
94
+ * identical, so the map is type-erased once here.
95
+ */
96
+ export type RouterEngine = HostedEngineRuntime<object, HostedAgent>;
97
+ /** Builds (or returns the memoized) driver runtime of one hosted engine. */
98
+ export type EngineBuilder = (engine: HostedEngineId) => RouterEngine;
99
+ /**
100
+ * The single AgentFactory, routing per session.
101
+ *
102
+ * Constructed by the plugin once the services the harness loop needs are
103
+ * active; its effects belong to the constructing fiber, so unloading the plugin
104
+ * tears every engine down.
105
+ */
106
+ export declare class RouterLoop extends AgentLoop {
107
+ private readonly engines;
108
+ private readonly live;
109
+ private readonly build;
110
+ private readonly records;
111
+ private readonly warn;
112
+ /**
113
+ * Diagnostics already reported: a condition that repeats on every build (a
114
+ * session whose engine cannot be read, a projection that will not fold) is
115
+ * worth naming once, not once per rebuild.
116
+ */
117
+ private readonly warned;
118
+ /**
119
+ * The model-selection half of routing: the seat a session's engine owns, moved
120
+ * at a switch ({@link RouterLoop.moveModelSelection}) and written at a build
121
+ * ({@link RouterLoop.engineOptions}). Owned here because each warning it owes
122
+ * is owed once per process, not once per session.
123
+ */
124
+ private readonly selectionReset;
125
+ /**
126
+ * @param ctx - the context the router's effects belong to.
127
+ * @param build - memoized builder for one hosted engine's runtime.
128
+ * @param records - the plugin's own per-session engine record, which outranks
129
+ * every preset-derived answer and is what {@link selectEngine} writes.
130
+ * @param warn - diagnostic sink for a skipped engine command.
131
+ */
132
+ constructor(ctx: Context, build: EngineBuilder, records: EngineRecordStore, warn: (message: string) => void);
133
+ /** The engine that drives one session's agent. */
134
+ private engineFor;
135
+ /**
136
+ * The engine to build one session on for a CREATE, with the plugin's own
137
+ * record read ahead of the caller's preset.
138
+ *
139
+ * Reading an engine is a read of the HOST's session state, and a session this
140
+ * plugin does not own is the host's business: a failure here (a session-query
141
+ * lease that refuses, a projection that cannot be computed, a corrupt log)
142
+ * must never become the reason that session cannot be created. Every failure
143
+ * degrades to the harness loop, which is what would have served the session if
144
+ * this plugin were not installed at all.
145
+ * @param sessionId - the session being created.
146
+ * @param presetId - the preset the caller composed with, when it named one.
147
+ * @param parent - the live parent agent, for a child session's inheritance.
148
+ * @returns the engine, or the harness loop when the answer could not be read.
149
+ */
150
+ private engineForCreate;
151
+ /**
152
+ * The engine to build a PERSISTED session on, degrading exactly as
153
+ * {@link engineForCreate} does.
154
+ *
155
+ * The read is the same plugin record first, then the durable `agentPreset`
156
+ * projection — and a resumed session is the case where those reads touch the
157
+ * host the most (a cold open, a repair, a projection fold), so it is the case
158
+ * where a read failure is most likely and least acceptable to surface as
159
+ * "this session will not open".
160
+ * @param sessionId - the persisted session being resumed.
161
+ * @param parent - the live parent agent, for a child session's inheritance.
162
+ * @returns the engine, or the harness loop when the answer could not be read.
163
+ */
164
+ private engineForResume;
165
+ /**
166
+ * Report one unreadable engine answer and hand the session to the harness
167
+ * loop. Owed once per session, not once per build: a session that fails to
168
+ * read keeps failing on every rebuild, and the point of the warning is to name
169
+ * the session, not to count the retries.
170
+ * @param sessionId - the session the plugin could not answer for.
171
+ * @param error - the failure the read produced.
172
+ * @returns the engine the session is degraded to.
173
+ */
174
+ private degradeToHarnessLoop;
175
+ /**
176
+ * Report one diagnostic once per process, for a condition that repeats on
177
+ * every build of the session it names.
178
+ * @param message - the diagnostic to report at most once.
179
+ */
180
+ private warnOnce;
181
+ /** The engine's runtime, built on first use and kept for the plugin's lifetime. */
182
+ private runtimeOf;
183
+ /**
184
+ * Record one published agent so a later engine change can rebuild it, and
185
+ * bridge the engine's command/skill surface into its session.
186
+ * @param engine - the engine that built the handle.
187
+ * @param handle - the published handle.
188
+ * @param recipe - how it was built, for a hot swap's successor.
189
+ * @param handover - a hosted handle's session handover; absent for the
190
+ * harness loop, whose handle carries neither the session's lifetime nor a
191
+ * way to retire the machine without releasing it.
192
+ * @returns the handle the harness tracks.
193
+ */
194
+ private adopt;
195
+ /** Drop one tracked session, if the record still points at that exact agent. */
196
+ private forget;
197
+ /**
198
+ * Create a session's agent on the engine it runs.
199
+ *
200
+ * The plugin's own record is consulted FIRST, ahead of the preset the caller
201
+ * composed with: a session that already has a record must be built on the
202
+ * engine the plugin reports for it, or the router and the Remote would answer
203
+ * differently about one session. Only a session with no record — every session
204
+ * this plugin has never switched, including every brand-new one — falls
205
+ * through to the preset.
206
+ * @param ownerCtx - caller context that owns the lifecycle.
207
+ * @param options - identities, metadata (carrying the preset), and setup.
208
+ * @returns the published handle.
209
+ */
210
+ createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>;
211
+ /**
212
+ * Resume a persisted session on the engine it runs.
213
+ *
214
+ * The read is {@link engineOfSession}: the plugin's own record first, then the
215
+ * durable `agentPreset` projection, which folds the session header together
216
+ * with every committed `agent-preset/selected`. The session header alone would
217
+ * name the preset the session was CREATED with, which is exactly the wrong
218
+ * answer for a session that switched engine while it was blank — and the
219
+ * record is the only answer for one that switched after it started, which no
220
+ * preset can express.
221
+ *
222
+ * A resume is not a create: the session's seat is one a previous build already
223
+ * put in its log (`model-selection-reset.ts`, the switch and build triggers), so
224
+ * a hosted engine's resume leaves the selection exactly as it finds it. Only
225
+ * the harness loop's own resume is wrapped, with the narrower trigger that
226
+ * applies to it — it makes real model calls, so it must not be left selecting a
227
+ * placeholder route.
228
+ * @param ownerCtx - caller context that owns load, setup, and the lifecycle.
229
+ * @param options - persisted identity, loop options, and setup.
230
+ * @returns the published handle.
231
+ */
232
+ resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>;
233
+ /**
234
+ * Wrap the caller's setup so the session's ENGINE gets its model seat written
235
+ * before the host installs the session's selection, and hand a hosted engine
236
+ * the `agentOptions` that seat names.
237
+ *
238
+ * The wrap is what makes the write land at all: the host installs the session's
239
+ * model selection from INSIDE the caller's own setup
240
+ * (`ApiSessionAgentController.selectionFor`), and that read takes the log's
241
+ * pending `model/selection` — so the write has to happen before it, and the
242
+ * caller's setup has to run unchanged afterwards. Both of the router's engines
243
+ * are wrapped, each with its own trigger: the harness loop's build keeps the
244
+ * narrow one (only a session that would otherwise select a placeholder route),
245
+ * while a hosted engine's build is where a NEW session gets its seat. The
246
+ * resume path calls this with `in-process` alone (see {@link resume}).
247
+ *
248
+ * The `agentOptions` half keeps the two answers consistent: a hosted engine is
249
+ * handed the very selection written into the session (`external/default`), not
250
+ * the deployment default that seat replaced. In-process keeps the caller's own
251
+ * options, which ARE its route.
252
+ *
253
+ * The caller's setup is copied, never mutated: the same options object is the
254
+ * router's rebuild recipe, which replays the caller's own closure.
255
+ * @param options - the create or resume options the caller supplied.
256
+ * @param engine - the engine building this session.
257
+ * @returns a copy whose setup writes the seat first, then the caller's own.
258
+ */
259
+ private engineOptions;
260
+ /**
261
+ * Report what one session ACTUALLY runs, and — when they differ — the engine
262
+ * its record names.
263
+ *
264
+ * This is the read the plugin's own Remote publishes, and the reason it is the
265
+ * router's to make is the live bookkeeping below: for a session with an agent
266
+ * this router built, {@link RouterLoop.live} says which engine built it, and
267
+ * that beats every record — it is what is running. A session this router does
268
+ * not drive (cold, or still on the base loop's slot during the mount window)
269
+ * has no live fact to report, so the record answers, exactly as it does for a
270
+ * session about to be built; and since every switch this plugin performs
271
+ * either swaps the agent in place or releases it, the two facts differ only
272
+ * for a release that did not take.
273
+ * @param sessionId - the session to report on.
274
+ * @returns the engine driving it now, plus the engine its record names for its
275
+ * next build when the two differ.
276
+ */
277
+ reportEngine(sessionId: SessionId): Promise<SessionEngineReport>;
278
+ /**
279
+ * Move one live session to another engine.
280
+ *
281
+ * The one entry point that makes "switch this session's engine" possible after
282
+ * a session has started, which the harness's own preset channel refuses
283
+ * (`agent-preset/locked`, `packages/preset/agent-presets/src/index.ts`). The
284
+ * order below is load-bearing:
285
+ *
286
+ * 1. the checks, so a session that cannot be moved is refused with a reason
287
+ * instead of half-moved;
288
+ * 2. the record, because it is the answer the router, the Remote, and the
289
+ * host's next resolve all read — and a record that could not be written
290
+ * must leave the session where it is;
291
+ * 3. the session's model seat, which moves with the choice
292
+ * ({@link moveModelSelection}) — before the move below, because the move is
293
+ * what detaches (or re-installs) the agent that reads it;
294
+ * 4. the move itself: an in-place swap between two hosted engines, or — when
295
+ * the harness loop is on either side — a release of the session's agent
296
+ * plus a request that the page reload (`reload: true`), which is what
297
+ * makes the host build the session again on the recorded engine.
298
+ *
299
+ * A session already on the requested engine is recorded and left running: the
300
+ * record is the user's explicit choice, while tearing an untouched agent down
301
+ * would cost the session's rebuild for no change at all. "Already on" is
302
+ * judged on the LIVE AGENT's engine, not on the record: the two can differ
303
+ * while a released agent is still in flight, and then re-selecting the engine
304
+ * the session is actually running is how a user takes that record back — the
305
+ * record follows the agent back, and nothing is torn down.
306
+ * @param sessionId - the session to move.
307
+ * @param engine - the engine it should run.
308
+ * @returns the engine now recorded for it, whether the page must reload for it
309
+ * to be built, or — as `ok: false` — the {@link LoopEngineRefusalCode} of the
310
+ * branch that refused it plus its own sentence about this session. Every
311
+ * refusal this method produces carries a code; the browser half localizes
312
+ * from it and keeps the sentence as detail.
313
+ */
314
+ selectEngine(sessionId: SessionId, engine: LoopEngineId): Promise<LoopEngineSelectResult>;
315
+ /**
316
+ * Make a just-recorded engine change take effect on the live session.
317
+ *
318
+ * Between two hosted engines the change is an IN-PLACE SWAP
319
+ * ({@link hotSwap}): the session's `Session` object is kept, its agent is
320
+ * replaced, and the browser half attached to that session sees no lifecycle
321
+ * edge at all.
322
+ *
323
+ * A change with the harness loop on EITHER side cannot be done in place, and
324
+ * this is a limit of the harness rather than a choice: `AgentLoop` neither
325
+ * hands a live session over nor accepts one it did not create. Its factory
326
+ * holds the session's store entry and write handle in private closure state
327
+ * (`agent-loop/src/index.ts` `prepare`), its publication enters the session
328
+ * (`sessions.enter`, which refuses an id already in the store), and its own
329
+ * agent class is not exported. So the session's agent is RELEASED instead
330
+ * ({@link release}) and the change lands on the session's next build, which
331
+ * the returned `reload: true` puts in the user's hands: the session goes cold
332
+ * with the record already naming its engine, and the page — reloaded, and
333
+ * reopening that session — is what makes the host resolve it again, on the
334
+ * engine the record names.
335
+ *
336
+ * The page really does have to reload, and that is the one cost of this path:
337
+ * a release emits `session/disposed`, and the browser half reads that as this
338
+ * session being gone, with no way back in that page's lifetime
339
+ * (`session.ts` `handleRemoved` sets a `removed` flag nothing ever clears), so
340
+ * a page that stayed put would show a session that can no longer be typed
341
+ * into. The alternative — leaving the live agent alone and letting the record
342
+ * wait for a process restart — is what this plugin used to do, and it cost the
343
+ * user a restart for a switch the host can perform itself.
344
+ * @param entry - the live session's record; the caller verified it is movable.
345
+ * @param engine - the engine just recorded for it.
346
+ * @returns the switch outcome, asking for the page reload that builds the
347
+ * session again when its agent had to be released.
348
+ */
349
+ private move;
350
+ /**
351
+ * Replace one live session's agent with a machine the incoming engine builds
352
+ * onto the SAME `Session`.
353
+ *
354
+ * The order is load-bearing. The outgoing machine is retired FIRST and
355
+ * completely: only one agent may be registered per session id (`agents.enter`
356
+ * refuses a duplicate), and two machines folded over one session's inbox would
357
+ * splice each other's queued messages, so the outgoing machine must be settled
358
+ * before any input can reach the successor. The SESSION is never released:
359
+ * its store entry and write handle travel in the lifetime the outgoing machine
360
+ * hands over, which is exactly why the page attached to this session stays
361
+ * usable across the swap.
362
+ *
363
+ * A failure after the handover cannot be undone — the outgoing machine is
364
+ * gone and the successor never published. The lifetime is then released, so
365
+ * the session goes cold and reopens on the recorded engine the way any session
366
+ * the host has not loaded does, and the refusal says what happened.
367
+ * @param entry - the live session's record; the caller verified it is movable.
368
+ * @param handover - the outgoing machine's session handover.
369
+ * @param engine - the hosted engine to build the successor on.
370
+ * @returns the switch outcome.
371
+ */
372
+ private hotSwap;
373
+ /** Read the host session-projection registry, structurally. */
374
+ private projections;
375
+ /**
376
+ * Drop one tracked session's agent so the host's next resolve rebuilds it.
377
+ *
378
+ * Two callers, and they both want the engine change to land at the session's
379
+ * next build rather than in place:
380
+ *
381
+ * - {@link move}, the engine picker's path involving the harness loop, which
382
+ * WAITS for the teardown (the outcome it is about to return says the
383
+ * session is cold, and that must be true when it says so);
384
+ * - the harness's own preset switch on a blank session, which fire-and-forgets
385
+ * it because it runs inside a synchronous event handler. It is the one
386
+ * engine change this plugin still makes by releasing the session rather
387
+ * than by moving it: the successor's composition is the NEW preset's, which
388
+ * only the API layer composes (a preset the harness mounted itself reaches
389
+ * the plugin as an id, not as a composition callback), so the plugin cannot
390
+ * build a correctly-composed successor in place.
391
+ *
392
+ * The record is forgotten before the teardown runs, so a teardown-driven event
393
+ * can never find the entry it is releasing and start a second teardown of the
394
+ * same agent; a failure is reported rather than thrown, and the returned
395
+ * promise settles either way.
396
+ * @param entry - the live session record to release.
397
+ * @returns the teardown, once it settled.
398
+ */
399
+ private release;
400
+ /**
401
+ * Follow the harness's own preset switch, for a BLANK session.
402
+ *
403
+ * The harness's preset switch on a blank session re-parents the live agent's
404
+ * scope and records the choice; it does not rebuild the agent, so the engine
405
+ * would keep running the one the session was created with. Dropping the agent
406
+ * instead lets the host's own next resolve fall back to a resume
407
+ * (`ApiSessionAgentController.resolve`), which composes from the recorded
408
+ * preset and lands on the new engine — the same path a reopened session
409
+ * takes, with the durable log as the only carry-over.
410
+ *
411
+ * LAST USER ACTION WINS: for a session this plugin already holds a record
412
+ * for, the harness's picker is an engine choice too, so the record follows it
413
+ * before the agent is released. Without that, the two entry points would
414
+ * contradict each other — the picker would rebuild the session on the preset's
415
+ * engine, while the very next resume would read the plugin's record and move
416
+ * it back. A session with NO record keeps reading its engine off the preset,
417
+ * exactly as before, and gets none: only an explicit choice is recorded.
418
+ *
419
+ * A session that has already run a turn is left alone — its live engine and
420
+ * its record both stay where they are, so the router and the Remote keep
421
+ * agreeing about it. (A started session cannot reach here through the
422
+ * harness's picker anyway: the roster refuses with `agent-preset/locked`.)
423
+ *
424
+ * The session's model seat moves with the choice, before the release
425
+ * ({@link moveModelSelection}) — the picker is an engine change like the
426
+ * picker's own switcher, and this is the one place the harness can make it.
427
+ */
428
+ private rebuildOnEngineChange;
429
+ /**
430
+ * Hand one session's model seat to the engine it is being switched to.
431
+ *
432
+ * A session's selection follows its engine: a hosted engine owns its model
433
+ * natively and the shared provider label it logs is served only by this
434
+ * plugin's placeholder route (which fails loud, `HOSTED_ENGINE_ROUTE`, when a
435
+ * real model call reaches it), while the harness loop DOES make real calls and therefore needs
436
+ * a real model selected. So a switch moves the seat — the shared
437
+ * `external/default` for a hosted engine, the deployment default for the
438
+ * harness loop. A selection that
439
+ * names a real model of its own is never touched: the model notice the browser
440
+ * half shows beside a hosted engine says that selection is inert there, not
441
+ * that it is gone. The write itself, its judgements, and why an engine change
442
+ * with no logged selection writes nothing are in `model-selection-reset.ts`;
443
+ * this method is only the router's half.
444
+ *
445
+ * The selection is written at the moment of the switch, not at the moment the
446
+ * engine changes. Between two hosted engines the move is an in-place swap whose
447
+ * `setup` installs the selection onto the successor, and for anything involving
448
+ * the harness loop the switch releases the session's agent and the page reloads
449
+ * ({@link move}) — either way the write has to be in the log before the move
450
+ * runs, which is what this ordering buys.
451
+ * @param entry - the live session being switched.
452
+ * @param engine - the engine it is being switched to.
453
+ * @returns nothing; a selection this process cannot name is skipped with one
454
+ * warning, and the session keeps what its log records.
455
+ */
456
+ private moveModelSelection;
457
+ }
458
+ //# sourceMappingURL=router-loop.d.ts.map
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The plugin's own per-session engine record: one sidecar file beside the
3
+ * harness home, deliberately NOT an event in the session log.
4
+ *
5
+ * The engine a session runs is a per-session host-plane fact, and the session
6
+ * log is where this project records such facts — so the obvious home for it is
7
+ * a plugin-authored session event. It cannot be done:
8
+ *
9
+ * - `Session.append` builds the event envelope itself and offers no way to set
10
+ * the envelope's `ignorable?: true` marker (the only options it takes are
11
+ * surface metadata);
12
+ * - the persistence read path refuses a stored event whose type is outside the
13
+ * harness's generated `KNOWN_SESSION_EVENT_TYPES` unless the envelope carries
14
+ * that marker;
15
+ * - so the append succeeds in memory, the write succeeds on disk, and the NEXT
16
+ * cold read of that session refuses the whole log:
17
+ * `SessionFormatUnsupportedError: … contains event type "…" unknown to this
18
+ * harness and not marked ignorable; refusing to interpret the log`.
19
+ *
20
+ * That is a destroyed session per engine switch, so this plugin keeps the fact
21
+ * out of the log instead. `docs/proposals/append-ignorable-events.md` asks the
22
+ * harness for the missing seam; if it lands, this record can move into the log
23
+ * with the file kept as the fallback for sessions recorded before the change.
24
+ *
25
+ * Storage semantics (all of them deliberate):
26
+ *
27
+ * - one small JSON document, `{ version, engines: { <sessionId>: <engine> } }`,
28
+ * written whole through a same-directory temp file + rename, so a crash
29
+ * mid-write can never leave a truncated record behind;
30
+ * - the in-memory view advances only after the write committed, so a failed
31
+ * write leaves both the file and the view exactly as they were;
32
+ * - a missing file is the normal first-run state (no diagnostic); an
33
+ * unreadable or unrecognizable file degrades to "no record" with ONE warn
34
+ * (the document is read at most once per process, so no lookup repeats a
35
+ * failure), because a broken record must cost a session its remembered
36
+ * engine, never its ability to open: the routing read falls back to the
37
+ * preset mapping;
38
+ * - entries are added, never removed: one line per session ever switched, which
39
+ * a deployment cannot notice, and a stale entry for a deleted session is
40
+ * simply never asked about.
41
+ *
42
+ * @module dsh-loop-engine-allinone/session-engine-store
43
+ */
44
+ import type { SessionId } from '@deepseek-ai/dsh-session';
45
+ import { type LoopEngineId } from './agent-preset-ids.ts';
46
+ /** Harness-home-relative directory holding the plugin's own records. */
47
+ export declare const ENGINE_RECORD_DIR = ".loop-engine";
48
+ /** File name of the per-session engine record document. */
49
+ export declare const ENGINE_RECORD_FILE = "engines.json";
50
+ /** Format version of the record document; a mismatch is treated as no record. */
51
+ export declare const ENGINE_RECORD_VERSION = 1;
52
+ /**
53
+ * The record file's path under the harness home (`$DSH_HOME/.loop-engine/engines.json`).
54
+ * @returns the absolute path of the engine record document.
55
+ */
56
+ export declare function resolveEngineRecordPath(): string;
57
+ /**
58
+ * Read one session's engine off the plugin's own record.
59
+ *
60
+ * The narrow face the routing read depends on, so a test can hand the router an
61
+ * exact set of records instead of a file.
62
+ */
63
+ export interface EngineRecordSource {
64
+ /**
65
+ * The engine this plugin's record names for one session.
66
+ * @param sessionId - the session to look up.
67
+ * @returns the recorded engine, or undefined when this session has no record.
68
+ */
69
+ engineOf(sessionId: SessionId): LoopEngineId | undefined;
70
+ }
71
+ /**
72
+ * The record as the ROUTER uses it: the read every routing decision makes, plus
73
+ * the write an engine switch commits.
74
+ */
75
+ export interface EngineRecordStore extends EngineRecordSource {
76
+ /**
77
+ * Record one session's engine, replacing any earlier record for it.
78
+ * @param sessionId - the session whose engine is recorded.
79
+ * @param engine - the engine that session now runs.
80
+ */
81
+ record(sessionId: SessionId, engine: LoopEngineId): void;
82
+ }
83
+ /**
84
+ * Replace one file atomically: same-directory temp file, then rename over the
85
+ * target, so a reader never sees a partial document. The temp name is unique
86
+ * per write, so two writers cannot collide on it.
87
+ * @param path - the file to replace (its directory is created as needed).
88
+ * @param text - the next file content.
89
+ */
90
+ export declare function writeFileAtomicSync(path: string, text: string): void;
91
+ /**
92
+ * The plugin's per-session engine record, held in memory and mirrored to one
93
+ * atomically written JSON file.
94
+ *
95
+ * The file is read once, lazily, on the first lookup; every later lookup is a
96
+ * map read, so routing never pays for filesystem I/O.
97
+ */
98
+ export declare class SessionEngineStore implements EngineRecordSource {
99
+ private readonly path;
100
+ private readonly warn;
101
+ private entries;
102
+ /**
103
+ * @param path - the record document's absolute path.
104
+ * @param warn - diagnostic sink for a record this build cannot read.
105
+ */
106
+ constructor(path: string, warn: (message: string) => void);
107
+ /**
108
+ * The engine this plugin's record names for one session.
109
+ * @param sessionId - the session to look up.
110
+ * @returns the recorded engine, or undefined when there is no usable record.
111
+ */
112
+ engineOf(sessionId: SessionId): LoopEngineId | undefined;
113
+ /**
114
+ * Record one session's engine, replacing any earlier record for it.
115
+ *
116
+ * The write commits before the in-memory view moves, so a caller that sees a
117
+ * rejection knows the store still answers what it answered before.
118
+ * @param sessionId - the session whose engine is recorded.
119
+ * @param engine - the engine that session now runs.
120
+ * @throws when the document could not be written; the previous file is intact.
121
+ */
122
+ record(sessionId: SessionId, engine: LoopEngineId): void;
123
+ /** The in-memory record, read from disk on first use. */
124
+ private load;
125
+ /** Read the document once, degrading to an empty record. */
126
+ private read;
127
+ /**
128
+ * Report one unusable record and answer "no record".
129
+ *
130
+ * The load is memoized, so this runs at most once per store — once per process
131
+ * for a path — rather than once per lookup: a broken record costs one line, and
132
+ * the degraded view serves every later read.
133
+ * @param problem - what is wrong with the record, with the path.
134
+ * @returns the empty record.
135
+ */
136
+ private degrade;
137
+ }
138
+ //# sourceMappingURL=session-engine-store.d.ts.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Shared loop-engine identity, namespaces, and schemas.
3
+ *
4
+ * Both the namespace literals and the engine/preset-id mapping live in
5
+ * zero-import modules (`./namespace.ts`, `./agent-preset-ids.ts`) so both halves
6
+ * agree on them: the node half brands the literal as a `SettingsNamespace` and
7
+ * builds these schemas, while the browser half imports the same literals
8
+ * without pulling host-side packages (`dsh-settings`, `schemastery`, `node:fs`)
9
+ * into the client bundle (cross-plugin value imports go through cordis
10
+ * services). This module re-exports them so node-side importers keep their
11
+ * paths.
12
+ *
13
+ * The two live schema families exist because one build serves two harness
14
+ * generations: the 0.1.5 line addresses a settings SECTION registered by a
15
+ * provider under a namespace string ({@link LOOP_ENGINE_SETTINGS_SCHEMA} via
16
+ * {@link loopEngineSettingsNamespace}), while the 0.1.7 line makes the plugin's
17
+ * three live fields profile-backed Config entries
18
+ * ({@link LOOP_ENGINE_ENGINE_SCHEMA} / {@link LOOP_ENGINE_SHOW_IN_COMPOSER_SCHEMA}
19
+ * / {@link LOOP_ENGINE_SHOW_ENGINE_BADGE_SCHEMA}).
20
+ * Which family a running plugin builds is decided by `./compat.ts`
21
+ * (`LEGACY_HARNESS`) in `./index.ts`.
22
+ *
23
+ * @module dsh-loop-engine-allinone/settings
24
+ */
25
+ import z from '@deepseek-ai/schemastery';
26
+ import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
27
+ import type { LoopEngineId } from './agent-preset-ids.ts';
28
+ export { LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL, LEGACY_LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL } from './namespace.ts';
29
+ export { HOSTED_ENGINE_IDS, HOSTED_PRESET_PREFIX, LOOP_ENGINE_IDS, SOURCE_PRESET_ID, engineOfPreset, enginePresetId, } from './agent-preset-ids.ts';
30
+ export type { HostedEngineId, LoopEngineId } from './agent-preset-ids.ts';
31
+ /** Stored and composed loop engine selection. */
32
+ export interface LoopEngineSettings {
33
+ /** The engine NEW sessions are created on; the engine a given session runs is the plugin's own per-session record. */
34
+ engine: LoopEngineId;
35
+ /** Whether the composer's loop engine picker is shown on the chat page. */
36
+ showInComposer: boolean;
37
+ /** Whether the conversation header shows this plugin's engine badge. */
38
+ showEngineBadge: boolean;
39
+ }
40
+ /**
41
+ * Schema of the loop engine settings section, as the 0.1.5 line's settings host
42
+ * consumes it. The engine union derives from {@link LOOP_ENGINE_IDS}, so adding
43
+ * an engine there also admits it here.
44
+ */
45
+ export declare const LOOP_ENGINE_SETTINGS_SCHEMA: z<LoopEngineSettings>;
46
+ /** The 0.1.5-line namespace branded as a settings namespace on the node side. */
47
+ export declare function loopEngineSettingsNamespace(): SettingsNamespace;
48
+ /**
49
+ * Live Config field schema for the default engine (0.1.7 line). `.volatile()`
50
+ * declares it profile-backed: the settings shell projects it as a form field,
51
+ * and a committed edit reaches the running plugin through the live Config
52
+ * reference. The engine union derives from {@link LOOP_ENGINE_IDS}, so adding
53
+ * an engine there admits it here too.
54
+ */
55
+ export declare const LOOP_ENGINE_ENGINE_SCHEMA: z<"claude-code" | "codex" | "pi" | "kimi" | "agy" | "zcode" | "qoderclicn" | "hermes" | "grok" | "minimaxcode" | "in-process", "claude-code" | "codex" | "pi" | "kimi" | "agy" | "zcode" | "qoderclicn" | "hermes" | "grok" | "minimaxcode" | "in-process", "volatile-defined">;
56
+ /** Live Config field schema for the composer picker toggle (0.1.7 line). */
57
+ export declare const LOOP_ENGINE_SHOW_IN_COMPOSER_SCHEMA: z<boolean, boolean, "volatile-defined">;
58
+ /** Live Config field schema for the conversation-header badge toggle (0.1.7 line). */
59
+ export declare const LOOP_ENGINE_SHOW_ENGINE_BADGE_SCHEMA: z<boolean, boolean, "volatile-defined">;
60
+ //# sourceMappingURL=settings.d.ts.map