@rebon/cli-win32-x64 1.2.0 → 1.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 (98) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +203 -144
  4. package/THIRD_PARTY_NOTICES.txt +1232 -128
  5. package/bin/rebon.js +22 -22
  6. package/lib/windows-managed.js +390 -390
  7. package/package.json +9 -2
  8. package/payload/compose-runtime/package.json +17 -17
  9. package/payload/compose-runtime/payload/compose/shims/dsh-anonymous-user-id.js +10 -10
  10. package/payload/compose-runtime/payload/compose/shims/dsh-credentials.js +13 -13
  11. package/payload/compose-runtime/payload/compose/shims/dsh-launch-environment.js +21 -21
  12. package/payload/compose-runtime/payload/compose/shims/dsh-llm.js +313 -313
  13. package/payload/compose-runtime/payload/compose/shims/dsh-settings.js +43 -43
  14. package/payload/compose-runtime/payload/compose/shims/dsh-tools.js +20 -20
  15. package/payload/compose-runtime/payload/compose/shims/dsh-web.js +10 -10
  16. package/payload/compose-runtime/payload/compose/shims/llm-adapter.js +32 -32
  17. package/payload/compose-runtime/payload/compose/shims/zod-lite.js +49 -49
  18. package/payload/compose-runtime/payload/vendor/cordis/LICENSE +21 -0
  19. package/payload/compose-runtime/payload/vendor/cordis/index.js +1530 -1530
  20. package/payload/compose-runtime/payload/vendor/cosmokit/LICENSE +21 -21
  21. package/payload/compose-runtime/payload/vendor/cosmokit/index.mjs +357 -357
  22. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-agent +21 -21
  23. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-agent-loop +21 -21
  24. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-llm-core +21 -21
  25. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-llm-deepseek +21 -21
  26. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-logger-console +21 -21
  27. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-scope +21 -21
  28. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-session +21 -21
  29. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-system-prompt +21 -21
  30. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-timeout +21 -21
  31. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-timer +21 -21
  32. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tool-todo +21 -21
  33. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tool-web +21 -21
  34. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tools-schema +21 -21
  35. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-turndown +21 -0
  36. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-turndown-plugin-gfm +21 -0
  37. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-web-search-exa +21 -21
  38. package/payload/compose-runtime/payload/vendor/dsh/agent-loop.js +1193 -1193
  39. package/payload/compose-runtime/payload/vendor/dsh/agent.js +714 -714
  40. package/payload/compose-runtime/payload/vendor/dsh/llm-core.js +269 -269
  41. package/payload/compose-runtime/payload/vendor/dsh/llm-deepseek.js +672 -672
  42. package/payload/compose-runtime/payload/vendor/dsh/logger-console.js +83 -83
  43. package/payload/compose-runtime/payload/vendor/dsh/scope.js +287 -287
  44. package/payload/compose-runtime/payload/vendor/dsh/session.js +1668 -1668
  45. package/payload/compose-runtime/payload/vendor/dsh/system-prompt.js +309 -309
  46. package/payload/compose-runtime/payload/vendor/dsh/timeout.js +100 -100
  47. package/payload/compose-runtime/payload/vendor/dsh/timer.js +128 -128
  48. package/payload/compose-runtime/payload/vendor/dsh/tool-todo.js +143 -143
  49. package/payload/compose-runtime/payload/vendor/dsh/tool-web.js +1527 -1527
  50. package/payload/compose-runtime/payload/vendor/dsh/tools-schema.js +849 -849
  51. package/payload/compose-runtime/payload/vendor/dsh/web-search-exa.js +122 -122
  52. package/payload/compose-runtime/payload/vendor/eventsource-parser/LICENSE +21 -21
  53. package/payload/compose-runtime/payload/vendor/eventsource-parser/index.js +177 -177
  54. package/payload/compose-runtime/payload/vendor/eventsource-parser/stream.js +48 -48
  55. package/payload/compose-runtime/payload/vendor/schemastery/LICENSE +21 -0
  56. package/payload/compose-runtime/payload/vendor/schemastery/index.mjs +656 -656
  57. package/payload/compose-runtime/payload-manifests.json +48 -48
  58. package/payload/compose-runtime/src/bridge.mjs +193 -195
  59. package/payload/compose-runtime/src/credentials-runtime.mjs +50 -50
  60. package/payload/compose-runtime/src/eventsource-stream.mjs +48 -48
  61. package/payload/compose-runtime/src/index.mjs +7 -7
  62. package/payload/compose-runtime/src/llm-runtime.mjs +326 -326
  63. package/payload/compose-runtime/src/loader.mjs +78 -78
  64. package/payload/compose-runtime/src/loop-assembly.mjs +237 -237
  65. package/payload/compose-runtime/src/payload.mjs +49 -49
  66. package/payload/compose-runtime/src/plugin.mjs +59 -59
  67. package/payload/compose-runtime/src/realm.mjs +223 -223
  68. package/payload/compose-runtime/src/registry.mjs +191 -191
  69. package/payload/compose-runtime/src/resolve.mjs +115 -115
  70. package/payload/compose-runtime/src/systemprompt-runtime.mjs +67 -67
  71. package/payload/compose-runtime/src/tools-runtime.mjs +239 -239
  72. package/payload/compose-runtime/src/web-runtime.mjs +209 -209
  73. package/payload/plugin-host/package.json +11 -11
  74. package/payload/plugin-host/src/bridge.mjs +64 -64
  75. package/payload/plugin-host/src/cli.mjs +147 -147
  76. package/payload/plugin-host/src/framing.mjs +113 -113
  77. package/payload/plugin-host/src/host.mjs +691 -691
  78. package/payload/plugin-host/src/json.mjs +208 -208
  79. package/payload/plugin-host/src/lifecycle.mjs +114 -114
  80. package/payload/plugin-host/src/loader.mjs +183 -183
  81. package/payload/plugin-host/src/methods.mjs +377 -377
  82. package/payload/plugin-host/src/ownership.mjs +57 -57
  83. package/payload/plugin-host/src/protocol.mjs +129 -129
  84. package/payload/plugin-host/src/sdk.mjs +7 -7
  85. package/payload/rebon-boa-helper.exe +0 -0
  86. package/payload/rebon-browser-mcp.exe +0 -0
  87. package/payload/rebon-computer-use.exe +0 -0
  88. package/payload/rebon-lsp-mcp.exe +0 -0
  89. package/payload/rebon.exe +2 -2
  90. package/payload/sandbox-win.exe +0 -0
  91. package/scripts/postinstall.js +112 -112
  92. package/payload/compose-runtime/payload/compose/credentials-runtime.js +0 -38
  93. package/payload/compose-runtime/payload/compose/llm-runtime.js +0 -330
  94. package/payload/compose-runtime/payload/compose/loop-assembly.js +0 -149
  95. package/payload/compose-runtime/payload/compose/serve-dispatch.js +0 -82
  96. package/payload/compose-runtime/payload/compose/systemprompt-runtime.js +0 -74
  97. package/payload/compose-runtime/payload/compose/tools-runtime.js +0 -228
  98. package/payload/compose-runtime/payload/compose/web-runtime.js +0 -204
@@ -1,326 +1,326 @@
1
- // The composition's `ctx.llm` seat, on the plugin plane.
2
- //
3
- // Ported from `js/compose/llm-runtime.js`. The dsh-facing surface is
4
- // unchanged — adapters extend `LlmAdapter`, register with
5
- // `ctx.llm.registerAdapter(providers, adapter)`, and in-composition consumers
6
- // (the agent loop) still go through `prepareCall` / `stream` — because that
7
- // surface is dsh's contract and the transport underneath it is not dsh's
8
- // business.
9
- //
10
- // What changed is everything that was transport:
11
- //
12
- // * **The pump is gone.** `_pump` parked on `op_llm_next` and `_dispatch`
13
- // wrote chunks back with `op_llm_emit`. On the plugin plane a model turn
14
- // arrives as an `llm/stream` request routed by provider, and its chunks go
15
- // out on that call. What `registerAdapter` hands the registration sink is
16
- // the handler for that method; there is nothing left to pump.
17
- // * **Announcing is not a call.** `_announce` used to push each route into
18
- // the kernel's `model-router` with `callService('model-router',
19
- // 'register', …)`. Registration is a protocol declaration now: the route
20
- // is reported by `plugin/load`, and its model catalog by the composition's
21
- // own `compose` service.
22
- // * **The disposer only forgets locally.** It used to issue the
23
- // compensating `callService('model-router', 'unregister', …)`. A Cordis
24
- // disposer is synchronous and a cross-process call is not, so that call
25
- // could not survive the move — and does not need to. Revocation is what
26
- // `plugin/unload` draining is for.
27
- import { Service } from 'cordis';
28
- import {
29
- LlmError,
30
- deepFreeze,
31
- callConfigEquals,
32
- errorChain,
33
- resolveRetryPolicy,
34
- } from '@deepseek-ai/dsh-llm';
35
- import { withCall } from './bridge.mjs';
36
- import { sinkOf } from './registry.mjs';
37
-
38
- // Re-exported from the payload so a plugin extending `LlmAdapter` from
39
- // `@deepseek-ai/dsh-llm` and the runtime checking one share a single class.
40
- export { LlmAdapter } from '@deepseek-ai/dsh-llm';
41
-
42
- /**
43
- * Normalize an adapter throw into dsh's terminal finish chunk
44
- * (`adapterFailureChunk`, transcribed lean: LlmError keeps its facts,
45
- * anything else flattens under UNKNOWN — the same rule the loop itself
46
- * applies at its turn boundary).
47
- */
48
- function failureChunk(error, signal) {
49
- const failure = error instanceof LlmError
50
- ? error.failure
51
- : { message: errorChain(error), code: 'UNKNOWN' };
52
- return {
53
- type: 'finish',
54
- reason: signal?.aborted || failure.code === 'ABORTED'
55
- ? { kind: 'aborted', failure }
56
- : { kind: 'error', failure },
57
- };
58
- }
59
-
60
- export default class RebonLlmRuntime extends Service {
61
- constructor(ctx) {
62
- super(ctx, 'llm');
63
- this.routes = new Map(); // provider -> adapter
64
- /** Catalog reads still in flight; `settle()` is what waits for them. */
65
- this.pending = new Set();
66
- }
67
-
68
- registerAdapter(providers, adapter) {
69
- if (!providers?.length) {
70
- throw new Error('an adapter must register at least one provider');
71
- }
72
- for (const provider of providers) {
73
- if (this.routes.has(provider)) {
74
- throw new Error(`an adapter for provider "${provider}" is already registered`);
75
- }
76
- }
77
- const runtime = this;
78
- // Read synchronously, before any await can interleave: which plugin is
79
- // registering is a fact about *now*, not about whenever this resolves.
80
- const sink = sinkOf(this.ctx);
81
- for (const provider of providers) {
82
- sink?.llm(provider, (request, callCtx) =>
83
- withCall(callCtx, () => runtime.serve(provider, request, callCtx)));
84
- }
85
- this.ctx.effect(() => {
86
- for (const provider of providers) {
87
- runtime.routes.set(provider, adapter);
88
- runtime._read(provider, adapter, sink);
89
- }
90
- return () => {
91
- // Local only. What rebon knows about these routes was reported at
92
- // load and is withdrawn by unload; a disposer cannot tell it anything
93
- // because a disposer cannot await an answer.
94
- for (const provider of providers) runtime.routes.delete(provider);
95
- };
96
- });
97
- // dsh handle shape: route replacement exists on it, but a static
98
- // composition never re-reads registration facts, so a call here is a
99
- // consumer bug — refuse loudly instead of silently diverging from dsh.
100
- return {
101
- providers: [...providers],
102
- replace() {
103
- throw new Error('adapter route replacement is not supported in a static composition');
104
- },
105
- };
106
- }
107
-
108
- /**
109
- * dsh surfaces these entries to its settings UI. The composition has no
110
- * such surface; record them so diagnostics can list what a composition
111
- * declared, and accept the call so unmodified dsh plugins load.
112
- */
113
- registerConfigurableProviders(entries) {
114
- this.configurableProviders ??= [];
115
- for (const entry of entries ?? []) {
116
- if (entry?.provider) {
117
- this.configurableProviders.push({
118
- provider: String(entry.provider),
119
- displayName: entry.displayName === undefined ? undefined : String(entry.displayName),
120
- });
121
- }
122
- }
123
- }
124
-
125
- // ---- JS consumer face ----
126
- //
127
- // These serve consumers INSIDE the composition (the agent loop); `serve()`
128
- // serves requests from rebon. Routing is the same in-JS adapter table.
129
- // v1 honesty boundary: no `llm/stream` middleware waterfall — dispatch goes
130
- // straight to the owning adapter.
131
-
132
- /** Resolve the owning adapter or refuse with dsh's NO_ADAPTER code. */
133
- _registration(provider) {
134
- const adapter = this.routes.get(provider);
135
- if (!adapter) {
136
- throw new LlmError(`no adapter registered for provider "${provider}"`, 'NO_ADAPTER');
137
- }
138
- return adapter;
139
- }
140
-
141
- /**
142
- * dsh `LlmRuntime.prepareCall`, transcribed: resolve the route's exact
143
- * model, materialize adapter defaults (recorded in `adapterDefaults` so
144
- * the loop's request header can re-resolve them later), and hand back a
145
- * one-shot stream bound to this registration.
146
- */
147
- async prepareCall(config, signal) {
148
- const adapter = this._registration(config.provider);
149
- const info = (await adapter.resolveModel(config.provider, config.model)) ?? {};
150
- signal?.throwIfAborted?.();
151
- const defaulted = config.maxTokens === undefined && info.defaultMaxTokens !== undefined
152
- ? { ...config, maxTokens: info.defaultMaxTokens }
153
- : config;
154
- const reasoning = info.reasoning;
155
- const requested = defaulted.reasoningEffort;
156
- let resolvedConfig = defaulted;
157
- if (reasoning === undefined) {
158
- if (requested !== undefined) {
159
- throw new LlmError(
160
- `provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`,
161
- 'UNSUPPORTED_REASONING_EFFORT',
162
- );
163
- }
164
- } else {
165
- const effective = requested ?? reasoning.defaultEffort;
166
- if (effective !== undefined) {
167
- if (!(reasoning.efforts ?? []).some((effort) => effort.id === effective)) {
168
- throw new LlmError(
169
- `provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`,
170
- 'UNSUPPORTED_REASONING_EFFORT',
171
- );
172
- }
173
- if (requested !== effective) resolvedConfig = { ...defaulted, reasoningEffort: effective };
174
- }
175
- }
176
- const frozen = deepFreeze(structuredClone(resolvedConfig));
177
- const context = info.context === undefined
178
- ? undefined
179
- : deepFreeze(structuredClone(info.context));
180
- const adapterDefaults = deepFreeze({
181
- ...(config.reasoningEffort === undefined && frozen.reasoningEffort !== undefined
182
- ? { reasoningEffort: true }
183
- : {}),
184
- ...(config.maxTokens === undefined && frozen.maxTokens !== undefined
185
- ? { maxTokens: true }
186
- : {}),
187
- });
188
- const runtime = this;
189
- let dispatched = false;
190
- return Object.freeze({
191
- config: frozen,
192
- // dsh contract: providerRetryPolicy returns an ALREADY-RESOLVED
193
- // policy (or undefined for the normal defaults) — never re-resolve.
194
- retryPolicy: adapter.providerRetryPolicy(config.provider)
195
- ?? resolveRetryPolicy(undefined, `llm: provider "${config.provider}" retryPolicy`),
196
- adapterDefaults,
197
- ...(context === undefined ? {} : { context }),
198
- stream(options) {
199
- if (dispatched) {
200
- throw new LlmError('a prepared LLM call can only be dispatched once', 'INVALID_PREPARED_CALL');
201
- }
202
- if (!callConfigEquals(options, frozen)) {
203
- throw new LlmError('prepared LLM call config changed before adapter dispatch', 'INVALID_PREPARED_CALL');
204
- }
205
- dispatched = true;
206
- return runtime._consumerStream(options);
207
- },
208
- });
209
- }
210
-
211
- /** Consumer fallback entry (the loop's NO_ADAPTER path): direct dispatch. */
212
- stream(options) {
213
- return this._consumerStream(options);
214
- }
215
-
216
- /**
217
- * Adapter boundary for in-composition consumers: selection, dispatch, and
218
- * iteration failures become one terminal failure chunk (dsh
219
- * `adapterStream` semantics) so the loop's assembler/request-error path
220
- * sees them instead of a raw throw.
221
- */
222
- async *_consumerStream(options) {
223
- let iterator;
224
- try {
225
- const adapter = this._registration(options.provider);
226
- iterator = adapter.stream(options)[Symbol.asyncIterator]();
227
- } catch (error) {
228
- yield failureChunk(error, options.signal);
229
- return;
230
- }
231
- while (true) {
232
- let item;
233
- try {
234
- item = await iterator.next();
235
- } catch (error) {
236
- yield failureChunk(error, options.signal);
237
- return;
238
- }
239
- if (item.done) return;
240
- yield item.value;
241
- }
242
- }
243
-
244
- // ---- what rebon is told, and what rebon asks for ----
245
-
246
- /**
247
- * Read one route's catalog for the load report.
248
- *
249
- * `listModels` is advisory in dsh and can take a network round trip, so a
250
- * failure is recorded rather than raised: a provider with no catalog is
251
- * still a working route whose callers name models explicitly. The route
252
- * table is the liveness witness — an adapter disposed while its catalog was
253
- * being read is no longer the live one, and its answer is dropped.
254
- */
255
- _read(provider, adapter, sink) {
256
- if (sink === undefined) return;
257
- const task = (async () => {
258
- let models = [];
259
- try {
260
- models = (await adapter.listModels(provider)) ?? [];
261
- } catch (error) {
262
- if (this.routes.get(provider) !== adapter) return;
263
- sink.catalog(provider, { models: [], defaultModel: 'default', catalogError: String(error?.message ?? error) });
264
- return;
265
- }
266
- if (this.routes.get(provider) !== adapter) return;
267
- // A key whose value is `undefined` is not JSON, and this report crosses
268
- // the wire: an unknown context window is an absent field, not a present
269
- // one holding nothing.
270
- const entries = models
271
- .filter((m) => m?.id)
272
- .map((m) => {
273
- const window = m.context?.window ?? m.contextWindow;
274
- return { id: m.id, ...(window === undefined ? {} : { contextWindow: window }) };
275
- });
276
- sink.catalog(provider, {
277
- models: entries,
278
- // The catalog is advisory (dsh semantics): with an empty one the route
279
- // still exists and callers name models explicitly; "default" is only
280
- // the resolve fallback for bare `{provider}` lookups.
281
- defaultModel: entries[0]?.id ?? 'default',
282
- });
283
- })();
284
- this.pending.add(task);
285
- void task.finally(() => this.pending.delete(task));
286
- }
287
-
288
- /** Waits for every catalog read in flight, so the load report is complete. */
289
- async settle() {
290
- while (this.pending.size > 0) await Promise.all([...this.pending]);
291
- }
292
-
293
- /**
294
- * Serves one `llm/stream` call: chunks out on the call, adapter throws left
295
- * to become its terminal.
296
- *
297
- * Deliberately the raw adapter rather than `_consumerStream`: an in-JS
298
- * consumer wants a failure flattened into a finish chunk it can assemble,
299
- * whereas rebon wants the call to end as an error carrying the adapter's own
300
- * machine code. Flattening here would report every failed turn as a
301
- * successful one whose last chunk happens to say otherwise.
302
- */
303
- async serve(provider, request, ctx) {
304
- const adapter = this._registration(provider);
305
- const options = {
306
- provider,
307
- model: request.model,
308
- messages: request.messages ?? [],
309
- system: request.system ?? undefined,
310
- tools: request.tools ?? undefined,
311
- maxTokens: request.maxTokens ?? undefined,
312
- temperature: request.temperature ?? undefined,
313
- stop: request.stop ?? undefined,
314
- reasoningEffort: request.reasoningEffort ?? undefined,
315
- sessionId: request.sessionId ?? undefined,
316
- purpose: request.purpose ?? undefined,
317
- signal: ctx.signal,
318
- };
319
- let count = 0;
320
- for await (const chunk of adapter.stream(options)) {
321
- await ctx.emit(chunk);
322
- count += 1;
323
- }
324
- return { chunks: count };
325
- }
326
- }
1
+ // The composition's `ctx.llm` seat, on the plugin plane.
2
+ //
3
+ // Ported from `js/compose/llm-runtime.js`. The dsh-facing surface is
4
+ // unchanged — adapters extend `LlmAdapter`, register with
5
+ // `ctx.llm.registerAdapter(providers, adapter)`, and in-composition consumers
6
+ // (the agent loop) still go through `prepareCall` / `stream` — because that
7
+ // surface is dsh's contract and the transport underneath it is not dsh's
8
+ // business.
9
+ //
10
+ // What changed is everything that was transport:
11
+ //
12
+ // * **The pump is gone.** `_pump` parked on `op_llm_next` and `_dispatch`
13
+ // wrote chunks back with `op_llm_emit`. On the plugin plane a model turn
14
+ // arrives as an `llm/stream` request routed by provider, and its chunks go
15
+ // out on that call. What `registerAdapter` hands the registration sink is
16
+ // the handler for that method; there is nothing left to pump.
17
+ // * **Announcing is not a call.** `_announce` used to push each route into
18
+ // the kernel's `model-router` with `callService('model-router',
19
+ // 'register', …)`. Registration is a protocol declaration now: the route
20
+ // is reported by `plugin/load`, and its model catalog by the composition's
21
+ // own `compose` service.
22
+ // * **The disposer only forgets locally.** It used to issue the
23
+ // compensating `callService('model-router', 'unregister', …)`. A Cordis
24
+ // disposer is synchronous and a cross-process call is not, so that call
25
+ // could not survive the move — and does not need to. Revocation is what
26
+ // `plugin/unload` draining is for.
27
+ import { Service } from 'cordis';
28
+ import {
29
+ LlmError,
30
+ deepFreeze,
31
+ callConfigEquals,
32
+ errorChain,
33
+ resolveRetryPolicy,
34
+ } from '@deepseek-ai/dsh-llm';
35
+ import { withCall } from './bridge.mjs';
36
+ import { sinkOf } from './registry.mjs';
37
+
38
+ // Re-exported from the payload so a plugin extending `LlmAdapter` from
39
+ // `@deepseek-ai/dsh-llm` and the runtime checking one share a single class.
40
+ export { LlmAdapter } from '@deepseek-ai/dsh-llm';
41
+
42
+ /**
43
+ * Normalize an adapter throw into dsh's terminal finish chunk
44
+ * (`adapterFailureChunk`, transcribed lean: LlmError keeps its facts,
45
+ * anything else flattens under UNKNOWN — the same rule the loop itself
46
+ * applies at its turn boundary).
47
+ */
48
+ function failureChunk(error, signal) {
49
+ const failure = error instanceof LlmError
50
+ ? error.failure
51
+ : { message: errorChain(error), code: 'UNKNOWN' };
52
+ return {
53
+ type: 'finish',
54
+ reason: signal?.aborted || failure.code === 'ABORTED'
55
+ ? { kind: 'aborted', failure }
56
+ : { kind: 'error', failure },
57
+ };
58
+ }
59
+
60
+ export default class RebonLlmRuntime extends Service {
61
+ constructor(ctx) {
62
+ super(ctx, 'llm');
63
+ this.routes = new Map(); // provider -> adapter
64
+ /** Catalog reads still in flight; `settle()` is what waits for them. */
65
+ this.pending = new Set();
66
+ }
67
+
68
+ registerAdapter(providers, adapter) {
69
+ if (!providers?.length) {
70
+ throw new Error('an adapter must register at least one provider');
71
+ }
72
+ for (const provider of providers) {
73
+ if (this.routes.has(provider)) {
74
+ throw new Error(`an adapter for provider "${provider}" is already registered`);
75
+ }
76
+ }
77
+ const runtime = this;
78
+ // Read synchronously, before any await can interleave: which plugin is
79
+ // registering is a fact about *now*, not about whenever this resolves.
80
+ const sink = sinkOf(this.ctx);
81
+ for (const provider of providers) {
82
+ sink?.llm(provider, (request, callCtx) =>
83
+ withCall(callCtx, () => runtime.serve(provider, request, callCtx)));
84
+ }
85
+ this.ctx.effect(() => {
86
+ for (const provider of providers) {
87
+ runtime.routes.set(provider, adapter);
88
+ runtime._read(provider, adapter, sink);
89
+ }
90
+ return () => {
91
+ // Local only. What rebon knows about these routes was reported at
92
+ // load and is withdrawn by unload; a disposer cannot tell it anything
93
+ // because a disposer cannot await an answer.
94
+ for (const provider of providers) runtime.routes.delete(provider);
95
+ };
96
+ });
97
+ // dsh handle shape: route replacement exists on it, but a static
98
+ // composition never re-reads registration facts, so a call here is a
99
+ // consumer bug — refuse loudly instead of silently diverging from dsh.
100
+ return {
101
+ providers: [...providers],
102
+ replace() {
103
+ throw new Error('adapter route replacement is not supported in a static composition');
104
+ },
105
+ };
106
+ }
107
+
108
+ /**
109
+ * dsh surfaces these entries to its settings UI. The composition has no
110
+ * such surface; record them so diagnostics can list what a composition
111
+ * declared, and accept the call so unmodified dsh plugins load.
112
+ */
113
+ registerConfigurableProviders(entries) {
114
+ this.configurableProviders ??= [];
115
+ for (const entry of entries ?? []) {
116
+ if (entry?.provider) {
117
+ this.configurableProviders.push({
118
+ provider: String(entry.provider),
119
+ displayName: entry.displayName === undefined ? undefined : String(entry.displayName),
120
+ });
121
+ }
122
+ }
123
+ }
124
+
125
+ // ---- JS consumer face ----
126
+ //
127
+ // These serve consumers INSIDE the composition (the agent loop); `serve()`
128
+ // serves requests from rebon. Routing is the same in-JS adapter table.
129
+ // v1 honesty boundary: no `llm/stream` middleware waterfall — dispatch goes
130
+ // straight to the owning adapter.
131
+
132
+ /** Resolve the owning adapter or refuse with dsh's NO_ADAPTER code. */
133
+ _registration(provider) {
134
+ const adapter = this.routes.get(provider);
135
+ if (!adapter) {
136
+ throw new LlmError(`no adapter registered for provider "${provider}"`, 'NO_ADAPTER');
137
+ }
138
+ return adapter;
139
+ }
140
+
141
+ /**
142
+ * dsh `LlmRuntime.prepareCall`, transcribed: resolve the route's exact
143
+ * model, materialize adapter defaults (recorded in `adapterDefaults` so
144
+ * the loop's request header can re-resolve them later), and hand back a
145
+ * one-shot stream bound to this registration.
146
+ */
147
+ async prepareCall(config, signal) {
148
+ const adapter = this._registration(config.provider);
149
+ const info = (await adapter.resolveModel(config.provider, config.model)) ?? {};
150
+ signal?.throwIfAborted?.();
151
+ const defaulted = config.maxTokens === undefined && info.defaultMaxTokens !== undefined
152
+ ? { ...config, maxTokens: info.defaultMaxTokens }
153
+ : config;
154
+ const reasoning = info.reasoning;
155
+ const requested = defaulted.reasoningEffort;
156
+ let resolvedConfig = defaulted;
157
+ if (reasoning === undefined) {
158
+ if (requested !== undefined) {
159
+ throw new LlmError(
160
+ `provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`,
161
+ 'UNSUPPORTED_REASONING_EFFORT',
162
+ );
163
+ }
164
+ } else {
165
+ const effective = requested ?? reasoning.defaultEffort;
166
+ if (effective !== undefined) {
167
+ if (!(reasoning.efforts ?? []).some((effort) => effort.id === effective)) {
168
+ throw new LlmError(
169
+ `provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`,
170
+ 'UNSUPPORTED_REASONING_EFFORT',
171
+ );
172
+ }
173
+ if (requested !== effective) resolvedConfig = { ...defaulted, reasoningEffort: effective };
174
+ }
175
+ }
176
+ const frozen = deepFreeze(structuredClone(resolvedConfig));
177
+ const context = info.context === undefined
178
+ ? undefined
179
+ : deepFreeze(structuredClone(info.context));
180
+ const adapterDefaults = deepFreeze({
181
+ ...(config.reasoningEffort === undefined && frozen.reasoningEffort !== undefined
182
+ ? { reasoningEffort: true }
183
+ : {}),
184
+ ...(config.maxTokens === undefined && frozen.maxTokens !== undefined
185
+ ? { maxTokens: true }
186
+ : {}),
187
+ });
188
+ const runtime = this;
189
+ let dispatched = false;
190
+ return Object.freeze({
191
+ config: frozen,
192
+ // dsh contract: providerRetryPolicy returns an ALREADY-RESOLVED
193
+ // policy (or undefined for the normal defaults) — never re-resolve.
194
+ retryPolicy: adapter.providerRetryPolicy(config.provider)
195
+ ?? resolveRetryPolicy(undefined, `llm: provider "${config.provider}" retryPolicy`),
196
+ adapterDefaults,
197
+ ...(context === undefined ? {} : { context }),
198
+ stream(options) {
199
+ if (dispatched) {
200
+ throw new LlmError('a prepared LLM call can only be dispatched once', 'INVALID_PREPARED_CALL');
201
+ }
202
+ if (!callConfigEquals(options, frozen)) {
203
+ throw new LlmError('prepared LLM call config changed before adapter dispatch', 'INVALID_PREPARED_CALL');
204
+ }
205
+ dispatched = true;
206
+ return runtime._consumerStream(options);
207
+ },
208
+ });
209
+ }
210
+
211
+ /** Consumer fallback entry (the loop's NO_ADAPTER path): direct dispatch. */
212
+ stream(options) {
213
+ return this._consumerStream(options);
214
+ }
215
+
216
+ /**
217
+ * Adapter boundary for in-composition consumers: selection, dispatch, and
218
+ * iteration failures become one terminal failure chunk (dsh
219
+ * `adapterStream` semantics) so the loop's assembler/request-error path
220
+ * sees them instead of a raw throw.
221
+ */
222
+ async *_consumerStream(options) {
223
+ let iterator;
224
+ try {
225
+ const adapter = this._registration(options.provider);
226
+ iterator = adapter.stream(options)[Symbol.asyncIterator]();
227
+ } catch (error) {
228
+ yield failureChunk(error, options.signal);
229
+ return;
230
+ }
231
+ while (true) {
232
+ let item;
233
+ try {
234
+ item = await iterator.next();
235
+ } catch (error) {
236
+ yield failureChunk(error, options.signal);
237
+ return;
238
+ }
239
+ if (item.done) return;
240
+ yield item.value;
241
+ }
242
+ }
243
+
244
+ // ---- what rebon is told, and what rebon asks for ----
245
+
246
+ /**
247
+ * Read one route's catalog for the load report.
248
+ *
249
+ * `listModels` is advisory in dsh and can take a network round trip, so a
250
+ * failure is recorded rather than raised: a provider with no catalog is
251
+ * still a working route whose callers name models explicitly. The route
252
+ * table is the liveness witness — an adapter disposed while its catalog was
253
+ * being read is no longer the live one, and its answer is dropped.
254
+ */
255
+ _read(provider, adapter, sink) {
256
+ if (sink === undefined) return;
257
+ const task = (async () => {
258
+ let models = [];
259
+ try {
260
+ models = (await adapter.listModels(provider)) ?? [];
261
+ } catch (error) {
262
+ if (this.routes.get(provider) !== adapter) return;
263
+ sink.catalog(provider, { models: [], defaultModel: 'default', catalogError: String(error?.message ?? error) });
264
+ return;
265
+ }
266
+ if (this.routes.get(provider) !== adapter) return;
267
+ // A key whose value is `undefined` is not JSON, and this report crosses
268
+ // the wire: an unknown context window is an absent field, not a present
269
+ // one holding nothing.
270
+ const entries = models
271
+ .filter((m) => m?.id)
272
+ .map((m) => {
273
+ const window = m.context?.window ?? m.contextWindow;
274
+ return { id: m.id, ...(window === undefined ? {} : { contextWindow: window }) };
275
+ });
276
+ sink.catalog(provider, {
277
+ models: entries,
278
+ // The catalog is advisory (dsh semantics): with an empty one the route
279
+ // still exists and callers name models explicitly; "default" is only
280
+ // the resolve fallback for bare `{provider}` lookups.
281
+ defaultModel: entries[0]?.id ?? 'default',
282
+ });
283
+ })();
284
+ this.pending.add(task);
285
+ void task.finally(() => this.pending.delete(task));
286
+ }
287
+
288
+ /** Waits for every catalog read in flight, so the load report is complete. */
289
+ async settle() {
290
+ while (this.pending.size > 0) await Promise.all([...this.pending]);
291
+ }
292
+
293
+ /**
294
+ * Serves one `llm/stream` call: chunks out on the call, adapter throws left
295
+ * to become its terminal.
296
+ *
297
+ * Deliberately the raw adapter rather than `_consumerStream`: an in-JS
298
+ * consumer wants a failure flattened into a finish chunk it can assemble,
299
+ * whereas rebon wants the call to end as an error carrying the adapter's own
300
+ * machine code. Flattening here would report every failed turn as a
301
+ * successful one whose last chunk happens to say otherwise.
302
+ */
303
+ async serve(provider, request, ctx) {
304
+ const adapter = this._registration(provider);
305
+ const options = {
306
+ provider,
307
+ model: request.model,
308
+ messages: request.messages ?? [],
309
+ system: request.system ?? undefined,
310
+ tools: request.tools ?? undefined,
311
+ maxTokens: request.maxTokens ?? undefined,
312
+ temperature: request.temperature ?? undefined,
313
+ stop: request.stop ?? undefined,
314
+ reasoningEffort: request.reasoningEffort ?? undefined,
315
+ sessionId: request.sessionId ?? undefined,
316
+ purpose: request.purpose ?? undefined,
317
+ signal: ctx.signal,
318
+ };
319
+ let count = 0;
320
+ for await (const chunk of adapter.stream(options)) {
321
+ await ctx.emit(chunk);
322
+ count += 1;
323
+ }
324
+ return { chunks: count };
325
+ }
326
+ }