You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(clipipe): opt-in structured-output parsing + Loop honors provider costUsd (A1)
CLIPipeProvider gains an opt-in `parse` option ('claude-json' preset or a
(stdout)=>Partial<GenerateResult> fn) that maps structured CLI output onto the
normalized GenerateResult/Usage shape; default (unset) stays raw-text/zero-usage
(byte-identical). GenerateResult gains optional `costUsd`, and the Loop prefers a
finite result.costUsd over estimateCost at both cost sites (resolveRoundCost) so
a CLI's own price feeds bareguard's USD axis without a local rate table. A
provider-supplied 0 is a valid priced value, distinct from null/unpriced.
Requested by adaptlearn (F2/A1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jdj7UfmMkPsvqqDN7ChhK
* release: v0.26.0 — CLIPipe structured output + provider costUsd (A1)
Version bump (package.json, package-lock ×2, context.md header), CHANGELOG cut
[Unreleased] -> [0.26.0], and bareagent.context.md provider-section update
(corrects the now-false 'CLIPipe always returns zero usage' claim + adds the
parse:'claude-json' surface and costUsd field).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jdj7UfmMkPsvqqDN7ChhK
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,6 +4,12 @@ All notable changes to bare-agent are documented here. Format: [Keep a Changelog
4
4
5
5
## [Unreleased]
6
6
7
+
## [0.26.0] — 2026-07-08
8
+
9
+
### Added
10
+
11
+
- **`CLIPipeProvider` opt-in structured-output parsing — surfaces real usage + cost (adaptlearn F2/A1).** `generate()` returned stdout verbatim as `text` and hard-coded `usage: { inputTokens: 0, outputTokens: 0 }`, so a bareguard `Gate` with a token or USD cap saw **zero usage** from CLI-piped runs — the budget axis was blind (and, under an active USD cap, fails closed on unpriced cost). But `claude -p --output-format json` emits a single JSON envelope carrying everything a provider result needs. New opt-in `new CLIPipeProvider({ parse })`: `'claude-json'` is a shipped preset that `JSON.parse`s stdout and maps the envelope onto the normalized `GenerateResult`/`Usage` shape — `text ← result` (the assistant text, not the raw JSON), `usage.inputTokens ← usage.input_tokens`, `outputTokens ← usage.output_tokens`, `cacheReadTokens`/`cacheCreationTokens ← usage.cache_{read,creation}_input_tokens` (absent ⇒ omitted, per the `Usage` contract — never a synthetic 0), `model ← ` the first `modelUsage` key, and `costUsd ← total_cost_usd`. Malformed JSON, a non-object envelope, `is_error: true`, or a non-`success` subtype throw a **loud `ProviderError`** — never a silent fall-back to raw text (the caller explicitly asked for structured output). A `parse: (stdout) => Partial<GenerateResult>` **function** is the CLI-agnostic escape hatch (merged over defaults); `'claude-json'` is a preset over it. To make the CLI's own price actually enforce a budget **without a local rate table**, `GenerateResult` gains an optional `costUsd?: number` and the **Loop now prefers a finite `result.costUsd` over `estimateCost`** (both the main and summarize cost paths) and forwards it to `onLlmResult` as `pricing: 'priced'` — a provider-supplied `0` is a valid priced value (a subscription/marginal-$0 run), distinct from omitted/null which still falls back to the rate table. Out of scope for A1 (unchanged): tool calls (`toolCalls` stays `[]`), streaming, and any claude-specific default args. **POC-first** — the real `claude -p "say OK" --output-format json` envelope was captured live (2026-07-08) before building, and the shipped provider was driven end-to-end against the real CLI (`text:"OK"`, `inputTokens` > 0, authoritative `costUsd` surfaced). Default (no `parse`) is **byte-identical to before** (raw stdout as text, zero usage — a regression guard test asserts a raw JSON envelope stays unparsed). `src/provider-clipipe.js`, `src/loop.js`, `types/index.d.ts`, `test/provider-clipipe.test.js` (+11), `test/loop.test.js` (+3, provider-cost preference incl. the `0`-is-priced and non-finite-falls-back cases).
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).
* @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.
0 commit comments