@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.
Files changed (91) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +203 -144
  4. package/THIRD_PARTY_NOTICES.txt +1232 -1232
  5. package/bin/rebon.js +22 -22
  6. package/lib/windows-managed.js +390 -390
  7. package/package.json +9 -2
  8. package/payload/compose-runtime/package.json +17 -17
  9. package/payload/compose-runtime/payload/compose/shims/dsh-anonymous-user-id.js +10 -10
  10. package/payload/compose-runtime/payload/compose/shims/dsh-credentials.js +13 -13
  11. package/payload/compose-runtime/payload/compose/shims/dsh-launch-environment.js +21 -21
  12. package/payload/compose-runtime/payload/compose/shims/dsh-llm.js +313 -313
  13. package/payload/compose-runtime/payload/compose/shims/dsh-settings.js +43 -43
  14. package/payload/compose-runtime/payload/compose/shims/dsh-tools.js +20 -20
  15. package/payload/compose-runtime/payload/compose/shims/dsh-web.js +10 -10
  16. package/payload/compose-runtime/payload/compose/shims/llm-adapter.js +32 -32
  17. package/payload/compose-runtime/payload/compose/shims/zod-lite.js +49 -49
  18. package/payload/compose-runtime/payload/vendor/cordis/LICENSE +21 -21
  19. package/payload/compose-runtime/payload/vendor/cordis/index.js +1530 -1530
  20. package/payload/compose-runtime/payload/vendor/cosmokit/LICENSE +21 -21
  21. package/payload/compose-runtime/payload/vendor/cosmokit/index.mjs +357 -357
  22. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-agent +21 -21
  23. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-agent-loop +21 -21
  24. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-llm-core +21 -21
  25. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-llm-deepseek +21 -21
  26. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-logger-console +21 -21
  27. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-scope +21 -21
  28. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-session +21 -21
  29. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-system-prompt +21 -21
  30. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-timeout +21 -21
  31. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-timer +21 -21
  32. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tool-todo +21 -21
  33. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tool-web +21 -21
  34. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-tools-schema +21 -21
  35. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-turndown +21 -21
  36. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-turndown-plugin-gfm +21 -21
  37. package/payload/compose-runtime/payload/vendor/dsh/LICENSE-web-search-exa +21 -21
  38. package/payload/compose-runtime/payload/vendor/dsh/agent-loop.js +1193 -1193
  39. package/payload/compose-runtime/payload/vendor/dsh/agent.js +714 -714
  40. package/payload/compose-runtime/payload/vendor/dsh/llm-core.js +269 -269
  41. package/payload/compose-runtime/payload/vendor/dsh/llm-deepseek.js +672 -672
  42. package/payload/compose-runtime/payload/vendor/dsh/logger-console.js +83 -83
  43. package/payload/compose-runtime/payload/vendor/dsh/scope.js +287 -287
  44. package/payload/compose-runtime/payload/vendor/dsh/session.js +1668 -1668
  45. package/payload/compose-runtime/payload/vendor/dsh/system-prompt.js +309 -309
  46. package/payload/compose-runtime/payload/vendor/dsh/timeout.js +100 -100
  47. package/payload/compose-runtime/payload/vendor/dsh/timer.js +128 -128
  48. package/payload/compose-runtime/payload/vendor/dsh/tool-todo.js +143 -143
  49. package/payload/compose-runtime/payload/vendor/dsh/tool-web.js +1527 -1527
  50. package/payload/compose-runtime/payload/vendor/dsh/tools-schema.js +849 -849
  51. package/payload/compose-runtime/payload/vendor/dsh/web-search-exa.js +122 -122
  52. package/payload/compose-runtime/payload/vendor/eventsource-parser/LICENSE +21 -21
  53. package/payload/compose-runtime/payload/vendor/eventsource-parser/index.js +177 -177
  54. package/payload/compose-runtime/payload/vendor/eventsource-parser/stream.js +48 -48
  55. package/payload/compose-runtime/payload/vendor/schemastery/LICENSE +21 -21
  56. package/payload/compose-runtime/payload/vendor/schemastery/index.mjs +656 -656
  57. package/payload/compose-runtime/payload-manifests.json +48 -48
  58. package/payload/compose-runtime/src/bridge.mjs +193 -195
  59. package/payload/compose-runtime/src/credentials-runtime.mjs +50 -50
  60. package/payload/compose-runtime/src/eventsource-stream.mjs +48 -48
  61. package/payload/compose-runtime/src/index.mjs +7 -7
  62. package/payload/compose-runtime/src/llm-runtime.mjs +326 -326
  63. package/payload/compose-runtime/src/loader.mjs +78 -78
  64. package/payload/compose-runtime/src/loop-assembly.mjs +237 -237
  65. package/payload/compose-runtime/src/payload.mjs +49 -49
  66. package/payload/compose-runtime/src/plugin.mjs +59 -59
  67. package/payload/compose-runtime/src/realm.mjs +223 -223
  68. package/payload/compose-runtime/src/registry.mjs +191 -191
  69. package/payload/compose-runtime/src/resolve.mjs +115 -115
  70. package/payload/compose-runtime/src/systemprompt-runtime.mjs +67 -67
  71. package/payload/compose-runtime/src/tools-runtime.mjs +239 -239
  72. package/payload/compose-runtime/src/web-runtime.mjs +209 -209
  73. package/payload/plugin-host/package.json +11 -11
  74. package/payload/plugin-host/src/bridge.mjs +64 -64
  75. package/payload/plugin-host/src/cli.mjs +147 -147
  76. package/payload/plugin-host/src/framing.mjs +113 -113
  77. package/payload/plugin-host/src/host.mjs +691 -691
  78. package/payload/plugin-host/src/json.mjs +208 -208
  79. package/payload/plugin-host/src/lifecycle.mjs +114 -114
  80. package/payload/plugin-host/src/loader.mjs +183 -183
  81. package/payload/plugin-host/src/methods.mjs +377 -377
  82. package/payload/plugin-host/src/ownership.mjs +57 -57
  83. package/payload/plugin-host/src/protocol.mjs +129 -129
  84. package/payload/plugin-host/src/sdk.mjs +7 -7
  85. package/payload/rebon-boa-helper.exe +0 -0
  86. package/payload/rebon-browser-mcp.exe +0 -0
  87. package/payload/rebon-computer-use.exe +0 -0
  88. package/payload/rebon-lsp-mcp.exe +0 -0
  89. package/payload/rebon.exe +2 -2
  90. package/payload/sandbox-win.exe +0 -0
  91. 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 things moved with the transport:
18
- //
19
- // * **The catalog is configuration, not a call.** It used to be
20
- // `describeTools()`, an op straight into rebon. There is no such method on
21
- // the plane, and there should not be: what rebon offers a model is settled
22
- // when the composition is built, so rebon hands it over at load. A snapshot
23
- // either way — the old op was read once during `apply` too.
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 };