@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.
- package/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +203 -144
- package/THIRD_PARTY_NOTICES.txt +1232 -128
- package/bin/rebon.js +22 -22
- package/lib/windows-managed.js +390 -390
- package/package.json +9 -2
- package/payload/compose-runtime/package.json +17 -17
- package/payload/compose-runtime/payload/compose/shims/dsh-anonymous-user-id.js +10 -10
- package/payload/compose-runtime/payload/compose/shims/dsh-credentials.js +13 -13
- package/payload/compose-runtime/payload/compose/shims/dsh-launch-environment.js +21 -21
- package/payload/compose-runtime/payload/compose/shims/dsh-llm.js +313 -313
- package/payload/compose-runtime/payload/compose/shims/dsh-settings.js +43 -43
- package/payload/compose-runtime/payload/compose/shims/dsh-tools.js +20 -20
- package/payload/compose-runtime/payload/compose/shims/dsh-web.js +10 -10
- package/payload/compose-runtime/payload/compose/shims/llm-adapter.js +32 -32
- package/payload/compose-runtime/payload/compose/shims/zod-lite.js +49 -49
- package/payload/compose-runtime/payload/vendor/cordis/LICENSE +21 -0
- package/payload/compose-runtime/payload/vendor/cordis/index.js +1530 -1530
- package/payload/compose-runtime/payload/vendor/cosmokit/LICENSE +21 -21
- package/payload/compose-runtime/payload/vendor/cosmokit/index.mjs +357 -357
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-agent +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-agent-loop +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-llm-core +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-llm-deepseek +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-logger-console +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-scope +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-session +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-system-prompt +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-timeout +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-timer +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tool-todo +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tool-web +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tools-schema +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-turndown +21 -0
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-turndown-plugin-gfm +21 -0
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-web-search-exa +21 -21
- package/payload/compose-runtime/payload/vendor/dsh/agent-loop.js +1193 -1193
- package/payload/compose-runtime/payload/vendor/dsh/agent.js +714 -714
- package/payload/compose-runtime/payload/vendor/dsh/llm-core.js +269 -269
- package/payload/compose-runtime/payload/vendor/dsh/llm-deepseek.js +672 -672
- package/payload/compose-runtime/payload/vendor/dsh/logger-console.js +83 -83
- package/payload/compose-runtime/payload/vendor/dsh/scope.js +287 -287
- package/payload/compose-runtime/payload/vendor/dsh/session.js +1668 -1668
- package/payload/compose-runtime/payload/vendor/dsh/system-prompt.js +309 -309
- package/payload/compose-runtime/payload/vendor/dsh/timeout.js +100 -100
- package/payload/compose-runtime/payload/vendor/dsh/timer.js +128 -128
- package/payload/compose-runtime/payload/vendor/dsh/tool-todo.js +143 -143
- package/payload/compose-runtime/payload/vendor/dsh/tool-web.js +1527 -1527
- package/payload/compose-runtime/payload/vendor/dsh/tools-schema.js +849 -849
- package/payload/compose-runtime/payload/vendor/dsh/web-search-exa.js +122 -122
- package/payload/compose-runtime/payload/vendor/eventsource-parser/LICENSE +21 -21
- package/payload/compose-runtime/payload/vendor/eventsource-parser/index.js +177 -177
- package/payload/compose-runtime/payload/vendor/eventsource-parser/stream.js +48 -48
- package/payload/compose-runtime/payload/vendor/schemastery/LICENSE +21 -0
- package/payload/compose-runtime/payload/vendor/schemastery/index.mjs +656 -656
- package/payload/compose-runtime/payload-manifests.json +48 -48
- package/payload/compose-runtime/src/bridge.mjs +193 -195
- package/payload/compose-runtime/src/credentials-runtime.mjs +50 -50
- package/payload/compose-runtime/src/eventsource-stream.mjs +48 -48
- package/payload/compose-runtime/src/index.mjs +7 -7
- package/payload/compose-runtime/src/llm-runtime.mjs +326 -326
- package/payload/compose-runtime/src/loader.mjs +78 -78
- package/payload/compose-runtime/src/loop-assembly.mjs +237 -237
- package/payload/compose-runtime/src/payload.mjs +49 -49
- package/payload/compose-runtime/src/plugin.mjs +59 -59
- package/payload/compose-runtime/src/realm.mjs +223 -223
- package/payload/compose-runtime/src/registry.mjs +191 -191
- package/payload/compose-runtime/src/resolve.mjs +115 -115
- package/payload/compose-runtime/src/systemprompt-runtime.mjs +67 -67
- package/payload/compose-runtime/src/tools-runtime.mjs +239 -239
- package/payload/compose-runtime/src/web-runtime.mjs +209 -209
- package/payload/plugin-host/package.json +11 -11
- package/payload/plugin-host/src/bridge.mjs +64 -64
- package/payload/plugin-host/src/cli.mjs +147 -147
- package/payload/plugin-host/src/framing.mjs +113 -113
- package/payload/plugin-host/src/host.mjs +691 -691
- package/payload/plugin-host/src/json.mjs +208 -208
- package/payload/plugin-host/src/lifecycle.mjs +114 -114
- package/payload/plugin-host/src/loader.mjs +183 -183
- package/payload/plugin-host/src/methods.mjs +377 -377
- package/payload/plugin-host/src/ownership.mjs +57 -57
- package/payload/plugin-host/src/protocol.mjs +129 -129
- package/payload/plugin-host/src/sdk.mjs +7 -7
- package/payload/rebon-boa-helper.exe +0 -0
- package/payload/rebon-browser-mcp.exe +0 -0
- package/payload/rebon-computer-use.exe +0 -0
- package/payload/rebon-lsp-mcp.exe +0 -0
- package/payload/rebon.exe +2 -2
- package/payload/sandbox-win.exe +0 -0
- package/scripts/postinstall.js +112 -112
- package/payload/compose-runtime/payload/compose/credentials-runtime.js +0 -38
- package/payload/compose-runtime/payload/compose/llm-runtime.js +0 -330
- package/payload/compose-runtime/payload/compose/loop-assembly.js +0 -149
- package/payload/compose-runtime/payload/compose/serve-dispatch.js +0 -82
- package/payload/compose-runtime/payload/compose/systemprompt-runtime.js +0 -74
- package/payload/compose-runtime/payload/compose/tools-runtime.js +0 -228
- package/payload/compose-runtime/payload/compose/web-runtime.js +0 -204
|
@@ -1,223 +1,223 @@
|
|
|
1
|
-
// The composition realm: one Cordis context, shared by every entry.
|
|
2
|
-
//
|
|
3
|
-
// This is the whole reason the composition exists rather than each plugin
|
|
4
|
-
// living alone. Cordis plugins reach each other through services on a context —
|
|
5
|
-
// `inject: ['tools']`, `ctx.llm.registerAdapter(...)` — and that only works if
|
|
6
|
-
// they are mounted on the same one. So the plane's "one plugin, one load" is
|
|
7
|
-
// the outside view, and inside the host they all land here.
|
|
8
|
-
//
|
|
9
|
-
// Two things live here that a flat list of plugins cannot express, and both are
|
|
10
|
-
// composition structure rather than plugin facts, which is why rebon describes
|
|
11
|
-
// them once when the composition is created rather than per load:
|
|
12
|
-
//
|
|
13
|
-
// * **groups** — a container that is not a module. Unloading one is unloading
|
|
14
|
-
// its members, which rebon does by naming them; nothing is loaded for the
|
|
15
|
-
// group itself.
|
|
16
|
-
// * **isolate** — Cordis's own realm narrowing. The agent loop needs its own
|
|
17
|
-
// `systemPrompt` because the real dsh service and rebon's seat share that
|
|
18
|
-
// name; without isolation one of them loses.
|
|
19
|
-
//
|
|
20
|
-
// Ordering note: this module imports Cordis by bare specifier, so it must not
|
|
21
|
-
// be imported before `installModuleResolution()` has run. Everything that
|
|
22
|
-
// reaches it does so through a dynamic import for exactly that reason.
|
|
23
|
-
import { Context } from 'cordis';
|
|
24
|
-
import { OWNER, RegistrationSink, SINK } from './registry.mjs';
|
|
25
|
-
import RebonLlmRuntime from './llm-runtime.mjs';
|
|
26
|
-
import RebonCredentials from './credentials-runtime.mjs';
|
|
27
|
-
import RebonToolsRuntime from './tools-runtime.mjs';
|
|
28
|
-
import RebonWebRuntime from './web-runtime.mjs';
|
|
29
|
-
import RebonSystemPromptRuntime from './systemprompt-runtime.mjs';
|
|
30
|
-
|
|
31
|
-
/** The built-in seats, in mount order: a dsh `inject` is satisfied at mount
|
|
32
|
-
* time, so a seat that arrives after the plugin needing it never arrives. */
|
|
33
|
-
const BUILTIN_SEATS = ['llm', 'credentials', 'tools', 'web', 'systemPrompt'];
|
|
34
|
-
|
|
35
|
-
let state;
|
|
36
|
-
|
|
37
|
-
class ComposeError extends Error {
|
|
38
|
-
constructor(code, message) {
|
|
39
|
-
super(message);
|
|
40
|
-
this.code = code;
|
|
41
|
-
this.name = 'ComposeError';
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/// Applies one node's `isolate` declaration to its parent context.
|
|
46
|
-
///
|
|
47
|
-
/// Cordis semantics, unchanged: `true` mints an entry-local realm; a string
|
|
48
|
-
/// labels a realm SHARED by every node using the same label, so an in-group
|
|
49
|
-
/// provider serves in-group consumers while the outside world keeps its own
|
|
50
|
-
/// service under the same name.
|
|
51
|
-
function derive(parent, node, realms) {
|
|
52
|
-
let derived = parent;
|
|
53
|
-
if (node.isolate && typeof node.isolate === 'object') {
|
|
54
|
-
for (const [service, label] of Object.entries(node.isolate)) {
|
|
55
|
-
if (label === true) {
|
|
56
|
-
derived = derived.isolate(service);
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
const key = `${service} ${label}`;
|
|
60
|
-
if (!realms.has(key)) realms.set(key, Symbol(`${service}#${label}`));
|
|
61
|
-
derived = derived.isolate(service, realms.get(key));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return derived;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/// Walks the declared structure into a placement per entry id.
|
|
68
|
-
///
|
|
69
|
-
/// The value is the context an entry is mounted *under*, not the entry's own —
|
|
70
|
-
/// that one is derived per load, because it carries the load's registration
|
|
71
|
-
/// sink and each load needs its own.
|
|
72
|
-
function place(nodes, parent, placements, groups, realms) {
|
|
73
|
-
for (const node of nodes ?? []) {
|
|
74
|
-
const id = node?.id;
|
|
75
|
-
if (typeof id !== 'string' || id.length === 0) {
|
|
76
|
-
throw new ComposeError('[BAD_STRUCTURE]', 'every entry in the composition structure needs an id');
|
|
77
|
-
}
|
|
78
|
-
if (placements.has(id) || groups.has(id)) {
|
|
79
|
-
throw new ComposeError('[BAD_STRUCTURE]', `duplicate entry id ${JSON.stringify(id)}`);
|
|
80
|
-
}
|
|
81
|
-
const derived = derive(parent, node, realms);
|
|
82
|
-
if (Array.isArray(node.group)) {
|
|
83
|
-
groups.set(id, derived);
|
|
84
|
-
place(node.group, derived, placements, groups, realms);
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
placements.set(id, derived);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/// Creates the realm. Called once, by the composition control plugin.
|
|
92
|
-
export async function createRealm({ web = null, entries = [] } = {}) {
|
|
93
|
-
if (state !== undefined) {
|
|
94
|
-
throw new ComposeError('[REALM_EXISTS]', 'the composition realm is already created');
|
|
95
|
-
}
|
|
96
|
-
const ctx = new Context();
|
|
97
|
-
const placements = new Map();
|
|
98
|
-
const groups = new Map();
|
|
99
|
-
const realms = new Map();
|
|
100
|
-
place(entries, ctx, placements, groups, realms);
|
|
101
|
-
|
|
102
|
-
await ctx.plugin(RebonLlmRuntime);
|
|
103
|
-
await ctx.plugin(RebonCredentials);
|
|
104
|
-
await ctx.plugin(RebonToolsRuntime);
|
|
105
|
-
await ctx.plugin(RebonWebRuntime, { web });
|
|
106
|
-
await ctx.plugin(RebonSystemPromptRuntime);
|
|
107
|
-
|
|
108
|
-
state = { ctx, placements, groups, realms, fibers: new Map(), sinks: new Map() };
|
|
109
|
-
return state;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/// The realm, or a refusal that names the ordering mistake.
|
|
113
|
-
export function realm() {
|
|
114
|
-
if (state === undefined) {
|
|
115
|
-
throw new ComposeError(
|
|
116
|
-
'[NO_REALM]',
|
|
117
|
-
'the composition realm does not exist yet; rebon loads the composition control plugin before any entry',
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
return state;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/// Tears the realm down. Only tests need this; a real host exits instead.
|
|
124
|
-
export async function destroyRealm() {
|
|
125
|
-
if (state === undefined) return;
|
|
126
|
-
for (const pluginId of [...state.fibers.keys()].reverse()) await disposeEntry(pluginId);
|
|
127
|
-
await state.ctx.dispose?.();
|
|
128
|
-
state = undefined;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function builtinSeats() {
|
|
132
|
-
return [...BUILTIN_SEATS];
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/// The services a Cordis plugin cannot start without.
|
|
136
|
-
///
|
|
137
|
-
/// Cordis accepts both spellings: a bare array is all-required, an object
|
|
138
|
-
/// separates `required` from `optional`.
|
|
139
|
-
function requiredInjects(plugin) {
|
|
140
|
-
const inject = plugin?.inject;
|
|
141
|
-
if (Array.isArray(inject)) return inject;
|
|
142
|
-
if (inject && typeof inject === 'object' && Array.isArray(inject.required)) return inject.required;
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/// Mounts one Cordis entry and hands back what it registered.
|
|
147
|
-
///
|
|
148
|
-
/// The sink rides on the entry's own derived context, which is what lets a seat
|
|
149
|
-
/// attribute a registration synchronously — the moment a plugin registers, the
|
|
150
|
-
/// context it registered through says which plugin it was.
|
|
151
|
-
export async function mountEntry(request, plugin) {
|
|
152
|
-
const live = realm();
|
|
153
|
-
const { pluginId } = request;
|
|
154
|
-
if (live.groups.has(pluginId)) {
|
|
155
|
-
throw new ComposeError(
|
|
156
|
-
'[GROUP_NOT_LOADABLE]',
|
|
157
|
-
`${pluginId} is a group in the composition structure, not a module; load its members`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
if (live.fibers.has(pluginId)) {
|
|
161
|
-
throw new ComposeError('[PLUGIN_ALREADY_LOADED]', `${pluginId} is already mounted`);
|
|
162
|
-
}
|
|
163
|
-
const parent = live.placements.get(pluginId) ?? live.ctx;
|
|
164
|
-
const sink = new RegistrationSink(pluginId, request);
|
|
165
|
-
const derived = parent.extend({ [OWNER]: pluginId, [SINK]: sink });
|
|
166
|
-
// Refused before mounting rather than waited for. Cordis lets a plugin wait
|
|
167
|
-
// for a service that has not arrived yet, and inside one composition that is
|
|
168
|
-
// a feature — but a load is a call rebon is waiting on, and a plugin waiting
|
|
169
|
-
// forever is a call that never answers. So a composition entry's required
|
|
170
|
-
// services must be there when it loads, and rebon orders entries so they
|
|
171
|
-
// are. Saying which one is missing turns a hang into a diagnosis.
|
|
172
|
-
for (const service of requiredInjects(plugin)) {
|
|
173
|
-
if (derived.get(service) === undefined) {
|
|
174
|
-
throw new ComposeError(
|
|
175
|
-
'[MISSING_INJECT]',
|
|
176
|
-
`${pluginId} requires service ${JSON.stringify(service)}, which nothing provides where it is mounted`,
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
let fiber;
|
|
181
|
-
try {
|
|
182
|
-
fiber = derived.plugin(plugin, request.config ?? undefined);
|
|
183
|
-
await fiber;
|
|
184
|
-
} catch (cause) {
|
|
185
|
-
// Whatever half-mounted has to go: a fiber left behind would keep serving
|
|
186
|
-
// through registrations rebon was never told about.
|
|
187
|
-
try {
|
|
188
|
-
await fiber?.dispose();
|
|
189
|
-
} catch {}
|
|
190
|
-
if (cause?.code) throw cause;
|
|
191
|
-
throw new ComposeError('[ACTIVATE_FAILED]', `mounting ${pluginId} failed: ${cause?.message ?? cause}`);
|
|
192
|
-
}
|
|
193
|
-
// Every catalog read has to finish before the report is sealed: a provider
|
|
194
|
-
// reported without its models would have rebon resolving against a table
|
|
195
|
-
// that is still being filled in.
|
|
196
|
-
await live.ctx.llm.settle();
|
|
197
|
-
sink.seal();
|
|
198
|
-
live.fibers.set(pluginId, fiber);
|
|
199
|
-
live.sinks.set(pluginId, sink);
|
|
200
|
-
return sink;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/// Disposes one entry. Idempotent: the host retires a plugin once, but a realm
|
|
204
|
-
/// torn down underneath it must not turn that into a second failure.
|
|
205
|
-
export async function disposeEntry(pluginId) {
|
|
206
|
-
if (state === undefined) return;
|
|
207
|
-
const fiber = state.fibers.get(pluginId);
|
|
208
|
-
state.fibers.delete(pluginId);
|
|
209
|
-
state.sinks.delete(pluginId);
|
|
210
|
-
if (fiber === undefined) return;
|
|
211
|
-
await fiber.dispose();
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/// What one entry reported beyond the protocol's own fields.
|
|
215
|
-
export function reportFor(pluginId) {
|
|
216
|
-
const sink = realm().sinks.get(pluginId);
|
|
217
|
-
return sink === undefined ? undefined : sink.report();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/// Every entry currently mounted, in load order.
|
|
221
|
-
export function mounted() {
|
|
222
|
-
return [...realm().fibers.keys()];
|
|
223
|
-
}
|
|
1
|
+
// The composition realm: one Cordis context, shared by every entry.
|
|
2
|
+
//
|
|
3
|
+
// This is the whole reason the composition exists rather than each plugin
|
|
4
|
+
// living alone. Cordis plugins reach each other through services on a context —
|
|
5
|
+
// `inject: ['tools']`, `ctx.llm.registerAdapter(...)` — and that only works if
|
|
6
|
+
// they are mounted on the same one. So the plane's "one plugin, one load" is
|
|
7
|
+
// the outside view, and inside the host they all land here.
|
|
8
|
+
//
|
|
9
|
+
// Two things live here that a flat list of plugins cannot express, and both are
|
|
10
|
+
// composition structure rather than plugin facts, which is why rebon describes
|
|
11
|
+
// them once when the composition is created rather than per load:
|
|
12
|
+
//
|
|
13
|
+
// * **groups** — a container that is not a module. Unloading one is unloading
|
|
14
|
+
// its members, which rebon does by naming them; nothing is loaded for the
|
|
15
|
+
// group itself.
|
|
16
|
+
// * **isolate** — Cordis's own realm narrowing. The agent loop needs its own
|
|
17
|
+
// `systemPrompt` because the real dsh service and rebon's seat share that
|
|
18
|
+
// name; without isolation one of them loses.
|
|
19
|
+
//
|
|
20
|
+
// Ordering note: this module imports Cordis by bare specifier, so it must not
|
|
21
|
+
// be imported before `installModuleResolution()` has run. Everything that
|
|
22
|
+
// reaches it does so through a dynamic import for exactly that reason.
|
|
23
|
+
import { Context } from 'cordis';
|
|
24
|
+
import { OWNER, RegistrationSink, SINK } from './registry.mjs';
|
|
25
|
+
import RebonLlmRuntime from './llm-runtime.mjs';
|
|
26
|
+
import RebonCredentials from './credentials-runtime.mjs';
|
|
27
|
+
import RebonToolsRuntime from './tools-runtime.mjs';
|
|
28
|
+
import RebonWebRuntime from './web-runtime.mjs';
|
|
29
|
+
import RebonSystemPromptRuntime from './systemprompt-runtime.mjs';
|
|
30
|
+
|
|
31
|
+
/** The built-in seats, in mount order: a dsh `inject` is satisfied at mount
|
|
32
|
+
* time, so a seat that arrives after the plugin needing it never arrives. */
|
|
33
|
+
const BUILTIN_SEATS = ['llm', 'credentials', 'tools', 'web', 'systemPrompt'];
|
|
34
|
+
|
|
35
|
+
let state;
|
|
36
|
+
|
|
37
|
+
class ComposeError extends Error {
|
|
38
|
+
constructor(code, message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.name = 'ComposeError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Applies one node's `isolate` declaration to its parent context.
|
|
46
|
+
///
|
|
47
|
+
/// Cordis semantics, unchanged: `true` mints an entry-local realm; a string
|
|
48
|
+
/// labels a realm SHARED by every node using the same label, so an in-group
|
|
49
|
+
/// provider serves in-group consumers while the outside world keeps its own
|
|
50
|
+
/// service under the same name.
|
|
51
|
+
function derive(parent, node, realms) {
|
|
52
|
+
let derived = parent;
|
|
53
|
+
if (node.isolate && typeof node.isolate === 'object') {
|
|
54
|
+
for (const [service, label] of Object.entries(node.isolate)) {
|
|
55
|
+
if (label === true) {
|
|
56
|
+
derived = derived.isolate(service);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const key = `${service} ${label}`;
|
|
60
|
+
if (!realms.has(key)) realms.set(key, Symbol(`${service}#${label}`));
|
|
61
|
+
derived = derived.isolate(service, realms.get(key));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return derived;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Walks the declared structure into a placement per entry id.
|
|
68
|
+
///
|
|
69
|
+
/// The value is the context an entry is mounted *under*, not the entry's own —
|
|
70
|
+
/// that one is derived per load, because it carries the load's registration
|
|
71
|
+
/// sink and each load needs its own.
|
|
72
|
+
function place(nodes, parent, placements, groups, realms) {
|
|
73
|
+
for (const node of nodes ?? []) {
|
|
74
|
+
const id = node?.id;
|
|
75
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
76
|
+
throw new ComposeError('[BAD_STRUCTURE]', 'every entry in the composition structure needs an id');
|
|
77
|
+
}
|
|
78
|
+
if (placements.has(id) || groups.has(id)) {
|
|
79
|
+
throw new ComposeError('[BAD_STRUCTURE]', `duplicate entry id ${JSON.stringify(id)}`);
|
|
80
|
+
}
|
|
81
|
+
const derived = derive(parent, node, realms);
|
|
82
|
+
if (Array.isArray(node.group)) {
|
|
83
|
+
groups.set(id, derived);
|
|
84
|
+
place(node.group, derived, placements, groups, realms);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
placements.set(id, derived);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Creates the realm. Called once, by the composition control plugin.
|
|
92
|
+
export async function createRealm({ web = null, entries = [] } = {}) {
|
|
93
|
+
if (state !== undefined) {
|
|
94
|
+
throw new ComposeError('[REALM_EXISTS]', 'the composition realm is already created');
|
|
95
|
+
}
|
|
96
|
+
const ctx = new Context();
|
|
97
|
+
const placements = new Map();
|
|
98
|
+
const groups = new Map();
|
|
99
|
+
const realms = new Map();
|
|
100
|
+
place(entries, ctx, placements, groups, realms);
|
|
101
|
+
|
|
102
|
+
await ctx.plugin(RebonLlmRuntime);
|
|
103
|
+
await ctx.plugin(RebonCredentials);
|
|
104
|
+
await ctx.plugin(RebonToolsRuntime);
|
|
105
|
+
await ctx.plugin(RebonWebRuntime, { web });
|
|
106
|
+
await ctx.plugin(RebonSystemPromptRuntime);
|
|
107
|
+
|
|
108
|
+
state = { ctx, placements, groups, realms, fibers: new Map(), sinks: new Map() };
|
|
109
|
+
return state;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// The realm, or a refusal that names the ordering mistake.
|
|
113
|
+
export function realm() {
|
|
114
|
+
if (state === undefined) {
|
|
115
|
+
throw new ComposeError(
|
|
116
|
+
'[NO_REALM]',
|
|
117
|
+
'the composition realm does not exist yet; rebon loads the composition control plugin before any entry',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return state;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Tears the realm down. Only tests need this; a real host exits instead.
|
|
124
|
+
export async function destroyRealm() {
|
|
125
|
+
if (state === undefined) return;
|
|
126
|
+
for (const pluginId of [...state.fibers.keys()].reverse()) await disposeEntry(pluginId);
|
|
127
|
+
await state.ctx.dispose?.();
|
|
128
|
+
state = undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function builtinSeats() {
|
|
132
|
+
return [...BUILTIN_SEATS];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// The services a Cordis plugin cannot start without.
|
|
136
|
+
///
|
|
137
|
+
/// Cordis accepts both spellings: a bare array is all-required, an object
|
|
138
|
+
/// separates `required` from `optional`.
|
|
139
|
+
function requiredInjects(plugin) {
|
|
140
|
+
const inject = plugin?.inject;
|
|
141
|
+
if (Array.isArray(inject)) return inject;
|
|
142
|
+
if (inject && typeof inject === 'object' && Array.isArray(inject.required)) return inject.required;
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Mounts one Cordis entry and hands back what it registered.
|
|
147
|
+
///
|
|
148
|
+
/// The sink rides on the entry's own derived context, which is what lets a seat
|
|
149
|
+
/// attribute a registration synchronously — the moment a plugin registers, the
|
|
150
|
+
/// context it registered through says which plugin it was.
|
|
151
|
+
export async function mountEntry(request, plugin) {
|
|
152
|
+
const live = realm();
|
|
153
|
+
const { pluginId } = request;
|
|
154
|
+
if (live.groups.has(pluginId)) {
|
|
155
|
+
throw new ComposeError(
|
|
156
|
+
'[GROUP_NOT_LOADABLE]',
|
|
157
|
+
`${pluginId} is a group in the composition structure, not a module; load its members`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (live.fibers.has(pluginId)) {
|
|
161
|
+
throw new ComposeError('[PLUGIN_ALREADY_LOADED]', `${pluginId} is already mounted`);
|
|
162
|
+
}
|
|
163
|
+
const parent = live.placements.get(pluginId) ?? live.ctx;
|
|
164
|
+
const sink = new RegistrationSink(pluginId, request);
|
|
165
|
+
const derived = parent.extend({ [OWNER]: pluginId, [SINK]: sink });
|
|
166
|
+
// Refused before mounting rather than waited for. Cordis lets a plugin wait
|
|
167
|
+
// for a service that has not arrived yet, and inside one composition that is
|
|
168
|
+
// a feature — but a load is a call rebon is waiting on, and a plugin waiting
|
|
169
|
+
// forever is a call that never answers. So a composition entry's required
|
|
170
|
+
// services must be there when it loads, and rebon orders entries so they
|
|
171
|
+
// are. Saying which one is missing turns a hang into a diagnosis.
|
|
172
|
+
for (const service of requiredInjects(plugin)) {
|
|
173
|
+
if (derived.get(service) === undefined) {
|
|
174
|
+
throw new ComposeError(
|
|
175
|
+
'[MISSING_INJECT]',
|
|
176
|
+
`${pluginId} requires service ${JSON.stringify(service)}, which nothing provides where it is mounted`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
let fiber;
|
|
181
|
+
try {
|
|
182
|
+
fiber = derived.plugin(plugin, request.config ?? undefined);
|
|
183
|
+
await fiber;
|
|
184
|
+
} catch (cause) {
|
|
185
|
+
// Whatever half-mounted has to go: a fiber left behind would keep serving
|
|
186
|
+
// through registrations rebon was never told about.
|
|
187
|
+
try {
|
|
188
|
+
await fiber?.dispose();
|
|
189
|
+
} catch {}
|
|
190
|
+
if (cause?.code) throw cause;
|
|
191
|
+
throw new ComposeError('[ACTIVATE_FAILED]', `mounting ${pluginId} failed: ${cause?.message ?? cause}`);
|
|
192
|
+
}
|
|
193
|
+
// Every catalog read has to finish before the report is sealed: a provider
|
|
194
|
+
// reported without its models would have rebon resolving against a table
|
|
195
|
+
// that is still being filled in.
|
|
196
|
+
await live.ctx.llm.settle();
|
|
197
|
+
sink.seal();
|
|
198
|
+
live.fibers.set(pluginId, fiber);
|
|
199
|
+
live.sinks.set(pluginId, sink);
|
|
200
|
+
return sink;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// Disposes one entry. Idempotent: the host retires a plugin once, but a realm
|
|
204
|
+
/// torn down underneath it must not turn that into a second failure.
|
|
205
|
+
export async function disposeEntry(pluginId) {
|
|
206
|
+
if (state === undefined) return;
|
|
207
|
+
const fiber = state.fibers.get(pluginId);
|
|
208
|
+
state.fibers.delete(pluginId);
|
|
209
|
+
state.sinks.delete(pluginId);
|
|
210
|
+
if (fiber === undefined) return;
|
|
211
|
+
await fiber.dispose();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// What one entry reported beyond the protocol's own fields.
|
|
215
|
+
export function reportFor(pluginId) {
|
|
216
|
+
const sink = realm().sinks.get(pluginId);
|
|
217
|
+
return sink === undefined ? undefined : sink.report();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Every entry currently mounted, in load order.
|
|
221
|
+
export function mounted() {
|
|
222
|
+
return [...realm().fibers.keys()];
|
|
223
|
+
}
|