bare-agent 0.12.1 → 0.13.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/README.md +4 -2
- package/bareagent.context.md +29 -4
- package/bin/cli.js +54 -2
- package/examples/README.md +15 -0
- package/examples/litectx-as-store.mjs +78 -0
- package/examples/litectx-mcp-child.mjs +57 -0
- package/examples/mcp-bridge-concurrent.js +106 -0
- package/examples/mcp-bridge-poc.js +77 -0
- package/examples/orchestrator/README.md +53 -0
- package/examples/orchestrator/orchestrator.json +14 -0
- package/examples/orchestrator/specialists/researcher.json +12 -0
- package/examples/orchestrator/specialists/summarizer.json +11 -0
- package/examples/replay-job.js +213 -0
- package/examples/wake.md +99 -0
- package/examples/wake.sh +84 -0
- package/examples/with-bareguard.mjs +65 -0
- package/index.d.ts +4 -1
- package/index.js +4 -0
- package/package.json +4 -2
- package/src/context-units.d.ts +44 -0
- package/src/context-units.js +225 -0
- package/src/loop.d.ts +11 -0
- package/src/loop.js +31 -1
- package/src/tools.d.ts +2 -1
- package/src/tools.js +2 -0
- package/tools/litectx-mcp.d.ts +28 -0
- package/tools/litectx-mcp.js +65 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// RT-1 — the msgs⇄units adapter for the `assemble` context-assembly seam (src/loop.js).
|
|
4
|
+
//
|
|
5
|
+
// Division of labour (litectx CE-PRD §8.2, line 321 — the frozen contract):
|
|
6
|
+
// - bareagent owns GRAMMAR: msgs→units, units→msgs, atomic bundling of a tool-call + its result(s),
|
|
7
|
+
// `pinned` flags, the pairing seatbelt, and fail-open. Broken tool-pairing and a dropped system
|
|
8
|
+
// prompt are UNREPRESENTABLE here, by construction — not by trusting the consumer.
|
|
9
|
+
// - litectx owns CONTENT + RELEVANCE: a single verb `assemble(units, ctx) → { units, dropped, tokens }`
|
|
10
|
+
// (the AssembleResult envelope — `dropped[]` is load-bearing, ships in the same slice, never silent)
|
|
11
|
+
// that does SELECT (keep / drop / reorder / recall-inject) + COMPRESS (rewrite a unit's `content`) +
|
|
12
|
+
// fit-to-`ctx.budget`, best-effort. It never learns the message grammar. `unitAssembler` unwraps `.units`.
|
|
13
|
+
//
|
|
14
|
+
// The neutral unit (the SOCKET litectx codes against — exactly these 7 enumerable fields):
|
|
15
|
+
// { id, role, content, kind, pinned, atomic, tokensApprox }
|
|
16
|
+
// id — stable within one assemble() call; litectx references units by it.
|
|
17
|
+
// role — 'system' | 'user' | 'assistant' | 'tool' (the message grammar role).
|
|
18
|
+
// content — flat string view litectx reads to score and MAY rewrite to COMPRESS.
|
|
19
|
+
// kind — litectx's memory-node kind ('code'|'doc'|'fact'|'episode') on units litectx
|
|
20
|
+
// INJECTS from recall. Transcript-derived units carry `null` — and that is the
|
|
21
|
+
// CORRECT value, not a placeholder. `role` and `kind` are orthogonal: `role`
|
|
22
|
+
// (user/assistant/tool/system) carries conversational position = bareagent's
|
|
23
|
+
// grammar, present on every unit; `kind` carries litectx's graph-node type, present
|
|
24
|
+
// only on units litectx owns. Typing a live turn would force litectx to learn the
|
|
25
|
+
// provider's conversation structure — the exact coupling the keystone boundary
|
|
26
|
+
// forbids (PRD §1.2 / litectx CE-PRD §8.2). RATIFIED by litectx (2026-06-12): do
|
|
27
|
+
// NOT define transcript-kinds. injected = kinded, transcript = null.
|
|
28
|
+
// pinned — true ⇒ bareagent guarantees the unit is never dropped and never reordered
|
|
29
|
+
// (system prompt, first user/task turn). litectx still sees its tokensApprox so the
|
|
30
|
+
// pin counts against the budget: pin-don't-hide.
|
|
31
|
+
// atomic — group-id (string) | null. litectx keeps/drops units sharing one group-id WHOLE,
|
|
32
|
+
// never split. bareagent pre-bundles an assistant tool-call + ALL its result(s) into
|
|
33
|
+
// one unit, so each bundle is its OWN group (group-id = the unit's id); everything
|
|
34
|
+
// else is null. NOTE: string|null, not boolean — a boolean collapses every bundle
|
|
35
|
+
// under one key and litectx would fit them all-or-nothing (test/context-units.test.js (real-litectx sweep)).
|
|
36
|
+
// tokensApprox — chars/4 estimate over the unit's backing messages, for litectx's budget math.
|
|
37
|
+
//
|
|
38
|
+
// `_msgs` (the verbatim backing messages) is attached NON-ENUMERABLE: it does not appear in the 7-field
|
|
39
|
+
// view, JSON, or iteration. It is bareagent's reconstruction backing — litectx must not read it. A unit
|
|
40
|
+
// with no backing (one litectx minted via recall-inject) is synthesised into a single message on the
|
|
41
|
+
// way out.
|
|
42
|
+
|
|
43
|
+
/** chars/4 token estimate over a list of messages (matches poc2 / the Loop's own heuristic). */
|
|
44
|
+
function approxTokens(msgs) {
|
|
45
|
+
let c = 0;
|
|
46
|
+
for (const m of msgs) {
|
|
47
|
+
if (m.content != null) c += String(m.content).length;
|
|
48
|
+
if (m.tool_calls) c += JSON.stringify(m.tool_calls).length;
|
|
49
|
+
}
|
|
50
|
+
return Math.ceil(c / 4);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Flat string view of a unit's backing messages — what litectx scores on and may rewrite. */
|
|
54
|
+
function renderContent(msgs) {
|
|
55
|
+
const parts = [];
|
|
56
|
+
for (const m of msgs) {
|
|
57
|
+
if (m.content != null && String(m.content).length) parts.push(String(m.content));
|
|
58
|
+
}
|
|
59
|
+
return parts.join('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let _seq = 0;
|
|
63
|
+
/** Attach the verbatim backing + original-content fingerprint as non-enumerable carry. */
|
|
64
|
+
function withBacking(unit, msgs) {
|
|
65
|
+
Object.defineProperty(unit, '_msgs', { value: msgs, enumerable: false, writable: true });
|
|
66
|
+
Object.defineProperty(unit, '_origContent', { value: unit.content, enumerable: false, writable: true });
|
|
67
|
+
return unit;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* msgs → neutral units. Bundles each assistant-tool-call message with the contiguous tool result(s)
|
|
72
|
+
* that answer its ids into ONE atomic unit (so pairing can never be split). system + first user turn
|
|
73
|
+
* are pinned.
|
|
74
|
+
* @param {Array<Record<string, any>>} msgs
|
|
75
|
+
* @returns {Array<Record<string, any>>}
|
|
76
|
+
*/
|
|
77
|
+
function toUnits(msgs) {
|
|
78
|
+
const units = [];
|
|
79
|
+
let seenUser = false;
|
|
80
|
+
for (let i = 0; i < msgs.length; i++) {
|
|
81
|
+
const m = msgs[i];
|
|
82
|
+
|
|
83
|
+
if (m.role === 'system') {
|
|
84
|
+
units.push(withBacking({ id: `u${_seq++}`, role: 'system', content: renderContent([m]), kind: null, pinned: true, atomic: null, tokensApprox: approxTokens([m]) }, [m]));
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (m.role === 'user') {
|
|
88
|
+
const pinned = !seenUser; // first user turn = the task; pin it
|
|
89
|
+
seenUser = true;
|
|
90
|
+
units.push(withBacking({ id: `u${_seq++}`, role: 'user', content: renderContent([m]), kind: null, pinned, atomic: null, tokensApprox: approxTokens([m]) }, [m]));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
94
|
+
// bundle this assistant message with the contiguous tool results answering its ids
|
|
95
|
+
const ids = new Set(m.tool_calls.map((t) => t.id));
|
|
96
|
+
const group = [m];
|
|
97
|
+
let j = i + 1;
|
|
98
|
+
while (j < msgs.length && msgs[j].role === 'tool' && ids.has(msgs[j].tool_call_id)) {
|
|
99
|
+
group.push(msgs[j]);
|
|
100
|
+
j++;
|
|
101
|
+
}
|
|
102
|
+
// `atomic` is litectx's group-id (string|null), NOT a boolean: units sharing one are kept/dropped
|
|
103
|
+
// whole. bareagent already pre-bundles a tool-call + its result(s) into ONE unit, so each bundle is
|
|
104
|
+
// its OWN group — group-id = the unit's id. (A boolean would collapse every bundle under one key,
|
|
105
|
+
// making litectx fit them all-or-nothing — see test/context-units.test.js (real-litectx sweep).)
|
|
106
|
+
const gid = `u${_seq++}`;
|
|
107
|
+
units.push(withBacking({ id: gid, role: 'assistant', content: renderContent(group.slice(1)), kind: null, pinned: false, atomic: gid, tokensApprox: approxTokens(group) }, group));
|
|
108
|
+
i = j - 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// plain assistant text, or a stray tool message (handled by the seatbelt on the way out)
|
|
112
|
+
units.push(withBacking({ id: `u${_seq++}`, role: m.role, content: renderContent([m]), kind: null, pinned: false, atomic: null, tokensApprox: approxTokens([m]) }, [m]));
|
|
113
|
+
}
|
|
114
|
+
return units;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Drop any tool-result whose tool_call_id has no open assistant tool-call before it, and any assistant
|
|
119
|
+
* tool-call message left with zero surviving results. The final grammar guard: even if litectx hands
|
|
120
|
+
* back something that would orphan a pair, the wire is always valid. Returns a fresh array.
|
|
121
|
+
*/
|
|
122
|
+
function pairingSeatbelt(msgs) {
|
|
123
|
+
// pass 1: which tool_call ids actually have a result present?
|
|
124
|
+
const resultIds = new Set();
|
|
125
|
+
for (const m of msgs) if (m.role === 'tool' && m.tool_call_id != null) resultIds.add(m.tool_call_id);
|
|
126
|
+
|
|
127
|
+
const out = [];
|
|
128
|
+
const open = new Set();
|
|
129
|
+
for (const m of msgs) {
|
|
130
|
+
if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
131
|
+
const surviving = m.tool_calls.filter((tc) => resultIds.has(tc.id));
|
|
132
|
+
if (!surviving.length) continue; // assistant tool-call with no results at all → drop the message
|
|
133
|
+
for (const tc of surviving) open.add(tc.id);
|
|
134
|
+
out.push(surviving.length === m.tool_calls.length ? m : { ...m, tool_calls: surviving });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (m.role === 'tool') {
|
|
138
|
+
if (!open.has(m.tool_call_id)) continue; // orphan result → drop
|
|
139
|
+
open.delete(m.tool_call_id);
|
|
140
|
+
out.push(m);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
out.push(m);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* units → msgs. Honors drop (absent units), reorder (order of the returned array), recall-inject
|
|
150
|
+
* (units with no backing → one synthesised message), and COMPRESS (a unit whose `content` was rewritten
|
|
151
|
+
* is reconstructed from the new content). Atomic units keep their assistant tool-call message verbatim
|
|
152
|
+
* so pairing holds; a content rewrite lands on the tool RESULT. A multi-result atomic bundle whose
|
|
153
|
+
* content was rewritten is kept VERBATIM — a flat string can't be faithfully split back into N
|
|
154
|
+
* results, and splitting is grammar (bareagent's), not litectx's to attempt. This isn't a special
|
|
155
|
+
* case: litectx's compress() is a pure text→text render that returns verbatim when handed no single
|
|
156
|
+
* parseable format (compress.js — "never returns less than the body losslessly"), so a flattened
|
|
157
|
+
* multi-result unit round-trips unchanged on both sides. RATIFIED by litectx (2026-06-12). The pairing
|
|
158
|
+
* seatbelt is the final guard.
|
|
159
|
+
* @param {Array<Record<string, any>>} units
|
|
160
|
+
* @returns {Array<Record<string, any>>}
|
|
161
|
+
*/
|
|
162
|
+
function fromUnits(units) {
|
|
163
|
+
const msgs = [];
|
|
164
|
+
for (const u of units) {
|
|
165
|
+
if (!u || typeof u !== 'object') continue;
|
|
166
|
+
const backing = u._msgs;
|
|
167
|
+
|
|
168
|
+
if (!Array.isArray(backing)) {
|
|
169
|
+
// litectx-minted (recall-inject): synthesise one message from role + content
|
|
170
|
+
msgs.push({ role: u.role || 'user', content: u.content == null ? '' : String(u.content) });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const edited = u.content !== u._origContent;
|
|
175
|
+
if (!edited) {
|
|
176
|
+
for (const m of backing) msgs.push(m); // verbatim — preserves tool_call_id pairing exactly
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!u.atomic) {
|
|
181
|
+
// single backing message, content rewritten
|
|
182
|
+
msgs.push({ ...backing[0], content: u.content == null ? '' : String(u.content) });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// atomic + content rewritten: assistant message stays verbatim (pairing); rewrite lands on the
|
|
187
|
+
// single result. Multi-result bundle → keep verbatim (unsplittable).
|
|
188
|
+
const results = backing.slice(1);
|
|
189
|
+
if (results.length === 1) {
|
|
190
|
+
msgs.push(backing[0]);
|
|
191
|
+
msgs.push({ ...results[0], content: u.content == null ? '' : String(u.content) });
|
|
192
|
+
} else {
|
|
193
|
+
for (const m of backing) msgs.push(m);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return pairingSeatbelt(msgs);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Wrap litectx's `assemble(units, ctx)` verb into the Loop's msgs-level `assemble(msgs, ctx)` seam.
|
|
201
|
+
* litectx ships the **`AssembleResult` envelope** `{ units, dropped, tokens }` (CE-PRD §8.2: `dropped[]`
|
|
202
|
+
* is load-bearing — it ships in the same slice, never silently truncated). This wrapper accepts that
|
|
203
|
+
* envelope (uses `.units`) OR a bare `units` array (a simpler consumer). `dropped`/`tokens` are litectx's
|
|
204
|
+
* accounting; the Loop's seam is msgs-in/msgs-out, so they're not threaded onward here (the canonical
|
|
205
|
+
* transcript already holds every dropped unit by id — restorable on demand).
|
|
206
|
+
* Fail-OPEN at this layer too: any other return shape → the original msgs are sent unchanged. A thrown
|
|
207
|
+
* error (incl. HaltError) is left to the Loop's own fail-open / HaltError handling — not swallowed here.
|
|
208
|
+
* @param {(units: Array<Record<string, any>>, ctx: any) => (any | Promise<any>)} assembleUnits
|
|
209
|
+
* @returns {(msgs: Array<Record<string, any>>, ctx: any) => Promise<Array<Record<string, any>>>}
|
|
210
|
+
*/
|
|
211
|
+
function unitAssembler(assembleUnits) {
|
|
212
|
+
if (typeof assembleUnits !== 'function') {
|
|
213
|
+
throw new Error('[context-units] unitAssembler(fn): fn must be (units, ctx) => units');
|
|
214
|
+
}
|
|
215
|
+
return async (msgs, ctx) => {
|
|
216
|
+
const units = toUnits(msgs);
|
|
217
|
+
const out = await assembleUnits(units, ctx);
|
|
218
|
+
// litectx returns { units, dropped, tokens }; a bare array is also accepted. Anything else → fail-open.
|
|
219
|
+
const view = Array.isArray(out) ? out : (out && Array.isArray(out.units) ? out.units : null);
|
|
220
|
+
if (!view) return msgs; // fail-open: unrecognised return → full context
|
|
221
|
+
return fromUnits(view);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = { toUnits, fromUnits, unitAssembler, approxTokens, pairingSeatbelt };
|
package/src/loop.d.ts
CHANGED
|
@@ -20,6 +20,16 @@ export type LoopOptions = {
|
|
|
20
20
|
onError?: Function | undefined;
|
|
21
21
|
throwOnError?: boolean | undefined;
|
|
22
22
|
policy?: Function | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* - async (msgs, ctx) => msgs. Context-assembly chokepoint: shape the
|
|
25
|
+
* window sent to the provider each round (e.g. a context-engineering library). Returns a VIEW — the
|
|
26
|
+
* canonical transcript is never mutated. Fail-open (a thrown error degrades to full context); a
|
|
27
|
+
* thrown HaltError propagates. `ctx` is the per-run opaque blob (`run(msgs, tools, { ctx })`), the
|
|
28
|
+
* same object forwarded to `policy`; litectx reads `ctx.task` (intent) and `ctx.budget`. The
|
|
29
|
+
* neutral-unit signature `assemble(units, ctx)` is provided by bareagent's msgs⇄units adapter
|
|
30
|
+
* (src/context-units.js), which composes over this msgs-level seam.
|
|
31
|
+
*/
|
|
32
|
+
assemble?: Function | undefined;
|
|
23
33
|
onLlmResult?: Function | undefined;
|
|
24
34
|
onToolResult?: Function | undefined;
|
|
25
35
|
/**
|
|
@@ -49,6 +59,7 @@ export class Loop {
|
|
|
49
59
|
throwOnError: boolean;
|
|
50
60
|
store: import("../types").Store | null;
|
|
51
61
|
policy: Function | null;
|
|
62
|
+
assemble: Function | null;
|
|
52
63
|
onLlmResult: Function | null;
|
|
53
64
|
onToolResult: Function | null;
|
|
54
65
|
_stopped: boolean;
|
package/src/loop.js
CHANGED
|
@@ -26,6 +26,13 @@ const { ToolError, HaltError } = require('./errors');
|
|
|
26
26
|
* @property {Function} [onError]
|
|
27
27
|
* @property {boolean} [throwOnError]
|
|
28
28
|
* @property {Function} [policy]
|
|
29
|
+
* @property {Function} [assemble] - async (msgs, ctx) => msgs. Context-assembly chokepoint: shape the
|
|
30
|
+
* window sent to the provider each round (e.g. a context-engineering library). Returns a VIEW — the
|
|
31
|
+
* canonical transcript is never mutated. Fail-open (a thrown error degrades to full context); a
|
|
32
|
+
* thrown HaltError propagates. `ctx` is the per-run opaque blob (`run(msgs, tools, { ctx })`), the
|
|
33
|
+
* same object forwarded to `policy`; litectx reads `ctx.task` (intent) and `ctx.budget`. The
|
|
34
|
+
* neutral-unit signature `assemble(units, ctx)` is provided by bareagent's msgs⇄units adapter
|
|
35
|
+
* (src/context-units.js), which composes over this msgs-level seam.
|
|
29
36
|
* @property {Function} [onLlmResult]
|
|
30
37
|
* @property {Function} [onToolResult]
|
|
31
38
|
* @property {number} [maxRounds] - Removed in v0.8; presence throws a migration error.
|
|
@@ -132,6 +139,10 @@ class Loop {
|
|
|
132
139
|
throw new Error('[Loop] options.policy must be a function (toolName, args, ctx) => true | string');
|
|
133
140
|
}
|
|
134
141
|
this.policy = options.policy || null;
|
|
142
|
+
if (options.assemble != null && typeof options.assemble !== 'function') {
|
|
143
|
+
throw new Error('[Loop] options.assemble must be a function (msgs, info) => msgs');
|
|
144
|
+
}
|
|
145
|
+
this.assemble = options.assemble || null;
|
|
135
146
|
if (options.onLlmResult != null && typeof options.onLlmResult !== 'function') {
|
|
136
147
|
throw new Error('[Loop] options.onLlmResult must be a function');
|
|
137
148
|
}
|
|
@@ -243,10 +254,29 @@ class Loop {
|
|
|
243
254
|
for (let round = 0; round < HARD_ROUND_LIMIT; round++) {
|
|
244
255
|
if (this._stopped) break;
|
|
245
256
|
|
|
257
|
+
// RT-1: context-assembly chokepoint. Let a caller (e.g. a context-engineering library) shape
|
|
258
|
+
// the window sent to the provider this round. Returns a VIEW — the canonical `msgs` transcript
|
|
259
|
+
// is never mutated, so result.msgs stays complete and correct. Fail-OPEN: an assembly error
|
|
260
|
+
// degrades to sending full context (a context-optimizer bug must not halt the agent); a thrown
|
|
261
|
+
// HaltError is a governance exit and propagates (same contract as onLlmResult).
|
|
262
|
+
let toSend = msgs;
|
|
263
|
+
if (this.assemble) {
|
|
264
|
+
try {
|
|
265
|
+
const view = await this.assemble(msgs, ctx);
|
|
266
|
+
if (Array.isArray(view)) {
|
|
267
|
+
toSend = view;
|
|
268
|
+
this._safeEmit({ type: 'loop:assemble', data: { round, before: msgs.length, after: toSend.length } });
|
|
269
|
+
}
|
|
270
|
+
} catch (err) {
|
|
271
|
+
if (err instanceof HaltError) throw err;
|
|
272
|
+
this._reportError('assemble', err, { round });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
246
276
|
let result;
|
|
247
277
|
const llmStartedAt = Date.now();
|
|
248
278
|
try {
|
|
249
|
-
const generate = () => this.provider.generate(
|
|
279
|
+
const generate = () => this.provider.generate(toSend, tools, options);
|
|
250
280
|
result = this.retry ? await this.retry.call(generate) : await generate();
|
|
251
281
|
} catch (err) {
|
|
252
282
|
this._reportError('provider', err, { round });
|
package/src/tools.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ import { createSpawnTool } from "../tools/spawn";
|
|
|
5
5
|
import { spawnChild } from "../tools/spawn";
|
|
6
6
|
import { createDeferTool } from "../tools/defer";
|
|
7
7
|
import { readQueue as readDeferQueue } from "../tools/defer";
|
|
8
|
-
|
|
8
|
+
import { liteCtxMcpBridgeConfig } from "../tools/litectx-mcp";
|
|
9
|
+
export { createBrowsingTools, createMobileTools, createShellTools, createSpawnTool, spawnChild, createDeferTool, readDeferQueue, liteCtxMcpBridgeConfig };
|
package/src/tools.js
CHANGED
|
@@ -5,6 +5,7 @@ const { createMobileTools } = require('../tools/mobile');
|
|
|
5
5
|
const { createShellTools } = require('../tools/shell');
|
|
6
6
|
const { createSpawnTool, spawnChild } = require('../tools/spawn');
|
|
7
7
|
const { createDeferTool, readQueue: readDeferQueue } = require('../tools/defer');
|
|
8
|
+
const { liteCtxMcpBridgeConfig } = require('../tools/litectx-mcp');
|
|
8
9
|
|
|
9
10
|
module.exports = {
|
|
10
11
|
createBrowsingTools,
|
|
@@ -14,4 +15,5 @@ module.exports = {
|
|
|
14
15
|
spawnChild,
|
|
15
16
|
createDeferTool,
|
|
16
17
|
readDeferQueue,
|
|
18
|
+
liteCtxMcpBridgeConfig,
|
|
17
19
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a curated bridge config that mounts litectx-mcp read-only on its own db.
|
|
3
|
+
* @param {object} opts
|
|
4
|
+
* @param {string} opts.root - the child's OWN litectx root/db (passed as `--root`). Isolation.
|
|
5
|
+
* @param {string} [opts.command='litectx-mcp'] - the server command (override for a fake/abs path).
|
|
6
|
+
* @param {string[]} [opts.args=[]] - extra args appended after `--root <root>` (e.g. `--no-embeddings`).
|
|
7
|
+
* @param {boolean} [opts.writable=false] - opt-in: allow remember/forget (still child-db-local).
|
|
8
|
+
* @param {string} [opts.name='litectx'] - server name (tool prefix → `<name>_recall`, …).
|
|
9
|
+
* @param {string} [opts.ttl='24h'] - bridge config TTL.
|
|
10
|
+
* @param {string} [opts.now] - ISO timestamp for `discovered` (default: now). Pre-seed fresh so
|
|
11
|
+
* `createMCPBridge` skips IDE discovery and connects straight to this curated server.
|
|
12
|
+
* @returns {import('../src/mcp-bridge').BridgeConfig}
|
|
13
|
+
*/
|
|
14
|
+
export function liteCtxMcpBridgeConfig(opts: {
|
|
15
|
+
root: string;
|
|
16
|
+
command?: string | undefined;
|
|
17
|
+
args?: string[] | undefined;
|
|
18
|
+
writable?: boolean | undefined;
|
|
19
|
+
name?: string | undefined;
|
|
20
|
+
ttl?: string | undefined;
|
|
21
|
+
now?: string | undefined;
|
|
22
|
+
}): import("../src/mcp-bridge").BridgeConfig;
|
|
23
|
+
/** Read/reason verbs a child is given by default. */
|
|
24
|
+
export const READ_VERBS: string[];
|
|
25
|
+
/** Write verbs — denied unless `writable`, then allowed (writes still land in the child's OWN db). */
|
|
26
|
+
export const WRITE_VERBS: string[];
|
|
27
|
+
/** Admin/review verbs — always denied to a child (human/hook + review flows). */
|
|
28
|
+
export const ADMIN_VERBS: string[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// RT-4 — mount litectx-mcp into a child agent, read-only, on its own db. A thin recipe helper: it
|
|
4
|
+
// builds the curated `.mcp-bridge.json` config that `createMCPBridge` consumes, so a parent composing
|
|
5
|
+
// a child's toolbox can't fat-finger the allow-list (e.g. accidentally allow a write verb). It encodes
|
|
6
|
+
// ONE thing — the agreed read-only default (PRD §4.2) — and knows only litectx-mcp's public verb names
|
|
7
|
+
// (the same strings a hand-written `.mcp-bridge.json` would carry). It does NOT import litectx: the
|
|
8
|
+
// dependency direction stays one-way; this is config curation, bareagent's job.
|
|
9
|
+
//
|
|
10
|
+
// Default toolbox (PRD §4.2):
|
|
11
|
+
// recall · get · impact · recent → allow (read / reason — the point of giving a child memory)
|
|
12
|
+
// remember · forget → deny (agent writes are by:"agent", suspect-until-curated;
|
|
13
|
+
// flip with { writable: true } as the explicit opt-in)
|
|
14
|
+
// index · promotions → deny (index is human/hook-driven; promotions is a review flow)
|
|
15
|
+
//
|
|
16
|
+
// Isolation is OWN-DB, not a scope column (this is what decouples RT-4 from RT-5): the child gets its
|
|
17
|
+
// own `--root` → physical isolation, zero schema change. An opted-in child's writes land in ITS db;
|
|
18
|
+
// promotion to the parent is an explicit parent-side `recall`→`remember`, never automatic.
|
|
19
|
+
|
|
20
|
+
/** Read/reason verbs a child is given by default. */
|
|
21
|
+
const READ_VERBS = ['recall', 'get', 'impact', 'recent'];
|
|
22
|
+
/** Write verbs — denied unless `writable`, then allowed (writes still land in the child's OWN db). */
|
|
23
|
+
const WRITE_VERBS = ['remember', 'forget'];
|
|
24
|
+
/** Admin/review verbs — always denied to a child (human/hook + review flows). */
|
|
25
|
+
const ADMIN_VERBS = ['index', 'promotions'];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build a curated bridge config that mounts litectx-mcp read-only on its own db.
|
|
29
|
+
* @param {object} opts
|
|
30
|
+
* @param {string} opts.root - the child's OWN litectx root/db (passed as `--root`). Isolation.
|
|
31
|
+
* @param {string} [opts.command='litectx-mcp'] - the server command (override for a fake/abs path).
|
|
32
|
+
* @param {string[]} [opts.args=[]] - extra args appended after `--root <root>` (e.g. `--no-embeddings`).
|
|
33
|
+
* @param {boolean} [opts.writable=false] - opt-in: allow remember/forget (still child-db-local).
|
|
34
|
+
* @param {string} [opts.name='litectx'] - server name (tool prefix → `<name>_recall`, …).
|
|
35
|
+
* @param {string} [opts.ttl='24h'] - bridge config TTL.
|
|
36
|
+
* @param {string} [opts.now] - ISO timestamp for `discovered` (default: now). Pre-seed fresh so
|
|
37
|
+
* `createMCPBridge` skips IDE discovery and connects straight to this curated server.
|
|
38
|
+
* @returns {import('../src/mcp-bridge').BridgeConfig}
|
|
39
|
+
*/
|
|
40
|
+
function liteCtxMcpBridgeConfig(opts) {
|
|
41
|
+
if (!opts || typeof opts.root !== 'string' || !opts.root) {
|
|
42
|
+
throw new Error('[litectx-mcp] requires opts.root (the child\'s own db root — own-db isolation)');
|
|
43
|
+
}
|
|
44
|
+
const { root, command = 'litectx-mcp', args = [], writable = false, name = 'litectx', ttl = '24h', now } = opts;
|
|
45
|
+
|
|
46
|
+
/** @type {Record<string, string>} */
|
|
47
|
+
const tools = {};
|
|
48
|
+
for (const v of READ_VERBS) tools[v] = 'allow';
|
|
49
|
+
for (const v of WRITE_VERBS) tools[v] = writable ? 'allow' : 'deny';
|
|
50
|
+
for (const v of ADMIN_VERBS) tools[v] = 'deny';
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
discovered: now || new Date().toISOString(),
|
|
54
|
+
ttl,
|
|
55
|
+
servers: {
|
|
56
|
+
[name]: {
|
|
57
|
+
command,
|
|
58
|
+
args: ['--root', root, ...args],
|
|
59
|
+
tools,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { liteCtxMcpBridgeConfig, READ_VERBS, WRITE_VERBS, ADMIN_VERBS };
|