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

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 +100 -0
  2. package/CONTRIBUTING.md +78 -0
  3. package/LICENSE +23 -0
  4. package/NOTICE +45 -0
  5. package/README.md +221 -2
  6. package/README.zh.md +89 -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 +150 -4
@@ -0,0 +1,508 @@
1
+ /**
2
+ * Per-session engine, browser half: read the engine a session actually runs
3
+ * from the plugin's OWN Remote, and switch it through that same Remote.
4
+ *
5
+ * The engine a session runs is NOT a client-cache fact, and reading one as if it
6
+ * were is the bug this module exists to prevent. The harness hands a page a
7
+ * projection HINT per session (`@deepseek-ai/dsh-api-session-controller`'s
8
+ * `projectionsFor()` returns `SessionProjectionHints`: "every currently cached
9
+ * wire value", partial by its own documentation), and its `agentPreset` cell
10
+ * answers a different question than "what does this session run": a session may
11
+ * switch engine while it is blank (and, with this plugin, at any time), and that
12
+ * choice lives in the plugin's own record while the session header keeps naming
13
+ * the preset the session was CREATED with. A page reading the hint therefore
14
+ * shows a claude-code session that switched to pi — and the engine it actually
15
+ * runs — as Claude Code, the misreport this plugin shipped a production incident
16
+ * for.
17
+ *
18
+ * So the browser half asks the host instead:
19
+ * `remote.loopEngine.engine({ sessionId })` to read, and
20
+ * `remote.loopEngine.select({ sessionId, engine })` to switch — the endpoints
21
+ * `src/engine-remote.ts` serves from the same read the router routes on. Nothing
22
+ * is derived here: the three-state arrives decided, and a refused switch arrives
23
+ * with the host's own reason.
24
+ *
25
+ * This module is deliberately React-free: the cache, the contribution, and the
26
+ * switch are plain logic, so they run in node under its own spec
27
+ * (`tests/session-engine-cache.spec.ts`). The one part that needs a component to
28
+ * render — the hook that subscribes a surface to the cache — lives in
29
+ * `./use-session-engine.ts`.
30
+ *
31
+ * The cache owns NO document-level side effect, deliberately. The chat
32
+ * turn-status row is painted through a document-level attribute
33
+ * (`./turn-status.ts`), so the only writer entitled to it is the session on
34
+ * screen — and this cache publishes for every session a surface has ever
35
+ * watched, the session the user just left included. Reflecting from
36
+ * `publish` therefore let a background session's (or a left session's late)
37
+ * answer take the attribute over from the session on screen, which painted ITS
38
+ * engine onto the row beside it — a session running pi drew Kimi's moon. The
39
+ * reflection is driven from `./use-session-engine.ts` instead: that hook is
40
+ * where a component knows which session it is the surface of, and the reflection
41
+ * it makes carries a focus guard. Here the cache answers one question only —
42
+ * what one session's engine is.
43
+ *
44
+ * One member of the framework's standard seat is still read from the slot props:
45
+ * the session-scope `sessionId`. The harness declares it by declaration-merging
46
+ * `SessionStandardProps` in `@deepseek-ai/dsh-client-ui-session/client`, and its
47
+ * published declarations elide the type-only imports that carry the merge — so a
48
+ * half compiled against the installed artifacts (this package's
49
+ * `tsconfig.build.json` pins `@deepseek-ai/*` to the artifact plane) never sees
50
+ * it. It is therefore restated structurally here, in the shape the harness
51
+ * declares (`packages/client/ui-session/src/client/index.ts`), and the
52
+ * components assert it off their slot props. The `useSessions` hook needs no
53
+ * such restatement any more: nothing here reads the session list.
54
+ *
55
+ * The Remote's client-side types are restated structurally for the same reason
56
+ * and one more: a third-party plugin cannot reach the main repository's
57
+ * generated `typert.remote-client.d.ts`, and it does not need to — the protocol
58
+ * has no schema registry, so a contribution is plain data (see
59
+ * `LOOP_ENGINE_REMOTE_CONTRIBUTION`).
60
+ *
61
+ * @module dsh-loop-engine-allinone/client/session-engine
62
+ */
63
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
64
+ import { type LoopEngineId, type LoopEngineRefusalCode, type LoopEngineSelectResult, type SessionEngine, type SessionEngineReport } from '../agent-preset-ids.ts';
65
+ import { type ReloadPage } from './reload.ts';
66
+ /**
67
+ * The three-state engine answer, re-exported so the client half names one type.
68
+ * It is DEFINED in the zero-import shared module: the node half decides it
69
+ * (`src/engine-of-session.ts`) and this half only renders what it is told.
70
+ */
71
+ export type { SessionEngine } from '../agent-preset-ids.ts';
72
+ /**
73
+ * What one session's engine read answers: the engine it ACTUALLY runs, plus the
74
+ * engine a committed switch has recorded for it but not yet reached.
75
+ *
76
+ * Re-exported for the same reason as the three-state: the split between the two
77
+ * facts is decided on the host (`src/engine-of-session.ts`) and this half renders
78
+ * both without ever merging them — the pending engine belongs in a "not in force
79
+ * yet" marker, never in the place that names what the session runs.
80
+ */
81
+ export type { SessionEngineReport } from '../agent-preset-ids.ts';
82
+ /**
83
+ * The standard seat members this half reads off a session-scope slot.
84
+ *
85
+ * The identity, plus the one live fact the turn-status row needs: whether the
86
+ * session is mid-turn. The engine itself is no longer read from a framework
87
+ * hook, it is asked of the host through this plugin's own Remote.
88
+ */
89
+ export interface SessionSeat {
90
+ /**
91
+ * Current Session identity. Undefined only off a seat rendered without a
92
+ * session: the composer then picks the new-session default, and the header chip
93
+ * (which only ever renders with a session) shows nothing.
94
+ */
95
+ sessionId?: string;
96
+ /**
97
+ * Selector hook over the current Session's snapshot (`SessionStandardProps` of
98
+ * every session-scoped seat). Optional for the same reason as the identity: a
99
+ * caller with no such seat — or a test rendering one directly — has none, and
100
+ * the turn-status gate is then left alone rather than guessed.
101
+ */
102
+ useSession?: SnapshotSelector;
103
+ }
104
+ /**
105
+ * The selector-hook shape this half needs from `useSession`: a read of one field
106
+ * of the Session snapshot, with no equality function of its own.
107
+ */
108
+ export type SnapshotSelector = <S>(select: (snapshot: SessionRunningState) => S) => S;
109
+ /** The one field of the Session snapshot the turn-status row reads. */
110
+ export interface SessionRunningState {
111
+ /** Whether a turn is in flight for this session. */
112
+ readonly running: boolean;
113
+ }
114
+ /** Cordis service key AND wire namespace of the plugin's own Remote. */
115
+ export declare const LOOP_ENGINE_REMOTE_NAMESPACE = "loopEngine";
116
+ /** Reporting endpoint of the plugin's own Remote (mirrors `src/engine-remote.ts`). */
117
+ export declare const LOOP_ENGINE_REMOTE_METHOD = "engine";
118
+ /** Switching endpoint of the plugin's own Remote (mirrors `src/engine-remote.ts`). */
119
+ export declare const LOOP_ENGINE_REMOTE_SELECT_METHOD = "select";
120
+ /** One refused Remote call (harness: the error branch of `RemoteResult`). */
121
+ export interface RemoteFailure {
122
+ /** The host's own framing of the refusal. */
123
+ readonly message: string;
124
+ /** The refusal's detail map; a `reason` entry carries the unframed cause. */
125
+ readonly details?: Readonly<Record<string, unknown>>;
126
+ }
127
+ /** What `remote.loopEngine.engine` resolves to (harness: `RemoteResult<SessionEngineReport>`). */
128
+ export type SessionEngineResult = {
129
+ readonly ok: true;
130
+ readonly value: SessionEngineReport;
131
+ } | {
132
+ readonly ok: false;
133
+ readonly error: RemoteFailure;
134
+ };
135
+ /** What `remote.loopEngine.select` resolves to (harness: `RemoteResult<LoopEngineSelectResult>`). */
136
+ export type EngineSelectResult = {
137
+ readonly ok: true;
138
+ readonly value: LoopEngineSelectResult;
139
+ } | {
140
+ readonly ok: false;
141
+ readonly error: RemoteFailure;
142
+ };
143
+ /** This plugin's own Remote namespace, as the browser half calls it. */
144
+ export interface SessionEngineRemote {
145
+ /** Report the engine one session runs (and the recorded engine it is not running, if any). */
146
+ engine(request: {
147
+ readonly sessionId: string;
148
+ }): Promise<SessionEngineResult>;
149
+ /** Move one session to another engine, or answer why it was not moved. */
150
+ select(request: {
151
+ readonly sessionId: string;
152
+ readonly engine: LoopEngineId;
153
+ }): Promise<EngineSelectResult>;
154
+ }
155
+ /** Outcome of one per-session engine switch, ready to render. */
156
+ export type SessionSwitchResult = {
157
+ readonly ok: true;
158
+ /**
159
+ * Present when the host made the switch land by RELEASING the session's
160
+ * agent — a change with the harness loop on either side of it. This page has
161
+ * already been reloaded (and the session remembered for the page that
162
+ * replaces it) by the time a caller sees this, so a surface renders it as
163
+ * "the switch is happening" rather than as a session that can be typed into.
164
+ */
165
+ readonly reload?: true;
166
+ }
167
+ /** This page cannot reach the plugin's own Remote at all. */
168
+ | {
169
+ readonly ok: false;
170
+ readonly kind: 'unavailable';
171
+ }
172
+ /**
173
+ * The host refused the switch. `code` says which refusal it was, for the
174
+ * surface to render in the user's language, and `reason` is the host's own
175
+ * sentence about this session — detail under that copy, or the whole message
176
+ * when the code is absent (an unknown code, a rejected CALL, a lost
177
+ * connection: none of those has a refusal code, and none of them may leave the
178
+ * surface with nothing to show).
179
+ */
180
+ | {
181
+ readonly ok: false;
182
+ readonly kind: 'refused';
183
+ readonly reason: string;
184
+ readonly code?: LoopEngineRefusalCode;
185
+ };
186
+ /** Move one session to another engine. */
187
+ export type SessionEngineSwitcher = (sessionId: string, engine: LoopEngineId) => Promise<SessionSwitchResult>;
188
+ /**
189
+ * Whether a pick on one session's engine can be committed at all — that is,
190
+ * whether the host has answered what that session runs.
191
+ *
192
+ * This is the composer's usability gate, and it is hostile to guessing on
193
+ * purpose. The reload judgement below is a statement about two engines, and with
194
+ * no answer there is nothing honest to state: an unknown session may reload in
195
+ * EITHER direction. Answering the question anyway would cost the user something
196
+ * either way — `true` raises a confirmation for a switch that may not reload
197
+ * (and the confirmation is the only thing keeping an UNEXPLAINED reload from
198
+ * looking like a glitch), while `false` commits a pick that reloads and takes
199
+ * this page's unsent draft with it. So the picker is simply not usable until the
200
+ * answer lands: the trigger is greyed out with its menu unopenable, and it keeps
201
+ * reading「读取中…」so the user can see what it is waiting for. The instant the
202
+ * answer arrives the control becomes usable, unchanged.
203
+ *
204
+ * It is a TYPE PREDICATE as well as the runtime gate, so "no pick is ever judged
205
+ * against an unknown engine" is enforced by the compiler: the composer narrows a
206
+ * session's report through this before it asks {@link switchNeedsReload}, whose
207
+ * parameter therefore cannot be undefined.
208
+ *
209
+ * A seat with NO session is not this function's business and never calls it: it
210
+ * writes the default for sessions created later, moves nothing, and reloads
211
+ * nothing, so there is no engine to wait for.
212
+ * @param report - the session's engine report, or undefined while the host has
213
+ * not answered yet (or cannot answer at all).
214
+ * @returns whether the engine is known — the one condition under which a pick on
215
+ * that session may be judged and sent.
216
+ */
217
+ export declare function engineSwitchReady(report: SessionEngineReport | undefined): report is SessionEngineReport;
218
+ /**
219
+ * Whether moving a session from `current` to `target` has to land through a page
220
+ * reload — the one thing a surface must ask the user about BEFORE it commits,
221
+ * because the reload is what takes this page's unsent draft and scroll position
222
+ * with it.
223
+ *
224
+ * The rule is the host's own split (`src/router-loop.ts` `move`): a move with the
225
+ * harness loop on EITHER side cannot be handed over in place — `AgentLoop` neither
226
+ * hands a live session over nor accepts one it did not create — so the session's
227
+ * agent is released, the session goes cold, and the page reloads to rebuild it on
228
+ * the recorded engine. Between two hosted engines nothing reloads: the agent is
229
+ * swapped in place and the conversation stays open. So the move reloads exactly
230
+ * when the two engines differ in being the in-process one.
231
+ *
232
+ * `current` is the engine the session ACTUALLY runs — the report's `engine`, never
233
+ * the engine its record names, which is why this takes a {@link SessionEngine} and
234
+ * not a bare id:
235
+ *
236
+ * - a state that names an engine answers the rule above directly;
237
+ * - `legacy` and `unset` name no engine, and both are built on the in-process
238
+ * loop (`src/agent-preset-ids.ts` `hostedEngineOf` — the router falls back to
239
+ * the harness loop for a preset this plugin does not own), so they are read as
240
+ * the in-process side of the rule rather than as "unknown": picking any hosted
241
+ * engine on such a session really does reload.
242
+ *
243
+ * There is deliberately NO third branch for "the host has not answered yet": that
244
+ * state never reaches here, because the composer gates every pick on
245
+ * {@link engineSwitchReady} first (the picker is disabled until the answer lands),
246
+ * and the parameter's non-optional type is what holds that down. A judgement made
247
+ * without an answer would be a guess in one direction or the other, which is
248
+ * exactly what the gate exists to avoid.
249
+ *
250
+ * It is a pure function of the two engines on purpose: the composer's confirmation
251
+ * is only as trustworthy as this judgement, and this way it is pinned by a test
252
+ * rather than by reading the component.
253
+ * @param current - the engine the session runs now, as the host reports it. Never
254
+ * unknown: callers hold a report that passed {@link engineSwitchReady}.
255
+ * @param target - the engine the user picked.
256
+ * @returns whether committing that pick releases the session's agent and reloads
257
+ * this page.
258
+ */
259
+ export declare function switchNeedsReload(current: SessionEngine, target: LoopEngineId): boolean;
260
+ /** One boundary schema of a wire field (a zod schema, as the Gateway holds it). */
261
+ interface RemoteSchema {
262
+ parse(value: unknown): unknown;
263
+ }
264
+ /**
265
+ * A codec of one wire field (harness: `TypertCodec`, strict branch).
266
+ *
267
+ * The two harness generations read DIFFERENT members of this object and each
268
+ * validates only its own: the 0.1.5 line's registry and client parse through
269
+ * `schema`, while the 0.1.7 line's registry requires a `create()` factory and
270
+ * parses through `codec.create()` (`typert: … strict codec has no create()
271
+ * factory`). One build serves both, so the codec carries both fields.
272
+ */
273
+ interface RemoteCodec {
274
+ readonly mode: 'strict';
275
+ /** Stable identity of the field's declared type, for diagnostics. */
276
+ readonly typeSymbol: string;
277
+ /** The 0.1.5 line's read: the boundary schema itself. */
278
+ readonly schema: RemoteSchema;
279
+ /**
280
+ * The 0.1.7 line's read: a factory returning the boundary schema.
281
+ * @returns the same boundary schema.
282
+ */
283
+ readonly create: () => RemoteSchema;
284
+ }
285
+ /** One ordered business parameter (harness: `InvocationParameterDescriptor`). */
286
+ interface RemoteParameter {
287
+ readonly name: string;
288
+ /** Wire field this parameter occupies in `args`. */
289
+ readonly wire: string;
290
+ readonly source: 'json';
291
+ readonly codec: RemoteCodec;
292
+ }
293
+ /** One exported method (harness: `InvocationDescriptor`). */
294
+ interface RemoteMethod {
295
+ readonly id: string;
296
+ readonly service: string;
297
+ readonly namespace: string;
298
+ readonly method: string;
299
+ readonly invocation: {
300
+ readonly kind: 'direct';
301
+ };
302
+ readonly parameters: readonly RemoteParameter[];
303
+ readonly result: RemoteCodec;
304
+ }
305
+ /** One package's exported methods (harness: `TypertRemoteContribution`). */
306
+ export interface RemoteContribution {
307
+ readonly package: string;
308
+ readonly descriptors: readonly RemoteMethod[];
309
+ }
310
+ /** The Client Remote service, as much of it as this half calls (harness: `ClientRemote`). */
311
+ export interface EngineRemoteHost {
312
+ /** Install one contribution's namespaces for the calling fiber. */
313
+ $mount(contribution: RemoteContribution): Promise<() => Promise<void>>;
314
+ }
315
+ /** The plugin's npm package name, as the contribution identifies itself. */
316
+ export declare const LOOP_ENGINE_PACKAGE = "dsh-loop-engine-allinone";
317
+ /** The plugin's own Remote, declared for the Gateway's client mount. */
318
+ export declare const LOOP_ENGINE_REMOTE_CONTRIBUTION: RemoteContribution;
319
+ /**
320
+ * What one session's engine is, cached per session.
321
+ *
322
+ * Every read goes to the host's authoritative answer and is cached by session id
323
+ * so the chip and the composer in the same header cannot disagree. Until the
324
+ * first answer arrives a session has NO engine here: {@link read} returns
325
+ * undefined, and the surfaces render nothing (chip) or a neutral loading state
326
+ * (composer) rather than a guess — a default or a list hint would be exactly the
327
+ * misreport this class exists to end.
328
+ *
329
+ * What is cached is the whole {@link SessionEngineReport}, the recorded engine
330
+ * that differs from the live one included, because the three surfaces read
331
+ * different halves of it: the chip, the composer's label, and the chat
332
+ * turn-status row all name the engine the session RUNS, and only the first two
333
+ * also carry the "recorded, not in force" marker. Merging the two here would
334
+ * make that distinction unavailable downstream; leaving the report intact lets
335
+ * each surface take the half it renders.
336
+ *
337
+ * A cached answer is never treated as final for the page's lifetime: a session
338
+ * the user comes back to is re-read when its surfaces appear again
339
+ * ({@link watch} → {@link refresh}). Coming back does not move the session — a
340
+ * switch is performed on the host, between hosted engines it swaps the agent in
341
+ * place, and the direction that has to release the session reloads this very
342
+ * page (`./reload.ts`) — but the answer this page holds is still only one this
343
+ * page took earlier: another window, a hot swap performed elsewhere, or a plugin
344
+ * reload can have moved the session since. The answer a surface holds is
345
+ * therefore correct for as long as that surface is on screen, and refreshed at
346
+ * every re-mount — not only when a switch this page performed invalidated it
347
+ * ({@link invalidate}).
348
+ *
349
+ * Its answer is also what the chat turn-status row draws (the third place a
350
+ * session's engine is shown), but through the surfaces rather than through this
351
+ * class: the row follows the session on screen because the hook both surfaces
352
+ * share reflects that session's answer with a focus guard — the same authority
353
+ * the chip and the composer read, where the settings default it used to follow
354
+ * was not. Nothing here writes to the document; see `./turn-status.ts` and
355
+ * `./use-session-engine.ts`.
356
+ */
357
+ export declare class SessionEngineCache {
358
+ private readonly engines;
359
+ private readonly watchers;
360
+ private readonly reading;
361
+ /**
362
+ * Monotonic read generation per session. A read started before an
363
+ * invalidation describes the session as it was BEFORE the switch, so however
364
+ * late it settles it must not publish — publishing it would put the old engine
365
+ * back on screen, which is the very misreport this cache exists to prevent.
366
+ */
367
+ private readonly generations;
368
+ private remote;
369
+ private disposed;
370
+ /**
371
+ * Attach the mounted Remote namespace. Values asked for before the mount
372
+ * settled are re-read, so a surface that mounted first is not left loading.
373
+ * @param remote - the namespace, or undefined when the mount failed.
374
+ */
375
+ attach(remote: SessionEngineRemote | undefined): void;
376
+ /**
377
+ * The engine report cached for one session, or undefined while the first
378
+ * answer is still in flight (or when the host could not answer at all).
379
+ * @param sessionId - the session whose engine is asked for.
380
+ * @returns the authoritative report, cached by session id.
381
+ */
382
+ read(sessionId: string): SessionEngineReport | undefined;
383
+ /**
384
+ * Follow one session's engine, re-reading it whenever a session's surfaces
385
+ * appear again.
386
+ *
387
+ * A watch is a surface appearing for this session, and that is the moment the
388
+ * cached answer can be stale in a way nothing else reports: a session whose
389
+ * surfaces all went away and came back — the page switched to another session
390
+ * and back — must not render what the page cached before. A re-mount is not
391
+ * what moves a session's engine (a switch is performed on the host, and either
392
+ * swaps the agent in place or releases the session and reloads this page), but
393
+ * the page's answer is old by then, and a surface that rendered it as current
394
+ * would be the one lying about what runs.
395
+ *
396
+ * Only the FIRST watcher of a session triggers the re-read, which is what
397
+ * makes this "the session's surfaces appeared" rather than "a component
398
+ * rendered": the chip and the composer are two watchers of one session, and
399
+ * the second joins the read the first one started instead of sending its own.
400
+ * While a session keeps its surfaces, nothing here asks again — the answer only
401
+ * changes through {@link invalidate} (a committed switch) or a later re-mount.
402
+ * @param sessionId - the session to follow.
403
+ * @param listener - called whenever that session's cached state changes.
404
+ * @returns the unsubscribe.
405
+ */
406
+ watch(sessionId: string, listener: () => void): () => void;
407
+ /**
408
+ * Ask the host for one session's engine report again, superseding nothing.
409
+ *
410
+ * This is the way a surface that has just been rendered asks "is what this page
411
+ * cached still what runs?" — the answer that can go stale without anyone
412
+ * invalidating it, because the page is not the only thing that can move a
413
+ * session (another window, a hot swap elsewhere, a plugin reload).
414
+ * A read already in flight for that session is joined rather than replaced (it
415
+ * was started no earlier than this call and describes the same moment), and the
416
+ * cached answer is kept until the new one lands, so a re-read never blinks the
417
+ * chip back to "reading". Use {@link invalidate} instead for an answer that is
418
+ * known to be WRONG.
419
+ * @param sessionId - the session whose answer should be re-read.
420
+ */
421
+ refresh(sessionId: string): void;
422
+ /**
423
+ * Forget one session's cached engine and ask the host again.
424
+ *
425
+ * A successful engine switch changes what the host answers for that session,
426
+ * so the picker MUST come through here: without it the composer would keep
427
+ * showing the engine the session no longer runs. Any read already in flight
428
+ * for that session is superseded and can no longer publish.
429
+ * @param sessionId - the session whose answer is now stale.
430
+ */
431
+ invalidate(sessionId: string): void;
432
+ /** Drop every cached answer and notification (plugin unload). */
433
+ dispose(): void;
434
+ /**
435
+ * Ask unless the session is already answered, being read, or unanswerable.
436
+ *
437
+ * For the paths that need an answer and have none: the plugin's mount settling
438
+ * (a surface asked before the Remote existed), a session whose cached answer
439
+ * was just dropped — and never a re-read of an answer this page already holds,
440
+ * which is {@link refresh}'s job and only a freshly rendered surface may ask
441
+ * for.
442
+ */
443
+ private readIfUnknown;
444
+ /**
445
+ * Start one read for a session, unless there is nothing to read it with.
446
+ * @param sessionId - the session to read.
447
+ */
448
+ private start;
449
+ /**
450
+ * One read, quiet on failure: a session the host cannot answer for stays
451
+ * unknown, and the surfaces keep their neutral state instead of claiming an
452
+ * engine. A refusal is not cached as an answer, so the next watch re-asks.
453
+ * @param remote - the mounted namespace.
454
+ * @param sessionId - the session asked about.
455
+ * @param generation - the read generation this answer belongs to.
456
+ */
457
+ private ask;
458
+ /**
459
+ * Notify one session's watchers.
460
+ *
461
+ * Nothing is painted here. The session's engine shows up in the chat
462
+ * turn-status row too, but that row hangs off a DOCUMENT-level attribute, whose
463
+ * only entitled owner is the session on screen — and a publish says nothing
464
+ * about what is on screen (this is called for every session a surface has ever
465
+ * watched, and for a session the user has already left, when its late answer
466
+ * lands). So painting here let a background session take the attribute over
467
+ * from the session on screen. The surfaces reflect instead, with the focus
468
+ * guard in `./turn-status.ts`; this is the notification the chip and the
469
+ * composer wake up on.
470
+ */
471
+ private publish;
472
+ }
473
+ /**
474
+ * Mount this plugin's own engine Remote and return the cache the session
475
+ * surfaces render from.
476
+ *
477
+ * `remote` is taken lazily rather than declared in the client half's `inject`:
478
+ * the settings page must keep working on a page that composes no Gateway, and
479
+ * the two session surfaces degrade to "engine not recorded" — never to a guess.
480
+ * @param ctx - the client root context.
481
+ * @returns the cache both session surfaces and the switcher share.
482
+ */
483
+ export declare function createSessionEngineCache(ctx: ClientContext): SessionEngineCache;
484
+ /**
485
+ * Build the per-session switch over this plugin's own Remote.
486
+ *
487
+ * Every refusal is reported rather than thrown: the host's own reason when it
488
+ * refused (the session is not open, it is mid-turn, it belongs to subagent
489
+ * routing, or the engine record could not be written), and this page's own words
490
+ * when the namespace is not reachable. A switch that DID land drops the cached
491
+ * answer, because the host's read for that session just changed.
492
+ *
493
+ * A switch the host had to make by RELEASING the session's agent is finished
494
+ * here rather than reported: the page is reloaded — the only action that clears
495
+ * the client state a `session/disposed` leaves behind — and the session is
496
+ * stashed for the page that replaces it, so the reload lands back on it
497
+ * (`./reload.ts` explains why each half is necessary). That is why this function
498
+ * owns the reload instead of leaving it to a component: it is the one place that
499
+ * knows the host released the agent, and it keeps the sequence (invalidate →
500
+ * stash → reload) in one testable step.
501
+ * @param ctx - the client context carrying the Remote namespace.
502
+ * @param onSwitched - called with the session id once the host accepted the
503
+ * switch, so the caller can drop the cached engine it just changed.
504
+ * @param page - the page to reload and to stash the return in.
505
+ * @returns the switch the composer's picker commits through.
506
+ */
507
+ export declare function sessionEngineSwitcher(ctx: ClientContext, onSwitched?: (sessionId: string) => void, page?: ReloadPage): SessionEngineSwitcher;
508
+ //# sourceMappingURL=session-engine.d.ts.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Loop engine selection store: a settings transport is the channel, and the
3
+ * store publishes a render-safe snapshot plus the write path. The transport is
4
+ * abstracted so one store follows either generation's client service — the
5
+ * 0.1.7 `ConfigForm` for the plugin's own profile entry, or the 0.1.5
6
+ * `SettingsScope` for its `agent-loop-engine` settings section.
7
+ * @module dsh-loop-engine-allinone/client/store
8
+ */
9
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
10
+ import type { LoopEngineId } from '../agent-preset-ids.ts';
11
+ import type { LoopEngineSettings } from '../settings.ts';
12
+ /** State rendered by the loop engine section. */
13
+ export interface LoopEngineState {
14
+ status: 'loading' | 'ready' | 'unavailable' | 'saving';
15
+ engine: LoopEngineId;
16
+ showInComposer: boolean;
17
+ showEngineBadge: boolean;
18
+ writable: boolean;
19
+ error: string | null;
20
+ }
21
+ /** The render-relevant slice of a settings snapshot, shared by both generations' transports. */
22
+ export interface LoopEngineSettingsSnapshot {
23
+ status: 'loading' | 'ready' | 'unavailable';
24
+ value: LoopEngineSettings | undefined;
25
+ writable: boolean;
26
+ }
27
+ /**
28
+ * The settings channel the store follows. Satisfied by the 0.1.7 `ConfigForm`
29
+ * and by the 0.1.5 `SettingsScope` (through a thin adapter in `./index.ts`); the
30
+ * write's boolean is optional because the 0.1.5 scope reports acceptance only
31
+ * through the snapshot it leaves behind.
32
+ */
33
+ export interface LoopEngineSettingsTransport {
34
+ /** @returns the current sync snapshot. */
35
+ getSnapshot(): LoopEngineSettingsSnapshot;
36
+ /**
37
+ * Observe snapshot replacements.
38
+ * @param listener - invoked after each snapshot change.
39
+ * @returns the disposer removing the listener.
40
+ */
41
+ subscribe(listener: () => void): () => void;
42
+ /**
43
+ * Persist one field.
44
+ * @param field - the field to write.
45
+ * @param value - the value to write.
46
+ * @returns whether the write was accepted, when the transport reports it.
47
+ */
48
+ set(field: 'engine' | 'showInComposer' | 'showEngineBadge', value: unknown): Promise<boolean | void>;
49
+ }
50
+ /**
51
+ * Narrow a wire section to the stored engine id and display toggles; an invalid
52
+ * one reads default. Used by the 0.1.5 `settingsScope.bind` decoder, whose scope
53
+ * receives raw wire sections.
54
+ * @param section - the wire section value.
55
+ * @returns the decoded selection, or `undefined` when the section is unusable.
56
+ */
57
+ export declare function decodeLoopEngine(section: unknown): {
58
+ engine: LoopEngineId;
59
+ showInComposer: boolean;
60
+ showEngineBadge: boolean;
61
+ } | undefined;
62
+ /** Coordinates the settings-backed loop engine selection. */
63
+ export declare class LoopEngineStore {
64
+ private readonly transport;
65
+ /** uSES-safe state source shared by the registered settings section. */
66
+ readonly store: SnapshotStore<LoopEngineState>;
67
+ private following;
68
+ private saving;
69
+ /**
70
+ * @param transport - the settings channel the store follows.
71
+ */
72
+ constructor(transport: LoopEngineSettingsTransport);
73
+ /** Begin following the transport and publish its current answer. */
74
+ load(): void;
75
+ /**
76
+ * Persist the selected engine. Success is the transport's accepted answer
77
+ * (when it reports one) checked against the snapshot the write left behind,
78
+ * so a refused write reports error after its recovery.
79
+ * @param engine - the engine to select for future Agent turns.
80
+ * @returns whether the write landed.
81
+ */
82
+ setEngine(engine: LoopEngineId): Promise<boolean>;
83
+ /**
84
+ * Persist whether the composer shows the engine picker. Unlike
85
+ * {@link setEngine}, landing does not reload the page — the toggle only
86
+ * changes composer visibility.
87
+ * @param show - whether the chat page composer reveals the engine picker.
88
+ * @returns whether the write landed.
89
+ */
90
+ setShowInComposer(show: boolean): Promise<boolean>;
91
+ /**
92
+ * Persist whether the conversation header shows the engine badge. Unlike
93
+ * {@link setEngine}, landing does not reload the page — the toggle only
94
+ * changes header visibility.
95
+ * @param show - whether the conversation header reveals the engine badge.
96
+ * @returns whether the write landed.
97
+ */
98
+ setShowEngineBadge(show: boolean): Promise<boolean>;
99
+ /** Stop following the transport. */
100
+ dispose(): void;
101
+ private derive;
102
+ }
103
+ //# sourceMappingURL=store.d.ts.map