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.
- package/README.md +44 -177
- package/README.zh.md +48 -100
- package/lib/client.js +887 -266
- package/lib/index.js +2212 -914
- package/lib/invariant.js +43 -45
- package/lib/types/agent-preset-ids.d.ts +303 -0
- package/lib/types/client/LoopEngineBadge.d.ts +44 -17
- package/lib/types/client/LoopEngineComposerSelect.d.ts +79 -13
- package/lib/types/client/LoopEngineSection.d.ts +5 -4
- package/lib/types/client/locales.d.ts +133 -7
- package/lib/types/client/reload.d.ts +135 -0
- package/lib/types/client/session-engine.d.ts +474 -0
- package/lib/types/client/store.d.ts +1 -1
- package/lib/types/client/turn-status.d.ts +112 -10
- package/lib/types/client/use-session-engine.d.ts +66 -0
- package/lib/types/commands.d.ts +11 -3
- package/lib/types/driver-core/host-servers.d.ts +106 -0
- package/lib/types/driver-core/hosted-engine-runtime.d.ts +190 -0
- package/lib/types/driver-core/hosted-tool-vocabulary.d.ts +72 -0
- package/lib/types/driver-core/model-handover.d.ts +116 -0
- package/lib/types/driver-core/ownership.d.ts +6 -5
- package/lib/types/driver-core/prompt.d.ts +32 -0
- package/lib/types/driver-core/session-lifetime.d.ts +62 -0
- package/lib/types/driver-core/session-model.d.ts +82 -0
- package/lib/types/engine-claude/agent.d.ts +23 -3
- package/lib/types/engine-claude/loop.d.ts +16 -15
- package/lib/types/engine-codex/agent.d.ts +22 -3
- package/lib/types/engine-codex/appserver/client.d.ts +15 -2
- package/lib/types/engine-codex/loop.d.ts +13 -15
- package/lib/types/engine-codex/model-handover.d.ts +44 -0
- package/lib/types/engine-kimi/acp/client.d.ts +10 -0
- package/lib/types/engine-kimi/agent.d.ts +19 -2
- package/lib/types/engine-kimi/commands.d.ts +18 -14
- package/lib/types/engine-kimi/loop.d.ts +14 -16
- package/lib/types/engine-kimi/model-handover.d.ts +32 -0
- package/lib/types/engine-kimi/process.d.ts +2 -2
- package/lib/types/engine-kimi/types.d.ts +1 -1
- package/lib/types/engine-of-session.d.ts +97 -0
- package/lib/types/engine-pi/agent.d.ts +25 -23
- package/lib/types/engine-pi/loop.d.ts +13 -23
- package/lib/types/engine-pi/model-handover.d.ts +35 -0
- package/lib/types/engine-pi/types.d.ts +2 -2
- package/lib/types/engine-remote.d.ts +192 -0
- package/lib/types/engine-surface.d.ts +36 -0
- package/lib/types/index.d.ts +51 -50
- package/lib/types/invariant.d.ts +8 -5
- package/lib/types/model-selection-reset.d.ts +271 -0
- package/lib/types/patch-manager.d.ts +57 -39
- package/lib/types/preset.d.ts +39 -26
- package/lib/types/provider-route.d.ts +83 -36
- package/lib/types/router-loop.d.ts +406 -0
- package/lib/types/session-engine-store.d.ts +138 -0
- package/lib/types/settings.d.ts +12 -11
- package/package.json +109 -104
|
@@ -0,0 +1,406 @@
|
|
|
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/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
|
+
* The model-selection half of routing: the seat a session's engine owns, moved
|
|
114
|
+
* at a switch ({@link RouterLoop.moveModelSelection}) and written at a build
|
|
115
|
+
* ({@link RouterLoop.engineOptions}). Owned here because each warning it owes
|
|
116
|
+
* is owed once per process, not once per session.
|
|
117
|
+
*/
|
|
118
|
+
private readonly selectionReset;
|
|
119
|
+
/**
|
|
120
|
+
* @param ctx - the context the router's effects belong to.
|
|
121
|
+
* @param build - memoized builder for one hosted engine's runtime.
|
|
122
|
+
* @param records - the plugin's own per-session engine record, which outranks
|
|
123
|
+
* every preset-derived answer and is what {@link selectEngine} writes.
|
|
124
|
+
* @param warn - diagnostic sink for a skipped engine command.
|
|
125
|
+
*/
|
|
126
|
+
constructor(ctx: Context, build: EngineBuilder, records: EngineRecordStore, warn: (message: string) => void);
|
|
127
|
+
/** The engine that drives one session's agent. */
|
|
128
|
+
private engineFor;
|
|
129
|
+
/** The engine's runtime, built on first use and kept for the plugin's lifetime. */
|
|
130
|
+
private runtimeOf;
|
|
131
|
+
/**
|
|
132
|
+
* Record one published agent so a later engine change can rebuild it, and
|
|
133
|
+
* bridge the engine's command/skill surface into its session.
|
|
134
|
+
* @param engine - the engine that built the handle.
|
|
135
|
+
* @param handle - the published handle.
|
|
136
|
+
* @param recipe - how it was built, for a hot swap's successor.
|
|
137
|
+
* @param handover - a hosted handle's session handover; absent for the
|
|
138
|
+
* harness loop, whose handle carries neither the session's lifetime nor a
|
|
139
|
+
* way to retire the machine without releasing it.
|
|
140
|
+
* @returns the handle the harness tracks.
|
|
141
|
+
*/
|
|
142
|
+
private adopt;
|
|
143
|
+
/** Drop one tracked session, if the record still points at that exact agent. */
|
|
144
|
+
private forget;
|
|
145
|
+
/**
|
|
146
|
+
* Create a session's agent on the engine it runs.
|
|
147
|
+
*
|
|
148
|
+
* The plugin's own record is consulted FIRST, ahead of the preset the caller
|
|
149
|
+
* composed with: a session that already has a record must be built on the
|
|
150
|
+
* engine the plugin reports for it, or the router and the Remote would answer
|
|
151
|
+
* differently about one session. Only a session with no record — every session
|
|
152
|
+
* this plugin has never switched, including every brand-new one — falls
|
|
153
|
+
* through to the preset.
|
|
154
|
+
* @param ownerCtx - caller context that owns the lifecycle.
|
|
155
|
+
* @param options - identities, metadata (carrying the preset), and setup.
|
|
156
|
+
* @returns the published handle.
|
|
157
|
+
*/
|
|
158
|
+
createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise<AgentHandle>;
|
|
159
|
+
/**
|
|
160
|
+
* Resume a persisted session on the engine it runs.
|
|
161
|
+
*
|
|
162
|
+
* The read is {@link engineOfSession}: the plugin's own record first, then the
|
|
163
|
+
* durable `agentPreset` projection, which folds the session header together
|
|
164
|
+
* with every committed `agent-preset/selected`. The session header alone would
|
|
165
|
+
* name the preset the session was CREATED with, which is exactly the wrong
|
|
166
|
+
* answer for a session that switched engine while it was blank — and the
|
|
167
|
+
* record is the only answer for one that switched after it started, which no
|
|
168
|
+
* preset can express.
|
|
169
|
+
*
|
|
170
|
+
* A resume is not a create: the session's seat is one a previous build already
|
|
171
|
+
* put in its log (`model-selection-reset.ts`, the switch and build triggers), so
|
|
172
|
+
* a hosted engine's resume leaves the selection exactly as it finds it. Only
|
|
173
|
+
* the harness loop's own resume is wrapped, with the narrower trigger that
|
|
174
|
+
* applies to it — it makes real model calls, so it must not be left selecting a
|
|
175
|
+
* placeholder route.
|
|
176
|
+
* @param ownerCtx - caller context that owns load, setup, and the lifecycle.
|
|
177
|
+
* @param options - persisted identity, loop options, and setup.
|
|
178
|
+
* @returns the published handle.
|
|
179
|
+
*/
|
|
180
|
+
resume(ownerCtx: Context, options: ResumeAgentOptions): Promise<AgentHandle>;
|
|
181
|
+
/**
|
|
182
|
+
* Wrap the caller's setup so the session's ENGINE gets its model seat written
|
|
183
|
+
* before the host installs the session's selection, and hand a hosted engine
|
|
184
|
+
* the `agentOptions` that seat names.
|
|
185
|
+
*
|
|
186
|
+
* The wrap is what makes the write land at all: the host installs the session's
|
|
187
|
+
* model selection from INSIDE the caller's own setup
|
|
188
|
+
* (`ApiSessionAgentController.selectionFor`), and that read takes the log's
|
|
189
|
+
* pending `model/selection` — so the write has to happen before it, and the
|
|
190
|
+
* caller's setup has to run unchanged afterwards. Both of the router's engines
|
|
191
|
+
* are wrapped, each with its own trigger: the harness loop's build keeps the
|
|
192
|
+
* narrow one (only a session that would otherwise select a placeholder route),
|
|
193
|
+
* while a hosted engine's build is where a NEW session gets its seat. The
|
|
194
|
+
* resume path calls this with `in-process` alone (see {@link resume}).
|
|
195
|
+
*
|
|
196
|
+
* The `agentOptions` half keeps the two answers consistent: a hosted engine is
|
|
197
|
+
* handed the very selection written into the session (`external/default`), not
|
|
198
|
+
* the deployment default that seat replaced. In-process keeps the caller's own
|
|
199
|
+
* options, which ARE its route.
|
|
200
|
+
*
|
|
201
|
+
* The caller's setup is copied, never mutated: the same options object is the
|
|
202
|
+
* router's rebuild recipe, which replays the caller's own closure.
|
|
203
|
+
* @param options - the create or resume options the caller supplied.
|
|
204
|
+
* @param engine - the engine building this session.
|
|
205
|
+
* @returns a copy whose setup writes the seat first, then the caller's own.
|
|
206
|
+
*/
|
|
207
|
+
private engineOptions;
|
|
208
|
+
/**
|
|
209
|
+
* Report what one session ACTUALLY runs, and — when they differ — the engine
|
|
210
|
+
* its record names.
|
|
211
|
+
*
|
|
212
|
+
* This is the read the plugin's own Remote publishes, and the reason it is the
|
|
213
|
+
* router's to make is the live bookkeeping below: for a session with an agent
|
|
214
|
+
* this router built, {@link RouterLoop.live} says which engine built it, and
|
|
215
|
+
* that beats every record — it is what is running. A session this router does
|
|
216
|
+
* not drive (cold, or still on the base loop's slot during the mount window)
|
|
217
|
+
* has no live fact to report, so the record answers, exactly as it does for a
|
|
218
|
+
* session about to be built; and since every switch this plugin performs
|
|
219
|
+
* either swaps the agent in place or releases it, the two facts differ only
|
|
220
|
+
* for a release that did not take.
|
|
221
|
+
* @param sessionId - the session to report on.
|
|
222
|
+
* @returns the engine driving it now, plus the engine its record names for its
|
|
223
|
+
* next build when the two differ.
|
|
224
|
+
*/
|
|
225
|
+
reportEngine(sessionId: SessionId): Promise<SessionEngineReport>;
|
|
226
|
+
/**
|
|
227
|
+
* Move one live session to another engine.
|
|
228
|
+
*
|
|
229
|
+
* The one entry point that makes "switch this session's engine" possible after
|
|
230
|
+
* a session has started, which the harness's own preset channel refuses
|
|
231
|
+
* (`agent-preset/locked`, `packages/preset/agent-presets/src/index.ts`). The
|
|
232
|
+
* order below is load-bearing:
|
|
233
|
+
*
|
|
234
|
+
* 1. the checks, so a session that cannot be moved is refused with a reason
|
|
235
|
+
* instead of half-moved;
|
|
236
|
+
* 2. the record, because it is the answer the router, the Remote, and the
|
|
237
|
+
* host's next resolve all read — and a record that could not be written
|
|
238
|
+
* must leave the session where it is;
|
|
239
|
+
* 3. the session's model seat, which moves with the choice
|
|
240
|
+
* ({@link moveModelSelection}) — before the move below, because the move is
|
|
241
|
+
* what detaches (or re-installs) the agent that reads it;
|
|
242
|
+
* 4. the move itself: an in-place swap between two hosted engines, or — when
|
|
243
|
+
* the harness loop is on either side — a release of the session's agent
|
|
244
|
+
* plus a request that the page reload (`reload: true`), which is what
|
|
245
|
+
* makes the host build the session again on the recorded engine.
|
|
246
|
+
*
|
|
247
|
+
* A session already on the requested engine is recorded and left running: the
|
|
248
|
+
* record is the user's explicit choice, while tearing an untouched agent down
|
|
249
|
+
* would cost the session's rebuild for no change at all. "Already on" is
|
|
250
|
+
* judged on the LIVE AGENT's engine, not on the record: the two can differ
|
|
251
|
+
* while a released agent is still in flight, and then re-selecting the engine
|
|
252
|
+
* the session is actually running is how a user takes that record back — the
|
|
253
|
+
* record follows the agent back, and nothing is torn down.
|
|
254
|
+
* @param sessionId - the session to move.
|
|
255
|
+
* @param engine - the engine it should run.
|
|
256
|
+
* @returns the engine now recorded for it, whether the page must reload for it
|
|
257
|
+
* to be built, or — as `ok: false` — the {@link LoopEngineRefusalCode} of the
|
|
258
|
+
* branch that refused it plus its own sentence about this session. Every
|
|
259
|
+
* refusal this method produces carries a code; the browser half localizes
|
|
260
|
+
* from it and keeps the sentence as detail.
|
|
261
|
+
*/
|
|
262
|
+
selectEngine(sessionId: SessionId, engine: LoopEngineId): Promise<LoopEngineSelectResult>;
|
|
263
|
+
/**
|
|
264
|
+
* Make a just-recorded engine change take effect on the live session.
|
|
265
|
+
*
|
|
266
|
+
* Between two hosted engines the change is an IN-PLACE SWAP
|
|
267
|
+
* ({@link hotSwap}): the session's `Session` object is kept, its agent is
|
|
268
|
+
* replaced, and the browser half attached to that session sees no lifecycle
|
|
269
|
+
* edge at all.
|
|
270
|
+
*
|
|
271
|
+
* A change with the harness loop on EITHER side cannot be done in place, and
|
|
272
|
+
* this is a limit of the harness rather than a choice: `AgentLoop` neither
|
|
273
|
+
* hands a live session over nor accepts one it did not create. Its factory
|
|
274
|
+
* holds the session's store entry and write handle in private closure state
|
|
275
|
+
* (`agent-loop/src/index.ts` `prepare`), its publication enters the session
|
|
276
|
+
* (`sessions.enter`, which refuses an id already in the store), and its own
|
|
277
|
+
* agent class is not exported. So the session's agent is RELEASED instead
|
|
278
|
+
* ({@link release}) and the change lands on the session's next build, which
|
|
279
|
+
* the returned `reload: true` puts in the user's hands: the session goes cold
|
|
280
|
+
* with the record already naming its engine, and the page — reloaded, and
|
|
281
|
+
* reopening that session — is what makes the host resolve it again, on the
|
|
282
|
+
* engine the record names.
|
|
283
|
+
*
|
|
284
|
+
* The page really does have to reload, and that is the one cost of this path:
|
|
285
|
+
* a release emits `session/disposed`, and the browser half reads that as this
|
|
286
|
+
* session being gone, with no way back in that page's lifetime
|
|
287
|
+
* (`session.ts` `handleRemoved` sets a `removed` flag nothing ever clears), so
|
|
288
|
+
* a page that stayed put would show a session that can no longer be typed
|
|
289
|
+
* into. The alternative — leaving the live agent alone and letting the record
|
|
290
|
+
* wait for a process restart — is what this plugin used to do, and it cost the
|
|
291
|
+
* user a restart for a switch the host can perform itself.
|
|
292
|
+
* @param entry - the live session's record; the caller verified it is movable.
|
|
293
|
+
* @param engine - the engine just recorded for it.
|
|
294
|
+
* @returns the switch outcome, asking for the page reload that builds the
|
|
295
|
+
* session again when its agent had to be released.
|
|
296
|
+
*/
|
|
297
|
+
private move;
|
|
298
|
+
/**
|
|
299
|
+
* Replace one live session's agent with a machine the incoming engine builds
|
|
300
|
+
* onto the SAME `Session`.
|
|
301
|
+
*
|
|
302
|
+
* The order is load-bearing. The outgoing machine is retired FIRST and
|
|
303
|
+
* completely: only one agent may be registered per session id (`agents.enter`
|
|
304
|
+
* refuses a duplicate), and two machines folded over one session's inbox would
|
|
305
|
+
* splice each other's queued messages, so the outgoing machine must be settled
|
|
306
|
+
* before any input can reach the successor. The SESSION is never released:
|
|
307
|
+
* its store entry and write handle travel in the lifetime the outgoing machine
|
|
308
|
+
* hands over, which is exactly why the page attached to this session stays
|
|
309
|
+
* usable across the swap.
|
|
310
|
+
*
|
|
311
|
+
* A failure after the handover cannot be undone — the outgoing machine is
|
|
312
|
+
* gone and the successor never published. The lifetime is then released, so
|
|
313
|
+
* the session goes cold and reopens on the recorded engine the way any session
|
|
314
|
+
* the host has not loaded does, and the refusal says what happened.
|
|
315
|
+
* @param entry - the live session's record; the caller verified it is movable.
|
|
316
|
+
* @param handover - the outgoing machine's session handover.
|
|
317
|
+
* @param engine - the hosted engine to build the successor on.
|
|
318
|
+
* @returns the switch outcome.
|
|
319
|
+
*/
|
|
320
|
+
private hotSwap;
|
|
321
|
+
/** Read the host session-projection registry, structurally. */
|
|
322
|
+
private projections;
|
|
323
|
+
/**
|
|
324
|
+
* Drop one tracked session's agent so the host's next resolve rebuilds it.
|
|
325
|
+
*
|
|
326
|
+
* Two callers, and they both want the engine change to land at the session's
|
|
327
|
+
* next build rather than in place:
|
|
328
|
+
*
|
|
329
|
+
* - {@link move}, the engine picker's path involving the harness loop, which
|
|
330
|
+
* WAITS for the teardown (the outcome it is about to return says the
|
|
331
|
+
* session is cold, and that must be true when it says so);
|
|
332
|
+
* - the harness's own preset switch on a blank session, which fire-and-forgets
|
|
333
|
+
* it because it runs inside a synchronous event handler. It is the one
|
|
334
|
+
* engine change this plugin still makes by releasing the session rather
|
|
335
|
+
* than by moving it: the successor's composition is the NEW preset's, which
|
|
336
|
+
* only the API layer composes (a preset the harness mounted itself reaches
|
|
337
|
+
* the plugin as an id, not as a composition callback), so the plugin cannot
|
|
338
|
+
* build a correctly-composed successor in place.
|
|
339
|
+
*
|
|
340
|
+
* The record is forgotten before the teardown runs, so a teardown-driven event
|
|
341
|
+
* can never find the entry it is releasing and start a second teardown of the
|
|
342
|
+
* same agent; a failure is reported rather than thrown, and the returned
|
|
343
|
+
* promise settles either way.
|
|
344
|
+
* @param entry - the live session record to release.
|
|
345
|
+
* @returns the teardown, once it settled.
|
|
346
|
+
*/
|
|
347
|
+
private release;
|
|
348
|
+
/**
|
|
349
|
+
* Follow the harness's own preset switch, for a BLANK session.
|
|
350
|
+
*
|
|
351
|
+
* The harness's preset switch on a blank session re-parents the live agent's
|
|
352
|
+
* scope and records the choice; it does not rebuild the agent, so the engine
|
|
353
|
+
* would keep running the one the session was created with. Dropping the agent
|
|
354
|
+
* instead lets the host's own next resolve fall back to a resume
|
|
355
|
+
* (`ApiSessionAgentController.resolve`), which composes from the recorded
|
|
356
|
+
* preset and lands on the new engine — the same path a reopened session
|
|
357
|
+
* takes, with the durable log as the only carry-over.
|
|
358
|
+
*
|
|
359
|
+
* LAST USER ACTION WINS: for a session this plugin already holds a record
|
|
360
|
+
* for, the harness's picker is an engine choice too, so the record follows it
|
|
361
|
+
* before the agent is released. Without that, the two entry points would
|
|
362
|
+
* contradict each other — the picker would rebuild the session on the preset's
|
|
363
|
+
* engine, while the very next resume would read the plugin's record and move
|
|
364
|
+
* it back. A session with NO record keeps reading its engine off the preset,
|
|
365
|
+
* exactly as before, and gets none: only an explicit choice is recorded.
|
|
366
|
+
*
|
|
367
|
+
* A session that has already run a turn is left alone — its live engine and
|
|
368
|
+
* its record both stay where they are, so the router and the Remote keep
|
|
369
|
+
* agreeing about it. (A started session cannot reach here through the
|
|
370
|
+
* harness's picker anyway: the roster refuses with `agent-preset/locked`.)
|
|
371
|
+
*
|
|
372
|
+
* The session's model seat moves with the choice, before the release
|
|
373
|
+
* ({@link moveModelSelection}) — the picker is an engine change like the
|
|
374
|
+
* picker's own switcher, and this is the one place the harness can make it.
|
|
375
|
+
*/
|
|
376
|
+
private rebuildOnEngineChange;
|
|
377
|
+
/**
|
|
378
|
+
* Hand one session's model seat to the engine it is being switched to.
|
|
379
|
+
*
|
|
380
|
+
* A session's selection follows its engine: a hosted engine owns its model
|
|
381
|
+
* natively and the shared provider label it logs is served only by this
|
|
382
|
+
* plugin's placeholder route (which fails loud, `HOSTED_ENGINE_ROUTE`, when a
|
|
383
|
+
* real model call reaches it), while the harness loop DOES make real calls and therefore needs
|
|
384
|
+
* a real model selected. So a switch moves the seat — the shared
|
|
385
|
+
* `external/default` for a hosted engine, the deployment default for the
|
|
386
|
+
* harness loop. A selection that
|
|
387
|
+
* names a real model of its own is never touched: the model notice the browser
|
|
388
|
+
* half shows beside a hosted engine says that selection is inert there, not
|
|
389
|
+
* that it is gone. The write itself, its judgements, and why an engine change
|
|
390
|
+
* with no logged selection writes nothing are in `model-selection-reset.ts`;
|
|
391
|
+
* this method is only the router's half.
|
|
392
|
+
*
|
|
393
|
+
* The selection is written at the moment of the switch, not at the moment the
|
|
394
|
+
* engine changes. Between two hosted engines the move is an in-place swap whose
|
|
395
|
+
* `setup` installs the selection onto the successor, and for anything involving
|
|
396
|
+
* the harness loop the switch releases the session's agent and the page reloads
|
|
397
|
+
* ({@link move}) — either way the write has to be in the log before the move
|
|
398
|
+
* runs, which is what this ordering buys.
|
|
399
|
+
* @param entry - the live session being switched.
|
|
400
|
+
* @param engine - the engine it is being switched to.
|
|
401
|
+
* @returns nothing; a selection this process cannot name is skipped with one
|
|
402
|
+
* warning, and the session keeps what its log records.
|
|
403
|
+
*/
|
|
404
|
+
private moveModelSelection;
|
|
405
|
+
}
|
|
406
|
+
//# 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/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
|
package/lib/types/settings.d.ts
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared loop-engine identity, namespace, and schema.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Both the namespace literal 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 this schema, while the browser half imports the same literals without
|
|
8
|
+
* pulling host-side packages (`dsh-settings`, `schemastery`, `node:fs`) into the
|
|
9
|
+
* client bundle (cross-plugin value imports go through cordis services, and
|
|
10
|
+
* `settings-scope.ts` follows the same discipline). This module re-exports them
|
|
11
|
+
* so node-side importers keep their paths.
|
|
10
12
|
*
|
|
11
13
|
* @module dsh-loop-engine/settings
|
|
12
14
|
*/
|
|
13
15
|
import z from '@deepseek-ai/schemastery';
|
|
14
16
|
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
17
|
+
import type { LoopEngineId } from './agent-preset-ids.ts';
|
|
15
18
|
export { LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL } from './namespace.ts';
|
|
16
|
-
|
|
17
|
-
export
|
|
18
|
-
/** Installed agent loop engine id. */
|
|
19
|
-
export type LoopEngineId = (typeof LOOP_ENGINE_IDS)[number];
|
|
19
|
+
export { HOSTED_ENGINE_IDS, HOSTED_PRESET_PREFIX, LOOP_ENGINE_IDS, SOURCE_PRESET_ID, engineOfPreset, enginePresetId, } from './agent-preset-ids.ts';
|
|
20
|
+
export type { HostedEngineId, LoopEngineId } from './agent-preset-ids.ts';
|
|
20
21
|
/** Stored and composed loop engine selection. */
|
|
21
22
|
export interface LoopEngineSettings {
|
|
22
|
-
/** The engine
|
|
23
|
+
/** The engine NEW sessions are created on; the engine a given session runs is the plugin's own per-session record. */
|
|
23
24
|
engine: LoopEngineId;
|
|
24
25
|
/** Whether the composer's loop engine picker is shown on the chat page. */
|
|
25
26
|
showInComposer: boolean;
|