@rebon/cli-linux-x64 0.17.2 → 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,191 @@
1
+ // What one Cordis entry registered, captured while it mounts.
2
+ //
3
+ // A Cordis plugin does not know the plugin plane exists: it calls
4
+ // `ctx.tools.register(...)`, `ctx.llm.registerAdapter(...)`,
5
+ // `ctx.systemPrompt.section(...)`, and the seats it calls are the ones that
6
+ // have to turn those into something `plugin/load` can answer with. This is
7
+ // where they put it.
8
+ //
9
+ // The sink reaches a seat through the registering context — `ctx[SINK]`, set
10
+ // when the entry's context is derived — for the same reason the old ledger's
11
+ // owner stamp did: it is read synchronously at registration time, so an async
12
+ // interleaving cannot attribute one plugin's registration to another.
13
+ //
14
+ // Two rules it enforces, both borrowed from the host's own registrar because a
15
+ // composition entry is a plugin and should not get a weaker contract:
16
+ //
17
+ // * **Declared first.** A registration the load request did not declare is
18
+ // refused where the plugin made it, rather than arriving later as a ready
19
+ // report rebon has to reject.
20
+ // * **Registration closes.** Once the entry's fiber has resolved, the host
21
+ // has already been told what this plugin provides; a later registration
22
+ // would be a capability nothing downstream knows about.
23
+
24
+ /** The key an entry's context carries its sink under. */
25
+ export const SINK = Symbol.for('rebon.composeSink');
26
+ /** The key an entry's context carries its plugin id under. */
27
+ export const OWNER = Symbol.for('rebon.composeOwner');
28
+
29
+ class RegistrationError extends Error {
30
+ constructor(code, message) {
31
+ super(message);
32
+ this.code = code;
33
+ this.name = 'RegistrationError';
34
+ }
35
+ }
36
+
37
+ export class RegistrationSink {
38
+ #sealed = false;
39
+
40
+ constructor(pluginId, declared = {}) {
41
+ this.pluginId = pluginId;
42
+ this.declared = {
43
+ services: new Set(declared.services ?? []),
44
+ eventTopics: new Set(declared.eventTopics ?? []),
45
+ llmProviders: new Set(declared.llmProviders ?? []),
46
+ tools: new Set(declared.tools ?? []),
47
+ };
48
+ /** Protocol-shaped registrations, answered by `plugin/load`. */
49
+ this.services = new Map();
50
+ this.topics = new Map();
51
+ this.llmProviders = new Map();
52
+ this.tools = new Map();
53
+ /** Handles on a session, handed out when a scope opens. */
54
+ this.scopeHandlers = [];
55
+ /** The sessions this plugin is currently attached to.
56
+ *
57
+ * Every entry gets one of these, not just the ones that ask: it is how a
58
+ * seat reaches rebon on behalf of code running outside any inbound call.
59
+ * An agent loop drives its own turn, and the adapter it reaches for a
60
+ * credential is running on the loop's schedule rather than answering
61
+ * anything — but it is still that plugin's code, in that session, and the
62
+ * call it makes should say so. */
63
+ this.sessions = new Set();
64
+ this.scopeHandlers.push((scopeCtx) => {
65
+ this.sessions.add(scopeCtx);
66
+ return () => this.sessions.delete(scopeCtx);
67
+ });
68
+ /** Facts with no protocol counterpart, fetched from the `compose` service. */
69
+ this.sections = [];
70
+ this.catalogs = new Map();
71
+ this.webProviders = [];
72
+ }
73
+
74
+ #admit(kind, name, declared) {
75
+ if (this.#sealed) {
76
+ throw new RegistrationError(
77
+ '[REGISTRATION_CLOSED]',
78
+ `${kind} ${JSON.stringify(name)} was registered after ${this.pluginId} finished loading`,
79
+ );
80
+ }
81
+ if (typeof name !== 'string' || name.length === 0) {
82
+ throw new RegistrationError('[WRONG_SHAPE]', `a ${kind} needs a non-empty name`);
83
+ }
84
+ if (!declared.has(name)) {
85
+ throw new RegistrationError(
86
+ '[UNAUTHORIZED_REGISTER]',
87
+ `${kind} ${JSON.stringify(name)} is not declared by ${this.pluginId}'s manifest`,
88
+ );
89
+ }
90
+ }
91
+
92
+ service(name, handler) {
93
+ this.#admit('service', name, this.declared.services);
94
+ this.services.set(name, handler);
95
+ }
96
+
97
+ topic(name, handler) {
98
+ this.#admit('topic', name, this.declared.eventTopics);
99
+ this.topics.set(name, handler);
100
+ }
101
+
102
+ llm(provider, handler) {
103
+ this.#admit('provider', provider, this.declared.llmProviders);
104
+ this.llmProviders.set(provider, handler);
105
+ }
106
+
107
+ tool(definition, handler) {
108
+ this.#admit('tool', definition?.name, this.declared.tools);
109
+ this.tools.set(definition.name, { definition, handler });
110
+ }
111
+
112
+ /// A handle on the session, for a plugin that acts rather than only answers.
113
+ ///
114
+ /// Not declared, because it is not a capability: every power the handle
115
+ /// carries is one of the declarations above, checked when it is used.
116
+ scope(handler) {
117
+ if (typeof handler !== 'function') {
118
+ throw new RegistrationError('[WRONG_SHAPE]', 'a scope handler must be a function');
119
+ }
120
+ this.scopeHandlers.push(handler);
121
+ }
122
+
123
+ // ---- extras ----------------------------------------------------------
124
+ //
125
+ // A model catalog, a prompt section and a web provider are things rebon
126
+ // needs and the protocol has no field for. They are reported by the
127
+ // composition's own `compose` service rather than by widening the ready
128
+ // report, which every plugin shares and only this one would use.
129
+
130
+ section(entry) {
131
+ this.sections.push(entry);
132
+ }
133
+
134
+ catalog(provider, catalog) {
135
+ this.catalogs.set(provider, catalog);
136
+ }
137
+
138
+ webProvider(kind, id, available) {
139
+ this.webProviders.push({ kind, id, available });
140
+ }
141
+
142
+ /// Closes registration and hands back what the host asked for.
143
+ seal() {
144
+ this.#sealed = true;
145
+ return this;
146
+ }
147
+
148
+ /// The `plugin/load` answer, in the shape the host's own loader produces.
149
+ loaded() {
150
+ return {
151
+ services: [...this.services.keys()],
152
+ eventTopics: [...this.topics.keys()],
153
+ llmProviders: [...this.llmProviders.keys()],
154
+ tools: [...this.tools.values()].map((entry) => entry.definition),
155
+ serviceHandlers: this.services,
156
+ topicHandlers: this.topics,
157
+ llmAdapters: this.llmProviders,
158
+ toolHandlers: new Map([...this.tools].map(([name, entry]) => [name, entry.handler])),
159
+ scopeHandlers: [...this.scopeHandlers],
160
+ };
161
+ }
162
+
163
+ /// What the `compose` service reports for this plugin.
164
+ report() {
165
+ return {
166
+ sections: this.sections.map((section) => ({ ...section })),
167
+ providers: [...this.catalogs].map(([provider, catalog]) => ({ provider, ...catalog })),
168
+ webProviders: this.webProviders.map((entry) => ({ ...entry })),
169
+ };
170
+ }
171
+ }
172
+
173
+ /// The session this plugin is attached to, if it is attached to one.
174
+ ///
175
+ /// With more than one open, the first is used: a plugin acting on its own
176
+ /// schedule is acting in one session, and which one is a fact about how rebon
177
+ /// opened them rather than something guessable here.
178
+ export function sessionOf(ctx) {
179
+ const sink = ctx?.[SINK];
180
+ if (sink === undefined) return undefined;
181
+ for (const session of sink.sessions) return session;
182
+ return undefined;
183
+ }
184
+
185
+ /// The sink the code currently registering belongs to, or undefined.
186
+ ///
187
+ /// Undefined is normal: the built-in seats mount into the realm itself, and a
188
+ /// seat calling another seat is not a registration by any plugin.
189
+ export function sinkOf(ctx) {
190
+ return ctx?.[SINK];
191
+ }
@@ -0,0 +1,115 @@
1
+ // Module resolution for the composition, on real Node.
2
+ //
3
+ // Node has no import map, but it has `module.registerHooks()` — synchronous,
4
+ // in-thread resolve/load hooks — and this is the composition's map written in
5
+ // that vocabulary. There used to be a second map, in the deno_core host's
6
+ // `build_import_map`, and a test here holding the two to the same specifier
7
+ // set; that host is gone, so this is the only map there is.
8
+ //
9
+ // Two entries it does not carry, both from the deleted one and both in the
10
+ // direction of deleting code:
11
+ //
12
+ // * the four `node:` entries. `node:async_hooks`, `node:util/types`,
13
+ // `node:path` and `node:crypto` resolved to hand-written shims because bare
14
+ // deno_core has no standard library; real Node has all four.
15
+ // * `eventsource-parser/stream` resolves to this package's module rather than
16
+ // the vendored one. The vendored build is not the published package: it was
17
+ // rewritten against the "Mini streams" protocol the old host installed,
18
+ // because the published `EventSourceParserStream` extends the WHATWG
19
+ // `TransformStream` and bare deno_core has none. On real Node that
20
+ // rewrite breaks — `body.pipeThrough(new EventSourceParserStream())` throws
21
+ // `ERR_INVALID_ARG_TYPE` — so the upstream shape comes back. See
22
+ // `eventsource-stream.mjs`.
23
+ import { registerHooks } from 'node:module';
24
+ import path from 'node:path';
25
+ import { pathToFileURL } from 'node:url';
26
+ import { payloadDir } from './payload.mjs';
27
+
28
+ /// Payload-relative module map, keyed by bare specifier.
29
+ ///
30
+ /// Mirrors `build_import_map` entry for entry, minus the `node:` shims.
31
+ export function payloadModules(dir) {
32
+ const at = (relative) => pathToFileURL(path.join(dir, relative)).href;
33
+ return new Map(Object.entries({
34
+ cordis: at('vendor/cordis/index.js'),
35
+ cosmokit: at('vendor/cosmokit/index.mjs'),
36
+ schemastery: at('vendor/schemastery/index.mjs'),
37
+ '@deepseek-ai/cordis': at('vendor/cordis/index.js'),
38
+ '@deepseek-ai/cosmokit': at('vendor/cosmokit/index.mjs'),
39
+ '@deepseek-ai/schemastery': at('vendor/schemastery/index.mjs'),
40
+ '@deepseek-ai/cordis-plugin-timer': at('vendor/dsh/timer.js'),
41
+ '@deepseek-ai/cordis-plugin-logger-console': at('vendor/dsh/logger-console.js'),
42
+ '@deepseek-ai/dsh-llm-deepseek': at('vendor/dsh/llm-deepseek.js'),
43
+ '@deepseek-ai/dsh-timeout': at('vendor/dsh/timeout.js'),
44
+ '@deepseek-ai/dsh-llm': at('compose/shims/dsh-llm.js'),
45
+ '@deepseek-ai/dsh-credentials': at('compose/shims/dsh-credentials.js'),
46
+ '@deepseek-ai/dsh-settings': at('compose/shims/dsh-settings.js'),
47
+ '@deepseek-ai/dsh-launch-environment': at('compose/shims/dsh-launch-environment.js'),
48
+ '@deepseek-ai/dsh-anonymous-user-id': at('compose/shims/dsh-anonymous-user-id.js'),
49
+ 'eventsource-parser': at('vendor/eventsource-parser/index.js'),
50
+ '@deepseek-ai/dsh-tools': at('compose/shims/dsh-tools.js'),
51
+ '@deepseek-ai/dsh-tool-todo': at('vendor/dsh/tool-todo.js'),
52
+ zod: at('compose/shims/zod-lite.js'),
53
+ '@deepseek-ai/dsh-web': at('compose/shims/dsh-web.js'),
54
+ '@deepseek-ai/dsh-web-search-exa': at('vendor/dsh/web-search-exa.js'),
55
+ '@deepseek-ai/dsh-tool-web': at('vendor/dsh/tool-web.js'),
56
+ '@deepseek-ai/dsh-scope': at('vendor/dsh/scope.js'),
57
+ '@deepseek-ai/dsh-session': at('vendor/dsh/session.js'),
58
+ '@deepseek-ai/dsh-agent': at('vendor/dsh/agent.js'),
59
+ '@deepseek-ai/dsh-system-prompt': at('vendor/dsh/system-prompt.js'),
60
+ '@deepseek-ai/dsh-agent-loop': at('vendor/dsh/agent-loop.js'),
61
+ }));
62
+ }
63
+
64
+ /// Specifiers this package answers itself rather than from the payload.
65
+ export function localModules() {
66
+ const at = (relative) => new URL(relative, import.meta.url).href;
67
+ return new Map(Object.entries({
68
+ rebon: at('./bridge.mjs'),
69
+ 'eventsource-parser/stream': at('./eventsource-stream.mjs'),
70
+ }));
71
+ }
72
+
73
+ let installed;
74
+
75
+ /// Installs the composition's module resolution, once per process.
76
+ ///
77
+ /// `extraModules` is the embedder's own map (the `modules` half of a
78
+ /// composition config: plugin name → absolute module file), which is how a
79
+ /// plugin outside the vendored payload gets a name.
80
+ ///
81
+ /// Returns the resolved map so callers can report what a specifier became
82
+ /// without repeating the lookup rules.
83
+ export function installModuleResolution({ dir = payloadDir(), extraModules = {} } = {}) {
84
+ const map = new Map([...payloadModules(dir), ...localModules()]);
85
+ for (const [name, file] of Object.entries(extraModules)) {
86
+ map.set(name, pathToFileURL(path.resolve(file)).href);
87
+ }
88
+ if (installed) {
89
+ // Hooks are process-wide and cannot be unregistered per call; re-running
90
+ // with a different map would leave two resolvers disagreeing about the
91
+ // same specifier, which is a debugging trap rather than a feature.
92
+ for (const [name, url] of map) installed.map.set(name, url);
93
+ return installed.map;
94
+ }
95
+
96
+ const payloadPrefix = pathToFileURL(dir.endsWith(path.sep) ? dir : dir + path.sep).href;
97
+ const state = { map };
98
+ registerHooks({
99
+ resolve(specifier, context, next) {
100
+ const mapped = state.map.get(specifier);
101
+ if (mapped !== undefined) return { url: mapped, format: 'module', shortCircuit: true };
102
+ return next(specifier, context);
103
+ },
104
+ // The payload's ESM is `.js` under no `package.json`, so Node's own rules
105
+ // read it as CommonJS and every `import` in it is a syntax error. The
106
+ // deno_core loader had no such ambiguity — everything it loaded was a
107
+ // module — so this is a fact about Node, not about the payload.
108
+ load(url, context, next) {
109
+ if (url.startsWith(payloadPrefix)) return next(url, { ...context, format: 'module' });
110
+ return next(url, context);
111
+ },
112
+ });
113
+ installed = state;
114
+ return state.map;
115
+ }
@@ -0,0 +1,67 @@
1
+ // The composition's `ctx.systemPrompt` seat, on the plugin plane.
2
+ //
3
+ // Ported from `js/compose/systemprompt-runtime.js`. The dsh-facing surface is
4
+ // unchanged: plugins call `ctx.systemPrompt.section({name, order, text})` and
5
+ // get the exact disposer back, duplicate names throw dsh's message, non-finite
6
+ // orders throw dsh's TypeError.
7
+ //
8
+ // What changed is where the section goes. It used to be
9
+ // `callService('system-prompt', 'register', …)` — a synchronous call into a
10
+ // kernel living in the same process, compensated by an `unregister` in the
11
+ // disposer. Neither survives a process boundary: the call has to be awaited and
12
+ // the disposer cannot await. So a section is *reported*: collected here, handed
13
+ // to rebon with the rest of the load report, and withdrawn when the plugin
14
+ // unloads.
15
+ //
16
+ // Deliberately NOT provided (v1 honesty boundary, unchanged): provider-function
17
+ // `text` (dsh resolves it per assembly with an AssembleContext this side does
18
+ // not have — a static composition registers static text), `complete` sections
19
+ // (replacing rebon's own prompt belongs to the loop seat, not a side door),
20
+ // `context()` / variables / tool-order surfaces. All fail loudly rather than
21
+ // silently degrade.
22
+ import { Service } from 'cordis';
23
+ import { sinkOf } from './registry.mjs';
24
+
25
+ export default class RebonSystemPromptRuntime extends Service {
26
+ constructor(ctx) {
27
+ super(ctx, 'systemPrompt');
28
+ this.names = new Set();
29
+ }
30
+
31
+ /** Register one ordered prompt section; returns the exact disposer. */
32
+ section(entry) {
33
+ const name = entry?.name;
34
+ if (typeof name !== 'string' || name.trim().length === 0) {
35
+ throw new TypeError('systemPrompt.section: section.name must be a non-empty string');
36
+ }
37
+ if (!Number.isFinite(entry.order)) {
38
+ throw new TypeError(`prompt section "${name}" order must be a finite number`);
39
+ }
40
+ if (typeof entry.text === 'function') {
41
+ throw new Error(
42
+ `prompt section "${name}": provider-function text is not supported in the composition (register static text)`,
43
+ );
44
+ }
45
+ if (typeof entry.text !== 'string') {
46
+ throw new TypeError(`prompt section "${name}" text must be a string`);
47
+ }
48
+ if (entry.complete) {
49
+ throw new Error(
50
+ `prompt section "${name}": complete-section replacement is not supported in the composition`,
51
+ );
52
+ }
53
+ if (this.names.has(name)) {
54
+ throw new Error(`prompt section "${name}" is already registered in this scope`);
55
+ }
56
+ const runtime = this;
57
+ // Caller-fork RAII through the Service proxy (llm/tools/web same pattern):
58
+ // the registering plugin's disposal takes the section with it.
59
+ sinkOf(this.ctx)?.section({ name, order: entry.order, text: entry.text });
60
+ return this.ctx.effect(() => {
61
+ runtime.names.add(name);
62
+ return () => {
63
+ runtime.names.delete(name);
64
+ };
65
+ });
66
+ }
67
+ }
@@ -0,0 +1,239 @@
1
+ // The composition's `ctx.tools` seat, on the plugin plane.
2
+ //
3
+ // Ported from `js/compose/tools-runtime.js`. The dsh-facing surface is
4
+ // unchanged: plugins call `ctx.tools.register(defineTool({...}))` and get the
5
+ // exact disposer back, definitions arrive with `parameters` already compiled to
6
+ // JSON Schema and argument validation baked into `execute`, and the loop drives
7
+ // tools through the symbol-keyed scheduler contract.
8
+ //
9
+ // What changed:
10
+ //
11
+ // * **The serve pump is gone.** `registerServeTarget(name, …)` and
12
+ // `op_tool_serve_next` are replaced by the tool being a *declared* tool of
13
+ // the loading plugin, called back through `tool/call`. A tool is offered to
14
+ // a model, so the registration carries the description and input schema
15
+ // rather than a name alone — which is what the plane needs anyway.
16
+ // * **`callService('tool-registry', 'register', …)` is gone.** The tool is
17
+ // reported by `plugin/load` and withdrawn by `plugin/unload` draining, so
18
+ // the compensating `unregister` a disposer used to make — and could not
19
+ // have made across a process boundary — has nothing left to compensate.
20
+ // Shadowing a builtin name is rebon's judgement to make and rebon's to log;
21
+ // the composition no longer hears about it, which is why the warning that
22
+ // used to be printed here is not.
23
+ // * **`exec.agent.session.append` publishes.** It was a kernel event emitted
24
+ // from inside the isolate; it is `event/emit` now, declared as
25
+ // `publishedTopics`, and it is the reason a tool-providing plugin declares
26
+ // `compose:session/append`.
27
+ import { Service } from 'cordis';
28
+ import { TOOL_RUNTIME_SCHEDULER } from '@deepseek-ai/dsh-tools';
29
+ import { invokeTool, publish, withCall } from './bridge.mjs';
30
+ import { sinkOf } from './registry.mjs';
31
+
32
+ /** The topic a tool body's session writes land on. */
33
+ export const SESSION_APPEND_TOPIC = 'compose:session/append';
34
+
35
+ /** Extract a `[BRACKET_CODE]` prefix from a seat error message, if any. */
36
+ function bracketCode(message) {
37
+ const match = /^\[([A-Z_]+)\]/.exec(message);
38
+ return match ? match[1] : undefined;
39
+ }
40
+
41
+ /** Per-run context/conclusion collection installed by scheduler.prepare. */
42
+ const runStates = new WeakMap();
43
+
44
+ export default class RebonToolsRuntime extends Service {
45
+ constructor(ctx) {
46
+ super(ctx, 'tools');
47
+ this.names = new Set();
48
+ // Local definition table for the scheduler's in-composition dispatch
49
+ // (register() below keeps it in step with what rebon was told).
50
+ this.defs = new Map();
51
+ }
52
+
53
+ /**
54
+ * dsh scheduler contract.
55
+ *
56
+ * agent-loop's tool-calls scheduler drives tools through this symbol-keyed
57
+ * view (dsh's private loop↔tools seam, contract transcribed from
58
+ * packages/core/tools/src/index.ts:451). Dispatch resolution: a
59
+ * composition-registered definition runs here; anything else goes to rebon's
60
+ * own tools through `tool/invoke`, whose refusals (stable bracketed codes)
61
+ * come back as error RESULTS for the model, not crashes. v1 honesty
62
+ * boundary: no pre/post-execute pipeline — prepare always dispatches,
63
+ * finalize/finish pass through.
64
+ *
65
+ * A getter rather than a field on purpose: read through the Cordis Service
66
+ * proxy, `this.ctx` is the *caller's* context — the loop plugin driving the
67
+ * turn. That is the identity a `tool/invoke` made on the loop's own schedule
68
+ * has to carry, and capturing it here is the only place it is still known.
69
+ */
70
+ get [TOOL_RUNTIME_SCHEDULER]() {
71
+ const runtime = this;
72
+ const via = this.ctx;
73
+ return {
74
+ async prepare(exec) {
75
+ const state = { contexts: [], concluded: false };
76
+ const run = {
77
+ ...exec,
78
+ rootCallId: exec.rootCallId ?? exec.callId,
79
+ deferContext(message) {
80
+ state.contexts.push(message);
81
+ },
82
+ concludeTurn() {
83
+ state.concluded = true;
84
+ },
85
+ };
86
+ runStates.set(run, state);
87
+ return { kind: 'dispatch', exec: run };
88
+ },
89
+ dispatch: (exec) => runtime._dispatch(exec, via),
90
+ async finalize(_exec, result) {
91
+ return result;
92
+ },
93
+ finish(_exec, result) {
94
+ return result;
95
+ },
96
+ };
97
+ }
98
+
99
+ async _dispatch(exec, via) {
100
+ const state = runStates.get(exec) ?? { contexts: [], concluded: false };
101
+ try {
102
+ const def = this.defs.get(exec.name);
103
+ let content;
104
+ if (def !== undefined) {
105
+ const value = await def.execute(exec.arguments, exec);
106
+ content = def.output.render(exec.arguments, value);
107
+ } else {
108
+ const value = await invokeTool(exec.name, exec.arguments ?? {}, { via });
109
+ content = [{
110
+ type: 'text',
111
+ text: typeof value === 'string' ? value : JSON.stringify(value),
112
+ }];
113
+ }
114
+ return {
115
+ kind: 'final-result',
116
+ result: {
117
+ content,
118
+ isError: false,
119
+ ...(state.contexts.length > 0 ? { additionalContexts: state.contexts } : {}),
120
+ ...(state.concluded ? { concludesTurn: true } : {}),
121
+ },
122
+ };
123
+ } catch (error) {
124
+ const message = String(error?.message ?? error);
125
+ return {
126
+ kind: 'final-result',
127
+ result: {
128
+ content: [{ type: 'text', text: `Error: ${message}` }],
129
+ isError: true,
130
+ error: {
131
+ message,
132
+ info: {
133
+ name: error?.name ?? 'Error',
134
+ code: error?.code ?? bracketCode(message) ?? 'TOOL_FAILED',
135
+ },
136
+ },
137
+ },
138
+ };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * dsh concurrency classification. v1: everything is exclusive — strictly
144
+ * serial scheduling is always semantically correct, and it sidesteps the
145
+ * parallel pool until `isConcurrencySafe` mapping is wired.
146
+ */
147
+ executionMode(_exec) {
148
+ return { kind: 'exclusive' };
149
+ }
150
+
151
+ /**
152
+ * Register one dsh-shaped tool definition. Mirrors dsh
153
+ * `ToolRuntime.register` validation; returns the exact disposer.
154
+ */
155
+ register(definition) {
156
+ const name = definition?.name;
157
+ if (typeof name !== 'string' || name.trim().length === 0) {
158
+ throw new TypeError('tools.register: definition.name must be a non-empty string');
159
+ }
160
+ const output = definition.output;
161
+ if (
162
+ output === undefined || typeof output !== 'object'
163
+ || typeof output.render !== 'function'
164
+ ) {
165
+ throw new TypeError(`tool "${name}" must declare output { schema, render }`);
166
+ }
167
+ if (typeof definition.execute !== 'function') {
168
+ throw new TypeError(`tool "${name}" must declare execute()`);
169
+ }
170
+ const timeoutMs = definition.timeoutMs;
171
+ if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
172
+ throw new TypeError(`tool "${name}" timeoutMs must be a positive finite number`);
173
+ }
174
+ if (this.names.has(name)) {
175
+ throw new Error(`tool "${name}" is already registered in this scope`);
176
+ }
177
+ const runtime = this;
178
+ // `this.ctx` is the CALLER's context through the Service proxy, so the
179
+ // registration is attributed to the plugin that made it and the effect
180
+ // unwinds when that plugin disposes — Cordis RAII, llm-runtime same
181
+ // pattern. Read synchronously: which plugin is registering is a fact
182
+ // about now, and an await would let another one interleave.
183
+ const sink = sinkOf(this.ctx);
184
+ sink?.tool(
185
+ {
186
+ name,
187
+ description: String(definition.description ?? ''),
188
+ inputSchema: definition.parameters ?? { type: 'object' },
189
+ },
190
+ (input, callCtx) => withCall(callCtx, () => runtime._execute(name, input, callCtx)),
191
+ );
192
+ return this.ctx.effect(() => {
193
+ runtime.names.add(name);
194
+ runtime.defs.set(name, definition);
195
+ return () => {
196
+ runtime.names.delete(name);
197
+ runtime.defs.delete(name);
198
+ };
199
+ });
200
+ }
201
+
202
+ /// Runs one registered tool for a `tool/call`.
203
+ ///
204
+ /// Resolved through the live table rather than closing over the definition:
205
+ /// a tool whose plugin disposed it is gone, and answering with a body that
206
+ /// is no longer registered would make disposal a lie.
207
+ async _execute(name, input, callCtx) {
208
+ const definition = this.defs.get(name);
209
+ if (definition === undefined) {
210
+ const error = new Error(`tool "${name}" is no longer registered in this composition`);
211
+ error.code = '[STALE_PROVIDER]';
212
+ throw error;
213
+ }
214
+ const callId = `compose:${callCtx.scopeId ?? 'scope'}:${name}`;
215
+ const exec = {
216
+ callId,
217
+ rootCallId: callId,
218
+ name,
219
+ arguments: input,
220
+ signal: callCtx.signal,
221
+ // dsh tools write per-session facts through `exec.agent.session.append`.
222
+ // On the plane that is a published event: the tool is telling rebon
223
+ // something happened, and rebon decides who hears it.
224
+ agent: {
225
+ session: {
226
+ append(type, data) {
227
+ void publish(SESSION_APPEND_TOPIC, { type, data, tool: name, callId })
228
+ .catch(() => {});
229
+ },
230
+ },
231
+ },
232
+ };
233
+ // defineTool's execute validates arguments first (ToolArgsError with
234
+ // INVALID_ARGS semantics flows out as this call's terminal).
235
+ const value = await definition.execute(input, exec);
236
+ const content = definition.output.render(input, value);
237
+ return { content, isError: false };
238
+ }
239
+ }