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.
- package/bareagent.context.md +5 -2
- package/package.json +1 -1
- package/src/loop.js +19 -2
- package/src/provider-clipipe.d.ts +15 -0
- package/src/provider-clipipe.js +70 -2
- package/types/index.d.ts +8 -0
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.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
|
|
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
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 =
|
|
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 =
|
|
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
|
package/src/provider-clipipe.js
CHANGED
|
@@ -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
|
|
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. */
|