bare-agent 0.25.0 → 0.26.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.
@@ -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.25.0 | Node.js >= 18 | zero required deps (`bareguard ^0.9.0` optional peer for governance) | Apache 2.0
4
+ > v0.26.0 | Node.js >= 18 | zero required deps (`bareguard ^0.9.0` optional peer for governance) | 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
 
@@ -59,6 +59,7 @@ Eight entry points:
59
59
  | Catch typed errors programmatically | ProviderError, ToolError, TimeoutError, CircuitOpenError |
60
60
  | Cache identical planner calls | Planner({ cacheTTL: 60000 }) |
61
61
  | Stream CLIPipe output in real-time | CLIPipeProvider({ onChunk: fn }) |
62
+ | Get real usage + cost from a CLI provider | CLIPipeProvider({ parse: 'claude-json' }) |
62
63
  | Browse the web (inline snapshots) | createBrowsingTools + Loop |
63
64
  | Browse the web (token-efficient, disk-based) | `barebrowse` CLI session — snapshots to `.barebrowse/*.yml` |
64
65
  | Assess website privacy risk | createBrowsingTools + Loop (requires `npm install wearehere`) |
@@ -739,9 +740,11 @@ new Ollama({ model: 'llama3.2', url: 'http://localhost:11434' })
739
740
  // CLIPipe — pipe prompts to any CLI tool via stdin/stdout
740
741
  new CLIPipe({ command: 'claude', args: ['--print'], systemPromptFlag: '--system-prompt', timeout: 30000 })
741
742
  new CLIPipe({ command: 'ollama', args: ['run', 'llama3.2'] })
743
+ // CLIPipe structured output (v0.26.0+) — map a CLI's JSON envelope to real usage + cost
744
+ new CLIPipe({ command: 'claude', args: ['-p', '--output-format', 'json'], parse: 'claude-json' })
742
745
  ```
743
746
 
744
- All return `{ text, toolCalls, usage: { inputTokens, outputTokens }, model? }`. The optional `model` (v0.16.1+) is the id the response was produced by — Loop prefers it over `provider.model` for cost accounting. CLIPipe always returns `toolCalls: []` and zero usage (CLI tools don't report tokens), and omits `model`.
747
+ All return `{ text, toolCalls, usage: { inputTokens, outputTokens }, model?, costUsd? }`. The optional `model` (v0.16.1+) is the id the response was produced by — Loop prefers it over `provider.model` for cost accounting. By default CLIPipe returns `toolCalls: []` and zero usage (CLI tools don't report tokens) and omits `model`. **Structured output (v0.26.0+):** set `parse: 'claude-json'` (a preset for `claude -p --output-format json`) — or a `(stdout) => Partial<GenerateResult>` function for any other CLI — and CLIPipe maps the CLI's JSON envelope onto real `usage`, `model`, and `costUsd`, throwing `ProviderError` on a malformed/error envelope (never a silent raw-text fall-back). `costUsd` (optional `GenerateResult` field) is an **authoritative** per-call price the provider reports itself; when finite the Loop prefers it over the internal rate-table `estimateCost`, so a CLI-piped run enforces a bareguard USD cap with no local pricing table (a `0` counts as priced, distinct from null/unpriced). `toolCalls` stays `[]` regardless (CLIPipe is tool-free).
745
748
 
746
749
  **Temperature graceful degradation (BA-10).** Newer models reject ANY non-default `temperature` with a `400` (`claude-sonnet-5`: `` `temperature` is deprecated for this model. ``; OpenAI o1/gpt-5-class: `Unsupported value: 'temperature' … Only the default (1) …`). All four providers detect that specific 400 (message names `temperature` as unsupported/deprecated AND a temperature was sent), **drop the param, warn once per instance, and retry once** — so a call that would otherwise throw succeeds at the model's default temperature. Keyed off the API error text, not a model list. A genuine out-of-range 400 is NOT degraded (it re-throws — dropping it would mask a caller bug). When a drop happens the result carries `temperatureDropped: true` (an optional `GenerateResult`/`Loop.run` field) so a caller can report the effective temperature — `recurse`'s `refineLeaf` uses it for an honest receipt. Dormant on models that accept temperature (byte-identical to before).
747
750
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-agent",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "files": [
5
5
  "index.js",
6
6
  "index.d.ts",
package/src/loop.js CHANGED
@@ -153,6 +153,23 @@ function estimateCost(model, usage) {
153
153
  return Number.isFinite(cost) ? cost : null;
154
154
  }
155
155
 
156
+ /**
157
+ * Resolve the priced USD for a round. A provider MAY report its own authoritative `costUsd` on the
158
+ * GenerateResult (e.g. CLIPipeProvider `parse:'claude-json'` surfacing the claude CLI's own
159
+ * `total_cost_usd` — a real price with NO local rate table). When present as a FINITE number it wins
160
+ * over the rate-table estimate — including `0`, a valid priced value (a subscription/marginal-$0 run),
161
+ * which stays 'priced', never demoted to the null/unpriced sentinel. A non-finite provider cost
162
+ * (±Inf/NaN) is NOT a price → fall through to estimateCost (same couldn't-price guard as above).
163
+ * @param {any} result - the GenerateResult from provider.generate()
164
+ * @param {string|null} model
165
+ * @param {Usage|null} usage
166
+ * @returns {number|null}
167
+ */
168
+ function resolveRoundCost(result, model, usage) {
169
+ if (result && Number.isFinite(result.costUsd)) return result.costUsd;
170
+ return estimateCost(model, usage);
171
+ }
172
+
156
173
  // R-C6: default instruction for the provider-bound `ctx.summarize` lent to the assemble seam.
157
174
  const DEFAULT_SUMMARY_INSTRUCTION =
158
175
  'You are a precise conversation summarizer. Produce a concise, factual summary of the following ' +
@@ -472,7 +489,7 @@ class Loop {
472
489
  const result = await loop.provider.generate(prompt, [], { temperature: 0, ...genOpts });
473
490
  const usage = (result && result.usage) || null;
474
491
  const model = (result && result.model) || loop.provider.model || null;
475
- const cost = estimateCost(model, usage);
492
+ const cost = resolveRoundCost(result, model, usage);
476
493
  if (cost !== null) { totalCost += cost; pricedAny = true; }
477
494
  addUsage(usage); // summarize tokens are real spend → count them in the cumulative meter
478
495
  metrics.context.summaries++; // §3.6 CE-activity rollup
@@ -616,7 +633,7 @@ class Loop {
616
633
  // Prefer the model the response reports (robust when provider.model is absent or varies per
617
634
  // response — e.g. FallbackProvider, or a CircuitBreaker-wrapped provider that drops .model).
618
635
  const model = result.model || this.provider.model || null;
619
- const roundCost = estimateCost(model, lastUsage);
636
+ const roundCost = resolveRoundCost(result, model, lastUsage);
620
637
  if (roundCost !== null) totalCost += roundCost;
621
638
 
622
639
  // Meter this round: count the turn, accumulate the four token tiers, and classify pricing —
@@ -30,6 +30,10 @@ export type CLIPipeOptions = {
30
30
  * - Called with each stdout chunk as it streams.
31
31
  */
32
32
  onChunk?: ((chunk: string) => void) | undefined;
33
+ /**
34
+ * - Opt-in structured-output parser for stdout. Default (unset) returns stdout verbatim as `text` with zero usage (no behavior change). `'claude-json'` is a shipped preset for `claude -p --output-format json`: it maps the CLI's result envelope onto `GenerateResult` (text←`result`, usage←`usage.*`, model←first `modelUsage` key, costUsd←`total_cost_usd`) and throws `ProviderError` on malformed JSON or an error envelope (`is_error`/non-success subtype). A function is the CLI-agnostic escape hatch: it receives trimmed stdout and returns a partial `GenerateResult` (merged over defaults); throw to signal a parse failure.
35
+ */
36
+ parse?: "claude-json" | ((stdout: string) => Partial<GenerateResult>) | undefined;
33
37
  };
34
38
  /** @typedef {import('../types').Message} Message */
35
39
  /** @typedef {import('../types').ToolDef} ToolDef */
@@ -43,6 +47,7 @@ export type CLIPipeOptions = {
43
47
  * @property {number} [timeout=30000] - Timeout in milliseconds.
44
48
  * @property {string} [systemPromptFlag] - CLI flag for system prompt (e.g. '--system'). When set, system messages are extracted and passed via this flag instead of stdin.
45
49
  * @property {(chunk: string) => void} [onChunk] - Called with each stdout chunk as it streams.
50
+ * @property {'claude-json'|((stdout: string) => Partial<GenerateResult>)} [parse] - Opt-in structured-output parser for stdout. Default (unset) returns stdout verbatim as `text` with zero usage (no behavior change). `'claude-json'` is a shipped preset for `claude -p --output-format json`: it maps the CLI's result envelope onto `GenerateResult` (text←`result`, usage←`usage.*`, model←first `modelUsage` key, costUsd←`total_cost_usd`) and throws `ProviderError` on malformed JSON or an error envelope (`is_error`/non-success subtype). A function is the CLI-agnostic escape hatch: it receives trimmed stdout and returns a partial `GenerateResult` (merged over defaults); throw to signal a parse failure.
46
51
  */
47
52
  export class CLIPipeProvider {
48
53
  /**
@@ -58,6 +63,7 @@ export class CLIPipeProvider {
58
63
  timeout: number;
59
64
  systemPromptFlag: string | null;
60
65
  onChunk: ((chunk: string) => void) | null;
66
+ parse: "claude-json" | ((stdout: string) => Partial<GenerateResult>) | null;
61
67
  /**
62
68
  * Generate a response by piping messages to the CLI command.
63
69
  * @param {Message[]} messages - Conversation messages in OpenAI format.
@@ -70,6 +76,15 @@ export class CLIPipeProvider {
70
76
  * @throws {Error} `[CLIPipeProvider] process produced no output` — when stdout is empty.
71
77
  */
72
78
  generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
79
+ /**
80
+ * Map the `claude -p --output-format json` result envelope onto a normalized GenerateResult.
81
+ * The caller explicitly opted into structured output, so a malformed or error envelope is a LOUD
82
+ * ProviderError — never a silent fall-back to raw text.
83
+ * @param {string} stdout - Trimmed stdout from the CLI.
84
+ * @returns {GenerateResult}
85
+ * @throws {ProviderError} On non-JSON stdout, or an error envelope (`is_error` / non-success subtype).
86
+ */
87
+ _parseClaudeJson(stdout: string): GenerateResult;
73
88
  /**
74
89
  * Convert OpenAI-format messages to a plain text prompt.
75
90
  * @param {Message[]} messages
@@ -16,6 +16,7 @@ const { ProviderError } = require('./errors');
16
16
  * @property {number} [timeout=30000] - Timeout in milliseconds.
17
17
  * @property {string} [systemPromptFlag] - CLI flag for system prompt (e.g. '--system'). When set, system messages are extracted and passed via this flag instead of stdin.
18
18
  * @property {(chunk: string) => void} [onChunk] - Called with each stdout chunk as it streams.
19
+ * @property {'claude-json'|((stdout: string) => Partial<GenerateResult>)} [parse] - Opt-in structured-output parser for stdout. Default (unset) returns stdout verbatim as `text` with zero usage (no behavior change). `'claude-json'` is a shipped preset for `claude -p --output-format json`: it maps the CLI's result envelope onto `GenerateResult` (text←`result`, usage←`usage.*`, model←first `modelUsage` key, costUsd←`total_cost_usd`) and throws `ProviderError` on malformed JSON or an error envelope (`is_error`/non-success subtype). A function is the CLI-agnostic escape hatch: it receives trimmed stdout and returns a partial `GenerateResult` (merged over defaults); throw to signal a parse failure.
19
20
  */
20
21
 
21
22
  class CLIPipeProvider {
@@ -33,6 +34,10 @@ class CLIPipeProvider {
33
34
  this.timeout = options.timeout ?? 30000;
34
35
  this.systemPromptFlag = options.systemPromptFlag || null;
35
36
  this.onChunk = options.onChunk || null;
37
+ if (options.parse != null && options.parse !== 'claude-json' && typeof options.parse !== 'function') {
38
+ throw new Error("[CLIPipeProvider] options.parse must be 'claude-json' or a function");
39
+ }
40
+ this.parse = options.parse || null;
36
41
  }
37
42
 
38
43
  /**
@@ -61,14 +66,77 @@ class CLIPipeProvider {
61
66
  }
62
67
 
63
68
  const prompt = this._formatPrompt(promptMessages);
64
- const text = await this._spawn(prompt, extraArgs);
69
+ const stdout = await this._spawn(prompt, extraArgs);
70
+
71
+ if (this.parse === 'claude-json') return this._parseClaudeJson(stdout);
72
+ if (typeof this.parse === 'function') {
73
+ const partial = this.parse(stdout) || {};
74
+ return {
75
+ text: '',
76
+ toolCalls: [],
77
+ ...partial,
78
+ usage: { inputTokens: 0, outputTokens: 0, ...(partial.usage || {}) },
79
+ };
80
+ }
65
81
  return {
66
- text,
82
+ text: stdout,
67
83
  toolCalls: [],
68
84
  usage: { inputTokens: 0, outputTokens: 0 },
69
85
  };
70
86
  }
71
87
 
88
+ /**
89
+ * Map the `claude -p --output-format json` result envelope onto a normalized GenerateResult.
90
+ * The caller explicitly opted into structured output, so a malformed or error envelope is a LOUD
91
+ * ProviderError — never a silent fall-back to raw text.
92
+ * @param {string} stdout - Trimmed stdout from the CLI.
93
+ * @returns {GenerateResult}
94
+ * @throws {ProviderError} On non-JSON stdout, or an error envelope (`is_error` / non-success subtype).
95
+ */
96
+ _parseClaudeJson(stdout) {
97
+ let obj;
98
+ try {
99
+ obj = JSON.parse(stdout);
100
+ } catch (_) {
101
+ const preview = stdout.length > 200 ? `${stdout.slice(0, 200)}…` : stdout;
102
+ throw new ProviderError(`[CLIPipeProvider] parse:'claude-json' expected JSON on stdout, got: ${preview}`, /** @type {any} */ ({ status: 0 }));
103
+ }
104
+ if (!obj || typeof obj !== 'object') {
105
+ throw new ProviderError(`[CLIPipeProvider] parse:'claude-json' expected a JSON object, got ${obj === null ? 'null' : typeof obj}`, /** @type {any} */ ({ status: 0 }));
106
+ }
107
+ if (obj.is_error === true || obj.subtype !== 'success') {
108
+ const detail = typeof obj.result === 'string' ? obj.result : JSON.stringify(obj.result ?? null);
109
+ throw new ProviderError(`[CLIPipeProvider] claude CLI reported failure (subtype='${obj.subtype}'): ${detail}`, /** @type {any} */ ({ status: 0 }));
110
+ }
111
+
112
+ const u = (obj.usage && typeof obj.usage === 'object') ? obj.usage : {};
113
+ /** @type {import('../types').Usage} */
114
+ const usage = {
115
+ inputTokens: Number(u.input_tokens) || 0,
116
+ outputTokens: Number(u.output_tokens) || 0,
117
+ };
118
+ // Absent cache tiers mean the model didn't cache — omit rather than emit a synthetic 0 (per Usage docs).
119
+ if (Number.isFinite(u.cache_read_input_tokens)) usage.cacheReadTokens = u.cache_read_input_tokens;
120
+ if (Number.isFinite(u.cache_creation_input_tokens)) usage.cacheCreationTokens = u.cache_creation_input_tokens;
121
+
122
+ // `modelUsage` is an object keyed by model id (e.g. {"claude-opus-4-8[1m]": {...}}) — take the first key.
123
+ const model = (obj.modelUsage && typeof obj.modelUsage === 'object')
124
+ ? (Object.keys(obj.modelUsage)[0] ?? null)
125
+ : null;
126
+
127
+ /** @type {GenerateResult} */
128
+ const result = {
129
+ text: typeof obj.result === 'string' ? obj.result : '',
130
+ toolCalls: [],
131
+ usage,
132
+ model,
133
+ };
134
+ // The CLI's own price is authoritative (subscription runs report an equivalent cost even at $0
135
+ // marginal) — feeds bareguard's USD axis with no local rate table. Only a finite number counts.
136
+ if (Number.isFinite(obj.total_cost_usd)) result.costUsd = obj.total_cost_usd;
137
+ return result;
138
+ }
139
+
72
140
  /**
73
141
  * Convert OpenAI-format messages to a plain text prompt.
74
142
  * @param {Message[]} messages
package/types/index.d.ts CHANGED
@@ -86,6 +86,14 @@ export interface GenerateResult {
86
86
  * temperature, not the one requested — callers reporting an effective temperature must honor this.
87
87
  */
88
88
  temperatureDropped?: boolean;
89
+ /**
90
+ * Authoritative per-call cost in USD, reported by the provider itself — e.g. CLIPipeProvider
91
+ * `parse:'claude-json'` surfacing the claude CLI's own `total_cost_usd`, a real price with no local
92
+ * rate table. When a FINITE number the Loop prefers it over `estimateCost` and treats the round as
93
+ * priced (feeding bareguard's USD budget axis). `0` is a valid priced value (a subscription/marginal-$0
94
+ * run) — distinct from omitted/null, which means "couldn't price" and falls back to the rate table.
95
+ */
96
+ costUsd?: number;
89
97
  }
90
98
 
91
99
  /** A conversation message in OpenAI chat format. */