@rebon/cli-darwin-arm64 0.17.3 → 0.19.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/bin/compose-runtime/package.json +17 -0
- package/bin/compose-runtime/payload/compose/credentials-runtime.js +38 -0
- package/bin/compose-runtime/payload/compose/llm-runtime.js +330 -0
- package/bin/compose-runtime/payload/compose/loop-assembly.js +149 -0
- package/bin/compose-runtime/payload/compose/serve-dispatch.js +82 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-anonymous-user-id.js +10 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-credentials.js +13 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-launch-environment.js +21 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-llm.js +313 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-settings.js +43 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-tools.js +20 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-web.js +10 -0
- package/bin/compose-runtime/payload/compose/shims/llm-adapter.js +32 -0
- package/bin/compose-runtime/payload/compose/shims/zod-lite.js +49 -0
- package/bin/compose-runtime/payload/compose/systemprompt-runtime.js +74 -0
- package/bin/compose-runtime/payload/compose/tools-runtime.js +228 -0
- package/bin/compose-runtime/payload/compose/web-runtime.js +204 -0
- package/bin/compose-runtime/payload/vendor/cordis/index.js +1530 -0
- package/bin/compose-runtime/payload/vendor/cosmokit/LICENSE +21 -0
- package/bin/compose-runtime/payload/vendor/cosmokit/index.mjs +357 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-agent +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-agent-loop +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-llm-core +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-llm-deepseek +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-logger-console +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-scope +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-session +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-system-prompt +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-timeout +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-timer +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-tool-todo +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-tool-web +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-tools-schema +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-web-search-exa +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/agent-loop.js +1193 -0
- package/bin/compose-runtime/payload/vendor/dsh/agent.js +714 -0
- package/bin/compose-runtime/payload/vendor/dsh/llm-core.js +269 -0
- package/bin/compose-runtime/payload/vendor/dsh/llm-deepseek.js +672 -0
- package/bin/compose-runtime/payload/vendor/dsh/logger-console.js +83 -0
- package/bin/compose-runtime/payload/vendor/dsh/scope.js +287 -0
- package/bin/compose-runtime/payload/vendor/dsh/session.js +1668 -0
- package/bin/compose-runtime/payload/vendor/dsh/system-prompt.js +309 -0
- package/bin/compose-runtime/payload/vendor/dsh/timeout.js +100 -0
- package/bin/compose-runtime/payload/vendor/dsh/timer.js +128 -0
- package/bin/compose-runtime/payload/vendor/dsh/tool-todo.js +143 -0
- package/bin/compose-runtime/payload/vendor/dsh/tool-web.js +1527 -0
- package/bin/compose-runtime/payload/vendor/dsh/tools-schema.js +849 -0
- package/bin/compose-runtime/payload/vendor/dsh/web-search-exa.js +122 -0
- package/bin/compose-runtime/payload/vendor/eventsource-parser/LICENSE +21 -0
- package/bin/compose-runtime/payload/vendor/eventsource-parser/index.js +177 -0
- package/bin/compose-runtime/payload/vendor/eventsource-parser/stream.js +48 -0
- package/bin/compose-runtime/payload/vendor/schemastery/index.mjs +656 -0
- package/bin/compose-runtime/payload-manifests.json +48 -0
- package/bin/compose-runtime/src/bridge.mjs +142 -0
- package/bin/compose-runtime/src/credentials-runtime.mjs +50 -0
- package/bin/compose-runtime/src/eventsource-stream.mjs +48 -0
- package/bin/compose-runtime/src/index.mjs +7 -0
- package/bin/compose-runtime/src/llm-runtime.mjs +326 -0
- package/bin/compose-runtime/src/loader.mjs +78 -0
- package/bin/compose-runtime/src/loop-assembly.mjs +237 -0
- package/bin/compose-runtime/src/payload.mjs +49 -0
- package/bin/compose-runtime/src/plugin.mjs +59 -0
- package/bin/compose-runtime/src/realm.mjs +223 -0
- package/bin/compose-runtime/src/registry.mjs +191 -0
- package/bin/compose-runtime/src/resolve.mjs +115 -0
- package/bin/compose-runtime/src/systemprompt-runtime.mjs +67 -0
- package/bin/compose-runtime/src/tools-runtime.mjs +239 -0
- package/bin/compose-runtime/src/web-runtime.mjs +209 -0
- package/bin/plugin-host/package.json +11 -0
- package/bin/plugin-host/src/bridge.mjs +64 -0
- package/bin/plugin-host/src/cli.mjs +105 -0
- package/bin/plugin-host/src/framing.mjs +113 -0
- package/bin/plugin-host/src/host.mjs +624 -0
- package/bin/plugin-host/src/json.mjs +196 -0
- package/bin/plugin-host/src/lifecycle.mjs +114 -0
- package/bin/plugin-host/src/loader.mjs +142 -0
- package/bin/plugin-host/src/methods.mjs +256 -0
- package/bin/plugin-host/src/protocol.mjs +129 -0
- package/bin/plugin-host/src/sdk.mjs +7 -0
- package/bin/rebon +0 -0
- package/bin/rebon-boa-helper +0 -0
- package/package.json +4 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rebon/compose-runtime",
|
|
3
|
+
"version": "0.0.0-private",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT OR Apache-2.0",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=24.19.0 <25"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test test/*.test.mjs"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./src/index.mjs",
|
|
15
|
+
"./control": "./src/plugin.mjs"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// dsh-compatible `ctx.credentials` seat for the composition host.
|
|
2
|
+
//
|
|
3
|
+
// dsh configuration carries credential *references* (environment-variable
|
|
4
|
+
// names); the provider owns the values. This provider resolves a reference
|
|
5
|
+
// through the kernel's fail-closed `credentials` JSON service (`resolveEnv`
|
|
6
|
+
// runs the `credentials/authorize` waterfall before touching the
|
|
7
|
+
// environment), so an unauthorized or unset reference is simply "absent" —
|
|
8
|
+
// the consumer's own missing-credential diagnosis then fires (dsh-llm's
|
|
9
|
+
// `MISSING_CREDENTIAL`), instead of this seat inventing one.
|
|
10
|
+
import { Service } from 'cordis';
|
|
11
|
+
import { callService } from 'rebon';
|
|
12
|
+
|
|
13
|
+
export default class RebonCredentials extends Service {
|
|
14
|
+
constructor(ctx) {
|
|
15
|
+
super(ctx, 'credentials');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** dsh CredentialProvider.resolve: `{value, source}` or undefined. */
|
|
19
|
+
async resolve(ref) {
|
|
20
|
+
try {
|
|
21
|
+
const out = callService('credentials', 'resolveEnv', { ref: String(ref) });
|
|
22
|
+
const value = out?.value;
|
|
23
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
24
|
+
return { value, source: 'env' };
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// Fail-closed kernel refusal (no authorizer, unset variable, bad ref)
|
|
28
|
+
// all read as "not configured here".
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** dsh CredentialProvider.describe: facts only, never the value. */
|
|
34
|
+
async describe(ref) {
|
|
35
|
+
const hit = await this.resolve(ref);
|
|
36
|
+
return { configured: hit !== undefined, ...(hit ? { source: hit.source } : {}), writable: false };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// Minimal dsh-compatible `ctx.llm` seat for the composition host.
|
|
2
|
+
//
|
|
3
|
+
// API shape follows dsh packages/llm: adapters extend `LlmAdapter` (only
|
|
4
|
+
// `stream(options)` is required) and register with
|
|
5
|
+
// `ctx.llm.registerAdapter(providers, adapter)`. Each registered route is
|
|
6
|
+
// mirrored into the kernel's model-router table (catalog fetched via the
|
|
7
|
+
// adapter's advisory `listModels`), and `llm/adapter-registered` /
|
|
8
|
+
// `llm/adapter-unregistered` kernel events let the Rust side bind this
|
|
9
|
+
// composition as the route's stream host. Model requests arrive over the
|
|
10
|
+
// host's llm ops (`op_llm_next`) and are dispatched to the owning adapter;
|
|
11
|
+
// cancellation aborts the per-request AbortController.
|
|
12
|
+
import { Service } from 'cordis';
|
|
13
|
+
import { callService, emit } from 'rebon';
|
|
14
|
+
import {
|
|
15
|
+
LlmError,
|
|
16
|
+
deepFreeze,
|
|
17
|
+
callConfigEquals,
|
|
18
|
+
errorChain,
|
|
19
|
+
resolveRetryPolicy,
|
|
20
|
+
} from './shims/dsh-llm.js';
|
|
21
|
+
// The base class every provider plugin extends. Re-exported here because
|
|
22
|
+
// that is where dsh consumers have always imported it from; it is defined
|
|
23
|
+
// apart from this module so importing it does not drag a host's transport in.
|
|
24
|
+
export { LlmAdapter } from './shims/llm-adapter.js';
|
|
25
|
+
|
|
26
|
+
const core = globalThis.Deno.core;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalize an adapter throw into dsh's terminal finish chunk
|
|
30
|
+
* (`adapterFailureChunk`, transcribed lean: LlmError keeps its facts,
|
|
31
|
+
* anything else flattens under UNKNOWN — the same rule the loop itself
|
|
32
|
+
* applies at its turn boundary).
|
|
33
|
+
*/
|
|
34
|
+
function failureChunk(error, signal) {
|
|
35
|
+
const failure = error instanceof LlmError
|
|
36
|
+
? error.failure
|
|
37
|
+
: { message: errorChain(error), code: 'UNKNOWN' };
|
|
38
|
+
return {
|
|
39
|
+
type: 'finish',
|
|
40
|
+
reason: signal?.aborted || failure.code === 'ABORTED'
|
|
41
|
+
? { kind: 'aborted', failure }
|
|
42
|
+
: { kind: 'error', failure },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export default class RebonLlmRuntime extends Service {
|
|
48
|
+
constructor(ctx, config) {
|
|
49
|
+
super(ctx, 'llm');
|
|
50
|
+
this.routes = new Map(); // provider -> adapter
|
|
51
|
+
this.aborts = new Map(); // stream id -> AbortController
|
|
52
|
+
// Per-loop hosts serve only in-composition consumers: no process-plane
|
|
53
|
+
// mirror, no adapter events (a process binding acting on them would
|
|
54
|
+
// tear down routes the shared composition still serves).
|
|
55
|
+
this.announce = config?.announce !== false;
|
|
56
|
+
void this._pump();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
registerAdapter(providers, adapter) {
|
|
60
|
+
if (!providers?.length) {
|
|
61
|
+
throw new Error('an adapter must register at least one provider');
|
|
62
|
+
}
|
|
63
|
+
for (const provider of providers) {
|
|
64
|
+
if (this.routes.has(provider)) {
|
|
65
|
+
throw new Error(`an adapter for provider "${provider}" is already registered`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const runtime = this;
|
|
69
|
+
// Owner captured in the synchronous half: _announce runs async, and
|
|
70
|
+
// by then the registering context must already be pinned down.
|
|
71
|
+
const owner = this.ctx[Symbol.for('rebon.composeOwner')] ?? '';
|
|
72
|
+
this.ctx.effect(() => {
|
|
73
|
+
for (const provider of providers) {
|
|
74
|
+
runtime.routes.set(provider, adapter);
|
|
75
|
+
if (runtime.announce) void runtime._announce(provider, adapter, owner);
|
|
76
|
+
}
|
|
77
|
+
return () => {
|
|
78
|
+
for (const provider of providers) {
|
|
79
|
+
runtime.routes.delete(provider);
|
|
80
|
+
if (!runtime.announce) continue;
|
|
81
|
+
try {
|
|
82
|
+
callService('model-router', 'unregister', { provider });
|
|
83
|
+
} catch {}
|
|
84
|
+
emit('llm/adapter-unregistered', { provider });
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
// dsh handle shape: route replacement exists on it, but a static
|
|
89
|
+
// composition never re-reads registration facts, so a call here is a
|
|
90
|
+
// consumer bug — refuse loudly instead of silently diverging from dsh.
|
|
91
|
+
return {
|
|
92
|
+
providers: [...providers],
|
|
93
|
+
replace() {
|
|
94
|
+
throw new Error('adapter route replacement is not supported in a static composition');
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* dsh surfaces these entries to its settings UI. The embedded runtime has
|
|
101
|
+
* no such surface; record them so diagnostics can list what a composition
|
|
102
|
+
* declared, and accept the call so unmodified dsh plugins load.
|
|
103
|
+
*/
|
|
104
|
+
registerConfigurableProviders(entries) {
|
|
105
|
+
this.configurableProviders ??= [];
|
|
106
|
+
for (const entry of entries ?? []) {
|
|
107
|
+
if (entry?.provider) {
|
|
108
|
+
this.configurableProviders.push({
|
|
109
|
+
provider: String(entry.provider),
|
|
110
|
+
displayName: entry.displayName === undefined ? undefined : String(entry.displayName),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- JS consumer face (loop round) ----
|
|
117
|
+
//
|
|
118
|
+
// The pump below serves REQUESTS FROM RUST; the two methods here serve
|
|
119
|
+
// consumers INSIDE the composition (the agent loop). Routing is the same
|
|
120
|
+
// in-JS adapter table. v1 honesty boundary: no `llm/stream` middleware
|
|
121
|
+
// waterfall — dispatch goes straight to the owning adapter.
|
|
122
|
+
|
|
123
|
+
/** Resolve the owning adapter or refuse with dsh's NO_ADAPTER code. */
|
|
124
|
+
_registration(provider) {
|
|
125
|
+
const adapter = this.routes.get(provider);
|
|
126
|
+
if (!adapter) {
|
|
127
|
+
throw new LlmError(`no adapter registered for provider "${provider}"`, 'NO_ADAPTER');
|
|
128
|
+
}
|
|
129
|
+
return adapter;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* dsh `LlmRuntime.prepareCall`, transcribed: resolve the route's exact
|
|
134
|
+
* model, materialize adapter defaults (recorded in `adapterDefaults` so
|
|
135
|
+
* the loop's request header can re-resolve them later), and hand back a
|
|
136
|
+
* one-shot stream bound to this registration.
|
|
137
|
+
*/
|
|
138
|
+
async prepareCall(config, signal) {
|
|
139
|
+
const adapter = this._registration(config.provider);
|
|
140
|
+
const info = (await adapter.resolveModel(config.provider, config.model)) ?? {};
|
|
141
|
+
signal?.throwIfAborted?.();
|
|
142
|
+
const defaulted = config.maxTokens === undefined && info.defaultMaxTokens !== undefined
|
|
143
|
+
? { ...config, maxTokens: info.defaultMaxTokens }
|
|
144
|
+
: config;
|
|
145
|
+
const reasoning = info.reasoning;
|
|
146
|
+
const requested = defaulted.reasoningEffort;
|
|
147
|
+
let resolvedConfig = defaulted;
|
|
148
|
+
if (reasoning === undefined) {
|
|
149
|
+
if (requested !== undefined) {
|
|
150
|
+
throw new LlmError(
|
|
151
|
+
`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`,
|
|
152
|
+
'UNSUPPORTED_REASONING_EFFORT',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
const effective = requested ?? reasoning.defaultEffort;
|
|
157
|
+
if (effective !== undefined) {
|
|
158
|
+
if (!(reasoning.efforts ?? []).some((effort) => effort.id === effective)) {
|
|
159
|
+
throw new LlmError(
|
|
160
|
+
`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`,
|
|
161
|
+
'UNSUPPORTED_REASONING_EFFORT',
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (requested !== effective) resolvedConfig = { ...defaulted, reasoningEffort: effective };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const frozen = deepFreeze(structuredClone(resolvedConfig));
|
|
168
|
+
const context = info.context === undefined
|
|
169
|
+
? undefined
|
|
170
|
+
: deepFreeze(structuredClone(info.context));
|
|
171
|
+
const adapterDefaults = deepFreeze({
|
|
172
|
+
...(config.reasoningEffort === undefined && frozen.reasoningEffort !== undefined
|
|
173
|
+
? { reasoningEffort: true }
|
|
174
|
+
: {}),
|
|
175
|
+
...(config.maxTokens === undefined && frozen.maxTokens !== undefined
|
|
176
|
+
? { maxTokens: true }
|
|
177
|
+
: {}),
|
|
178
|
+
});
|
|
179
|
+
const runtime = this;
|
|
180
|
+
let dispatched = false;
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
config: frozen,
|
|
183
|
+
// dsh contract: providerRetryPolicy returns an ALREADY-RESOLVED
|
|
184
|
+
// policy (or undefined for the normal defaults) — never re-resolve.
|
|
185
|
+
retryPolicy: adapter.providerRetryPolicy(config.provider)
|
|
186
|
+
?? resolveRetryPolicy(undefined, `llm: provider "${config.provider}" retryPolicy`),
|
|
187
|
+
adapterDefaults,
|
|
188
|
+
...(context === undefined ? {} : { context }),
|
|
189
|
+
stream(options) {
|
|
190
|
+
if (dispatched) {
|
|
191
|
+
throw new LlmError('a prepared LLM call can only be dispatched once', 'INVALID_PREPARED_CALL');
|
|
192
|
+
}
|
|
193
|
+
if (!callConfigEquals(options, frozen)) {
|
|
194
|
+
throw new LlmError('prepared LLM call config changed before adapter dispatch', 'INVALID_PREPARED_CALL');
|
|
195
|
+
}
|
|
196
|
+
dispatched = true;
|
|
197
|
+
return runtime._consumerStream(options);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Consumer fallback entry (the loop's NO_ADAPTER path): direct dispatch. */
|
|
203
|
+
stream(options) {
|
|
204
|
+
return this._consumerStream(options);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Adapter boundary for in-composition consumers: selection, dispatch, and
|
|
209
|
+
* iteration failures become one terminal failure chunk (dsh
|
|
210
|
+
* `adapterStream` semantics) so the loop's assembler/request-error path
|
|
211
|
+
* sees them instead of a raw throw.
|
|
212
|
+
*/
|
|
213
|
+
async *_consumerStream(options) {
|
|
214
|
+
let iterator;
|
|
215
|
+
try {
|
|
216
|
+
const adapter = this._registration(options.provider);
|
|
217
|
+
iterator = adapter.stream(options)[Symbol.asyncIterator]();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
yield failureChunk(error, options.signal);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
while (true) {
|
|
223
|
+
let item;
|
|
224
|
+
try {
|
|
225
|
+
item = await iterator.next();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
yield failureChunk(error, options.signal);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (item.done) return;
|
|
231
|
+
yield item.value;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Mirror one route into the kernel table, catalog included (advisory).
|
|
237
|
+
*
|
|
238
|
+
* `listModels` is an adapter call that can take as long as a network
|
|
239
|
+
* round-trip, and an unload may complete inside that await — the effect's
|
|
240
|
+
* disposer having already issued the compensating `unregister`. Announcing
|
|
241
|
+
* afterwards would resurrect the route of a plugin that is gone, with
|
|
242
|
+
* nothing left to tear it down. The route table is the liveness witness:
|
|
243
|
+
* the disposer deletes this provider's entry, so anything but our own
|
|
244
|
+
* adapter still sitting there means the registration we are announcing is
|
|
245
|
+
* no longer the live one.
|
|
246
|
+
*/
|
|
247
|
+
async _announce(provider, adapter, owner = '') {
|
|
248
|
+
let models = [];
|
|
249
|
+
try {
|
|
250
|
+
models = (await adapter.listModels(provider)) ?? [];
|
|
251
|
+
} catch {}
|
|
252
|
+
if (this.routes.get(provider) !== adapter) return;
|
|
253
|
+
const entries = models
|
|
254
|
+
.filter((m) => m?.id)
|
|
255
|
+
.map((m) => ({ id: m.id, contextWindow: m.context?.window ?? m.contextWindow }));
|
|
256
|
+
try {
|
|
257
|
+
callService('model-router', 'register', {
|
|
258
|
+
provider,
|
|
259
|
+
models: entries,
|
|
260
|
+
// The catalog is advisory (dsh semantics): with an empty one the
|
|
261
|
+
// route still exists and callers name models explicitly; "default"
|
|
262
|
+
// is only the resolve fallback for bare `{provider}` lookups.
|
|
263
|
+
defaultModel: entries[0]?.id ?? 'default',
|
|
264
|
+
}, owner);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
emit('llm/adapter-error', { provider, error: String(e?.message ?? e) });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
emit('llm/adapter-registered', { provider });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Host instruction loop: dispatch requests, abort on cancel. */
|
|
273
|
+
async _pump() {
|
|
274
|
+
while (true) {
|
|
275
|
+
const instr = JSON.parse(await core.ops.op_llm_next());
|
|
276
|
+
if (instr.closed) break;
|
|
277
|
+
if (instr.kind === 'cancel') {
|
|
278
|
+
this.aborts.get(instr.id)?.abort();
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
void this._dispatch(instr.id, instr.request);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async _dispatch(id, request) {
|
|
286
|
+
const send = (obj) => core.ops.op_llm_emit(id, JSON.stringify(obj));
|
|
287
|
+
const adapter = this.routes.get(request.provider);
|
|
288
|
+
if (!adapter) {
|
|
289
|
+
send({ err: `no adapter registered for provider route "${request.provider}"` });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const controller = new AbortController();
|
|
293
|
+
this.aborts.set(id, controller);
|
|
294
|
+
try {
|
|
295
|
+
const options = {
|
|
296
|
+
provider: request.provider,
|
|
297
|
+
model: request.model,
|
|
298
|
+
messages: request.messages ?? [],
|
|
299
|
+
system: request.system ?? undefined,
|
|
300
|
+
tools: request.tools ?? undefined,
|
|
301
|
+
maxTokens: request.maxTokens ?? undefined,
|
|
302
|
+
temperature: request.temperature ?? undefined,
|
|
303
|
+
stop: request.stop ?? undefined,
|
|
304
|
+
reasoningEffort: request.reasoningEffort ?? undefined,
|
|
305
|
+
sessionId: request.sessionId ?? undefined,
|
|
306
|
+
purpose: request.purpose ?? undefined,
|
|
307
|
+
signal: controller.signal,
|
|
308
|
+
};
|
|
309
|
+
for await (const chunk of adapter.stream(options)) {
|
|
310
|
+
send({ chunk });
|
|
311
|
+
}
|
|
312
|
+
send({ done: true });
|
|
313
|
+
} catch (e) {
|
|
314
|
+
// dsh contract: an adapter may throw on abort; normalize either way.
|
|
315
|
+
if (controller.signal.aborted) {
|
|
316
|
+
send({ chunk: { type: 'finish', reason: 'aborted' } });
|
|
317
|
+
send({ done: true });
|
|
318
|
+
} else {
|
|
319
|
+
// LlmError carries a stable machine code beside the message —
|
|
320
|
+
// surface it so the Rust side can route/report on it.
|
|
321
|
+
send({
|
|
322
|
+
err: String(e?.message ?? e),
|
|
323
|
+
...(typeof e?.code === 'string' && e.code.length > 0 ? { code: e.code } : {}),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
} finally {
|
|
327
|
+
this.aborts.delete(id);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// rebon-loop-assembly: the composition-side glue that lets the REAL
|
|
2
|
+
// dsh-agent-loop drive a turn inside this isolate (assembly doc:
|
|
3
|
+
// docs/dsh-agent-loop-assembly.md §4).
|
|
4
|
+
//
|
|
5
|
+
// Three duties, all rebon-owned (the dsh packages stay unmodified):
|
|
6
|
+
// 1. Tool-schema supply: registers a dsh systemPrompt tool provider that
|
|
7
|
+
// merges the composition's own registered definitions with the R5 core
|
|
8
|
+
// seat catalog (describeTools) — the same two sources the tools seat's
|
|
9
|
+
// scheduler dispatches into, so model-visible and dispatchable stay in
|
|
10
|
+
// step (local definition shadows a seat name in BOTH places).
|
|
11
|
+
// 2. Observability: relays every dsh `session/event` (chunks included —
|
|
12
|
+
// the embedder's session face streams from them) and `agent/error`
|
|
13
|
+
// onto the kernel event plane (`loop:event` / `loop:agent-error`),
|
|
14
|
+
// stamped with the embedder's `tag` so co-resident loop hosts stay
|
|
15
|
+
// distinguishable.
|
|
16
|
+
// 3. Drive: `loop:control` serve target (followup/steer/cancel/status)
|
|
17
|
+
// is the embedder's inbound face; config `{kickoff: "..."}` remains as
|
|
18
|
+
// an e2e self-drive convenience.
|
|
19
|
+
import { emit, describeTools, logger } from 'rebon';
|
|
20
|
+
import { createUserMessage, errorChain } from '@deepseek-ai/dsh-llm';
|
|
21
|
+
import { registerServeTarget } from './serve-dispatch.js';
|
|
22
|
+
|
|
23
|
+
/** Best-effort warn: the kernel logger seat is optional (tools-runtime rule). */
|
|
24
|
+
function warn(message) {
|
|
25
|
+
try {
|
|
26
|
+
logger.warn(message);
|
|
27
|
+
} catch {
|
|
28
|
+
console.warn(message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const name = 'rebon-loop-assembly';
|
|
33
|
+
export const inject = ['systemPrompt', 'tools'];
|
|
34
|
+
|
|
35
|
+
export async function apply(ctx, config = {}) {
|
|
36
|
+
// Embedder-chosen discriminator: with several loop hosts in one process,
|
|
37
|
+
// every relayed event carries the owning host's tag so kernel-plane
|
|
38
|
+
// subscribers can tell their loop apart.
|
|
39
|
+
const tag = typeof config.tag === 'string' && config.tag.length > 0 ? config.tag : undefined;
|
|
40
|
+
const stamp = (payload) => (tag === undefined ? payload : { tag, ...payload });
|
|
41
|
+
// Optional prompt contribution into the loop realm's REAL dsh
|
|
42
|
+
// systemPrompt (this is the loop's own prompt plane, not rebon's P2.8
|
|
43
|
+
// seat — the isolate keeps them apart).
|
|
44
|
+
if (config.section !== undefined && config.section !== null && typeof config.section === 'object') {
|
|
45
|
+
ctx.systemPrompt.section(config.section);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let seatSchemas = [];
|
|
49
|
+
try {
|
|
50
|
+
const catalog = await describeTools();
|
|
51
|
+
seatSchemas = (Array.isArray(catalog) ? catalog : []).map((tool) => ({
|
|
52
|
+
name: tool.name,
|
|
53
|
+
description: String(tool.description ?? ''),
|
|
54
|
+
parameters: tool.inputSchema ?? { type: 'object' },
|
|
55
|
+
}));
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// An unbound tool plane is a legal composition (prompt-only loop); the
|
|
58
|
+
// schemas face is then just the composition's own registrations.
|
|
59
|
+
warn(`loop-assembly: R5 seat catalog unavailable: ${String(error?.message ?? error)}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ctx.systemPrompt.tools(() => {
|
|
63
|
+
const merged = new Map();
|
|
64
|
+
for (const schema of seatSchemas) merged.set(schema.name, schema);
|
|
65
|
+
for (const [toolName, def] of ctx.tools.defs) {
|
|
66
|
+
merged.set(toolName, {
|
|
67
|
+
name: toolName,
|
|
68
|
+
description: String(def.description ?? ''),
|
|
69
|
+
parameters: def.parameters ?? { type: 'object' },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return { schemas: [...merged.values()] };
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Full-fidelity relay: the dsh session log IS the loop's truth, and the
|
|
76
|
+
// embedder's session face (streaming UI, transcript projection) needs
|
|
77
|
+
// every event — chunks included.
|
|
78
|
+
ctx.on('session/event', (session, event) => {
|
|
79
|
+
emit('loop:event', stamp({
|
|
80
|
+
sessionId: String(session.id),
|
|
81
|
+
seq: event.seq,
|
|
82
|
+
type: event.type,
|
|
83
|
+
data: event.data ?? null,
|
|
84
|
+
}));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
ctx.on('agent/error', (payload) => {
|
|
88
|
+
emit('loop:agent-error', stamp({
|
|
89
|
+
agentId: String(payload.agent?.id ?? ''),
|
|
90
|
+
turn: payload.turn,
|
|
91
|
+
step: payload.step,
|
|
92
|
+
error: errorChain(payload.error),
|
|
93
|
+
}));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const live = new Map();
|
|
97
|
+
const kicked = new Set();
|
|
98
|
+
ctx.on('agent/created', ({ agent }) => {
|
|
99
|
+
live.set(String(agent.id), agent);
|
|
100
|
+
emit('loop:agent-created', stamp({ agentId: String(agent.id) }));
|
|
101
|
+
const kickoff = config.kickoff;
|
|
102
|
+
if (typeof kickoff !== 'string' || kickoff.length === 0) return;
|
|
103
|
+
if (kicked.has(agent.id)) return;
|
|
104
|
+
kicked.add(agent.id);
|
|
105
|
+
agent.followup(createUserMessage({
|
|
106
|
+
content: [{ type: 'text', text: kickoff }],
|
|
107
|
+
source: { kind: 'user' },
|
|
108
|
+
}));
|
|
109
|
+
});
|
|
110
|
+
ctx.on('agent/disposed', ({ agent }) => {
|
|
111
|
+
live.delete(String(agent.id));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Inbound control face: the embedder drives the loop over the serve
|
|
115
|
+
// plane (request/response — the same plane rebon uses to call
|
|
116
|
+
// composition tools). Commands address the single configured agent by
|
|
117
|
+
// default, or a specific one via `agentId`.
|
|
118
|
+
const resolveAgent = (input) => {
|
|
119
|
+
if (typeof input?.agentId === 'string') return live.get(input.agentId);
|
|
120
|
+
if (live.size === 1) return live.values().next().value;
|
|
121
|
+
return undefined;
|
|
122
|
+
};
|
|
123
|
+
const userMessage = (text) => createUserMessage({
|
|
124
|
+
content: [{ type: 'text', text: String(text) }],
|
|
125
|
+
source: { kind: 'user' },
|
|
126
|
+
});
|
|
127
|
+
registerServeTarget('loop:control', (input) => {
|
|
128
|
+
const agent = resolveAgent(input);
|
|
129
|
+
if (agent === undefined) {
|
|
130
|
+
const known = [...live.keys()];
|
|
131
|
+
throw new Error(`loop:control: no live agent${input?.agentId ? ` "${input.agentId}"` : ''} (live: ${known.join(', ') || '(none)'})`);
|
|
132
|
+
}
|
|
133
|
+
switch (input?.kind) {
|
|
134
|
+
case 'followup':
|
|
135
|
+
agent.followup(userMessage(input.text ?? ''));
|
|
136
|
+
return { accepted: true, agentId: String(agent.id) };
|
|
137
|
+
case 'steer':
|
|
138
|
+
agent.steer(userMessage(input.text ?? ''));
|
|
139
|
+
return { accepted: true, agentId: String(agent.id) };
|
|
140
|
+
case 'cancel':
|
|
141
|
+
agent.cancel({ kind: 'user' });
|
|
142
|
+
return { accepted: true, agentId: String(agent.id) };
|
|
143
|
+
case 'status':
|
|
144
|
+
return { agentId: String(agent.id), status: agent.status };
|
|
145
|
+
default:
|
|
146
|
+
throw new Error(`loop:control: unknown command kind ${JSON.stringify(input?.kind)}`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Shared serve pump over the tool-serve ops (P1.6/P2.7): one pump per
|
|
2
|
+
// composition isolate, exact-name target dispatch. The tools seat registers
|
|
3
|
+
// each tool under its own name; the web seat registers providers under the
|
|
4
|
+
// reserved `web:<kind>:<id>` namespace. Handlers run concurrently (one slow
|
|
5
|
+
// call never head-of-line blocks the pump); `cancel` aborts the per-call
|
|
6
|
+
// AbortController.
|
|
7
|
+
//
|
|
8
|
+
// Envelope contract with the Rust side: `{ok: <handler return>}` on
|
|
9
|
+
// success; `{err: message, code?}` on throw (a thrown `code` property —
|
|
10
|
+
// dsh WebError/HarnessError — rides along for machine routing).
|
|
11
|
+
|
|
12
|
+
const core = globalThis.Deno.core;
|
|
13
|
+
|
|
14
|
+
const targets = new Map(); // target name -> async (input, signal, id) => value
|
|
15
|
+
const aborts = new Map(); // serve call id -> AbortController
|
|
16
|
+
let pumping = false;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register one serve target. Exact-name dispatch; duplicates throw. Returns
|
|
20
|
+
* the disposer.
|
|
21
|
+
*/
|
|
22
|
+
export function registerServeTarget(name, handler) {
|
|
23
|
+
if (targets.has(name)) {
|
|
24
|
+
throw new Error(`serve target "${name}" is already registered`);
|
|
25
|
+
}
|
|
26
|
+
targets.set(name, handler);
|
|
27
|
+
return () => targets.delete(name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Start the pump once; later calls are no-ops. */
|
|
31
|
+
export function ensureServePump() {
|
|
32
|
+
if (pumping) return;
|
|
33
|
+
pumping = true;
|
|
34
|
+
void pump();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function pump() {
|
|
38
|
+
while (true) {
|
|
39
|
+
const instr = JSON.parse(await core.ops.op_tool_serve_next());
|
|
40
|
+
if (instr.closed) break;
|
|
41
|
+
if (instr.kind === 'cancel') {
|
|
42
|
+
aborts.get(instr.id)?.abort();
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (instr.kind !== 'request') continue;
|
|
46
|
+
void serve(instr.id, instr.tool, instr.input);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function serve(id, target, input) {
|
|
51
|
+
const handler = targets.get(target);
|
|
52
|
+
if (!handler) {
|
|
53
|
+
emitEnvelope(id, {
|
|
54
|
+
err: `[UNKNOWN_TOOL] target "${target}" is not registered in the composition`,
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const abort = new AbortController();
|
|
59
|
+
aborts.set(id, abort);
|
|
60
|
+
try {
|
|
61
|
+
const value = await handler(input, abort.signal, id);
|
|
62
|
+
emitEnvelope(id, { ok: value });
|
|
63
|
+
} catch (e) {
|
|
64
|
+
emitEnvelope(id, {
|
|
65
|
+
err: String(e?.message ?? e),
|
|
66
|
+
...(typeof e?.code === 'string' ? { code: e.code } : {}),
|
|
67
|
+
});
|
|
68
|
+
} finally {
|
|
69
|
+
aborts.delete(id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function emitEnvelope(id, envelope) {
|
|
74
|
+
try {
|
|
75
|
+
core.ops.op_tool_serve_emit(id, JSON.stringify(envelope));
|
|
76
|
+
} catch (e) {
|
|
77
|
+
core.ops.op_tool_serve_emit(
|
|
78
|
+
id,
|
|
79
|
+
JSON.stringify({ err: `serve result not serializable: ${String(e?.message ?? e)}` }),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Embedded-runtime shim of `@deepseek-ai/dsh-anonymous-user-id`.
|
|
2
|
+
//
|
|
3
|
+
// The real package persists a random id under the dsh home for telemetry
|
|
4
|
+
// correlation. The embedded runtime is not a dsh install and keeps no dsh
|
|
5
|
+
// home; a fixed marker keeps the (harmless, non-secret) telemetry header
|
|
6
|
+
// shape intact while identifying the traffic as rebon-embedded.
|
|
7
|
+
|
|
8
|
+
export function getOrCreateAnonymousUserId() {
|
|
9
|
+
return 'rebon-embedded';
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Embedded-runtime shim of `@deepseek-ai/dsh-credentials` (runtime surface
|
|
2
|
+
// only). The credentials *service* itself is the composition host's
|
|
3
|
+
// credentials-runtime.js seat; consumers reach it via `ctx.get('credentials')`.
|
|
4
|
+
|
|
5
|
+
const REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
6
|
+
|
|
7
|
+
/** Brand a raw string as a CredentialRef (a POSIX environment-variable name). */
|
|
8
|
+
export function credentialRef(value) {
|
|
9
|
+
if (!REF_PATTERN.test(value)) {
|
|
10
|
+
throw new TypeError(`credential ref "${value}" must match ${String(REF_PATTERN)}`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Embedded-runtime shim of `@deepseek-ai/dsh-launch-environment`.
|
|
2
|
+
//
|
|
3
|
+
// The embedded composition deliberately exposes NO ambient environment to
|
|
4
|
+
// plugins: every credential goes through the fail-closed credentials seat,
|
|
5
|
+
// and connection facts come from plugin config. An always-empty snapshot
|
|
6
|
+
// makes each consumer take its documented fallback (dsh-llm-deepseek's
|
|
7
|
+
// baseURL falls to the public API; its ambient-key branch never wins because
|
|
8
|
+
// the credentials seat is mounted).
|
|
9
|
+
|
|
10
|
+
const EMPTY_SNAPSHOT = Object.freeze({
|
|
11
|
+
get: (_name) => undefined,
|
|
12
|
+
getFrom: (_name, _sources) => undefined,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/** The launcher snapshot for this run: embedded hosts provide none. */
|
|
16
|
+
export function launchEnvironmentOf(_ctx) {
|
|
17
|
+
return EMPTY_SNAPSHOT;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Context slot name the real launcher fills; exported for API parity. */
|
|
21
|
+
export const DSH_LAUNCH_ENVIRONMENT_KEY = 'launchEnvironment';
|