bare-agent 0.13.1 → 0.14.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 +1 -1
- package/bareagent.context.md +2 -2
- package/package.json +2 -2
- package/src/loop.d.ts +10 -1
- package/src/loop.js +100 -2
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ Every piece works alone — take what you need, ignore the rest.
|
|
|
66
66
|
|
|
67
67
|
| Component | What it does |
|
|
68
68
|
|---|---|
|
|
69
|
-
| **Loop** | Think → act → observe → repeat. Calls any LLM, executes your tools, loops until done. Returns estimated USD cost per run. Governance via `Loop({ policy })` — wire bareguard's `Gate` through `wireGate(gate)` and every tool call (native, MCP, browsing, mobile) traverses one chokepoint with per-caller `ctx` routing. Bareguard owns the audit log, budget caps, and halt decisions; Loop respects the verdict. Context engineering via `Loop({ assemble })` — a per-round `assemble(msgs, ctx)` chokepoint to recall/compress/trim the window sent to the model (the seam litectx plugs into); returns a view, the canonical transcript stays intact, fail-open. The exported `unitAssembler`/`toUnits`/`fromUnits` adapter lets a consumer work over a neutral unit `{id, role, content, kind, pinned, atomic, tokensApprox}` — bareagent owns the grammar (atomic tool-pair bundling, pinned system/task, a pairing seatbelt), the consumer owns content + relevance. The CE function reads its inputs from the per-run `ctx` — litectx's budget-fitter uses `ctx.budget` (and `ctx.task`), so you **must** populate it via `run(msgs, tools, { ctx })`: an unset `ctx.budget` means the fitter has no budget, keeps everything, and returns the window unchanged — a silent no-op, not a bug (see `examples/litectx-assemble.mjs`). `onError` + `loop:error` surface every silent-ish failure (callback throw, Checkpoint timeout) |
|
|
69
|
+
| **Loop** | Think → act → observe → repeat. Calls any LLM, executes your tools, loops until done. Returns estimated USD cost per run. Governance via `Loop({ policy })` — wire bareguard's `Gate` through `wireGate(gate)` and every tool call (native, MCP, browsing, mobile) traverses one chokepoint with per-caller `ctx` routing. Bareguard owns the audit log, budget caps, and halt decisions; Loop respects the verdict. Context engineering via `Loop({ assemble })` — a per-round `assemble(msgs, ctx)` chokepoint to recall/compress/trim the window sent to the model (the seam litectx plugs into); returns a view, the canonical transcript stays intact, fail-open. The exported `unitAssembler`/`toUnits`/`fromUnits` adapter lets a consumer work over a neutral unit `{id, role, content, kind, pinned, atomic, tokensApprox}` — bareagent owns the grammar (atomic tool-pair bundling, pinned system/task, a pairing seatbelt), the consumer owns content + relevance. The CE function reads its inputs from the per-run `ctx` — litectx's budget-fitter uses `ctx.budget` (and `ctx.task`), so you **must** populate it via `run(msgs, tools, { ctx })`: an unset `ctx.budget` means the fitter has no budget, keeps everything, and returns the window unchanged — a silent no-op, not a bug (see `examples/litectx-assemble.mjs`). For summary-window compaction the Loop also lends a provider-bound `ctx.summarize(excerpt) => Promise<string>` (R-C6): the consumer owns when/what to summarize and the splice, bareagent makes the one model call (counted against the budget via `onLlmResult`, tagged `kind:'summarize'`). `onError` + `loop:error` surface every silent-ish failure (callback throw, Checkpoint timeout) |
|
|
70
70
|
| **Planner** | Break a goal into a step DAG via LLM. Built-in caching (`cacheTTL`) |
|
|
71
71
|
| **runPlan** | Execute steps in parallel waves. Dependency-aware, failure propagation, per-step retry |
|
|
72
72
|
| **Retry** | Exponential/linear backoff with jitter. Respects `err.retryable` |
|
package/bareagent.context.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# bareagent — Integration Guide
|
|
2
2
|
|
|
3
3
|
> For AI assistants and developers wiring bareagent into a project.
|
|
4
|
-
> v0.
|
|
4
|
+
> v0.14.0 | Node.js >= 18 | one required dep (`bareguard ^0.4.2`) | Apache 2.0
|
|
5
5
|
>
|
|
6
6
|
> Full human guide with composition examples, design philosophy, and recipes: [Usage Guide](docs/02-features/usage-guide.md)
|
|
7
7
|
|
|
@@ -306,7 +306,7 @@ if (result.error?.startsWith('halt:')) {
|
|
|
306
306
|
}
|
|
307
307
|
```
|
|
308
308
|
|
|
309
|
-
**Why four pieces (`policy` + `onLlmResult` + `onToolResult` + `filterTools`).** `policy` runs `gate.check` *before* every tool call. `onLlmResult` fires after every successful `provider.generate` — without it, `budget.maxCostUsd` never sees LLM cost and is silently undercounted for token-heavy / tool-light workloads (every chatbot). `onToolResult` fires after every `tool.execute` and carries the per-run `ctx` opaque blob into `gate.record` so per-principal accounting works. `filterTools` is a `gate.allows` pre-filter — denied tools are dropped from the catalog the LLM ever sees, no `gate.check` round-trip per call.
|
|
309
|
+
**Why four pieces (`policy` + `onLlmResult` + `onToolResult` + `filterTools`).** `policy` runs `gate.check` *before* every tool call. `onLlmResult` fires after every successful `provider.generate` — without it, `budget.maxCostUsd` never sees LLM cost and is silently undercounted for token-heavy / tool-light workloads (every chatbot). It also fires for the out-of-band `ctx.summarize` call (R-C6) tagged `kind:'summarize'`; main-loop rounds carry `kind:'turn'` — so summary-window tokens count against the budget too, and a consumer can tell the two apart. `onToolResult` fires after every `tool.execute` and carries the per-run `ctx` opaque blob into `gate.record` so per-principal accounting works. `filterTools` is a `gate.allows` pre-filter — denied tools are dropped from the catalog the LLM ever sees, no `gate.check` round-trip per call.
|
|
310
310
|
|
|
311
311
|
Halt-severity decisions exit the loop cleanly via a typed `HaltError` — full mechanics (sealed `msgs`, `halt:<rule>` error token, `loop:done{halted:true}` event, `throwOnError:true` interaction, `halt:unknown` coalesce) are in the **Halt decisions throw `HaltError`** paragraph below. Short version: check `result.error?.startsWith('halt:')` after the run.
|
|
312
312
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bare-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"files": [
|
|
5
5
|
"index.js",
|
|
6
6
|
"index.d.ts",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
},
|
|
100
100
|
"devDependencies": {
|
|
101
101
|
"@types/node": "^22.19.19",
|
|
102
|
-
"litectx": "^0.
|
|
102
|
+
"litectx": "^0.13.0",
|
|
103
103
|
"typescript": "^5.7.0"
|
|
104
104
|
}
|
|
105
105
|
}
|
package/src/loop.d.ts
CHANGED
|
@@ -27,9 +27,18 @@ export type LoopOptions = {
|
|
|
27
27
|
* thrown HaltError propagates. `ctx` is the per-run opaque blob (`run(msgs, tools, { ctx })`), the
|
|
28
28
|
* same object forwarded to `policy`; litectx reads `ctx.task` (intent) and `ctx.budget`. The
|
|
29
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.
|
|
30
|
+
* (src/context-units.js), which composes over this msgs-level seam. When `ctx` is an object, the
|
|
31
|
+
* Loop also lends a provider-bound `ctx.summarize(excerpt, opts?) => Promise<string>` (R-C6,
|
|
32
|
+
* non-enumerable): assemble calls it to roll a summary window — bareagent makes the one model
|
|
33
|
+
* call, the consumer owns the trigger/N/splice. Its usage is forwarded to `onLlmResult` so the
|
|
34
|
+
* summary tokens count against the budget.
|
|
31
35
|
*/
|
|
32
36
|
assemble?: Function | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* - async (event) => void after each LLM call; forwards usage to
|
|
39
|
+
* gate.record (via wireGate). `event.kind` discriminates the source: `'turn'` for a main-loop round,
|
|
40
|
+
* `'summarize'` for an out-of-band `ctx.summarize` call (R-C6). Both count against the budget.
|
|
41
|
+
*/
|
|
33
42
|
onLlmResult?: Function | undefined;
|
|
34
43
|
onToolResult?: Function | undefined;
|
|
35
44
|
/**
|
package/src/loop.js
CHANGED
|
@@ -32,8 +32,14 @@ const { ToolError, HaltError } = require('./errors');
|
|
|
32
32
|
* thrown HaltError propagates. `ctx` is the per-run opaque blob (`run(msgs, tools, { ctx })`), the
|
|
33
33
|
* same object forwarded to `policy`; litectx reads `ctx.task` (intent) and `ctx.budget`. The
|
|
34
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.
|
|
36
|
-
*
|
|
35
|
+
* (src/context-units.js), which composes over this msgs-level seam. When `ctx` is an object, the
|
|
36
|
+
* Loop also lends a provider-bound `ctx.summarize(excerpt, opts?) => Promise<string>` (R-C6,
|
|
37
|
+
* non-enumerable): assemble calls it to roll a summary window — bareagent makes the one model
|
|
38
|
+
* call, the consumer owns the trigger/N/splice. Its usage is forwarded to `onLlmResult` so the
|
|
39
|
+
* summary tokens count against the budget.
|
|
40
|
+
* @property {Function} [onLlmResult] - async (event) => void after each LLM call; forwards usage to
|
|
41
|
+
* gate.record (via wireGate). `event.kind` discriminates the source: `'turn'` for a main-loop round,
|
|
42
|
+
* `'summarize'` for an out-of-band `ctx.summarize` call (R-C6). Both count against the budget.
|
|
37
43
|
* @property {Function} [onToolResult]
|
|
38
44
|
* @property {number} [maxRounds] - Removed in v0.8; presence throws a migration error.
|
|
39
45
|
*/
|
|
@@ -105,6 +111,38 @@ function estimateCost(model, usage) {
|
|
|
105
111
|
);
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
// R-C6: default instruction for the provider-bound `ctx.summarize` lent to the assemble seam.
|
|
115
|
+
const DEFAULT_SUMMARY_INSTRUCTION =
|
|
116
|
+
'You are a precise conversation summarizer. Produce a concise, factual summary of the following ' +
|
|
117
|
+
'conversation excerpt. Preserve concrete facts, decisions, and identifiers (names, ids, file ' +
|
|
118
|
+
'paths, numbers), and note any open or unresolved threads. Do not invent information. Output ' +
|
|
119
|
+
'only the summary prose, with no preamble.';
|
|
120
|
+
|
|
121
|
+
// Flatten an excerpt (array of OpenAI-format messages, or a raw string) into one prose block for the
|
|
122
|
+
// summarizer's single user turn. Rendering to text — rather than forwarding raw messages — sidesteps
|
|
123
|
+
// tool-call/result pairing entirely: a summary input never needs to be a valid wire transcript.
|
|
124
|
+
/**
|
|
125
|
+
* @param {Array<any>|string|null|undefined} excerpt
|
|
126
|
+
* @returns {string}
|
|
127
|
+
*/
|
|
128
|
+
function renderForSummary(excerpt) {
|
|
129
|
+
if (excerpt == null) return '';
|
|
130
|
+
if (typeof excerpt === 'string') return excerpt;
|
|
131
|
+
if (!Array.isArray(excerpt)) return String(excerpt);
|
|
132
|
+
const parts = [];
|
|
133
|
+
for (const m of excerpt) {
|
|
134
|
+
if (m == null) continue;
|
|
135
|
+
if (typeof m === 'string') { parts.push(m); continue; }
|
|
136
|
+
const role = m.role || 'message';
|
|
137
|
+
let text = m.content != null ? String(m.content) : '';
|
|
138
|
+
if (Array.isArray(m.tool_calls) && m.tool_calls.length) {
|
|
139
|
+
text += (text ? '\n' : '') + `[tool_calls: ${JSON.stringify(m.tool_calls)}]`;
|
|
140
|
+
}
|
|
141
|
+
parts.push(`${role}: ${text}`);
|
|
142
|
+
}
|
|
143
|
+
return parts.join('\n\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
108
146
|
class Loop {
|
|
109
147
|
/**
|
|
110
148
|
* `policy` is async `(toolName, args, ctx) => true | string`. Recommended wiring: a closure
|
|
@@ -250,6 +288,65 @@ class Loop {
|
|
|
250
288
|
let lastUsage = { inputTokens: 0, outputTokens: 0 };
|
|
251
289
|
let totalCost = 0;
|
|
252
290
|
|
|
291
|
+
// R-C6: lend a provider-bound summarizer to the assemble seam via `ctx.summarize`. litectx owns
|
|
292
|
+
// the trigger/N/splice (its restorable COMPRESS path keeps summarized turns recoverable by id);
|
|
293
|
+
// bareagent lends ONLY the single model call. Attached NON-ENUMERABLE so it never shows up in the
|
|
294
|
+
// caller's ctx via JSON/iteration/deepEqual — preserving the `assemble(units, ctx)` identity
|
|
295
|
+
// contract (test/loop-assemble.test.js). `summarize(excerpt, opts?) => Promise<string>`:
|
|
296
|
+
// excerpt — array of OpenAI-format messages (or a raw string) litectx wants compressed
|
|
297
|
+
// opts — { instruction?, ...generateOpts } (instruction overrides the default; the rest pass
|
|
298
|
+
// through to provider.generate; temperature defaults to 0 for determinism)
|
|
299
|
+
// The summary call's usage is forwarded to onLlmResult so its tokens count against the budget
|
|
300
|
+
// (BA1 lineage — token-only flows must not be invisible to the gate); a HaltError there is a
|
|
301
|
+
// governance exit and propagates, matching the main-loop onLlmResult contract.
|
|
302
|
+
if (ctx && typeof ctx === 'object') {
|
|
303
|
+
const loop = this;
|
|
304
|
+
/**
|
|
305
|
+
* @param {Array<any>|string} excerpt
|
|
306
|
+
* @param {Record<string, any>} [opts]
|
|
307
|
+
* @returns {Promise<string>}
|
|
308
|
+
*/
|
|
309
|
+
const summarize = async (excerpt, opts = {}) => {
|
|
310
|
+
const { instruction, ...genOpts } = opts || {};
|
|
311
|
+
const prompt = [
|
|
312
|
+
{ role: 'system', content: instruction || DEFAULT_SUMMARY_INSTRUCTION },
|
|
313
|
+
{ role: 'user', content: renderForSummary(excerpt) },
|
|
314
|
+
];
|
|
315
|
+
const startedAt = Date.now();
|
|
316
|
+
const result = await loop.provider.generate(prompt, [], { temperature: 0, ...genOpts });
|
|
317
|
+
const usage = (result && result.usage) || null;
|
|
318
|
+
const model = loop.provider.model || null;
|
|
319
|
+
const cost = estimateCost(model, usage);
|
|
320
|
+
if (cost !== null) totalCost += cost;
|
|
321
|
+
loop._safeEmit({ type: 'loop:summarize', data: { usage, costUsd: cost, durationMs: Date.now() - startedAt } });
|
|
322
|
+
if (loop.onLlmResult) {
|
|
323
|
+
try {
|
|
324
|
+
await loop.onLlmResult({
|
|
325
|
+
model,
|
|
326
|
+
provider: loop.provider.name || null,
|
|
327
|
+
usage,
|
|
328
|
+
costUsd: cost,
|
|
329
|
+
durationMs: Date.now() - startedAt,
|
|
330
|
+
ctx,
|
|
331
|
+
kind: 'summarize',
|
|
332
|
+
});
|
|
333
|
+
} catch (err) {
|
|
334
|
+
if (err instanceof HaltError) throw err;
|
|
335
|
+
loop._reportError('onLlmResult', err, { phase: 'summarize' });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return (result && result.text) || '';
|
|
339
|
+
};
|
|
340
|
+
// Fail-OPEN to match the assemble seam's own contract: a frozen / sealed / non-configurable ctx
|
|
341
|
+
// must NOT crash the agent. On failure the seam is simply unavailable (consumers already handle
|
|
342
|
+
// ctx.summarize being absent — it only exists when ctx is an object), reported, never silent.
|
|
343
|
+
try {
|
|
344
|
+
Object.defineProperty(ctx, 'summarize', { value: summarize, enumerable: false, configurable: true, writable: true });
|
|
345
|
+
} catch (err) {
|
|
346
|
+
this._reportError('summarize-attach', err);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
253
350
|
try {
|
|
254
351
|
for (let round = 0; round < HARD_ROUND_LIMIT; round++) {
|
|
255
352
|
if (this._stopped) break;
|
|
@@ -301,6 +398,7 @@ class Loop {
|
|
|
301
398
|
costUsd: roundCost,
|
|
302
399
|
durationMs: Date.now() - llmStartedAt,
|
|
303
400
|
ctx,
|
|
401
|
+
kind: 'turn',
|
|
304
402
|
});
|
|
305
403
|
} catch (err) {
|
|
306
404
|
if (err instanceof HaltError) throw err;
|