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 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` |
@@ -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.13.1 | Node.js >= 18 | one required dep (`bareguard ^0.4.2`) | Apache 2.0
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.13.1",
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.11.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
- * @property {Function} [onLlmResult]
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;