@rebon/cli-win32-x64 1.3.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 -1232
- 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 -21
- 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 -21
- package/payload/compose-runtime/payload/vendor/dsh/LICENSE-turndown-plugin-gfm +21 -21
- 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 -21
- 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
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
// What a plugin package *is*, when the composition is in the picture.
|
|
2
|
-
//
|
|
3
|
-
// The host knows one shape: a module exporting `activate`, which registers
|
|
4
|
-
// through the api it is handed. A Cordis plugin knows nothing about any of
|
|
5
|
-
// that — it exports `apply(ctx, config)` and registers by calling services on
|
|
6
|
-
// a context it shares with every other entry. Both are plugins on the plane;
|
|
7
|
-
// they differ only in what "loading" means, which is exactly the question the
|
|
8
|
-
// host delegates.
|
|
9
|
-
//
|
|
10
|
-
// The rule, in order, and deliberately not a heuristic — a package's shape is
|
|
11
|
-
// something its author decides, so it should be decidable by reading it:
|
|
12
|
-
//
|
|
13
|
-
// 1. `activate` is a function → the host's own loader
|
|
14
|
-
// 2. `apply` is the module's own function → a Cordis plugin (the module)
|
|
15
|
-
// 3. `default` is an object with `apply` → a Cordis plugin
|
|
16
|
-
// 4. `default` is a function → a Cordis functional plugin
|
|
17
|
-
// 5. anything else → the host's own loader, which
|
|
18
|
-
// refuses it by name
|
|
19
|
-
//
|
|
20
|
-
// Step 1 comes first so a rebon-native plugin is never mistaken for a Cordis
|
|
21
|
-
// one, and step 4 is last so the ambiguous case (a bare default function) goes
|
|
22
|
-
// to the composition rather than being guessed at from argument counts.
|
|
23
|
-
import { resolveEntry } from '../../plugin-host/src/loader.mjs';
|
|
24
|
-
|
|
25
|
-
const own = (value, key) => value != null && Object.prototype.hasOwnProperty.call(value, key);
|
|
26
|
-
|
|
27
|
-
class LoaderError extends Error {
|
|
28
|
-
constructor(code, message) {
|
|
29
|
-
super(message);
|
|
30
|
-
this.code = code;
|
|
31
|
-
this.name = 'LoaderError';
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/// The Cordis plugin a module holds, or undefined if it does not hold one.
|
|
36
|
-
export function cordisPluginOf(module) {
|
|
37
|
-
if (typeof module?.activate === 'function') return undefined;
|
|
38
|
-
if (own(module, 'apply') && typeof module.apply === 'function') return module;
|
|
39
|
-
const fallback = module?.default;
|
|
40
|
-
if (fallback && typeof fallback === 'object' && typeof fallback.apply === 'function') return fallback;
|
|
41
|
-
if (typeof fallback === 'function') return fallback;
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/// Builds the load/unload seams the host runs with.
|
|
46
|
-
///
|
|
47
|
-
/// `next` is the host's own loader, handed back untouched for anything this
|
|
48
|
-
/// module does not recognise — an adapter that reimplemented it would be a
|
|
49
|
-
/// second set of rules for the same shape.
|
|
50
|
-
export async function createLoader({ next }) {
|
|
51
|
-
return {
|
|
52
|
-
async load(request) {
|
|
53
|
-
const url = resolveEntry(request.root, request.entry);
|
|
54
|
-
let module;
|
|
55
|
-
try {
|
|
56
|
-
module = await import(url);
|
|
57
|
-
} catch (cause) {
|
|
58
|
-
throw new LoaderError('[ENTRY_FAILED]', `plugin entry ${request.entry} failed to load: ${cause?.message ?? cause}`);
|
|
59
|
-
}
|
|
60
|
-
const plugin = cordisPluginOf(module);
|
|
61
|
-
// Importing is idempotent, so handing the already-imported module back to
|
|
62
|
-
// the host's loader costs nothing and keeps one import per entry — which
|
|
63
|
-
// matters, because a module evaluated twice would register twice.
|
|
64
|
-
if (plugin === undefined) return next(request, async () => module);
|
|
65
|
-
|
|
66
|
-
// Imported lazily: this module is loaded by the host at startup, long
|
|
67
|
-
// before module resolution for the payload has been installed.
|
|
68
|
-
const { mountEntry } = await import('./realm.mjs');
|
|
69
|
-
const sink = await mountEntry(request, plugin);
|
|
70
|
-
return sink.loaded();
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
async unload(pluginId) {
|
|
74
|
-
const { disposeEntry } = await import('./realm.mjs');
|
|
75
|
-
await disposeEntry(pluginId);
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
}
|
|
1
|
+
// What a plugin package *is*, when the composition is in the picture.
|
|
2
|
+
//
|
|
3
|
+
// The host knows one shape: a module exporting `activate`, which registers
|
|
4
|
+
// through the api it is handed. A Cordis plugin knows nothing about any of
|
|
5
|
+
// that — it exports `apply(ctx, config)` and registers by calling services on
|
|
6
|
+
// a context it shares with every other entry. Both are plugins on the plane;
|
|
7
|
+
// they differ only in what "loading" means, which is exactly the question the
|
|
8
|
+
// host delegates.
|
|
9
|
+
//
|
|
10
|
+
// The rule, in order, and deliberately not a heuristic — a package's shape is
|
|
11
|
+
// something its author decides, so it should be decidable by reading it:
|
|
12
|
+
//
|
|
13
|
+
// 1. `activate` is a function → the host's own loader
|
|
14
|
+
// 2. `apply` is the module's own function → a Cordis plugin (the module)
|
|
15
|
+
// 3. `default` is an object with `apply` → a Cordis plugin
|
|
16
|
+
// 4. `default` is a function → a Cordis functional plugin
|
|
17
|
+
// 5. anything else → the host's own loader, which
|
|
18
|
+
// refuses it by name
|
|
19
|
+
//
|
|
20
|
+
// Step 1 comes first so a rebon-native plugin is never mistaken for a Cordis
|
|
21
|
+
// one, and step 4 is last so the ambiguous case (a bare default function) goes
|
|
22
|
+
// to the composition rather than being guessed at from argument counts.
|
|
23
|
+
import { resolveEntry } from '../../plugin-host/src/loader.mjs';
|
|
24
|
+
|
|
25
|
+
const own = (value, key) => value != null && Object.prototype.hasOwnProperty.call(value, key);
|
|
26
|
+
|
|
27
|
+
class LoaderError extends Error {
|
|
28
|
+
constructor(code, message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.name = 'LoaderError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// The Cordis plugin a module holds, or undefined if it does not hold one.
|
|
36
|
+
export function cordisPluginOf(module) {
|
|
37
|
+
if (typeof module?.activate === 'function') return undefined;
|
|
38
|
+
if (own(module, 'apply') && typeof module.apply === 'function') return module;
|
|
39
|
+
const fallback = module?.default;
|
|
40
|
+
if (fallback && typeof fallback === 'object' && typeof fallback.apply === 'function') return fallback;
|
|
41
|
+
if (typeof fallback === 'function') return fallback;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Builds the load/unload seams the host runs with.
|
|
46
|
+
///
|
|
47
|
+
/// `next` is the host's own loader, handed back untouched for anything this
|
|
48
|
+
/// module does not recognise — an adapter that reimplemented it would be a
|
|
49
|
+
/// second set of rules for the same shape.
|
|
50
|
+
export async function createLoader({ next }) {
|
|
51
|
+
return {
|
|
52
|
+
async load(request) {
|
|
53
|
+
const url = resolveEntry(request.root, request.entry);
|
|
54
|
+
let module;
|
|
55
|
+
try {
|
|
56
|
+
module = await import(url);
|
|
57
|
+
} catch (cause) {
|
|
58
|
+
throw new LoaderError('[ENTRY_FAILED]', `plugin entry ${request.entry} failed to load: ${cause?.message ?? cause}`);
|
|
59
|
+
}
|
|
60
|
+
const plugin = cordisPluginOf(module);
|
|
61
|
+
// Importing is idempotent, so handing the already-imported module back to
|
|
62
|
+
// the host's loader costs nothing and keeps one import per entry — which
|
|
63
|
+
// matters, because a module evaluated twice would register twice.
|
|
64
|
+
if (plugin === undefined) return next(request, async () => module);
|
|
65
|
+
|
|
66
|
+
// Imported lazily: this module is loaded by the host at startup, long
|
|
67
|
+
// before module resolution for the payload has been installed.
|
|
68
|
+
const { mountEntry } = await import('./realm.mjs');
|
|
69
|
+
const sink = await mountEntry(request, plugin);
|
|
70
|
+
return sink.loaded();
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async unload(pluginId) {
|
|
74
|
+
const { disposeEntry } = await import('./realm.mjs');
|
|
75
|
+
await disposeEntry(pluginId);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -1,237 +1,237 @@
|
|
|
1
|
-
// The composition-side glue that lets the REAL dsh agent loop drive a turn
|
|
2
|
-
// inside the composition realm.
|
|
3
|
-
//
|
|
4
|
-
// Ported from `js/compose/loop-assembly.js`. Three duties, all rebon-owned
|
|
5
|
-
// (the dsh packages stay unmodified):
|
|
6
|
-
//
|
|
7
|
-
// 1. **Tool-schema supply** — a dsh systemPrompt tool provider merging the
|
|
8
|
-
// composition's own registered definitions with rebon's core tool catalog,
|
|
9
|
-
// so what the model is offered and what the scheduler can dispatch stay one
|
|
10
|
-
// fact (a local definition shadows a core tool name in both places).
|
|
11
|
-
// 2. **Observability** — every dsh `session/event` (chunks included: the
|
|
12
|
-
// embedder's session face streams from them) and `agent/error` is published
|
|
13
|
-
// onto rebon's event plane.
|
|
14
|
-
// 3. **Drive** — `loop:control` is the embedder's inbound face
|
|
15
|
-
// (followup / steer / cancel / status).
|
|
16
|
-
//
|
|
17
|
-
// Two
|
|
18
|
-
//
|
|
19
|
-
// * **The catalog is configuration, not a call.**
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// * **Events publish through a scope handle.** A loop produces turn events on
|
|
25
|
-
// its own schedule, with no inbound call to hang them on, so the assembly
|
|
26
|
-
// takes a handle on each open session and fans its events to whichever are
|
|
27
|
-
// live. A loop exists before any session opens, so the first events are
|
|
28
|
-
// held — boundedly — until one does, and the self-drive kickoff waits for
|
|
29
|
-
// the same moment: an agent that ran a whole turn before anyone was
|
|
30
|
-
// listening would be a turn rebon never saw.
|
|
31
|
-
import { createUserMessage, errorChain } from '@deepseek-ai/dsh-llm';
|
|
32
|
-
import { logger } from './bridge.mjs';
|
|
33
|
-
import { sinkOf } from './registry.mjs';
|
|
34
|
-
|
|
35
|
-
export const name = 'rebon-loop-assembly';
|
|
36
|
-
export const inject = ['systemPrompt', 'tools'];
|
|
37
|
-
|
|
38
|
-
/** Topics this assembly publishes; a loop plugin declares exactly these. */
|
|
39
|
-
export const LOOP_TOPICS = Object.freeze([
|
|
40
|
-
'loop:event',
|
|
41
|
-
'loop:agent-error',
|
|
42
|
-
'loop:agent-created',
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
export async function apply(ctx, config = {}) {
|
|
46
|
-
const sink = sinkOf(ctx);
|
|
47
|
-
if (sink === undefined) {
|
|
48
|
-
throw new Error('rebon-loop-assembly must be mounted as a composition entry');
|
|
49
|
-
}
|
|
50
|
-
// Embedder-chosen discriminator: with several loops in one composition, every
|
|
51
|
-
// published event carries the owning loop's tag so subscribers can tell them
|
|
52
|
-
// apart.
|
|
53
|
-
const tag = typeof config.tag === 'string' && config.tag.length > 0 ? config.tag : undefined;
|
|
54
|
-
const stamp = (payload) => (tag === undefined ? payload : { tag, ...payload });
|
|
55
|
-
|
|
56
|
-
// Sessions this loop is attached to. A published event goes to all of them;
|
|
57
|
-
// in the ordinary case there is exactly one.
|
|
58
|
-
const sessions = new Set();
|
|
59
|
-
// A loop starts existing when it is loaded, and a session opens after — so
|
|
60
|
-
// the first events (an agent being created, above all) happen with nobody
|
|
61
|
-
// listening. Holding them until the first session attaches is the difference
|
|
62
|
-
// between "rebon saw the loop start" and "rebon saw the loop already
|
|
63
|
-
// running"; the bound is here so a loop nobody ever attaches to cannot grow
|
|
64
|
-
// without limit, and passing it is said out loud rather than swallowed.
|
|
65
|
-
const PRE_ATTACH_LIMIT = 256;
|
|
66
|
-
let attached = false;
|
|
67
|
-
let dropped = 0;
|
|
68
|
-
const held = [];
|
|
69
|
-
const deliver = (session, topic, payload) => {
|
|
70
|
-
// Best effort, and never a reason for the loop to fail: an event nobody
|
|
71
|
-
// could be told about is not a turn that went wrong.
|
|
72
|
-
void session.publish(topic, payload).catch((cause) => {
|
|
73
|
-
logger.warn(`loop-assembly: publishing ${topic} failed: ${cause?.message ?? cause}`);
|
|
74
|
-
});
|
|
75
|
-
};
|
|
76
|
-
sink.scope((scopeCtx) => {
|
|
77
|
-
sessions.add(scopeCtx);
|
|
78
|
-
if (!attached) {
|
|
79
|
-
attached = true;
|
|
80
|
-
if (dropped > 0) {
|
|
81
|
-
logger.warn(`loop-assembly: dropped ${dropped} event(s) produced before any session attached`);
|
|
82
|
-
}
|
|
83
|
-
for (const [topic, payload] of held.splice(0)) deliver(scopeCtx, topic, payload);
|
|
84
|
-
}
|
|
85
|
-
kickoff();
|
|
86
|
-
return () => sessions.delete(scopeCtx);
|
|
87
|
-
});
|
|
88
|
-
const announce = (topic, payload) => {
|
|
89
|
-
const stamped = stamp(payload);
|
|
90
|
-
if (!attached) {
|
|
91
|
-
if (held.length >= PRE_ATTACH_LIMIT) {
|
|
92
|
-
held.shift();
|
|
93
|
-
dropped += 1;
|
|
94
|
-
}
|
|
95
|
-
held.push([topic, stamped]);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
for (const session of sessions) deliver(session, topic, stamped);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// Optional prompt contribution into the loop realm's REAL dsh systemPrompt
|
|
102
|
-
// (the loop's own prompt plane, not rebon's seat — `isolate` keeps them
|
|
103
|
-
// apart, which is why the composition structure declares it).
|
|
104
|
-
if (config.section !== undefined && config.section !== null && typeof config.section === 'object') {
|
|
105
|
-
ctx.systemPrompt.section(config.section);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const seatSchemas = (Array.isArray(config.toolCatalog) ? config.toolCatalog : []).map((tool) => ({
|
|
109
|
-
name: String(tool?.name ?? ''),
|
|
110
|
-
description: String(tool?.description ?? ''),
|
|
111
|
-
parameters: tool?.inputSchema ?? tool?.parameters ?? { type: 'object' },
|
|
112
|
-
})).filter((tool) => tool.name.length > 0);
|
|
113
|
-
|
|
114
|
-
ctx.systemPrompt.tools(() => {
|
|
115
|
-
const merged = new Map();
|
|
116
|
-
for (const schema of seatSchemas) merged.set(schema.name, schema);
|
|
117
|
-
for (const [toolName, def] of ctx.tools.defs) {
|
|
118
|
-
merged.set(toolName, {
|
|
119
|
-
name: toolName,
|
|
120
|
-
description: String(def.description ?? ''),
|
|
121
|
-
parameters: def.parameters ?? { type: 'object' },
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
return { schemas: [...merged.values()] };
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// Full-fidelity relay: the dsh session log IS the loop's truth, and the
|
|
128
|
-
// embedder's session face (streaming UI, transcript projection) needs every
|
|
129
|
-
// event — chunks included.
|
|
130
|
-
ctx.on('session/event', (session, event) => {
|
|
131
|
-
announce('loop:event', {
|
|
132
|
-
sessionId: String(session.id),
|
|
133
|
-
seq: event.seq,
|
|
134
|
-
type: event.type,
|
|
135
|
-
data: event.data ?? null,
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
ctx.on('agent/error', (payload) => {
|
|
140
|
-
announce('loop:agent-error', {
|
|
141
|
-
agentId: String(payload.agent?.id ?? ''),
|
|
142
|
-
turn: payload.turn,
|
|
143
|
-
step: payload.step,
|
|
144
|
-
error: errorChain(payload.error),
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// Which agents are this loop's.
|
|
149
|
-
//
|
|
150
|
-
// The dsh `agents` registry is one per composition realm, so with more than
|
|
151
|
-
// one loop mounted every assembly hears every agent being created. Naming the
|
|
152
|
-
// configured ids is how an assembly knows which of them it is responsible
|
|
153
|
-
// for; dsh appends a session suffix to the configured id, so the match is on
|
|
154
|
-
// that stem. With no list the assembly adopts everything, which is the
|
|
155
|
-
// single-loop composition and the honest default for it.
|
|
156
|
-
const owned = Array.isArray(config.agents)
|
|
157
|
-
? config.agents.map(String).filter((id) => id.length > 0)
|
|
158
|
-
: undefined;
|
|
159
|
-
const isMine = (agentId) => owned === undefined
|
|
160
|
-
|| owned.some((id) => agentId === id || agentId.startsWith(`${id}-`));
|
|
161
|
-
|
|
162
|
-
const live = new Map();
|
|
163
|
-
const kicked = new Set();
|
|
164
|
-
// The self-drive convenience, deliberately tied to a session rather than to
|
|
165
|
-
// an agent appearing: an agent created before anyone is listening would run a
|
|
166
|
-
// whole turn whose events had nowhere to go.
|
|
167
|
-
function kickoff() {
|
|
168
|
-
const text = config.kickoff;
|
|
169
|
-
if (typeof text !== 'string' || text.length === 0) return;
|
|
170
|
-
if (!attached) return;
|
|
171
|
-
for (const [id, agent] of live) {
|
|
172
|
-
if (kicked.has(id)) continue;
|
|
173
|
-
kicked.add(id);
|
|
174
|
-
agent.followup(createUserMessage({
|
|
175
|
-
content: [{ type: 'text', text }],
|
|
176
|
-
source: { kind: 'user' },
|
|
177
|
-
}));
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
ctx.on('agent/created', ({ agent }) => {
|
|
181
|
-
const agentId = String(agent.id);
|
|
182
|
-
if (!isMine(agentId)) return;
|
|
183
|
-
live.set(agentId, agent);
|
|
184
|
-
announce('loop:agent-created', { agentId });
|
|
185
|
-
kickoff();
|
|
186
|
-
});
|
|
187
|
-
ctx.on('agent/disposed', ({ agent }) => {
|
|
188
|
-
live.delete(String(agent.id));
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
// Session events carry no agent, so an assembly cannot tell whose turn it is
|
|
192
|
-
// from the event alone. Ownership of the *session* is the same question one
|
|
193
|
-
// step earlier: a session belongs to whichever agent is running in it.
|
|
194
|
-
const ownedSessions = new Set();
|
|
195
|
-
ctx.on('agent/created', ({ agent }) => {
|
|
196
|
-
if (isMine(String(agent.id)) && agent.session?.id !== undefined) {
|
|
197
|
-
ownedSessions.add(String(agent.session.id));
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// Inbound control face: the embedder drives the loop with ordinary service
|
|
202
|
-
// calls. Commands address the single configured agent by default, or a
|
|
203
|
-
// specific one via `agentId`.
|
|
204
|
-
const resolveAgent = (input) => {
|
|
205
|
-
if (typeof input?.agentId === 'string') return live.get(input.agentId);
|
|
206
|
-
if (live.size === 1) return live.values().next().value;
|
|
207
|
-
return undefined;
|
|
208
|
-
};
|
|
209
|
-
const userMessage = (text) => createUserMessage({
|
|
210
|
-
content: [{ type: 'text', text: String(text) }],
|
|
211
|
-
source: { kind: 'user' },
|
|
212
|
-
});
|
|
213
|
-
sink.service('loop:control', (input) => {
|
|
214
|
-
const agent = resolveAgent(input);
|
|
215
|
-
if (agent === undefined) {
|
|
216
|
-
const known = [...live.keys()];
|
|
217
|
-
throw new Error(`loop:control: no live agent${input?.agentId ? ` "${input.agentId}"` : ''} (live: ${known.join(', ') || '(none)'})`);
|
|
218
|
-
}
|
|
219
|
-
switch (input?.kind) {
|
|
220
|
-
case 'followup':
|
|
221
|
-
agent.followup(userMessage(input.text ?? ''));
|
|
222
|
-
return { accepted: true, agentId: String(agent.id) };
|
|
223
|
-
case 'steer':
|
|
224
|
-
agent.steer(userMessage(input.text ?? ''));
|
|
225
|
-
return { accepted: true, agentId: String(agent.id) };
|
|
226
|
-
case 'cancel':
|
|
227
|
-
agent.cancel({ kind: 'user' });
|
|
228
|
-
return { accepted: true, agentId: String(agent.id) };
|
|
229
|
-
case 'status':
|
|
230
|
-
return { agentId: String(agent.id), status: agent.status };
|
|
231
|
-
default:
|
|
232
|
-
throw new Error(`loop:control: unknown command kind ${JSON.stringify(input?.kind)}`);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export default { name, inject, apply };
|
|
1
|
+
// The composition-side glue that lets the REAL dsh agent loop drive a turn
|
|
2
|
+
// inside the composition realm.
|
|
3
|
+
//
|
|
4
|
+
// Ported from `js/compose/loop-assembly.js`. Three duties, all rebon-owned
|
|
5
|
+
// (the dsh packages stay unmodified):
|
|
6
|
+
//
|
|
7
|
+
// 1. **Tool-schema supply** — a dsh systemPrompt tool provider merging the
|
|
8
|
+
// composition's own registered definitions with rebon's core tool catalog,
|
|
9
|
+
// so what the model is offered and what the scheduler can dispatch stay one
|
|
10
|
+
// fact (a local definition shadows a core tool name in both places).
|
|
11
|
+
// 2. **Observability** — every dsh `session/event` (chunks included: the
|
|
12
|
+
// embedder's session face streams from them) and `agent/error` is published
|
|
13
|
+
// onto rebon's event plane.
|
|
14
|
+
// 3. **Drive** — `loop:control` is the embedder's inbound face
|
|
15
|
+
// (followup / steer / cancel / status).
|
|
16
|
+
//
|
|
17
|
+
// Two consequences of the transport:
|
|
18
|
+
//
|
|
19
|
+
// * **The catalog is configuration, not a call.** There is no `describeTools`
|
|
20
|
+
// method on the plane, and there should not be: what rebon offers a model
|
|
21
|
+
// is settled when the composition is built, so rebon hands it over at
|
|
22
|
+
// load. It is a snapshot either way; asking would only make the moment it
|
|
23
|
+
// was taken harder to see.
|
|
24
|
+
// * **Events publish through a scope handle.** A loop produces turn events on
|
|
25
|
+
// its own schedule, with no inbound call to hang them on, so the assembly
|
|
26
|
+
// takes a handle on each open session and fans its events to whichever are
|
|
27
|
+
// live. A loop exists before any session opens, so the first events are
|
|
28
|
+
// held — boundedly — until one does, and the self-drive kickoff waits for
|
|
29
|
+
// the same moment: an agent that ran a whole turn before anyone was
|
|
30
|
+
// listening would be a turn rebon never saw.
|
|
31
|
+
import { createUserMessage, errorChain } from '@deepseek-ai/dsh-llm';
|
|
32
|
+
import { logger } from './bridge.mjs';
|
|
33
|
+
import { sinkOf } from './registry.mjs';
|
|
34
|
+
|
|
35
|
+
export const name = 'rebon-loop-assembly';
|
|
36
|
+
export const inject = ['systemPrompt', 'tools'];
|
|
37
|
+
|
|
38
|
+
/** Topics this assembly publishes; a loop plugin declares exactly these. */
|
|
39
|
+
export const LOOP_TOPICS = Object.freeze([
|
|
40
|
+
'loop:event',
|
|
41
|
+
'loop:agent-error',
|
|
42
|
+
'loop:agent-created',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export async function apply(ctx, config = {}) {
|
|
46
|
+
const sink = sinkOf(ctx);
|
|
47
|
+
if (sink === undefined) {
|
|
48
|
+
throw new Error('rebon-loop-assembly must be mounted as a composition entry');
|
|
49
|
+
}
|
|
50
|
+
// Embedder-chosen discriminator: with several loops in one composition, every
|
|
51
|
+
// published event carries the owning loop's tag so subscribers can tell them
|
|
52
|
+
// apart.
|
|
53
|
+
const tag = typeof config.tag === 'string' && config.tag.length > 0 ? config.tag : undefined;
|
|
54
|
+
const stamp = (payload) => (tag === undefined ? payload : { tag, ...payload });
|
|
55
|
+
|
|
56
|
+
// Sessions this loop is attached to. A published event goes to all of them;
|
|
57
|
+
// in the ordinary case there is exactly one.
|
|
58
|
+
const sessions = new Set();
|
|
59
|
+
// A loop starts existing when it is loaded, and a session opens after — so
|
|
60
|
+
// the first events (an agent being created, above all) happen with nobody
|
|
61
|
+
// listening. Holding them until the first session attaches is the difference
|
|
62
|
+
// between "rebon saw the loop start" and "rebon saw the loop already
|
|
63
|
+
// running"; the bound is here so a loop nobody ever attaches to cannot grow
|
|
64
|
+
// without limit, and passing it is said out loud rather than swallowed.
|
|
65
|
+
const PRE_ATTACH_LIMIT = 256;
|
|
66
|
+
let attached = false;
|
|
67
|
+
let dropped = 0;
|
|
68
|
+
const held = [];
|
|
69
|
+
const deliver = (session, topic, payload) => {
|
|
70
|
+
// Best effort, and never a reason for the loop to fail: an event nobody
|
|
71
|
+
// could be told about is not a turn that went wrong.
|
|
72
|
+
void session.publish(topic, payload).catch((cause) => {
|
|
73
|
+
logger.warn(`loop-assembly: publishing ${topic} failed: ${cause?.message ?? cause}`);
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
sink.scope((scopeCtx) => {
|
|
77
|
+
sessions.add(scopeCtx);
|
|
78
|
+
if (!attached) {
|
|
79
|
+
attached = true;
|
|
80
|
+
if (dropped > 0) {
|
|
81
|
+
logger.warn(`loop-assembly: dropped ${dropped} event(s) produced before any session attached`);
|
|
82
|
+
}
|
|
83
|
+
for (const [topic, payload] of held.splice(0)) deliver(scopeCtx, topic, payload);
|
|
84
|
+
}
|
|
85
|
+
kickoff();
|
|
86
|
+
return () => sessions.delete(scopeCtx);
|
|
87
|
+
});
|
|
88
|
+
const announce = (topic, payload) => {
|
|
89
|
+
const stamped = stamp(payload);
|
|
90
|
+
if (!attached) {
|
|
91
|
+
if (held.length >= PRE_ATTACH_LIMIT) {
|
|
92
|
+
held.shift();
|
|
93
|
+
dropped += 1;
|
|
94
|
+
}
|
|
95
|
+
held.push([topic, stamped]);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for (const session of sessions) deliver(session, topic, stamped);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Optional prompt contribution into the loop realm's REAL dsh systemPrompt
|
|
102
|
+
// (the loop's own prompt plane, not rebon's seat — `isolate` keeps them
|
|
103
|
+
// apart, which is why the composition structure declares it).
|
|
104
|
+
if (config.section !== undefined && config.section !== null && typeof config.section === 'object') {
|
|
105
|
+
ctx.systemPrompt.section(config.section);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const seatSchemas = (Array.isArray(config.toolCatalog) ? config.toolCatalog : []).map((tool) => ({
|
|
109
|
+
name: String(tool?.name ?? ''),
|
|
110
|
+
description: String(tool?.description ?? ''),
|
|
111
|
+
parameters: tool?.inputSchema ?? tool?.parameters ?? { type: 'object' },
|
|
112
|
+
})).filter((tool) => tool.name.length > 0);
|
|
113
|
+
|
|
114
|
+
ctx.systemPrompt.tools(() => {
|
|
115
|
+
const merged = new Map();
|
|
116
|
+
for (const schema of seatSchemas) merged.set(schema.name, schema);
|
|
117
|
+
for (const [toolName, def] of ctx.tools.defs) {
|
|
118
|
+
merged.set(toolName, {
|
|
119
|
+
name: toolName,
|
|
120
|
+
description: String(def.description ?? ''),
|
|
121
|
+
parameters: def.parameters ?? { type: 'object' },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return { schemas: [...merged.values()] };
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Full-fidelity relay: the dsh session log IS the loop's truth, and the
|
|
128
|
+
// embedder's session face (streaming UI, transcript projection) needs every
|
|
129
|
+
// event — chunks included.
|
|
130
|
+
ctx.on('session/event', (session, event) => {
|
|
131
|
+
announce('loop:event', {
|
|
132
|
+
sessionId: String(session.id),
|
|
133
|
+
seq: event.seq,
|
|
134
|
+
type: event.type,
|
|
135
|
+
data: event.data ?? null,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
ctx.on('agent/error', (payload) => {
|
|
140
|
+
announce('loop:agent-error', {
|
|
141
|
+
agentId: String(payload.agent?.id ?? ''),
|
|
142
|
+
turn: payload.turn,
|
|
143
|
+
step: payload.step,
|
|
144
|
+
error: errorChain(payload.error),
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Which agents are this loop's.
|
|
149
|
+
//
|
|
150
|
+
// The dsh `agents` registry is one per composition realm, so with more than
|
|
151
|
+
// one loop mounted every assembly hears every agent being created. Naming the
|
|
152
|
+
// configured ids is how an assembly knows which of them it is responsible
|
|
153
|
+
// for; dsh appends a session suffix to the configured id, so the match is on
|
|
154
|
+
// that stem. With no list the assembly adopts everything, which is the
|
|
155
|
+
// single-loop composition and the honest default for it.
|
|
156
|
+
const owned = Array.isArray(config.agents)
|
|
157
|
+
? config.agents.map(String).filter((id) => id.length > 0)
|
|
158
|
+
: undefined;
|
|
159
|
+
const isMine = (agentId) => owned === undefined
|
|
160
|
+
|| owned.some((id) => agentId === id || agentId.startsWith(`${id}-`));
|
|
161
|
+
|
|
162
|
+
const live = new Map();
|
|
163
|
+
const kicked = new Set();
|
|
164
|
+
// The self-drive convenience, deliberately tied to a session rather than to
|
|
165
|
+
// an agent appearing: an agent created before anyone is listening would run a
|
|
166
|
+
// whole turn whose events had nowhere to go.
|
|
167
|
+
function kickoff() {
|
|
168
|
+
const text = config.kickoff;
|
|
169
|
+
if (typeof text !== 'string' || text.length === 0) return;
|
|
170
|
+
if (!attached) return;
|
|
171
|
+
for (const [id, agent] of live) {
|
|
172
|
+
if (kicked.has(id)) continue;
|
|
173
|
+
kicked.add(id);
|
|
174
|
+
agent.followup(createUserMessage({
|
|
175
|
+
content: [{ type: 'text', text }],
|
|
176
|
+
source: { kind: 'user' },
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
ctx.on('agent/created', ({ agent }) => {
|
|
181
|
+
const agentId = String(agent.id);
|
|
182
|
+
if (!isMine(agentId)) return;
|
|
183
|
+
live.set(agentId, agent);
|
|
184
|
+
announce('loop:agent-created', { agentId });
|
|
185
|
+
kickoff();
|
|
186
|
+
});
|
|
187
|
+
ctx.on('agent/disposed', ({ agent }) => {
|
|
188
|
+
live.delete(String(agent.id));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Session events carry no agent, so an assembly cannot tell whose turn it is
|
|
192
|
+
// from the event alone. Ownership of the *session* is the same question one
|
|
193
|
+
// step earlier: a session belongs to whichever agent is running in it.
|
|
194
|
+
const ownedSessions = new Set();
|
|
195
|
+
ctx.on('agent/created', ({ agent }) => {
|
|
196
|
+
if (isMine(String(agent.id)) && agent.session?.id !== undefined) {
|
|
197
|
+
ownedSessions.add(String(agent.session.id));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Inbound control face: the embedder drives the loop with ordinary service
|
|
202
|
+
// calls. Commands address the single configured agent by default, or a
|
|
203
|
+
// specific one via `agentId`.
|
|
204
|
+
const resolveAgent = (input) => {
|
|
205
|
+
if (typeof input?.agentId === 'string') return live.get(input.agentId);
|
|
206
|
+
if (live.size === 1) return live.values().next().value;
|
|
207
|
+
return undefined;
|
|
208
|
+
};
|
|
209
|
+
const userMessage = (text) => createUserMessage({
|
|
210
|
+
content: [{ type: 'text', text: String(text) }],
|
|
211
|
+
source: { kind: 'user' },
|
|
212
|
+
});
|
|
213
|
+
sink.service('loop:control', (input) => {
|
|
214
|
+
const agent = resolveAgent(input);
|
|
215
|
+
if (agent === undefined) {
|
|
216
|
+
const known = [...live.keys()];
|
|
217
|
+
throw new Error(`loop:control: no live agent${input?.agentId ? ` "${input.agentId}"` : ''} (live: ${known.join(', ') || '(none)'})`);
|
|
218
|
+
}
|
|
219
|
+
switch (input?.kind) {
|
|
220
|
+
case 'followup':
|
|
221
|
+
agent.followup(userMessage(input.text ?? ''));
|
|
222
|
+
return { accepted: true, agentId: String(agent.id) };
|
|
223
|
+
case 'steer':
|
|
224
|
+
agent.steer(userMessage(input.text ?? ''));
|
|
225
|
+
return { accepted: true, agentId: String(agent.id) };
|
|
226
|
+
case 'cancel':
|
|
227
|
+
agent.cancel({ kind: 'user' });
|
|
228
|
+
return { accepted: true, agentId: String(agent.id) };
|
|
229
|
+
case 'status':
|
|
230
|
+
return { agentId: String(agent.id), status: agent.status };
|
|
231
|
+
default:
|
|
232
|
+
throw new Error(`loop:control: unknown command kind ${JSON.stringify(input?.kind)}`);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export default { name, inject, apply };
|