@rebon/cli-linux-x64 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +63 -4
- package/THIRD_PARTY_NOTICES.txt +1122 -18
- package/bin/compose-runtime/payload/compose/shims/zod-lite.js +2 -2
- package/bin/compose-runtime/payload/vendor/cordis/LICENSE +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-turndown +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-turndown-plugin-gfm +21 -0
- package/bin/compose-runtime/payload/vendor/schemastery/LICENSE +21 -0
- package/bin/compose-runtime/src/bridge.mjs +11 -13
- package/bin/compose-runtime/src/loop-assembly.mjs +6 -6
- package/bin/plugin-host/package.json +1 -1
- package/bin/plugin-host/src/cli.mjs +9 -9
- package/bin/plugin-host/src/ownership.mjs +5 -5
- package/bin/rebon +2 -2
- package/bin/rebon-boa-helper +0 -0
- package/bin/rebon-browser-mcp +0 -0
- package/bin/rebon-computer-use +0 -0
- package/bin/rebon-lsp-mcp +0 -0
- package/bin/sandbox-win +0 -0
- package/package.json +9 -2
- package/bin/compose-runtime/payload/compose/credentials-runtime.js +0 -38
- package/bin/compose-runtime/payload/compose/llm-runtime.js +0 -330
- package/bin/compose-runtime/payload/compose/loop-assembly.js +0 -149
- package/bin/compose-runtime/payload/compose/serve-dispatch.js +0 -82
- package/bin/compose-runtime/payload/compose/systemprompt-runtime.js +0 -74
- package/bin/compose-runtime/payload/compose/tools-runtime.js +0 -228
- package/bin/compose-runtime/payload/compose/web-runtime.js +0 -204
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
// rebon-loop-assembly: the composition-side glue that lets the REAL
|
|
2
|
-
// dsh-agent-loop drive a turn inside this isolate (assembly doc:
|
|
3
|
-
// docs/dsh-agent-loop-assembly.md §4).
|
|
4
|
-
//
|
|
5
|
-
// Three duties, all rebon-owned (the dsh packages stay unmodified):
|
|
6
|
-
// 1. Tool-schema supply: registers a dsh systemPrompt tool provider that
|
|
7
|
-
// merges the composition's own registered definitions with the R5 core
|
|
8
|
-
// seat catalog (describeTools) — the same two sources the tools seat's
|
|
9
|
-
// scheduler dispatches into, so model-visible and dispatchable stay in
|
|
10
|
-
// step (local definition shadows a seat name in BOTH places).
|
|
11
|
-
// 2. Observability: relays every dsh `session/event` (chunks included —
|
|
12
|
-
// the embedder's session face streams from them) and `agent/error`
|
|
13
|
-
// onto the kernel event plane (`loop:event` / `loop:agent-error`),
|
|
14
|
-
// stamped with the embedder's `tag` so co-resident loop hosts stay
|
|
15
|
-
// distinguishable.
|
|
16
|
-
// 3. Drive: `loop:control` serve target (followup/steer/cancel/status)
|
|
17
|
-
// is the embedder's inbound face; config `{kickoff: "..."}` remains as
|
|
18
|
-
// an e2e self-drive convenience.
|
|
19
|
-
import { emit, describeTools, logger } from 'rebon';
|
|
20
|
-
import { createUserMessage, errorChain } from '@deepseek-ai/dsh-llm';
|
|
21
|
-
import { registerServeTarget } from './serve-dispatch.js';
|
|
22
|
-
|
|
23
|
-
/** Best-effort warn: the kernel logger seat is optional (tools-runtime rule). */
|
|
24
|
-
function warn(message) {
|
|
25
|
-
try {
|
|
26
|
-
logger.warn(message);
|
|
27
|
-
} catch {
|
|
28
|
-
console.warn(message);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const name = 'rebon-loop-assembly';
|
|
33
|
-
export const inject = ['systemPrompt', 'tools'];
|
|
34
|
-
|
|
35
|
-
export async function apply(ctx, config = {}) {
|
|
36
|
-
// Embedder-chosen discriminator: with several loop hosts in one process,
|
|
37
|
-
// every relayed event carries the owning host's tag so kernel-plane
|
|
38
|
-
// subscribers can tell their loop apart.
|
|
39
|
-
const tag = typeof config.tag === 'string' && config.tag.length > 0 ? config.tag : undefined;
|
|
40
|
-
const stamp = (payload) => (tag === undefined ? payload : { tag, ...payload });
|
|
41
|
-
// Optional prompt contribution into the loop realm's REAL dsh
|
|
42
|
-
// systemPrompt (this is the loop's own prompt plane, not rebon's P2.8
|
|
43
|
-
// seat — the isolate keeps them apart).
|
|
44
|
-
if (config.section !== undefined && config.section !== null && typeof config.section === 'object') {
|
|
45
|
-
ctx.systemPrompt.section(config.section);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let seatSchemas = [];
|
|
49
|
-
try {
|
|
50
|
-
const catalog = await describeTools();
|
|
51
|
-
seatSchemas = (Array.isArray(catalog) ? catalog : []).map((tool) => ({
|
|
52
|
-
name: tool.name,
|
|
53
|
-
description: String(tool.description ?? ''),
|
|
54
|
-
parameters: tool.inputSchema ?? { type: 'object' },
|
|
55
|
-
}));
|
|
56
|
-
} catch (error) {
|
|
57
|
-
// An unbound tool plane is a legal composition (prompt-only loop); the
|
|
58
|
-
// schemas face is then just the composition's own registrations.
|
|
59
|
-
warn(`loop-assembly: R5 seat catalog unavailable: ${String(error?.message ?? error)}`);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
ctx.systemPrompt.tools(() => {
|
|
63
|
-
const merged = new Map();
|
|
64
|
-
for (const schema of seatSchemas) merged.set(schema.name, schema);
|
|
65
|
-
for (const [toolName, def] of ctx.tools.defs) {
|
|
66
|
-
merged.set(toolName, {
|
|
67
|
-
name: toolName,
|
|
68
|
-
description: String(def.description ?? ''),
|
|
69
|
-
parameters: def.parameters ?? { type: 'object' },
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
return { schemas: [...merged.values()] };
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Full-fidelity relay: the dsh session log IS the loop's truth, and the
|
|
76
|
-
// embedder's session face (streaming UI, transcript projection) needs
|
|
77
|
-
// every event — chunks included.
|
|
78
|
-
ctx.on('session/event', (session, event) => {
|
|
79
|
-
emit('loop:event', stamp({
|
|
80
|
-
sessionId: String(session.id),
|
|
81
|
-
seq: event.seq,
|
|
82
|
-
type: event.type,
|
|
83
|
-
data: event.data ?? null,
|
|
84
|
-
}));
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
ctx.on('agent/error', (payload) => {
|
|
88
|
-
emit('loop:agent-error', stamp({
|
|
89
|
-
agentId: String(payload.agent?.id ?? ''),
|
|
90
|
-
turn: payload.turn,
|
|
91
|
-
step: payload.step,
|
|
92
|
-
error: errorChain(payload.error),
|
|
93
|
-
}));
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const live = new Map();
|
|
97
|
-
const kicked = new Set();
|
|
98
|
-
ctx.on('agent/created', ({ agent }) => {
|
|
99
|
-
live.set(String(agent.id), agent);
|
|
100
|
-
emit('loop:agent-created', stamp({ agentId: String(agent.id) }));
|
|
101
|
-
const kickoff = config.kickoff;
|
|
102
|
-
if (typeof kickoff !== 'string' || kickoff.length === 0) return;
|
|
103
|
-
if (kicked.has(agent.id)) return;
|
|
104
|
-
kicked.add(agent.id);
|
|
105
|
-
agent.followup(createUserMessage({
|
|
106
|
-
content: [{ type: 'text', text: kickoff }],
|
|
107
|
-
source: { kind: 'user' },
|
|
108
|
-
}));
|
|
109
|
-
});
|
|
110
|
-
ctx.on('agent/disposed', ({ agent }) => {
|
|
111
|
-
live.delete(String(agent.id));
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Inbound control face: the embedder drives the loop over the serve
|
|
115
|
-
// plane (request/response — the same plane rebon uses to call
|
|
116
|
-
// composition tools). Commands address the single configured agent by
|
|
117
|
-
// default, or a specific one via `agentId`.
|
|
118
|
-
const resolveAgent = (input) => {
|
|
119
|
-
if (typeof input?.agentId === 'string') return live.get(input.agentId);
|
|
120
|
-
if (live.size === 1) return live.values().next().value;
|
|
121
|
-
return undefined;
|
|
122
|
-
};
|
|
123
|
-
const userMessage = (text) => createUserMessage({
|
|
124
|
-
content: [{ type: 'text', text: String(text) }],
|
|
125
|
-
source: { kind: 'user' },
|
|
126
|
-
});
|
|
127
|
-
registerServeTarget('loop:control', (input) => {
|
|
128
|
-
const agent = resolveAgent(input);
|
|
129
|
-
if (agent === undefined) {
|
|
130
|
-
const known = [...live.keys()];
|
|
131
|
-
throw new Error(`loop:control: no live agent${input?.agentId ? ` "${input.agentId}"` : ''} (live: ${known.join(', ') || '(none)'})`);
|
|
132
|
-
}
|
|
133
|
-
switch (input?.kind) {
|
|
134
|
-
case 'followup':
|
|
135
|
-
agent.followup(userMessage(input.text ?? ''));
|
|
136
|
-
return { accepted: true, agentId: String(agent.id) };
|
|
137
|
-
case 'steer':
|
|
138
|
-
agent.steer(userMessage(input.text ?? ''));
|
|
139
|
-
return { accepted: true, agentId: String(agent.id) };
|
|
140
|
-
case 'cancel':
|
|
141
|
-
agent.cancel({ kind: 'user' });
|
|
142
|
-
return { accepted: true, agentId: String(agent.id) };
|
|
143
|
-
case 'status':
|
|
144
|
-
return { agentId: String(agent.id), status: agent.status };
|
|
145
|
-
default:
|
|
146
|
-
throw new Error(`loop:control: unknown command kind ${JSON.stringify(input?.kind)}`);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// Shared serve pump over the tool-serve ops (P1.6/P2.7): one pump per
|
|
2
|
-
// composition isolate, exact-name target dispatch. The tools seat registers
|
|
3
|
-
// each tool under its own name; the web seat registers providers under the
|
|
4
|
-
// reserved `web:<kind>:<id>` namespace. Handlers run concurrently (one slow
|
|
5
|
-
// call never head-of-line blocks the pump); `cancel` aborts the per-call
|
|
6
|
-
// AbortController.
|
|
7
|
-
//
|
|
8
|
-
// Envelope contract with the Rust side: `{ok: <handler return>}` on
|
|
9
|
-
// success; `{err: message, code?}` on throw (a thrown `code` property —
|
|
10
|
-
// dsh WebError/HarnessError — rides along for machine routing).
|
|
11
|
-
|
|
12
|
-
const core = globalThis.Deno.core;
|
|
13
|
-
|
|
14
|
-
const targets = new Map(); // target name -> async (input, signal, id) => value
|
|
15
|
-
const aborts = new Map(); // serve call id -> AbortController
|
|
16
|
-
let pumping = false;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Register one serve target. Exact-name dispatch; duplicates throw. Returns
|
|
20
|
-
* the disposer.
|
|
21
|
-
*/
|
|
22
|
-
export function registerServeTarget(name, handler) {
|
|
23
|
-
if (targets.has(name)) {
|
|
24
|
-
throw new Error(`serve target "${name}" is already registered`);
|
|
25
|
-
}
|
|
26
|
-
targets.set(name, handler);
|
|
27
|
-
return () => targets.delete(name);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Start the pump once; later calls are no-ops. */
|
|
31
|
-
export function ensureServePump() {
|
|
32
|
-
if (pumping) return;
|
|
33
|
-
pumping = true;
|
|
34
|
-
void pump();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function pump() {
|
|
38
|
-
while (true) {
|
|
39
|
-
const instr = JSON.parse(await core.ops.op_tool_serve_next());
|
|
40
|
-
if (instr.closed) break;
|
|
41
|
-
if (instr.kind === 'cancel') {
|
|
42
|
-
aborts.get(instr.id)?.abort();
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
if (instr.kind !== 'request') continue;
|
|
46
|
-
void serve(instr.id, instr.tool, instr.input);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function serve(id, target, input) {
|
|
51
|
-
const handler = targets.get(target);
|
|
52
|
-
if (!handler) {
|
|
53
|
-
emitEnvelope(id, {
|
|
54
|
-
err: `[UNKNOWN_TOOL] target "${target}" is not registered in the composition`,
|
|
55
|
-
});
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const abort = new AbortController();
|
|
59
|
-
aborts.set(id, abort);
|
|
60
|
-
try {
|
|
61
|
-
const value = await handler(input, abort.signal, id);
|
|
62
|
-
emitEnvelope(id, { ok: value });
|
|
63
|
-
} catch (e) {
|
|
64
|
-
emitEnvelope(id, {
|
|
65
|
-
err: String(e?.message ?? e),
|
|
66
|
-
...(typeof e?.code === 'string' ? { code: e.code } : {}),
|
|
67
|
-
});
|
|
68
|
-
} finally {
|
|
69
|
-
aborts.delete(id);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function emitEnvelope(id, envelope) {
|
|
74
|
-
try {
|
|
75
|
-
core.ops.op_tool_serve_emit(id, JSON.stringify(envelope));
|
|
76
|
-
} catch (e) {
|
|
77
|
-
core.ops.op_tool_serve_emit(
|
|
78
|
-
id,
|
|
79
|
-
JSON.stringify({ err: `serve result not serializable: ${String(e?.message ?? e)}` }),
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// Minimal dsh-compatible `ctx.systemPrompt` seat for the composition host
|
|
2
|
-
// (P2.8-JS, docs/dsh-tools-assembly.md / todo-atomization P2.8).
|
|
3
|
-
//
|
|
4
|
-
// API shape follows dsh packages/core/system-prompt: plugins call
|
|
5
|
-
// `ctx.systemPrompt.section({name, order, text})` and get back the exact
|
|
6
|
-
// disposer. Faithful validation subset: duplicate names throw the dsh
|
|
7
|
-
// message, non-finite orders throw the dsh TypeError. Registrations mirror
|
|
8
|
-
// into the kernel's process-level `system-prompt` service — riding the F1
|
|
9
|
-
// effect ledger, so a hard-killed composition never leaves stale sections —
|
|
10
|
-
// and rebon's own prompt assembly (P2.8 registry) renders them inside its
|
|
11
|
-
// stable runtime-context plane, sorted by (order, name).
|
|
12
|
-
//
|
|
13
|
-
// Deliberately NOT provided (v1 honesty boundary): provider-function
|
|
14
|
-
// `text` (dsh resolves it per assembly with an AssembleContext we do not
|
|
15
|
-
// have — a static composition registers static text), `complete` sections
|
|
16
|
-
// (replacing rebon's own prompt belongs to the P3.12 loop seat, not a
|
|
17
|
-
// side door), `context()` / variables / tool-order surfaces. All fail
|
|
18
|
-
// loudly rather than silently degrade.
|
|
19
|
-
import { Service } from 'cordis';
|
|
20
|
-
import { callService } from 'rebon';
|
|
21
|
-
|
|
22
|
-
export default class RebonSystemPromptRuntime extends Service {
|
|
23
|
-
constructor(ctx) {
|
|
24
|
-
super(ctx, 'systemPrompt');
|
|
25
|
-
this.names = new Set();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Register one ordered prompt section; returns the exact disposer. */
|
|
29
|
-
section(entry) {
|
|
30
|
-
const name = entry?.name;
|
|
31
|
-
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
32
|
-
throw new TypeError('systemPrompt.section: section.name must be a non-empty string');
|
|
33
|
-
}
|
|
34
|
-
if (!Number.isFinite(entry.order)) {
|
|
35
|
-
throw new TypeError(`prompt section "${name}" order must be a finite number`);
|
|
36
|
-
}
|
|
37
|
-
if (typeof entry.text === 'function') {
|
|
38
|
-
throw new Error(
|
|
39
|
-
`prompt section "${name}": provider-function text is not supported in the embedded runtime (register static text)`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
if (typeof entry.text !== 'string') {
|
|
43
|
-
throw new TypeError(`prompt section "${name}" text must be a string`);
|
|
44
|
-
}
|
|
45
|
-
if (entry.complete) {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`prompt section "${name}": complete-section replacement is not supported in the embedded runtime`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
if (this.names.has(name)) {
|
|
51
|
-
throw new Error(`prompt section "${name}" is already registered in this scope`);
|
|
52
|
-
}
|
|
53
|
-
const runtime = this;
|
|
54
|
-
// Caller-fork RAII through the Service proxy (llm/tools/web same
|
|
55
|
-
// pattern): plugin disposal unwinds the kernel mirror. Owner captured
|
|
56
|
-
// synchronously for ledger attribution (single-plugin unload sweep).
|
|
57
|
-
const owner = this.ctx[Symbol.for('rebon.composeOwner')] ?? '';
|
|
58
|
-
return this.ctx.effect(() => {
|
|
59
|
-
const registered = callService('system-prompt', 'register', {
|
|
60
|
-
name,
|
|
61
|
-
order: entry.order,
|
|
62
|
-
text: entry.text,
|
|
63
|
-
}, owner);
|
|
64
|
-
runtime.names.add(name);
|
|
65
|
-
const token = registered?.token;
|
|
66
|
-
return () => {
|
|
67
|
-
runtime.names.delete(name);
|
|
68
|
-
try {
|
|
69
|
-
callService('system-prompt', 'unregister', { name, token });
|
|
70
|
-
} catch {}
|
|
71
|
-
};
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
// Minimal dsh-compatible `ctx.tools` seat for the composition host (P1.6,
|
|
2
|
-
// docs/dsh-tools-assembly.md).
|
|
3
|
-
//
|
|
4
|
-
// API shape follows dsh packages/core/tools: plugins call
|
|
5
|
-
// `ctx.tools.register(defineTool({...}))` and get back the exact disposer.
|
|
6
|
-
// Definitions arrive with `parameters` already compiled to JSON Schema and
|
|
7
|
-
// argument validation baked into `execute` (the vendored dsh-tools schema
|
|
8
|
-
// layer does both). Each registration is mirrored into the kernel's
|
|
9
|
-
// process-level `tool-registry` service — that call rides the bridge effect
|
|
10
|
-
// ledger, so a hard-killed composition can never leave the kernel table
|
|
11
|
-
// dirty — and execution requests come back over the shared serve pump
|
|
12
|
-
// (serve-dispatch.js, target = the tool name), running the registered body
|
|
13
|
-
// in this isolate.
|
|
14
|
-
//
|
|
15
|
-
// Deliberately NOT provided (v1 honesty boundary, see the assembly doc):
|
|
16
|
-
// `restrict`/`guard` (dsh agent-scope concepts), code mode, output-schema
|
|
17
|
-
// enforcement of the canonical value, presenter projections. `exec` carries
|
|
18
|
-
// the identity fields plus an `agent.session.append` bridge that lands as
|
|
19
|
-
// the kernel event `compose:session/append` — observable, but not a claim
|
|
20
|
-
// of rebon transcript persistence (that is the P2.9 session seat).
|
|
21
|
-
import { Service } from 'cordis';
|
|
22
|
-
import { callService, emit, invokeTool } from 'rebon';
|
|
23
|
-
import { TOOL_RUNTIME_SCHEDULER } from '@deepseek-ai/dsh-tools';
|
|
24
|
-
import { ensureServePump, registerServeTarget } from './serve-dispatch.js';
|
|
25
|
-
|
|
26
|
-
/** Extract a `[BRACKET_CODE]` prefix from a seat error message, if any. */
|
|
27
|
-
function bracketCode(message) {
|
|
28
|
-
const match = /^\[([A-Z_]+)\]/.exec(message);
|
|
29
|
-
return match ? match[1] : undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Per-run context/conclusion collection installed by scheduler.prepare. */
|
|
33
|
-
const runStates = new WeakMap();
|
|
34
|
-
|
|
35
|
-
export default class RebonToolsRuntime extends Service {
|
|
36
|
-
constructor(ctx) {
|
|
37
|
-
super(ctx, 'tools');
|
|
38
|
-
this.names = new Set();
|
|
39
|
-
// Local definition table for the scheduler's in-composition dispatch
|
|
40
|
-
// (register() below keeps it in step with the kernel mirror).
|
|
41
|
-
this.defs = new Map();
|
|
42
|
-
ensureServePump();
|
|
43
|
-
|
|
44
|
-
// ---- dsh scheduler contract (loop round) ----
|
|
45
|
-
//
|
|
46
|
-
// agent-loop's tool-calls scheduler drives tools through this
|
|
47
|
-
// symbol-keyed view (dsh's private loop↔tools seam, contract
|
|
48
|
-
// transcribed from packages/core/tools/src/index.ts:451). Dispatch
|
|
49
|
-
// resolution: a composition-registered definition runs in-isolate;
|
|
50
|
-
// anything else goes to the R5 core seat, whose refusals (stable
|
|
51
|
-
// bracketed codes) come back as error RESULTS for the model, not
|
|
52
|
-
// crashes. v1 honesty boundary: no pre/post-execute pipeline —
|
|
53
|
-
// prepare always dispatches, finalize/finish pass through.
|
|
54
|
-
const runtime = this;
|
|
55
|
-
this[TOOL_RUNTIME_SCHEDULER] = {
|
|
56
|
-
async prepare(exec) {
|
|
57
|
-
const state = { contexts: [], concluded: false };
|
|
58
|
-
const run = {
|
|
59
|
-
...exec,
|
|
60
|
-
rootCallId: exec.rootCallId ?? exec.callId,
|
|
61
|
-
deferContext(message) {
|
|
62
|
-
state.contexts.push(message);
|
|
63
|
-
},
|
|
64
|
-
concludeTurn() {
|
|
65
|
-
state.concluded = true;
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
runStates.set(run, state);
|
|
69
|
-
return { kind: 'dispatch', exec: run };
|
|
70
|
-
},
|
|
71
|
-
async dispatch(exec) {
|
|
72
|
-
const state = runStates.get(exec) ?? { contexts: [], concluded: false };
|
|
73
|
-
try {
|
|
74
|
-
const def = runtime.defs.get(exec.name);
|
|
75
|
-
let content;
|
|
76
|
-
if (def !== undefined) {
|
|
77
|
-
const value = await def.execute(exec.arguments, exec);
|
|
78
|
-
content = def.output.render(exec.arguments, value);
|
|
79
|
-
} else {
|
|
80
|
-
const value = await invokeTool(exec.name, exec.arguments ?? {});
|
|
81
|
-
content = [{
|
|
82
|
-
type: 'text',
|
|
83
|
-
text: typeof value === 'string' ? value : JSON.stringify(value),
|
|
84
|
-
}];
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
kind: 'final-result',
|
|
88
|
-
result: {
|
|
89
|
-
content,
|
|
90
|
-
isError: false,
|
|
91
|
-
...(state.contexts.length > 0 ? { additionalContexts: state.contexts } : {}),
|
|
92
|
-
...(state.concluded ? { concludesTurn: true } : {}),
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
} catch (error) {
|
|
96
|
-
const message = String(error?.message ?? error);
|
|
97
|
-
return {
|
|
98
|
-
kind: 'final-result',
|
|
99
|
-
result: {
|
|
100
|
-
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
101
|
-
isError: true,
|
|
102
|
-
error: {
|
|
103
|
-
message,
|
|
104
|
-
info: {
|
|
105
|
-
name: error?.name ?? 'Error',
|
|
106
|
-
code: bracketCode(message) ?? 'TOOL_FAILED',
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
async finalize(_exec, result) {
|
|
114
|
-
return result;
|
|
115
|
-
},
|
|
116
|
-
finish(_exec, result) {
|
|
117
|
-
return result;
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* dsh concurrency classification. v1: everything is exclusive — strictly
|
|
124
|
-
* serial scheduling is always semantically correct, and it sidesteps the
|
|
125
|
-
* parallel pool until `isConcurrencySafe` mapping is wired.
|
|
126
|
-
*/
|
|
127
|
-
executionMode(_exec) {
|
|
128
|
-
return { kind: 'exclusive' };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Register one dsh-shaped tool definition. Mirrors dsh
|
|
133
|
-
* `ToolRuntime.register` validation; returns the exact disposer.
|
|
134
|
-
*/
|
|
135
|
-
register(definition) {
|
|
136
|
-
const name = definition?.name;
|
|
137
|
-
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
138
|
-
throw new TypeError('tools.register: definition.name must be a non-empty string');
|
|
139
|
-
}
|
|
140
|
-
if (name.startsWith('web:')) {
|
|
141
|
-
throw new Error(`tool name "${name}" uses the reserved web-provider serve namespace`);
|
|
142
|
-
}
|
|
143
|
-
const output = definition.output;
|
|
144
|
-
if (
|
|
145
|
-
output === undefined || typeof output !== 'object'
|
|
146
|
-
|| typeof output.render !== 'function'
|
|
147
|
-
) {
|
|
148
|
-
throw new TypeError(`tool "${name}" must declare output { schema, render }`);
|
|
149
|
-
}
|
|
150
|
-
if (typeof definition.execute !== 'function') {
|
|
151
|
-
throw new TypeError(`tool "${name}" must declare execute()`);
|
|
152
|
-
}
|
|
153
|
-
const timeoutMs = definition.timeoutMs;
|
|
154
|
-
if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
|
|
155
|
-
throw new TypeError(`tool "${name}" timeoutMs must be a positive finite number`);
|
|
156
|
-
}
|
|
157
|
-
if (this.names.has(name)) {
|
|
158
|
-
throw new Error(`tool "${name}" is already registered in this scope`);
|
|
159
|
-
}
|
|
160
|
-
const runtime = this;
|
|
161
|
-
// `this.ctx` is the CALLER's context through the Service proxy, so the
|
|
162
|
-
// effect (and with it the kernel mirror) unwinds when the registering
|
|
163
|
-
// plugin disposes — cordis RAII, llm-runtime same pattern. ctx.effect
|
|
164
|
-
// returns the disposer, which dsh's register contract hands back.
|
|
165
|
-
// Ledger attribution: the composition-entry id, captured synchronously
|
|
166
|
-
// (never ambient — async interleavings must not mis-attribute).
|
|
167
|
-
const owner = this.ctx[Symbol.for('rebon.composeOwner')] ?? '';
|
|
168
|
-
return this.ctx.effect(() => {
|
|
169
|
-
const registered = callService('tool-registry', 'register', {
|
|
170
|
-
name,
|
|
171
|
-
description: String(definition.description ?? ''),
|
|
172
|
-
inputSchema: definition.parameters ?? { type: 'object' },
|
|
173
|
-
}, owner);
|
|
174
|
-
if (registered?.shadowed) {
|
|
175
|
-
// Same rule as R3: builtin always wins at dispatch; loud, not
|
|
176
|
-
// fatal — and best-effort, because the Rust registry already
|
|
177
|
-
// logged it and a missing logger seat must not fail registration.
|
|
178
|
-
const message = `composition tool "${name}" shares a builtin tool's name; the builtin wins`;
|
|
179
|
-
try {
|
|
180
|
-
callService('logger', 'warn', { message });
|
|
181
|
-
} catch {
|
|
182
|
-
console.warn(message);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
runtime.names.add(name);
|
|
186
|
-
runtime.defs.set(name, definition);
|
|
187
|
-
const disposeServe = registerServeTarget(name, (input, signal, id) =>
|
|
188
|
-
runtime._execute(definition, input, signal, id),
|
|
189
|
-
);
|
|
190
|
-
const token = registered?.token;
|
|
191
|
-
return () => {
|
|
192
|
-
disposeServe();
|
|
193
|
-
runtime.names.delete(name);
|
|
194
|
-
runtime.defs.delete(name);
|
|
195
|
-
try {
|
|
196
|
-
callService('tool-registry', 'unregister', { name, token });
|
|
197
|
-
} catch {}
|
|
198
|
-
};
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async _execute(definition, input, signal, id) {
|
|
203
|
-
const callId = `compose:${id}`;
|
|
204
|
-
const tool = definition.name;
|
|
205
|
-
const exec = {
|
|
206
|
-
callId,
|
|
207
|
-
rootCallId: callId,
|
|
208
|
-
name: tool,
|
|
209
|
-
arguments: input,
|
|
210
|
-
signal,
|
|
211
|
-
// v1 bridge: dsh tools write per-session facts through
|
|
212
|
-
// `exec.agent.session.append`; here that lands as a kernel event
|
|
213
|
-
// the embedder can observe. No persistence claim.
|
|
214
|
-
agent: {
|
|
215
|
-
session: {
|
|
216
|
-
append(type, data) {
|
|
217
|
-
emit('compose:session/append', { type, data, tool, callId });
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
};
|
|
222
|
-
// defineTool's execute validates arguments first (ToolArgsError with
|
|
223
|
-
// INVALID_ARGS semantics flows into the err envelope's message).
|
|
224
|
-
const value = await definition.execute(input, exec);
|
|
225
|
-
const content = definition.output.render(input, value);
|
|
226
|
-
return { content, isError: false };
|
|
227
|
-
}
|
|
228
|
-
}
|