@rebon/cli-darwin-arm64 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.
- 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,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"note": "What rebon declares on behalf of the packages it ships. Every plugin on the plane loads against a manifest — the ceiling on what it may register and reach — and a third-party package declares its own under `capabilities.kernelPlugins` in its `rebon-plugin.json`. These packages are vendored into rebon's payload, so rebon is the one that has to say what they do. `root` names which package the `entry` is relative to. It defaults to `package`, which in this table means the vendored JS tree an entry belongs to; `payload` says the same thing explicitly, and `runtime` names the composition runtime itself. `invokableTools: \"$rebon/tools\"` expands to every tool this build exposes to the plane — the honest declaration for a plugin that runs whatever tool a model picks.",
|
|
4
|
+
"packages": {
|
|
5
|
+
"cordis": { "entry": "vendor/cordis/index.js" },
|
|
6
|
+
"@deepseek-ai/cordis-plugin-timer": { "entry": "vendor/dsh/timer.js" },
|
|
7
|
+
"@deepseek-ai/cordis-plugin-logger-console": { "entry": "vendor/dsh/logger-console.js" },
|
|
8
|
+
|
|
9
|
+
"@deepseek-ai/dsh-llm-deepseek": {
|
|
10
|
+
"entry": "vendor/dsh/llm-deepseek.js",
|
|
11
|
+
"llmProviders": ["deepseek-official"],
|
|
12
|
+
"seats": ["credentials"]
|
|
13
|
+
},
|
|
14
|
+
"@deepseek-ai/dsh-tool-todo": {
|
|
15
|
+
"entry": "vendor/dsh/tool-todo.js",
|
|
16
|
+
"tools": ["todo_write"],
|
|
17
|
+
"publishedTopics": ["compose:session/append"]
|
|
18
|
+
},
|
|
19
|
+
"@deepseek-ai/dsh-tool-web": {
|
|
20
|
+
"entry": "vendor/dsh/tool-web.js",
|
|
21
|
+
"tools": ["web_search", "web_fetch"],
|
|
22
|
+
"invokableTools": ["WebSearch", "WebFetch"],
|
|
23
|
+
"publishedTopics": ["compose:session/append"]
|
|
24
|
+
},
|
|
25
|
+
"@deepseek-ai/dsh-web-search-exa": {
|
|
26
|
+
"entry": "vendor/dsh/web-search-exa.js",
|
|
27
|
+
"services": ["web:search:exa"],
|
|
28
|
+
"seats": ["credentials"]
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
"@deepseek-ai/dsh-session": { "entry": "vendor/dsh/session.js" },
|
|
32
|
+
"@deepseek-ai/dsh-agent": { "entry": "vendor/dsh/agent.js" },
|
|
33
|
+
"@deepseek-ai/dsh-system-prompt": { "entry": "vendor/dsh/system-prompt.js" },
|
|
34
|
+
"@deepseek-ai/dsh-agent-loop": {
|
|
35
|
+
"entry": "vendor/dsh/agent-loop.js",
|
|
36
|
+
"invokableTools": "$rebon/tools",
|
|
37
|
+
"seats": ["credentials"]
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
"rebon-loop-assembly": {
|
|
41
|
+
"root": "runtime",
|
|
42
|
+
"entry": "src/loop-assembly.mjs",
|
|
43
|
+
"services": ["loop:control"],
|
|
44
|
+
"publishedTopics": ["loop:event", "loop:agent-error", "loop:agent-created"],
|
|
45
|
+
"seats": ["logger"]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// The composition's doorway to rebon, on the plugin plane.
|
|
2
|
+
//
|
|
3
|
+
// On deno_core this was `js/bridge.js`: seven synchronous ops reaching a
|
|
4
|
+
// kernel that lived in the same process. Across a process boundary none of
|
|
5
|
+
// that survives unchanged, and the differences are the protocol's, not this
|
|
6
|
+
// module's:
|
|
7
|
+
//
|
|
8
|
+
// * **Everything is async.** `callService` returned a value from an op; a
|
|
9
|
+
// `seat/call` is a request that has to be answered. The one place this
|
|
10
|
+
// bites is a cordis disposer, which is synchronous and cannot await — see
|
|
11
|
+
// the note on `seat()` below.
|
|
12
|
+
// * **There is no ambient kernel.** A call is only meaningful inside the
|
|
13
|
+
// scope incarnation it was made for, so the doorway exists only inside a
|
|
14
|
+
// handler. `AsyncLocalStorage` is what carries the handler's context down
|
|
15
|
+
// to code that has no way to be handed it — a dsh credential provider is
|
|
16
|
+
// called by an adapter that knows nothing about rebon.
|
|
17
|
+
// * **Registration is not a call.** `op_rebon_call_service('model-router',
|
|
18
|
+
// 'register', …)` and its compensating `unregister` are gone. What a
|
|
19
|
+
// composition provides is reported once, in the `plugin/load` ready
|
|
20
|
+
// report, and revoked by `plugin/unload` draining. That is the only shape
|
|
21
|
+
// that works: the unregisters lived in cordis disposers.
|
|
22
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
23
|
+
import { sessionOf } from './registry.mjs';
|
|
24
|
+
|
|
25
|
+
const calls = new AsyncLocalStorage();
|
|
26
|
+
|
|
27
|
+
/// Runs `fn` with `ctx` as the call everything underneath reaches through.
|
|
28
|
+
///
|
|
29
|
+
/// Every handler the composition serves — service, tool, llm adapter, event
|
|
30
|
+
/// delivery — is wrapped in this, so anything it calls can find its way back
|
|
31
|
+
/// to rebon without the call context being threaded through dsh code that has
|
|
32
|
+
/// no parameter for it.
|
|
33
|
+
export function withCall(ctx, fn) {
|
|
34
|
+
return calls.run(ctx, fn);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// The call this code is running inside.
|
|
38
|
+
export function currentCall() {
|
|
39
|
+
return calls.getStore();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// The call this code is running inside, or a refusal that says why not.
|
|
43
|
+
///
|
|
44
|
+
/// The refusal is worth its own code: "cannot read properties of undefined" at
|
|
45
|
+
/// the bottom of a dsh stack says nothing, whereas this names the actual rule —
|
|
46
|
+
/// rebon is reachable from inside a call, and a disposer or a module top level
|
|
47
|
+
/// is not inside one.
|
|
48
|
+
function require(what, via) {
|
|
49
|
+
const ctx = calls.getStore();
|
|
50
|
+
if (ctx !== undefined) return ctx;
|
|
51
|
+
// Nothing is being answered, so this is a plugin acting on its own schedule.
|
|
52
|
+
// `via` is the Cordis context of the plugin whose code is running, and the
|
|
53
|
+
// session it is attached to is the honest identity for what it does — the
|
|
54
|
+
// work is still that plugin's, in that session. Without a session there is
|
|
55
|
+
// no identity to use, and inventing one would make the ledger a guess.
|
|
56
|
+
const session = sessionOf(via);
|
|
57
|
+
if (session !== undefined) return session;
|
|
58
|
+
throw new Error(
|
|
59
|
+
`[NO_CALL_CONTEXT] ${what} is not reachable from here: no call rebon made is in flight, `
|
|
60
|
+
+ 'and this plugin is not attached to a session. Teardown paths must rely on unload draining instead',
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Calls a kernel seat: the composition's `credentials`, `settings`, `web` and
|
|
65
|
+
/// the rest of what rebon installs a plugin to use.
|
|
66
|
+
///
|
|
67
|
+
/// A request, therefore answerable, therefore **not** usable from a cordis
|
|
68
|
+
/// disposer: `dispose()` is synchronous and a cross-process call cannot be.
|
|
69
|
+
/// Nothing in the composition needs it to be — the calls that used to happen
|
|
70
|
+
/// on the way out were unregistrations, and those are the protocol's job now.
|
|
71
|
+
export function seat(name, method, params = null, options = {}) {
|
|
72
|
+
const { via, ...call } = options;
|
|
73
|
+
return require(`seat ${JSON.stringify(name)}`, via).seat(name, method, params, call);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Invokes one rebon core tool on the session this call belongs to.
|
|
77
|
+
///
|
|
78
|
+
/// Three gates stand in front of it and only the first is here: the plugin's
|
|
79
|
+
/// manifest must declare the tool, rebon's build must expose it, and the
|
|
80
|
+
/// embedder's broker decides whether this run is permitted.
|
|
81
|
+
export function invokeTool(tool, input = null, options = {}) {
|
|
82
|
+
const { via, ...call } = options;
|
|
83
|
+
return require(`tool ${JSON.stringify(tool)}`, via).invoke(tool, input, call);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Publishes one event onto rebon's event plane.
|
|
87
|
+
///
|
|
88
|
+
/// Not the same thing as `emit` below, and the difference is the audience: a
|
|
89
|
+
/// chunk is a piece of the answer the caller of *this* call is reading, an
|
|
90
|
+
/// event is a fact anyone listening may act on. The composition's runtime
|
|
91
|
+
/// events — a dsh tool body appending to the session log — are the second kind.
|
|
92
|
+
///
|
|
93
|
+
/// Declared as `publishedTopics` in the manifest, separately from what the
|
|
94
|
+
/// composition listens to.
|
|
95
|
+
export function publish(topic, event = null, options = {}) {
|
|
96
|
+
const { via, ...call } = options;
|
|
97
|
+
return require(`topic ${JSON.stringify(topic)}`, via).publish(topic, event, call);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Emits one piece of a streamed answer on the call this code is serving.
|
|
101
|
+
///
|
|
102
|
+
/// This is the transport `op_llm_emit` and `op_tool_serve_emit` used to be,
|
|
103
|
+
/// with the pumps gone: a chunk belongs to a call, and the call is the one the
|
|
104
|
+
/// handler was entered with rather than an id looked up in a table.
|
|
105
|
+
export function emit(piece) {
|
|
106
|
+
return require('emit').emit(piece);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// The workspace root of the session this call belongs to, or undefined
|
|
110
|
+
/// outside a call.
|
|
111
|
+
export function workspaceRoot() {
|
|
112
|
+
return calls.getStore()?.workspaceRoot;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// The abort signal rebon raises when it asks this call to stop.
|
|
116
|
+
export function callSignal() {
|
|
117
|
+
return calls.getStore()?.signal;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Diagnostics.
|
|
121
|
+
///
|
|
122
|
+
/// Best-effort on purpose, and never a reason for a call to fail: a log line
|
|
123
|
+
/// that cannot be delivered is not worth failing the work that produced it.
|
|
124
|
+
/// Inside a call it goes to the kernel's `logger` seat, so the line is
|
|
125
|
+
/// attributed to a scope; outside one it goes to stderr, which the supervisor
|
|
126
|
+
/// already collects as the host's diagnostic tail. stdout is never touched —
|
|
127
|
+
/// that is the protocol's channel.
|
|
128
|
+
const LEVELS = ['trace', 'debug', 'info', 'warn', 'error'];
|
|
129
|
+
export const logger = Object.freeze(Object.fromEntries(LEVELS.map((level) => [
|
|
130
|
+
level,
|
|
131
|
+
(message) => {
|
|
132
|
+
const line = String(message);
|
|
133
|
+
const ctx = calls.getStore();
|
|
134
|
+
if (ctx === undefined) {
|
|
135
|
+
process.stderr.write(`[compose:${level}] ${line}\n`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
void ctx.seat('logger', level, { message: line }).catch(() => {
|
|
139
|
+
process.stderr.write(`[compose:${level}] ${line}\n`);
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
])));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// The composition's `ctx.credentials` seat, on the plugin plane.
|
|
2
|
+
//
|
|
3
|
+
// dsh configuration carries credential *references* (environment-variable
|
|
4
|
+
// names); the provider owns the values. This resolves a reference through
|
|
5
|
+
// rebon's `credentials` seat, whose `resolveEnv` runs the fail-closed
|
|
6
|
+
// authorize waterfall before touching the environment — so an unauthorized or
|
|
7
|
+
// unset reference is simply "absent", and the consumer's own missing-credential
|
|
8
|
+
// diagnosis fires (dsh-llm's `MISSING_CREDENTIAL`) instead of this seat
|
|
9
|
+
// inventing one.
|
|
10
|
+
//
|
|
11
|
+
// Ported from `js/compose/credentials-runtime.js`. The one change is that the
|
|
12
|
+
// seat call is now a request across a process boundary, which means it is
|
|
13
|
+
// awaited — and that it can only be made from inside a call rebon is waiting
|
|
14
|
+
// on, or by the plugin being attached to a session. Both hold where a
|
|
15
|
+
// credential is actually wanted: inside a model turn rebon asked for, or inside
|
|
16
|
+
// one an agent loop is running in a session of its own. `bridge.mjs` names the
|
|
17
|
+
// refusal if anything ever tries from a disposer or a module top level.
|
|
18
|
+
import { Service } from 'cordis';
|
|
19
|
+
import { seat } from './bridge.mjs';
|
|
20
|
+
|
|
21
|
+
export default class RebonCredentials extends Service {
|
|
22
|
+
constructor(ctx) {
|
|
23
|
+
super(ctx, 'credentials');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** dsh CredentialProvider.resolve: `{value, source}` or undefined. */
|
|
27
|
+
async resolve(ref) {
|
|
28
|
+
try {
|
|
29
|
+
// `via` is this seat's caller — the plugin whose code wants the
|
|
30
|
+
// credential. Inside a call that changes nothing; outside one it is what
|
|
31
|
+
// lets an agent loop's own model turn resolve a key at all, attributed to
|
|
32
|
+
// the plugin and session it belongs to.
|
|
33
|
+
const out = await seat('credentials', 'resolveEnv', { ref: String(ref) }, { via: this.ctx });
|
|
34
|
+
const value = out?.value;
|
|
35
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
36
|
+
return { value, source: 'env' };
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// A refusal from the seat (no authorizer, unset variable, bad ref) and
|
|
40
|
+
// an absent value are the same fact to a consumer: not configured here.
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** dsh CredentialProvider.describe: facts only, never the value. */
|
|
46
|
+
async describe(ref) {
|
|
47
|
+
const hit = await this.resolve(ref);
|
|
48
|
+
return { configured: hit !== undefined, ...(hit ? { source: hit.source } : {}), writable: false };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// `eventsource-parser/stream` for real Node — the published package's shape.
|
|
2
|
+
//
|
|
3
|
+
// The vendored build under `js/vendor/eventsource-parser/stream.js` is not the
|
|
4
|
+
// published module: it was rewritten so that `EventSourceParserStream` is a
|
|
5
|
+
// "Mini streams" transform (an object with `transformIterable`), because bare
|
|
6
|
+
// deno_core has no WHATWG `TransformStream` for the real one to extend. The
|
|
7
|
+
// dsh DeepSeek adapter consumes it as a platform stream —
|
|
8
|
+
// `response.body.pipeThrough(new TextDecoderStream()).pipeThrough(new
|
|
9
|
+
// EventSourceParserStream())` — which on real Node throws
|
|
10
|
+
// `ERR_INVALID_ARG_TYPE: transform.readable must be an instance of
|
|
11
|
+
// ReadableStream` against the rewritten class.
|
|
12
|
+
//
|
|
13
|
+
// So on Node the upstream shape comes back. Parsing itself is still the
|
|
14
|
+
// vendored upstream `createParser`, so framing semantics — chunk reassembly,
|
|
15
|
+
// CRLF/BOM, comment skipping, multi-`data:` joining — stay byte-identical to
|
|
16
|
+
// the published package on both runtimes; only the plumbing differs.
|
|
17
|
+
import { createParser } from 'eventsource-parser';
|
|
18
|
+
|
|
19
|
+
export { ParseError } from 'eventsource-parser';
|
|
20
|
+
|
|
21
|
+
export class EventSourceParserStream extends TransformStream {
|
|
22
|
+
constructor({ onError, onRetry, onComment, maxBufferSize } = {}) {
|
|
23
|
+
let parser;
|
|
24
|
+
super({
|
|
25
|
+
start(controller) {
|
|
26
|
+
parser = createParser({
|
|
27
|
+
onEvent: (event) => controller.enqueue(event),
|
|
28
|
+
onError(error) {
|
|
29
|
+
// Upstream's rule, kept: a callback observes, the string
|
|
30
|
+
// `'terminate'` aborts, and a buffer overflow aborts regardless of
|
|
31
|
+
// what the consumer asked for — there is no way to keep parsing
|
|
32
|
+
// past it.
|
|
33
|
+
if (typeof onError === 'function') onError(error);
|
|
34
|
+
if (onError === 'terminate' || error.type === 'max-buffer-size-exceeded') {
|
|
35
|
+
controller.error(error);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
onRetry,
|
|
39
|
+
onComment,
|
|
40
|
+
maxBufferSize,
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
transform(chunk) {
|
|
44
|
+
parser.feed(chunk);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The package's root export: the loader seam the plugin host runs with.
|
|
2
|
+
//
|
|
3
|
+
// `rebon-plugin-host --loader <this file>` is what turns a generic plugin host
|
|
4
|
+
// into one that can mount a Cordis composition. Everything else in this package
|
|
5
|
+
// hangs off that.
|
|
6
|
+
export { createLoader, cordisPluginOf } from './loader.mjs';
|
|
7
|
+
export { activate as composeControl } from './plugin.mjs';
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// The composition's `ctx.llm` seat, on the plugin plane.
|
|
2
|
+
//
|
|
3
|
+
// Ported from `js/compose/llm-runtime.js`. The dsh-facing surface is
|
|
4
|
+
// unchanged — adapters extend `LlmAdapter`, register with
|
|
5
|
+
// `ctx.llm.registerAdapter(providers, adapter)`, and in-composition consumers
|
|
6
|
+
// (the agent loop) still go through `prepareCall` / `stream` — because that
|
|
7
|
+
// surface is dsh's contract and the transport underneath it is not dsh's
|
|
8
|
+
// business.
|
|
9
|
+
//
|
|
10
|
+
// What changed is everything that was transport:
|
|
11
|
+
//
|
|
12
|
+
// * **The pump is gone.** `_pump` parked on `op_llm_next` and `_dispatch`
|
|
13
|
+
// wrote chunks back with `op_llm_emit`. On the plugin plane a model turn
|
|
14
|
+
// arrives as an `llm/stream` request routed by provider, and its chunks go
|
|
15
|
+
// out on that call. What `registerAdapter` hands the registration sink is
|
|
16
|
+
// the handler for that method; there is nothing left to pump.
|
|
17
|
+
// * **Announcing is not a call.** `_announce` used to push each route into
|
|
18
|
+
// the kernel's `model-router` with `callService('model-router',
|
|
19
|
+
// 'register', …)`. Registration is a protocol declaration now: the route
|
|
20
|
+
// is reported by `plugin/load`, and its model catalog by the composition's
|
|
21
|
+
// own `compose` service.
|
|
22
|
+
// * **The disposer only forgets locally.** It used to issue the
|
|
23
|
+
// compensating `callService('model-router', 'unregister', …)`. A Cordis
|
|
24
|
+
// disposer is synchronous and a cross-process call is not, so that call
|
|
25
|
+
// could not survive the move — and does not need to. Revocation is what
|
|
26
|
+
// `plugin/unload` draining is for.
|
|
27
|
+
import { Service } from 'cordis';
|
|
28
|
+
import {
|
|
29
|
+
LlmError,
|
|
30
|
+
deepFreeze,
|
|
31
|
+
callConfigEquals,
|
|
32
|
+
errorChain,
|
|
33
|
+
resolveRetryPolicy,
|
|
34
|
+
} from '@deepseek-ai/dsh-llm';
|
|
35
|
+
import { withCall } from './bridge.mjs';
|
|
36
|
+
import { sinkOf } from './registry.mjs';
|
|
37
|
+
|
|
38
|
+
// Re-exported from the payload so a plugin extending `LlmAdapter` from
|
|
39
|
+
// `@deepseek-ai/dsh-llm` and the runtime checking one share a single class.
|
|
40
|
+
export { LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Normalize an adapter throw into dsh's terminal finish chunk
|
|
44
|
+
* (`adapterFailureChunk`, transcribed lean: LlmError keeps its facts,
|
|
45
|
+
* anything else flattens under UNKNOWN — the same rule the loop itself
|
|
46
|
+
* applies at its turn boundary).
|
|
47
|
+
*/
|
|
48
|
+
function failureChunk(error, signal) {
|
|
49
|
+
const failure = error instanceof LlmError
|
|
50
|
+
? error.failure
|
|
51
|
+
: { message: errorChain(error), code: 'UNKNOWN' };
|
|
52
|
+
return {
|
|
53
|
+
type: 'finish',
|
|
54
|
+
reason: signal?.aborted || failure.code === 'ABORTED'
|
|
55
|
+
? { kind: 'aborted', failure }
|
|
56
|
+
: { kind: 'error', failure },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default class RebonLlmRuntime extends Service {
|
|
61
|
+
constructor(ctx) {
|
|
62
|
+
super(ctx, 'llm');
|
|
63
|
+
this.routes = new Map(); // provider -> adapter
|
|
64
|
+
/** Catalog reads still in flight; `settle()` is what waits for them. */
|
|
65
|
+
this.pending = new Set();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
registerAdapter(providers, adapter) {
|
|
69
|
+
if (!providers?.length) {
|
|
70
|
+
throw new Error('an adapter must register at least one provider');
|
|
71
|
+
}
|
|
72
|
+
for (const provider of providers) {
|
|
73
|
+
if (this.routes.has(provider)) {
|
|
74
|
+
throw new Error(`an adapter for provider "${provider}" is already registered`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const runtime = this;
|
|
78
|
+
// Read synchronously, before any await can interleave: which plugin is
|
|
79
|
+
// registering is a fact about *now*, not about whenever this resolves.
|
|
80
|
+
const sink = sinkOf(this.ctx);
|
|
81
|
+
for (const provider of providers) {
|
|
82
|
+
sink?.llm(provider, (request, callCtx) =>
|
|
83
|
+
withCall(callCtx, () => runtime.serve(provider, request, callCtx)));
|
|
84
|
+
}
|
|
85
|
+
this.ctx.effect(() => {
|
|
86
|
+
for (const provider of providers) {
|
|
87
|
+
runtime.routes.set(provider, adapter);
|
|
88
|
+
runtime._read(provider, adapter, sink);
|
|
89
|
+
}
|
|
90
|
+
return () => {
|
|
91
|
+
// Local only. What rebon knows about these routes was reported at
|
|
92
|
+
// load and is withdrawn by unload; a disposer cannot tell it anything
|
|
93
|
+
// because a disposer cannot await an answer.
|
|
94
|
+
for (const provider of providers) runtime.routes.delete(provider);
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
// dsh handle shape: route replacement exists on it, but a static
|
|
98
|
+
// composition never re-reads registration facts, so a call here is a
|
|
99
|
+
// consumer bug — refuse loudly instead of silently diverging from dsh.
|
|
100
|
+
return {
|
|
101
|
+
providers: [...providers],
|
|
102
|
+
replace() {
|
|
103
|
+
throw new Error('adapter route replacement is not supported in a static composition');
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* dsh surfaces these entries to its settings UI. The composition has no
|
|
110
|
+
* such surface; record them so diagnostics can list what a composition
|
|
111
|
+
* declared, and accept the call so unmodified dsh plugins load.
|
|
112
|
+
*/
|
|
113
|
+
registerConfigurableProviders(entries) {
|
|
114
|
+
this.configurableProviders ??= [];
|
|
115
|
+
for (const entry of entries ?? []) {
|
|
116
|
+
if (entry?.provider) {
|
|
117
|
+
this.configurableProviders.push({
|
|
118
|
+
provider: String(entry.provider),
|
|
119
|
+
displayName: entry.displayName === undefined ? undefined : String(entry.displayName),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- JS consumer face ----
|
|
126
|
+
//
|
|
127
|
+
// These serve consumers INSIDE the composition (the agent loop); `serve()`
|
|
128
|
+
// serves requests from rebon. Routing is the same in-JS adapter table.
|
|
129
|
+
// v1 honesty boundary: no `llm/stream` middleware waterfall — dispatch goes
|
|
130
|
+
// straight to the owning adapter.
|
|
131
|
+
|
|
132
|
+
/** Resolve the owning adapter or refuse with dsh's NO_ADAPTER code. */
|
|
133
|
+
_registration(provider) {
|
|
134
|
+
const adapter = this.routes.get(provider);
|
|
135
|
+
if (!adapter) {
|
|
136
|
+
throw new LlmError(`no adapter registered for provider "${provider}"`, 'NO_ADAPTER');
|
|
137
|
+
}
|
|
138
|
+
return adapter;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* dsh `LlmRuntime.prepareCall`, transcribed: resolve the route's exact
|
|
143
|
+
* model, materialize adapter defaults (recorded in `adapterDefaults` so
|
|
144
|
+
* the loop's request header can re-resolve them later), and hand back a
|
|
145
|
+
* one-shot stream bound to this registration.
|
|
146
|
+
*/
|
|
147
|
+
async prepareCall(config, signal) {
|
|
148
|
+
const adapter = this._registration(config.provider);
|
|
149
|
+
const info = (await adapter.resolveModel(config.provider, config.model)) ?? {};
|
|
150
|
+
signal?.throwIfAborted?.();
|
|
151
|
+
const defaulted = config.maxTokens === undefined && info.defaultMaxTokens !== undefined
|
|
152
|
+
? { ...config, maxTokens: info.defaultMaxTokens }
|
|
153
|
+
: config;
|
|
154
|
+
const reasoning = info.reasoning;
|
|
155
|
+
const requested = defaulted.reasoningEffort;
|
|
156
|
+
let resolvedConfig = defaulted;
|
|
157
|
+
if (reasoning === undefined) {
|
|
158
|
+
if (requested !== undefined) {
|
|
159
|
+
throw new LlmError(
|
|
160
|
+
`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`,
|
|
161
|
+
'UNSUPPORTED_REASONING_EFFORT',
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const effective = requested ?? reasoning.defaultEffort;
|
|
166
|
+
if (effective !== undefined) {
|
|
167
|
+
if (!(reasoning.efforts ?? []).some((effort) => effort.id === effective)) {
|
|
168
|
+
throw new LlmError(
|
|
169
|
+
`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`,
|
|
170
|
+
'UNSUPPORTED_REASONING_EFFORT',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (requested !== effective) resolvedConfig = { ...defaulted, reasoningEffort: effective };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const frozen = deepFreeze(structuredClone(resolvedConfig));
|
|
177
|
+
const context = info.context === undefined
|
|
178
|
+
? undefined
|
|
179
|
+
: deepFreeze(structuredClone(info.context));
|
|
180
|
+
const adapterDefaults = deepFreeze({
|
|
181
|
+
...(config.reasoningEffort === undefined && frozen.reasoningEffort !== undefined
|
|
182
|
+
? { reasoningEffort: true }
|
|
183
|
+
: {}),
|
|
184
|
+
...(config.maxTokens === undefined && frozen.maxTokens !== undefined
|
|
185
|
+
? { maxTokens: true }
|
|
186
|
+
: {}),
|
|
187
|
+
});
|
|
188
|
+
const runtime = this;
|
|
189
|
+
let dispatched = false;
|
|
190
|
+
return Object.freeze({
|
|
191
|
+
config: frozen,
|
|
192
|
+
// dsh contract: providerRetryPolicy returns an ALREADY-RESOLVED
|
|
193
|
+
// policy (or undefined for the normal defaults) — never re-resolve.
|
|
194
|
+
retryPolicy: adapter.providerRetryPolicy(config.provider)
|
|
195
|
+
?? resolveRetryPolicy(undefined, `llm: provider "${config.provider}" retryPolicy`),
|
|
196
|
+
adapterDefaults,
|
|
197
|
+
...(context === undefined ? {} : { context }),
|
|
198
|
+
stream(options) {
|
|
199
|
+
if (dispatched) {
|
|
200
|
+
throw new LlmError('a prepared LLM call can only be dispatched once', 'INVALID_PREPARED_CALL');
|
|
201
|
+
}
|
|
202
|
+
if (!callConfigEquals(options, frozen)) {
|
|
203
|
+
throw new LlmError('prepared LLM call config changed before adapter dispatch', 'INVALID_PREPARED_CALL');
|
|
204
|
+
}
|
|
205
|
+
dispatched = true;
|
|
206
|
+
return runtime._consumerStream(options);
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Consumer fallback entry (the loop's NO_ADAPTER path): direct dispatch. */
|
|
212
|
+
stream(options) {
|
|
213
|
+
return this._consumerStream(options);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Adapter boundary for in-composition consumers: selection, dispatch, and
|
|
218
|
+
* iteration failures become one terminal failure chunk (dsh
|
|
219
|
+
* `adapterStream` semantics) so the loop's assembler/request-error path
|
|
220
|
+
* sees them instead of a raw throw.
|
|
221
|
+
*/
|
|
222
|
+
async *_consumerStream(options) {
|
|
223
|
+
let iterator;
|
|
224
|
+
try {
|
|
225
|
+
const adapter = this._registration(options.provider);
|
|
226
|
+
iterator = adapter.stream(options)[Symbol.asyncIterator]();
|
|
227
|
+
} catch (error) {
|
|
228
|
+
yield failureChunk(error, options.signal);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
while (true) {
|
|
232
|
+
let item;
|
|
233
|
+
try {
|
|
234
|
+
item = await iterator.next();
|
|
235
|
+
} catch (error) {
|
|
236
|
+
yield failureChunk(error, options.signal);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (item.done) return;
|
|
240
|
+
yield item.value;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---- what rebon is told, and what rebon asks for ----
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Read one route's catalog for the load report.
|
|
248
|
+
*
|
|
249
|
+
* `listModels` is advisory in dsh and can take a network round trip, so a
|
|
250
|
+
* failure is recorded rather than raised: a provider with no catalog is
|
|
251
|
+
* still a working route whose callers name models explicitly. The route
|
|
252
|
+
* table is the liveness witness — an adapter disposed while its catalog was
|
|
253
|
+
* being read is no longer the live one, and its answer is dropped.
|
|
254
|
+
*/
|
|
255
|
+
_read(provider, adapter, sink) {
|
|
256
|
+
if (sink === undefined) return;
|
|
257
|
+
const task = (async () => {
|
|
258
|
+
let models = [];
|
|
259
|
+
try {
|
|
260
|
+
models = (await adapter.listModels(provider)) ?? [];
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (this.routes.get(provider) !== adapter) return;
|
|
263
|
+
sink.catalog(provider, { models: [], defaultModel: 'default', catalogError: String(error?.message ?? error) });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (this.routes.get(provider) !== adapter) return;
|
|
267
|
+
// A key whose value is `undefined` is not JSON, and this report crosses
|
|
268
|
+
// the wire: an unknown context window is an absent field, not a present
|
|
269
|
+
// one holding nothing.
|
|
270
|
+
const entries = models
|
|
271
|
+
.filter((m) => m?.id)
|
|
272
|
+
.map((m) => {
|
|
273
|
+
const window = m.context?.window ?? m.contextWindow;
|
|
274
|
+
return { id: m.id, ...(window === undefined ? {} : { contextWindow: window }) };
|
|
275
|
+
});
|
|
276
|
+
sink.catalog(provider, {
|
|
277
|
+
models: entries,
|
|
278
|
+
// The catalog is advisory (dsh semantics): with an empty one the route
|
|
279
|
+
// still exists and callers name models explicitly; "default" is only
|
|
280
|
+
// the resolve fallback for bare `{provider}` lookups.
|
|
281
|
+
defaultModel: entries[0]?.id ?? 'default',
|
|
282
|
+
});
|
|
283
|
+
})();
|
|
284
|
+
this.pending.add(task);
|
|
285
|
+
void task.finally(() => this.pending.delete(task));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Waits for every catalog read in flight, so the load report is complete. */
|
|
289
|
+
async settle() {
|
|
290
|
+
while (this.pending.size > 0) await Promise.all([...this.pending]);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Serves one `llm/stream` call: chunks out on the call, adapter throws left
|
|
295
|
+
* to become its terminal.
|
|
296
|
+
*
|
|
297
|
+
* Deliberately the raw adapter rather than `_consumerStream`: an in-JS
|
|
298
|
+
* consumer wants a failure flattened into a finish chunk it can assemble,
|
|
299
|
+
* whereas rebon wants the call to end as an error carrying the adapter's own
|
|
300
|
+
* machine code. Flattening here would report every failed turn as a
|
|
301
|
+
* successful one whose last chunk happens to say otherwise.
|
|
302
|
+
*/
|
|
303
|
+
async serve(provider, request, ctx) {
|
|
304
|
+
const adapter = this._registration(provider);
|
|
305
|
+
const options = {
|
|
306
|
+
provider,
|
|
307
|
+
model: request.model,
|
|
308
|
+
messages: request.messages ?? [],
|
|
309
|
+
system: request.system ?? undefined,
|
|
310
|
+
tools: request.tools ?? undefined,
|
|
311
|
+
maxTokens: request.maxTokens ?? undefined,
|
|
312
|
+
temperature: request.temperature ?? undefined,
|
|
313
|
+
stop: request.stop ?? undefined,
|
|
314
|
+
reasoningEffort: request.reasoningEffort ?? undefined,
|
|
315
|
+
sessionId: request.sessionId ?? undefined,
|
|
316
|
+
purpose: request.purpose ?? undefined,
|
|
317
|
+
signal: ctx.signal,
|
|
318
|
+
};
|
|
319
|
+
let count = 0;
|
|
320
|
+
for await (const chunk of adapter.stream(options)) {
|
|
321
|
+
await ctx.emit(chunk);
|
|
322
|
+
count += 1;
|
|
323
|
+
}
|
|
324
|
+
return { chunks: count };
|
|
325
|
+
}
|
|
326
|
+
}
|