@rebon/cli-linux-x64 0.17.3 → 0.18.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 (82) hide show
  1. package/bin/compose-runtime/package.json +17 -0
  2. package/bin/compose-runtime/payload/compose/credentials-runtime.js +38 -0
  3. package/bin/compose-runtime/payload/compose/llm-runtime.js +330 -0
  4. package/bin/compose-runtime/payload/compose/loop-assembly.js +149 -0
  5. package/bin/compose-runtime/payload/compose/serve-dispatch.js +82 -0
  6. package/bin/compose-runtime/payload/compose/shims/dsh-anonymous-user-id.js +10 -0
  7. package/bin/compose-runtime/payload/compose/shims/dsh-credentials.js +13 -0
  8. package/bin/compose-runtime/payload/compose/shims/dsh-launch-environment.js +21 -0
  9. package/bin/compose-runtime/payload/compose/shims/dsh-llm.js +313 -0
  10. package/bin/compose-runtime/payload/compose/shims/dsh-settings.js +43 -0
  11. package/bin/compose-runtime/payload/compose/shims/dsh-tools.js +20 -0
  12. package/bin/compose-runtime/payload/compose/shims/dsh-web.js +10 -0
  13. package/bin/compose-runtime/payload/compose/shims/llm-adapter.js +32 -0
  14. package/bin/compose-runtime/payload/compose/shims/zod-lite.js +49 -0
  15. package/bin/compose-runtime/payload/compose/systemprompt-runtime.js +74 -0
  16. package/bin/compose-runtime/payload/compose/tools-runtime.js +228 -0
  17. package/bin/compose-runtime/payload/compose/web-runtime.js +204 -0
  18. package/bin/compose-runtime/payload/vendor/cordis/index.js +1530 -0
  19. package/bin/compose-runtime/payload/vendor/cosmokit/LICENSE +21 -0
  20. package/bin/compose-runtime/payload/vendor/cosmokit/index.mjs +357 -0
  21. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-agent +21 -0
  22. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-agent-loop +21 -0
  23. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-llm-core +21 -0
  24. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-llm-deepseek +21 -0
  25. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-logger-console +21 -0
  26. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-scope +21 -0
  27. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-session +21 -0
  28. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-system-prompt +21 -0
  29. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-timeout +21 -0
  30. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-timer +21 -0
  31. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-tool-todo +21 -0
  32. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-tool-web +21 -0
  33. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-tools-schema +21 -0
  34. package/bin/compose-runtime/payload/vendor/dsh/LICENSE-web-search-exa +21 -0
  35. package/bin/compose-runtime/payload/vendor/dsh/agent-loop.js +1193 -0
  36. package/bin/compose-runtime/payload/vendor/dsh/agent.js +714 -0
  37. package/bin/compose-runtime/payload/vendor/dsh/llm-core.js +269 -0
  38. package/bin/compose-runtime/payload/vendor/dsh/llm-deepseek.js +672 -0
  39. package/bin/compose-runtime/payload/vendor/dsh/logger-console.js +83 -0
  40. package/bin/compose-runtime/payload/vendor/dsh/scope.js +287 -0
  41. package/bin/compose-runtime/payload/vendor/dsh/session.js +1668 -0
  42. package/bin/compose-runtime/payload/vendor/dsh/system-prompt.js +309 -0
  43. package/bin/compose-runtime/payload/vendor/dsh/timeout.js +100 -0
  44. package/bin/compose-runtime/payload/vendor/dsh/timer.js +128 -0
  45. package/bin/compose-runtime/payload/vendor/dsh/tool-todo.js +143 -0
  46. package/bin/compose-runtime/payload/vendor/dsh/tool-web.js +1527 -0
  47. package/bin/compose-runtime/payload/vendor/dsh/tools-schema.js +849 -0
  48. package/bin/compose-runtime/payload/vendor/dsh/web-search-exa.js +122 -0
  49. package/bin/compose-runtime/payload/vendor/eventsource-parser/LICENSE +21 -0
  50. package/bin/compose-runtime/payload/vendor/eventsource-parser/index.js +177 -0
  51. package/bin/compose-runtime/payload/vendor/eventsource-parser/stream.js +48 -0
  52. package/bin/compose-runtime/payload/vendor/schemastery/index.mjs +656 -0
  53. package/bin/compose-runtime/payload-manifests.json +48 -0
  54. package/bin/compose-runtime/src/bridge.mjs +142 -0
  55. package/bin/compose-runtime/src/credentials-runtime.mjs +50 -0
  56. package/bin/compose-runtime/src/eventsource-stream.mjs +48 -0
  57. package/bin/compose-runtime/src/index.mjs +7 -0
  58. package/bin/compose-runtime/src/llm-runtime.mjs +326 -0
  59. package/bin/compose-runtime/src/loader.mjs +78 -0
  60. package/bin/compose-runtime/src/loop-assembly.mjs +237 -0
  61. package/bin/compose-runtime/src/payload.mjs +49 -0
  62. package/bin/compose-runtime/src/plugin.mjs +59 -0
  63. package/bin/compose-runtime/src/realm.mjs +223 -0
  64. package/bin/compose-runtime/src/registry.mjs +191 -0
  65. package/bin/compose-runtime/src/resolve.mjs +115 -0
  66. package/bin/compose-runtime/src/systemprompt-runtime.mjs +67 -0
  67. package/bin/compose-runtime/src/tools-runtime.mjs +239 -0
  68. package/bin/compose-runtime/src/web-runtime.mjs +209 -0
  69. package/bin/plugin-host/package.json +11 -0
  70. package/bin/plugin-host/src/bridge.mjs +64 -0
  71. package/bin/plugin-host/src/cli.mjs +105 -0
  72. package/bin/plugin-host/src/framing.mjs +113 -0
  73. package/bin/plugin-host/src/host.mjs +624 -0
  74. package/bin/plugin-host/src/json.mjs +196 -0
  75. package/bin/plugin-host/src/lifecycle.mjs +114 -0
  76. package/bin/plugin-host/src/loader.mjs +142 -0
  77. package/bin/plugin-host/src/methods.mjs +256 -0
  78. package/bin/plugin-host/src/protocol.mjs +129 -0
  79. package/bin/plugin-host/src/sdk.mjs +7 -0
  80. package/bin/rebon +0 -0
  81. package/bin/rebon-boa-helper +0 -0
  82. package/package.json +4 -1
@@ -0,0 +1,48 @@
1
+ {
2
+ "version": 1,
3
+ "note": "What rebon declares on behalf of the packages it ships. Every plugin on the plane loads against a manifest — the ceiling on what it may register and reach — and a third-party package declares its own under `capabilities.kernelPlugins` in its `rebon-plugin.json`. These packages are vendored into rebon's payload, so rebon is the one that has to say what they do. `root` names which package the `entry` is relative to. It defaults to `package`, which in this table means the vendored JS tree an entry belongs to; `payload` says the same thing explicitly, and `runtime` names the composition runtime itself. `invokableTools: \"$rebon/tools\"` expands to every tool this build exposes to the plane — the honest declaration for a plugin that runs whatever tool a model picks.",
4
+ "packages": {
5
+ "cordis": { "entry": "vendor/cordis/index.js" },
6
+ "@deepseek-ai/cordis-plugin-timer": { "entry": "vendor/dsh/timer.js" },
7
+ "@deepseek-ai/cordis-plugin-logger-console": { "entry": "vendor/dsh/logger-console.js" },
8
+
9
+ "@deepseek-ai/dsh-llm-deepseek": {
10
+ "entry": "vendor/dsh/llm-deepseek.js",
11
+ "llmProviders": ["deepseek-official"],
12
+ "seats": ["credentials"]
13
+ },
14
+ "@deepseek-ai/dsh-tool-todo": {
15
+ "entry": "vendor/dsh/tool-todo.js",
16
+ "tools": ["todo_write"],
17
+ "publishedTopics": ["compose:session/append"]
18
+ },
19
+ "@deepseek-ai/dsh-tool-web": {
20
+ "entry": "vendor/dsh/tool-web.js",
21
+ "tools": ["web_search", "web_fetch"],
22
+ "invokableTools": ["WebSearch", "WebFetch"],
23
+ "publishedTopics": ["compose:session/append"]
24
+ },
25
+ "@deepseek-ai/dsh-web-search-exa": {
26
+ "entry": "vendor/dsh/web-search-exa.js",
27
+ "services": ["web:search:exa"],
28
+ "seats": ["credentials"]
29
+ },
30
+
31
+ "@deepseek-ai/dsh-session": { "entry": "vendor/dsh/session.js" },
32
+ "@deepseek-ai/dsh-agent": { "entry": "vendor/dsh/agent.js" },
33
+ "@deepseek-ai/dsh-system-prompt": { "entry": "vendor/dsh/system-prompt.js" },
34
+ "@deepseek-ai/dsh-agent-loop": {
35
+ "entry": "vendor/dsh/agent-loop.js",
36
+ "invokableTools": "$rebon/tools",
37
+ "seats": ["credentials"]
38
+ },
39
+
40
+ "rebon-loop-assembly": {
41
+ "root": "runtime",
42
+ "entry": "src/loop-assembly.mjs",
43
+ "services": ["loop:control"],
44
+ "publishedTopics": ["loop:event", "loop:agent-error", "loop:agent-created"],
45
+ "seats": ["logger"]
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,142 @@
1
+ // The composition's doorway to rebon, on the plugin plane.
2
+ //
3
+ // On deno_core this was `js/bridge.js`: seven synchronous ops reaching a
4
+ // kernel that lived in the same process. Across a process boundary none of
5
+ // that survives unchanged, and the differences are the protocol's, not this
6
+ // module's:
7
+ //
8
+ // * **Everything is async.** `callService` returned a value from an op; a
9
+ // `seat/call` is a request that has to be answered. The one place this
10
+ // bites is a cordis disposer, which is synchronous and cannot await — see
11
+ // the note on `seat()` below.
12
+ // * **There is no ambient kernel.** A call is only meaningful inside the
13
+ // scope incarnation it was made for, so the doorway exists only inside a
14
+ // handler. `AsyncLocalStorage` is what carries the handler's context down
15
+ // to code that has no way to be handed it — a dsh credential provider is
16
+ // called by an adapter that knows nothing about rebon.
17
+ // * **Registration is not a call.** `op_rebon_call_service('model-router',
18
+ // 'register', …)` and its compensating `unregister` are gone. What a
19
+ // composition provides is reported once, in the `plugin/load` ready
20
+ // report, and revoked by `plugin/unload` draining. That is the only shape
21
+ // that works: the unregisters lived in cordis disposers.
22
+ import { AsyncLocalStorage } from 'node:async_hooks';
23
+ import { sessionOf } from './registry.mjs';
24
+
25
+ const calls = new AsyncLocalStorage();
26
+
27
+ /// Runs `fn` with `ctx` as the call everything underneath reaches through.
28
+ ///
29
+ /// Every handler the composition serves — service, tool, llm adapter, event
30
+ /// delivery — is wrapped in this, so anything it calls can find its way back
31
+ /// to rebon without the call context being threaded through dsh code that has
32
+ /// no parameter for it.
33
+ export function withCall(ctx, fn) {
34
+ return calls.run(ctx, fn);
35
+ }
36
+
37
+ /// The call this code is running inside.
38
+ export function currentCall() {
39
+ return calls.getStore();
40
+ }
41
+
42
+ /// The call this code is running inside, or a refusal that says why not.
43
+ ///
44
+ /// The refusal is worth its own code: "cannot read properties of undefined" at
45
+ /// the bottom of a dsh stack says nothing, whereas this names the actual rule —
46
+ /// rebon is reachable from inside a call, and a disposer or a module top level
47
+ /// is not inside one.
48
+ function require(what, via) {
49
+ const ctx = calls.getStore();
50
+ if (ctx !== undefined) return ctx;
51
+ // Nothing is being answered, so this is a plugin acting on its own schedule.
52
+ // `via` is the Cordis context of the plugin whose code is running, and the
53
+ // session it is attached to is the honest identity for what it does — the
54
+ // work is still that plugin's, in that session. Without a session there is
55
+ // no identity to use, and inventing one would make the ledger a guess.
56
+ const session = sessionOf(via);
57
+ if (session !== undefined) return session;
58
+ throw new Error(
59
+ `[NO_CALL_CONTEXT] ${what} is not reachable from here: no call rebon made is in flight, `
60
+ + 'and this plugin is not attached to a session. Teardown paths must rely on unload draining instead',
61
+ );
62
+ }
63
+
64
+ /// Calls a kernel seat: the composition's `credentials`, `settings`, `web` and
65
+ /// the rest of what rebon installs a plugin to use.
66
+ ///
67
+ /// A request, therefore answerable, therefore **not** usable from a cordis
68
+ /// disposer: `dispose()` is synchronous and a cross-process call cannot be.
69
+ /// Nothing in the composition needs it to be — the calls that used to happen
70
+ /// on the way out were unregistrations, and those are the protocol's job now.
71
+ export function seat(name, method, params = null, options = {}) {
72
+ const { via, ...call } = options;
73
+ return require(`seat ${JSON.stringify(name)}`, via).seat(name, method, params, call);
74
+ }
75
+
76
+ /// Invokes one rebon core tool on the session this call belongs to.
77
+ ///
78
+ /// Three gates stand in front of it and only the first is here: the plugin's
79
+ /// manifest must declare the tool, rebon's build must expose it, and the
80
+ /// embedder's broker decides whether this run is permitted.
81
+ export function invokeTool(tool, input = null, options = {}) {
82
+ const { via, ...call } = options;
83
+ return require(`tool ${JSON.stringify(tool)}`, via).invoke(tool, input, call);
84
+ }
85
+
86
+ /// Publishes one event onto rebon's event plane.
87
+ ///
88
+ /// Not the same thing as `emit` below, and the difference is the audience: a
89
+ /// chunk is a piece of the answer the caller of *this* call is reading, an
90
+ /// event is a fact anyone listening may act on. The composition's runtime
91
+ /// events — a dsh tool body appending to the session log — are the second kind.
92
+ ///
93
+ /// Declared as `publishedTopics` in the manifest, separately from what the
94
+ /// composition listens to.
95
+ export function publish(topic, event = null, options = {}) {
96
+ const { via, ...call } = options;
97
+ return require(`topic ${JSON.stringify(topic)}`, via).publish(topic, event, call);
98
+ }
99
+
100
+ /// Emits one piece of a streamed answer on the call this code is serving.
101
+ ///
102
+ /// This is the transport `op_llm_emit` and `op_tool_serve_emit` used to be,
103
+ /// with the pumps gone: a chunk belongs to a call, and the call is the one the
104
+ /// handler was entered with rather than an id looked up in a table.
105
+ export function emit(piece) {
106
+ return require('emit').emit(piece);
107
+ }
108
+
109
+ /// The workspace root of the session this call belongs to, or undefined
110
+ /// outside a call.
111
+ export function workspaceRoot() {
112
+ return calls.getStore()?.workspaceRoot;
113
+ }
114
+
115
+ /// The abort signal rebon raises when it asks this call to stop.
116
+ export function callSignal() {
117
+ return calls.getStore()?.signal;
118
+ }
119
+
120
+ /// Diagnostics.
121
+ ///
122
+ /// Best-effort on purpose, and never a reason for a call to fail: a log line
123
+ /// that cannot be delivered is not worth failing the work that produced it.
124
+ /// Inside a call it goes to the kernel's `logger` seat, so the line is
125
+ /// attributed to a scope; outside one it goes to stderr, which the supervisor
126
+ /// already collects as the host's diagnostic tail. stdout is never touched —
127
+ /// that is the protocol's channel.
128
+ const LEVELS = ['trace', 'debug', 'info', 'warn', 'error'];
129
+ export const logger = Object.freeze(Object.fromEntries(LEVELS.map((level) => [
130
+ level,
131
+ (message) => {
132
+ const line = String(message);
133
+ const ctx = calls.getStore();
134
+ if (ctx === undefined) {
135
+ process.stderr.write(`[compose:${level}] ${line}\n`);
136
+ return;
137
+ }
138
+ void ctx.seat('logger', level, { message: line }).catch(() => {
139
+ process.stderr.write(`[compose:${level}] ${line}\n`);
140
+ });
141
+ },
142
+ ])));
@@ -0,0 +1,50 @@
1
+ // The composition's `ctx.credentials` seat, on the plugin plane.
2
+ //
3
+ // dsh configuration carries credential *references* (environment-variable
4
+ // names); the provider owns the values. This resolves a reference through
5
+ // rebon's `credentials` seat, whose `resolveEnv` runs the fail-closed
6
+ // authorize waterfall before touching the environment — so an unauthorized or
7
+ // unset reference is simply "absent", and the consumer's own missing-credential
8
+ // diagnosis fires (dsh-llm's `MISSING_CREDENTIAL`) instead of this seat
9
+ // inventing one.
10
+ //
11
+ // Ported from `js/compose/credentials-runtime.js`. The one change is that the
12
+ // seat call is now a request across a process boundary, which means it is
13
+ // awaited — and that it can only be made from inside a call rebon is waiting
14
+ // on, or by the plugin being attached to a session. Both hold where a
15
+ // credential is actually wanted: inside a model turn rebon asked for, or inside
16
+ // one an agent loop is running in a session of its own. `bridge.mjs` names the
17
+ // refusal if anything ever tries from a disposer or a module top level.
18
+ import { Service } from 'cordis';
19
+ import { seat } from './bridge.mjs';
20
+
21
+ export default class RebonCredentials extends Service {
22
+ constructor(ctx) {
23
+ super(ctx, 'credentials');
24
+ }
25
+
26
+ /** dsh CredentialProvider.resolve: `{value, source}` or undefined. */
27
+ async resolve(ref) {
28
+ try {
29
+ // `via` is this seat's caller — the plugin whose code wants the
30
+ // credential. Inside a call that changes nothing; outside one it is what
31
+ // lets an agent loop's own model turn resolve a key at all, attributed to
32
+ // the plugin and session it belongs to.
33
+ const out = await seat('credentials', 'resolveEnv', { ref: String(ref) }, { via: this.ctx });
34
+ const value = out?.value;
35
+ if (typeof value === 'string' && value.length > 0) {
36
+ return { value, source: 'env' };
37
+ }
38
+ } catch {
39
+ // A refusal from the seat (no authorizer, unset variable, bad ref) and
40
+ // an absent value are the same fact to a consumer: not configured here.
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ /** dsh CredentialProvider.describe: facts only, never the value. */
46
+ async describe(ref) {
47
+ const hit = await this.resolve(ref);
48
+ return { configured: hit !== undefined, ...(hit ? { source: hit.source } : {}), writable: false };
49
+ }
50
+ }
@@ -0,0 +1,48 @@
1
+ // `eventsource-parser/stream` for real Node — the published package's shape.
2
+ //
3
+ // The vendored build under `js/vendor/eventsource-parser/stream.js` is not the
4
+ // published module: it was rewritten so that `EventSourceParserStream` is a
5
+ // "Mini streams" transform (an object with `transformIterable`), because bare
6
+ // deno_core has no WHATWG `TransformStream` for the real one to extend. The
7
+ // dsh DeepSeek adapter consumes it as a platform stream —
8
+ // `response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new
9
+ // EventSourceParserStream())` — which on real Node throws
10
+ // `ERR_INVALID_ARG_TYPE: transform.readable must be an instance of
11
+ // ReadableStream` against the rewritten class.
12
+ //
13
+ // So on Node the upstream shape comes back. Parsing itself is still the
14
+ // vendored upstream `createParser`, so framing semantics — chunk reassembly,
15
+ // CRLF/BOM, comment skipping, multi-`data:` joining — stay byte-identical to
16
+ // the published package on both runtimes; only the plumbing differs.
17
+ import { createParser } from 'eventsource-parser';
18
+
19
+ export { ParseError } from 'eventsource-parser';
20
+
21
+ export class EventSourceParserStream extends TransformStream {
22
+ constructor({ onError, onRetry, onComment, maxBufferSize } = {}) {
23
+ let parser;
24
+ super({
25
+ start(controller) {
26
+ parser = createParser({
27
+ onEvent: (event) => controller.enqueue(event),
28
+ onError(error) {
29
+ // Upstream's rule, kept: a callback observes, the string
30
+ // `'terminate'` aborts, and a buffer overflow aborts regardless of
31
+ // what the consumer asked for — there is no way to keep parsing
32
+ // past it.
33
+ if (typeof onError === 'function') onError(error);
34
+ if (onError === 'terminate' || error.type === 'max-buffer-size-exceeded') {
35
+ controller.error(error);
36
+ }
37
+ },
38
+ onRetry,
39
+ onComment,
40
+ maxBufferSize,
41
+ });
42
+ },
43
+ transform(chunk) {
44
+ parser.feed(chunk);
45
+ },
46
+ });
47
+ }
48
+ }
@@ -0,0 +1,7 @@
1
+ // The package's root export: the loader seam the plugin host runs with.
2
+ //
3
+ // `rebon-plugin-host --loader <this file>` is what turns a generic plugin host
4
+ // into one that can mount a Cordis composition. Everything else in this package
5
+ // hangs off that.
6
+ export { createLoader, cordisPluginOf } from './loader.mjs';
7
+ export { activate as composeControl } from './plugin.mjs';
@@ -0,0 +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
+ }