bare-agent 0.23.0 → 0.24.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 +2 -2
- package/bareagent.context.md +4 -2
- package/package.json +1 -1
- package/src/loop.d.ts +4 -2
- package/src/loop.js +8 -3
- package/src/provider-anthropic.d.ts +3 -0
- package/src/provider-anthropic.js +18 -1
- package/src/provider-gemini.d.ts +3 -0
- package/src/provider-gemini.js +17 -1
- package/src/provider-ollama.d.ts +3 -0
- package/src/provider-ollama.js +17 -1
- package/src/provider-openai.d.ts +3 -0
- package/src/provider-openai.js +17 -1
- package/src/provider-temperature.d.ts +27 -0
- package/src/provider-temperature.js +60 -0
- package/src/recurse.d.ts +19 -13
- package/src/recurse.js +27 -7
- package/types/index.d.ts +6 -0
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ Every piece works alone — take what you need, ignore the rest. Two axes: **Act
|
|
|
94
94
|
|
|
95
95
|
### Recurse — break a hard task into a tree *(the RLM primitive)*
|
|
96
96
|
|
|
97
|
-
`recurse(task, ctx, opts)` does **decompose → fan-out → verify → synthesize** in one call — Recursive Language Models as a single import, composed *around* the Loop (never a new engine). The default is **model-driven**: the worker is handed a `spawn_child` tool and decides whether to split, bounded by depth + bareguard (no second guard layer). Forced fan-out (`count` / `mode:'fanout'`) and data-driven width (`mode:'partition'`, measured from a corpus) are opt-in. Give workers a stance with `opts.persona` (prepended to every worker, carries down the tree, deliberately kept out of the isolated verifier), and tell them *where they are* with `opts.context` (a read-only paths/cwd blob threaded to every worker so a sliced child can locate its artifact — facts, not a stance). For a leaf that should self-correct, pass `opts.refineLeaf` (opt-in): a definite leaf becomes a bounded generate→sense→regenerate loop driven by *your* deterministic sensor (test/compile/lint), feeding the gap back with escalating temperature. The headline guarantee: **aggregation is code, never a model-stated number**, and a dead worker or exhausted guard returns an honest `{ incomplete, missingSlices }` — never a faked pass.
|
|
97
|
+
`recurse(task, ctx, opts)` does **decompose → fan-out → verify → synthesize** in one call — Recursive Language Models as a single import, composed *around* the Loop (never a new engine). The default is **model-driven**: the worker is handed a `spawn_child` tool and decides whether to split, bounded by depth + bareguard (no second guard layer). Forced fan-out (`count` / `mode:'fanout'`) and data-driven width (`mode:'partition'`, measured from a corpus) are opt-in. Give workers a stance with `opts.persona` (prepended to every worker, carries down the tree, deliberately kept out of the isolated verifier), and tell them *where they are* with `opts.context` (a read-only paths/cwd blob threaded to every worker so a sliced child can locate its artifact — facts, not a stance). For a leaf that should self-correct, pass `opts.refineLeaf` (opt-in): a definite leaf becomes a bounded generate→sense→regenerate loop driven by *your* deterministic sensor (test/compile/lint), feeding the gap back (with escalating temperature on models that accept it; on a temperature-fixed model like `claude-sonnet-5` the gap critique carries recovery, and the receipt records the effective temps). The headline guarantee: **aggregation is code, never a model-stated number**, and a dead worker or exhausted guard returns an honest `{ incomplete, missingSlices }` — never a faked pass.
|
|
98
98
|
|
|
99
99
|
Over a corpus, context reaches a worker as a **handle routed by question shape** (`opts.retrieval`):
|
|
100
100
|
|
|
@@ -121,7 +121,7 @@ console.log(result.count, result.matchedIds); // a code-derived count + the id
|
|
|
121
121
|
|
|
122
122
|
**Govern — one gate over both axes.** `wireGate(gate)` routes every LLM + tool call through one bareguard policy + audit + budget. Denied tools never reach the model; halts (turn / budget / content caps) exit cleanly. `require('bare-agent/bareguard')`
|
|
123
123
|
|
|
124
|
-
**Providers:** OpenAI-compatible (OpenAI, OpenRouter, Groq, vLLM, LM Studio), Anthropic, Gemini (native), Ollama, CLIPipe, Fallback — or bring your own (one `generate` method). All return the same shape; swap freely. Usage including prompt-cache tiers is normalized, so `result.metrics` reports honest cumulative tokens + cost — and `null`, never a silent `0`, for a model it couldn't price.
|
|
124
|
+
**Providers:** OpenAI-compatible (OpenAI, OpenRouter, Groq, vLLM, LM Studio), Anthropic, Gemini (native), Ollama, CLIPipe, Fallback — or bring your own (one `generate` method). All return the same shape; swap freely. Usage including prompt-cache tiers is normalized, so `result.metrics` reports honest cumulative tokens + cost — and `null`, never a silent `0`, for a model it couldn't price. A model that rejects a non-default `temperature` (e.g. `claude-sonnet-5`, OpenAI o1/gpt-5-class return a `400`) is handled gracefully — the provider drops the param and retries once rather than failing the call, surfacing `temperatureDropped` so a caller can report the effective value.
|
|
125
125
|
|
|
126
126
|
**Tools:** Any function is a tool — REST, MCP, CLI, shell. Built-in web + mobile (optional).
|
|
127
127
|
|
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.24.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
|
|
|
@@ -673,7 +673,7 @@ const out = await recurse('Audit auth.js, billing.js, gateway.js for authz bugs'
|
|
|
673
673
|
|
|
674
674
|
**Worker context (`opts.context`, v0.23.0):** a read-only working-context string (paths/cwd) PREPENDED to every worker's TASK message as a `Working context:` block — so a sliced child can **locate its artifact** (the Planner paraphrases the goal into subtasks and drops absolute paths; without this, workers guess `.`/`~`/`/tmp` and get denied). Forwarded to the Planner as `info` (path-aware slices) and shown to the verifier too (neutral FACTS, not a stance — distinct from `persona`, which is a privileged SYSTEM-prompt stance). Carries down the tree. **Security:** it still becomes part of the prompt, so pass caller-trusted run-state only, never untrusted/end-user text (lower-privilege than `persona` — user message, not system — but still an injection surface). Absent ⇒ the task message is unchanged.
|
|
675
675
|
|
|
676
|
-
**Leaf self-correction (`opts.refineLeaf`, v0.23.0, opt-in):** turn a **definite leaf** (a node offered no `spawn_child` — `simple` tier or at `maxDepth`) into a bounded generate→sense→regenerate loop instead of a single pass: `{ sensor, maxIterations?, temperatures? }`. `sensor(result, { task, context, contract }) → Verdict` is YOUR **deterministic** close (test/compile/lint — not a model judge); on a non-pass its `critique` (the gap, not the transcript) is fed FRESH into the next attempt and the **retry temperature ESCALATES** (default `[0.2, 0.7, 1.0]` — load-bearing: a weak model at a flat temperature regenerates identical wrong code and ignores even crisp feedback). Each attempt is gate-checked + metered; a HaltError mid-loop → clean `{ incomplete }`; honest non-recovery → `receipts.refineLeaf.passed === false` (never a faked pass); `receipts.tokens` sums all attempts. The error-keyed `recall` stays YOUR tool (`opts.tools`), keyed off the fed-back critique — bareagent stays litectx-agnostic. Carries down (engages at the leaves). Absent ⇒ a leaf is a single pass.
|
|
676
|
+
**Leaf self-correction (`opts.refineLeaf`, v0.23.0, opt-in):** turn a **definite leaf** (a node offered no `spawn_child` — `simple` tier or at `maxDepth`) into a bounded generate→sense→regenerate loop instead of a single pass: `{ sensor, maxIterations?, temperatures? }`. `sensor(result, { task, context, contract }) → Verdict` is YOUR **deterministic** close (test/compile/lint — not a model judge); on a non-pass its `critique` (the gap, not the transcript) is fed FRESH into the next attempt and — **on models that accept `temperature`** — the **retry temperature ESCALATES** (default `[0.2, 0.7, 1.0]` — load-bearing there: a weak model at a flat temperature regenerates identical wrong code and ignores even crisp feedback). On a **temperature-fixed model** (e.g. `claude-sonnet-5`, which 400s any non-default temperature) the provider silently drops the param (see below), the escalation lever is inert, and the fed-back gap critique carries recovery alone; `receipts.refineLeaf.temperatures` then records the EFFECTIVE temps — a `null` marks an attempt that ran at the model's default (never the ignored requested value). Each attempt is gate-checked + metered; a HaltError mid-loop → clean `{ incomplete }`; honest non-recovery → `receipts.refineLeaf.passed === false` (never a faked pass); `receipts.tokens` sums all attempts. The error-keyed `recall` stays YOUR tool (`opts.tools`), keyed off the fed-back critique — bareagent stays litectx-agnostic. Carries down (engages at the leaves). Absent ⇒ a leaf is a single pass.
|
|
677
677
|
|
|
678
678
|
```javascript
|
|
679
679
|
const out = await recurse('Fix the failing function in calc.js', ctx, {
|
|
@@ -741,6 +741,8 @@ new CLIPipe({ command: 'ollama', args: ['run', 'llama3.2'] })
|
|
|
741
741
|
|
|
742
742
|
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`.
|
|
743
743
|
|
|
744
|
+
**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).
|
|
745
|
+
|
|
744
746
|
**Error body (v0.11.0):** on an HTTP error the OpenAI/Anthropic/Ollama providers throw a `ProviderError` whose `message` carries the upstream error string. The full parsed response is **not** attached to `err.body` by default (so an unexpected field can't leak through logs that dump the error object). Pass `{ exposeErrorBody: true }` to attach it for debugging.
|
|
745
747
|
|
|
746
748
|
**Plaintext-key warning (Unreleased):** the OpenAI provider's `baseUrl` accepts `http://` (for local/OpenAI-compatible endpoints), but a `Bearer` key sent over plaintext http to a **non-loopback** host is exposed on the wire. The provider now warns once when that happens. Loopback hosts (`localhost`/`127.0.0.0/8`/`::1` — local proxies, Ollama-style endpoints) stay silent, since that's the legitimate keyless-local case. The header is **not** stripped (some local proxies want a key), so use `https` for any remote endpoint, or drop `apiKey` when the local endpoint needs none.
|
package/package.json
CHANGED
package/src/loop.d.ts
CHANGED
|
@@ -111,7 +111,7 @@ export class Loop {
|
|
|
111
111
|
* thunk is re-evaluated each round (D4/eval-assist F2) so a tool set that grows mid-run — e.g. a skill
|
|
112
112
|
* unlocking its tools — is offered on the next round; a static array is resolved once at wire time.
|
|
113
113
|
* @param {Record<string, any>} [options={}] - Per-run overrides (system, temperature, ctx, etc.).
|
|
114
|
-
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics}>}
|
|
114
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics, temperatureDropped?: boolean}>}
|
|
115
115
|
* On halt the returned `error` is `halt:<rule>` (or `halt:unknown` if the
|
|
116
116
|
* thrown HaltError carried no `rule`), and `msgs` is sanitized so any
|
|
117
117
|
* dangling assistant `tool_calls` from the halted round are paired with
|
|
@@ -129,6 +129,7 @@ export class Loop {
|
|
|
129
129
|
error: string | null;
|
|
130
130
|
msgs: Message[];
|
|
131
131
|
metrics: RunMetrics;
|
|
132
|
+
temperatureDropped?: boolean;
|
|
132
133
|
}>;
|
|
133
134
|
/**
|
|
134
135
|
* Health check — validates provider, store, and tools without throwing.
|
|
@@ -156,7 +157,7 @@ export class Loop {
|
|
|
156
157
|
* @param {string} text - User message.
|
|
157
158
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
158
159
|
* @param {Record<string, any>} [options={}] - Per-run overrides.
|
|
159
|
-
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics}>}
|
|
160
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics, temperatureDropped?: boolean}>}
|
|
160
161
|
*/
|
|
161
162
|
chat(text: string, tools?: ToolDef[], options?: Record<string, any>): Promise<{
|
|
162
163
|
text: string;
|
|
@@ -166,6 +167,7 @@ export class Loop {
|
|
|
166
167
|
error: string | null;
|
|
167
168
|
msgs: Message[];
|
|
168
169
|
metrics: RunMetrics;
|
|
170
|
+
temperatureDropped?: boolean;
|
|
169
171
|
}>;
|
|
170
172
|
stop(): void;
|
|
171
173
|
}
|
package/src/loop.js
CHANGED
|
@@ -294,7 +294,7 @@ class Loop {
|
|
|
294
294
|
* thunk is re-evaluated each round (D4/eval-assist F2) so a tool set that grows mid-run — e.g. a skill
|
|
295
295
|
* unlocking its tools — is offered on the next round; a static array is resolved once at wire time.
|
|
296
296
|
* @param {Record<string, any>} [options={}] - Per-run overrides (system, temperature, ctx, etc.).
|
|
297
|
-
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics}>}
|
|
297
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics, temperatureDropped?: boolean}>}
|
|
298
298
|
* On halt the returned `error` is `halt:<rule>` (or `halt:unknown` if the
|
|
299
299
|
* thrown HaltError carried no `rule`), and `msgs` is sanitized so any
|
|
300
300
|
* dangling assistant `tool_calls` from the halted round are paired with
|
|
@@ -356,6 +356,10 @@ class Loop {
|
|
|
356
356
|
|
|
357
357
|
let lastUsage = { inputTokens: 0, outputTokens: 0 };
|
|
358
358
|
let totalCost = 0;
|
|
359
|
+
// BA-10: sticky across rounds — true if ANY round's `temperature` was dropped by the model (400,
|
|
360
|
+
// unsupported/deprecated) and retried without it. Surfaced on the result so an upstream receipt
|
|
361
|
+
// (recurse's refineLeaf) can report the EFFECTIVE temperature rather than the ignored request.
|
|
362
|
+
let temperatureDropped = false;
|
|
359
363
|
|
|
360
364
|
// The meter (Feature 3): bareagent is the canonical run counter. Accumulates across rounds and is
|
|
361
365
|
// returned as `result.metrics`. `tokens` is CUMULATIVE over all four tiers (fixes the last-round-only
|
|
@@ -583,6 +587,7 @@ class Loop {
|
|
|
583
587
|
}
|
|
584
588
|
|
|
585
589
|
lastUsage = result.usage || lastUsage;
|
|
590
|
+
if (result.temperatureDropped) temperatureDropped = true;
|
|
586
591
|
// Publish the latest measured usage to ctx (non-enumerable, fail-open) so a transcript-bound seam —
|
|
587
592
|
// e.g. F2 stash auto-compaction — can read EXACT provider-counted `inputTokens` to gauge context
|
|
588
593
|
// pressure on the NEXT round's trim. Symmetric with lending ctx.summarize; the Loop stays unaware of
|
|
@@ -642,7 +647,7 @@ class Loop {
|
|
|
642
647
|
try { await flush(msgs, ctx); }
|
|
643
648
|
catch (err) { if (err instanceof HaltError) throw err; this._reportError('trim-flush', err, { round }); }
|
|
644
649
|
}
|
|
645
|
-
return { text: result.text, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, msgs, metrics: finalizeMetrics() };
|
|
650
|
+
return { text: result.text, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, msgs, metrics: finalizeMetrics(), ...(temperatureDropped && { temperatureDropped: true }) };
|
|
646
651
|
}
|
|
647
652
|
|
|
648
653
|
// Execute tool calls
|
|
@@ -863,7 +868,7 @@ class Loop {
|
|
|
863
868
|
* @param {string} text - User message.
|
|
864
869
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
865
870
|
* @param {Record<string, any>} [options={}] - Per-run overrides.
|
|
866
|
-
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics}>}
|
|
871
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics, temperatureDropped?: boolean}>}
|
|
867
872
|
*/
|
|
868
873
|
async chat(text, tools = [], options = {}) {
|
|
869
874
|
this._history.push({ role: 'user', content: text });
|
|
@@ -54,6 +54,9 @@ export class AnthropicProvider {
|
|
|
54
54
|
* @throws {Error} `[AnthropicProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
55
55
|
*/
|
|
56
56
|
generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
|
|
57
|
+
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
58
|
+
_warnTemperatureDropped(): void;
|
|
59
|
+
_warnedTempDropped: boolean | undefined;
|
|
57
60
|
/**
|
|
58
61
|
* @param {Message} msg
|
|
59
62
|
* @returns {any}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const https = require('https');
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const { ProviderError } = require('./errors');
|
|
6
|
+
const { requestWithTemperatureFallback } = require('./provider-temperature');
|
|
6
7
|
|
|
7
8
|
/** @param {string} hostname @returns {boolean} */
|
|
8
9
|
function isLoopbackHost(hostname) {
|
|
@@ -91,7 +92,15 @@ class AnthropicProvider {
|
|
|
91
92
|
}));
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
// BA-10: some models (e.g. claude-sonnet-5) reject a non-default `temperature` with a 400 — drop it
|
|
96
|
+
// and retry once rather than let the whole call fail. `temperatureDropped` flows back so an upstream
|
|
97
|
+
// receipt (recurse's refineLeaf) can report the effective temperature, not the one the model ignored.
|
|
98
|
+
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
99
|
+
request: () => this._request(body),
|
|
100
|
+
hadTemperature: () => body.temperature != null,
|
|
101
|
+
stripTemperature: () => { delete body.temperature; },
|
|
102
|
+
warnOnce: () => this._warnTemperatureDropped(),
|
|
103
|
+
});
|
|
95
104
|
|
|
96
105
|
let text = '';
|
|
97
106
|
/** @type {import('../types').ToolCall[]} */
|
|
@@ -115,9 +124,17 @@ class AnthropicProvider {
|
|
|
115
124
|
cacheReadTokens: data.usage?.cache_read_input_tokens || 0,
|
|
116
125
|
cacheCreationTokens: data.usage?.cache_creation_input_tokens || 0,
|
|
117
126
|
},
|
|
127
|
+
...(temperatureDropped && { temperatureDropped: true }),
|
|
118
128
|
};
|
|
119
129
|
}
|
|
120
130
|
|
|
131
|
+
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
132
|
+
_warnTemperatureDropped() {
|
|
133
|
+
if (this._warnedTempDropped) return;
|
|
134
|
+
this._warnedTempDropped = true;
|
|
135
|
+
console.warn(`[AnthropicProvider] '${this.model}' rejected a non-default 'temperature' (unsupported/deprecated) — retrying without it. Further drops from this provider instance are silent.`);
|
|
136
|
+
}
|
|
137
|
+
|
|
121
138
|
/**
|
|
122
139
|
* @param {Message} msg
|
|
123
140
|
* @returns {any}
|
package/src/provider-gemini.d.ts
CHANGED
|
@@ -50,6 +50,9 @@ export class GeminiProvider {
|
|
|
50
50
|
* @throws {Error} `[GeminiProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
51
51
|
*/
|
|
52
52
|
generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
|
|
53
|
+
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
54
|
+
_warnTemperatureDropped(): void;
|
|
55
|
+
_warnedTempDropped: boolean | undefined;
|
|
53
56
|
/**
|
|
54
57
|
* Normalize Gemini `usageMetadata` to the neutral {@link Usage} shape. Like OpenAI, `promptTokenCount`
|
|
55
58
|
* INCLUDES the cached tokens (`cachedContentTokenCount`), so subtract for the uncached remainder
|
package/src/provider-gemini.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const https = require('https');
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const { ProviderError } = require('./errors');
|
|
6
|
+
const { requestWithTemperatureFallback } = require('./provider-temperature');
|
|
6
7
|
|
|
7
8
|
/** @typedef {import('../types').Message} Message */
|
|
8
9
|
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
@@ -103,7 +104,14 @@ class GeminiProvider {
|
|
|
103
104
|
if (options.temperature != null) genConfig.temperature = options.temperature;
|
|
104
105
|
if (Object.keys(genConfig).length) body.generationConfig = genConfig;
|
|
105
106
|
|
|
106
|
-
|
|
107
|
+
// BA-10: graceful degrade if a model rejects a non-default `temperature` (Gemini nests it under
|
|
108
|
+
// generationConfig). Keyed off the API error text, so dormant on models that accept temperature.
|
|
109
|
+
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
110
|
+
request: () => this._request(`/models/${this.model}:generateContent`, body),
|
|
111
|
+
hadTemperature: () => body.generationConfig?.temperature != null,
|
|
112
|
+
stripTemperature: () => { if (body.generationConfig) delete body.generationConfig.temperature; },
|
|
113
|
+
warnOnce: () => this._warnTemperatureDropped(),
|
|
114
|
+
});
|
|
107
115
|
|
|
108
116
|
let text = '';
|
|
109
117
|
/** @type {ToolCall[]} */
|
|
@@ -123,9 +131,17 @@ class GeminiProvider {
|
|
|
123
131
|
toolCalls,
|
|
124
132
|
model: data.modelVersion || this.model,
|
|
125
133
|
usage: this._normalizeUsage(data.usageMetadata),
|
|
134
|
+
...(temperatureDropped && { temperatureDropped: true }),
|
|
126
135
|
};
|
|
127
136
|
}
|
|
128
137
|
|
|
138
|
+
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
139
|
+
_warnTemperatureDropped() {
|
|
140
|
+
if (this._warnedTempDropped) return;
|
|
141
|
+
this._warnedTempDropped = true;
|
|
142
|
+
console.warn(`[GeminiProvider] '${this.model}' rejected a non-default 'temperature' (unsupported/deprecated) — retrying without it. Further drops from this provider instance are silent.`);
|
|
143
|
+
}
|
|
144
|
+
|
|
129
145
|
/**
|
|
130
146
|
* Normalize Gemini `usageMetadata` to the neutral {@link Usage} shape. Like OpenAI, `promptTokenCount`
|
|
131
147
|
* INCLUDES the cached tokens (`cachedContentTokenCount`), so subtract for the uncached remainder
|
package/src/provider-ollama.d.ts
CHANGED
|
@@ -32,6 +32,9 @@ export class OllamaProvider {
|
|
|
32
32
|
* @throws {Error} `[OllamaProvider] ...` — on HTTP errors or invalid JSON response.
|
|
33
33
|
*/
|
|
34
34
|
generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
|
|
35
|
+
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
36
|
+
_warnTemperatureDropped(): void;
|
|
37
|
+
_warnedTempDropped: boolean | undefined;
|
|
35
38
|
/**
|
|
36
39
|
* @param {string} path
|
|
37
40
|
* @param {Record<string, any>} body
|
package/src/provider-ollama.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const http = require('http');
|
|
4
4
|
const { ProviderError } = require('./errors');
|
|
5
|
+
const { requestWithTemperatureFallback } = require('./provider-temperature');
|
|
5
6
|
|
|
6
7
|
/** @typedef {import('../types').Message} Message */
|
|
7
8
|
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
@@ -48,7 +49,14 @@ class OllamaProvider {
|
|
|
48
49
|
}));
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
// BA-10: graceful degrade if a model rejects a non-default `temperature` (Ollama nests it under
|
|
53
|
+
// `options`). Keyed off the API error text, so dormant on models that accept temperature.
|
|
54
|
+
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
55
|
+
request: () => this._request('/api/chat', body),
|
|
56
|
+
hadTemperature: () => body.options?.temperature != null,
|
|
57
|
+
stripTemperature: () => { if (body.options) delete body.options.temperature; },
|
|
58
|
+
warnOnce: () => this._warnTemperatureDropped(),
|
|
59
|
+
});
|
|
52
60
|
const msg = data.message || {};
|
|
53
61
|
|
|
54
62
|
return {
|
|
@@ -65,9 +73,17 @@ class OllamaProvider {
|
|
|
65
73
|
inputTokens: data.prompt_eval_count || 0,
|
|
66
74
|
outputTokens: data.eval_count || 0,
|
|
67
75
|
},
|
|
76
|
+
...(temperatureDropped && { temperatureDropped: true }),
|
|
68
77
|
};
|
|
69
78
|
}
|
|
70
79
|
|
|
80
|
+
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
81
|
+
_warnTemperatureDropped() {
|
|
82
|
+
if (this._warnedTempDropped) return;
|
|
83
|
+
this._warnedTempDropped = true;
|
|
84
|
+
console.warn(`[OllamaProvider] '${this.model}' rejected a non-default 'temperature' (unsupported/deprecated) — retrying without it. Further drops from this provider instance are silent.`);
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
/**
|
|
72
88
|
* @param {string} path
|
|
73
89
|
* @param {Record<string, any>} body
|
package/src/provider-openai.d.ts
CHANGED
|
@@ -44,6 +44,9 @@ export class OpenAIProvider {
|
|
|
44
44
|
* @throws {Error} `[OpenAIProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
45
45
|
*/
|
|
46
46
|
generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
|
|
47
|
+
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
48
|
+
_warnTemperatureDropped(): void;
|
|
49
|
+
_warnedTempDropped: boolean | undefined;
|
|
47
50
|
/**
|
|
48
51
|
* Normalize OpenAI usage to the neutral {@link Usage} shape. OpenAI auto-caches prompt prefixes
|
|
49
52
|
* (>=1024 tokens) and reports the cached portion in `prompt_tokens_details.cached_tokens` —
|
package/src/provider-openai.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const https = require('https');
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const { ProviderError } = require('./errors');
|
|
6
|
+
const { requestWithTemperatureFallback } = require('./provider-temperature');
|
|
6
7
|
|
|
7
8
|
/** @typedef {import('../types').Message} Message */
|
|
8
9
|
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
@@ -61,7 +62,14 @@ class OpenAIProvider {
|
|
|
61
62
|
}));
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
// BA-10: newer models (o1/gpt-5-class) reject a non-default `temperature` with a 400 — drop it and
|
|
66
|
+
// retry once. `temperatureDropped` flows back so an upstream receipt can report the effective value.
|
|
67
|
+
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
68
|
+
request: () => this._request('/chat/completions', body),
|
|
69
|
+
hadTemperature: () => body.temperature != null,
|
|
70
|
+
stripTemperature: () => { delete body.temperature; },
|
|
71
|
+
warnOnce: () => this._warnTemperatureDropped(),
|
|
72
|
+
});
|
|
65
73
|
const choice = data.choices[0];
|
|
66
74
|
const msg = choice.message;
|
|
67
75
|
|
|
@@ -74,9 +82,17 @@ class OpenAIProvider {
|
|
|
74
82
|
})),
|
|
75
83
|
model: data.model || this.model,
|
|
76
84
|
usage: this._normalizeUsage(data.usage),
|
|
85
|
+
...(temperatureDropped && { temperatureDropped: true }),
|
|
77
86
|
};
|
|
78
87
|
}
|
|
79
88
|
|
|
89
|
+
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
90
|
+
_warnTemperatureDropped() {
|
|
91
|
+
if (this._warnedTempDropped) return;
|
|
92
|
+
this._warnedTempDropped = true;
|
|
93
|
+
console.warn(`[OpenAIProvider] '${this.model}' rejected a non-default 'temperature' (unsupported/deprecated) — retrying without it. Further drops from this provider instance are silent.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
/**
|
|
81
97
|
* Normalize OpenAI usage to the neutral {@link Usage} shape. OpenAI auto-caches prompt prefixes
|
|
82
98
|
* (>=1024 tokens) and reports the cached portion in `prompt_tokens_details.cached_tokens` —
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this error mean the model rejected `temperature` as unsupported/deprecated?
|
|
3
|
+
* @param {any} err - the rejection from a provider `_request` (a {@link ProviderError} carries `.status`).
|
|
4
|
+
* @returns {boolean}
|
|
5
|
+
*/
|
|
6
|
+
export function isTemperatureUnsupported(err: any): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Issue a provider request; if it 400s because `temperature` is unsupported AND a temperature was
|
|
9
|
+
* actually sent, strip it and retry ONCE. Returns whether the temperature was dropped so the caller
|
|
10
|
+
* can report the EFFECTIVE temperature (the receipt must not claim a value the model ignored).
|
|
11
|
+
*
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {() => Promise<any>} opts.request - issues the API call (rejects `ProviderError` on 4xx).
|
|
14
|
+
* @param {() => boolean} opts.hadTemperature - was a temperature actually in the request body?
|
|
15
|
+
* @param {() => void} opts.stripTemperature - mutate the request body to remove the temperature.
|
|
16
|
+
* @param {() => void} [opts.warnOnce] - emit the one-time degrade warning (caller dedupes per instance).
|
|
17
|
+
* @returns {Promise<{ data: any, temperatureDropped: boolean }>}
|
|
18
|
+
*/
|
|
19
|
+
export function requestWithTemperatureFallback({ request, hadTemperature, stripTemperature, warnOnce }: {
|
|
20
|
+
request: () => Promise<any>;
|
|
21
|
+
hadTemperature: () => boolean;
|
|
22
|
+
stripTemperature: () => void;
|
|
23
|
+
warnOnce?: (() => void) | undefined;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
data: any;
|
|
26
|
+
temperatureDropped: boolean;
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Graceful degradation for models that reject a non-default `temperature` (BA-10 / relayfact F34).
|
|
4
|
+
//
|
|
5
|
+
// Newer models — claude-sonnet-5 ("`temperature` is deprecated for this model."), OpenAI o1/gpt-5-class
|
|
6
|
+
// ("Unsupported value: 'temperature' … Only the default (1) …") — return a 400 for ANY non-default
|
|
7
|
+
// temperature. Left unhandled, the whole `generate` throws; upstream (e.g. recurse's `refineLeaf`) then
|
|
8
|
+
// collapses to `incomplete` with the executable close never run — a failure that LOOKS like "the model
|
|
9
|
+
// couldn't do it" when in fact no attempt was ever made.
|
|
10
|
+
//
|
|
11
|
+
// The fix keys off the API's own error TEXT, never a hardcoded model list, so it survives future models
|
|
12
|
+
// that drop the param and stays dormant on every model that accepts it. It retries ONCE without the
|
|
13
|
+
// temperature, and ONLY for the unsupported/deprecated class — a genuine out-of-range 400 re-throws
|
|
14
|
+
// (dropping it would mask a caller bug).
|
|
15
|
+
|
|
16
|
+
/** The error names `temperature` … */
|
|
17
|
+
const TEMP_NAMED = /temperature/i;
|
|
18
|
+
// … AND indicates it's unsupported/deprecated (NOT merely out of range — that stays a hard error).
|
|
19
|
+
// The `only…default` alternative uses a BOUNDED gap (`[^.]{0,40}`, not `.*`): an unbounded `.*` here is a
|
|
20
|
+
// quadratic-blowup footgun on a long provider/proxy-supplied error message that repeats "only" with no
|
|
21
|
+
// "default" (O(n) start positions × O(n) backtrack). The bound keeps it linear and still matches
|
|
22
|
+
// "Only the default (1) value is supported."
|
|
23
|
+
const TEMP_UNSUPPORTED = /(deprecat|unsupported|not support|does not support|no longer support|only\b[^.]{0,40}\bdefault|must be omitted|isn't supported|is not allowed)/i;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Does this error mean the model rejected `temperature` as unsupported/deprecated?
|
|
27
|
+
* @param {any} err - the rejection from a provider `_request` (a {@link ProviderError} carries `.status`).
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
function isTemperatureUnsupported(err) {
|
|
31
|
+
const msg = err && typeof err.message === 'string' ? err.message : '';
|
|
32
|
+
return !!err && err.status === 400 && TEMP_NAMED.test(msg) && TEMP_UNSUPPORTED.test(msg);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Issue a provider request; if it 400s because `temperature` is unsupported AND a temperature was
|
|
37
|
+
* actually sent, strip it and retry ONCE. Returns whether the temperature was dropped so the caller
|
|
38
|
+
* can report the EFFECTIVE temperature (the receipt must not claim a value the model ignored).
|
|
39
|
+
*
|
|
40
|
+
* @param {object} opts
|
|
41
|
+
* @param {() => Promise<any>} opts.request - issues the API call (rejects `ProviderError` on 4xx).
|
|
42
|
+
* @param {() => boolean} opts.hadTemperature - was a temperature actually in the request body?
|
|
43
|
+
* @param {() => void} opts.stripTemperature - mutate the request body to remove the temperature.
|
|
44
|
+
* @param {() => void} [opts.warnOnce] - emit the one-time degrade warning (caller dedupes per instance).
|
|
45
|
+
* @returns {Promise<{ data: any, temperatureDropped: boolean }>}
|
|
46
|
+
*/
|
|
47
|
+
async function requestWithTemperatureFallback({ request, hadTemperature, stripTemperature, warnOnce }) {
|
|
48
|
+
try {
|
|
49
|
+
return { data: await request(), temperatureDropped: false };
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (isTemperatureUnsupported(err) && hadTemperature()) {
|
|
52
|
+
stripTemperature();
|
|
53
|
+
if (warnOnce) warnOnce();
|
|
54
|
+
return { data: await request(), temperatureDropped: true };
|
|
55
|
+
}
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { isTemperatureUnsupported, requestWithTemperatureFallback };
|
package/src/recurse.d.ts
CHANGED
|
@@ -90,9 +90,11 @@ export type RecurseOptions = {
|
|
|
90
90
|
* tier or at `maxDepth`) into a bounded generate→sense→regenerate loop instead of a single pass, so a failed
|
|
91
91
|
* slice can self-correct. `sensor` is a DETERMINISTIC close (test/compile/lint — NOT a model judge, R-S8) that
|
|
92
92
|
* returns a `Verdict`; on a non-pass its `critique` (the GAP, not the transcript) is fed FRESH into the next
|
|
93
|
-
* attempt (D6/A1 anti-anchoring) and the **retry temperature ESCALATES**
|
|
94
|
-
* `[0.2,0.7,1.0]`) — the live-validated
|
|
95
|
-
* (`poc/ba8-leaf-refine.mjs`: 0/5 → 2-3/5; flat temp recovers 0/5).
|
|
93
|
+
* attempt (D6/A1 anti-anchoring) and, on models that ACCEPT `temperature`, the **retry temperature ESCALATES**
|
|
94
|
+
* (`temperatures`, default `[0.2,0.7,1.0]`) — the live-validated lever that lets a weak model escape a
|
|
95
|
+
* repeat-the-same-mistake rut (`poc/ba8-leaf-refine.mjs`: 0/5 → 2-3/5; flat temp recovers 0/5). On a
|
|
96
|
+
* temperature-fixed model (BA-10) the provider drops the param, `receipts.refineLeaf.temperatures` records
|
|
97
|
+
* `null`, and the fed-back gap critique carries recovery alone. `maxIterations` defaults to
|
|
96
98
|
* `temperatures.length`; the REAL bound is bareguard (each attempt is gate-checked + metered). CARRIES DOWN the
|
|
97
99
|
* tree (preserved by `forChild`), so it engages at the leaves of a Family-A decomposition. Recovery is PARTIAL
|
|
98
100
|
* (a stubborn blind spot may persist) — `receipts.refineLeaf.passed` reports honestly. Does NOT apply to a node
|
|
@@ -218,14 +220,15 @@ export type RecurseNode = {
|
|
|
218
220
|
*/
|
|
219
221
|
tokens: object | null;
|
|
220
222
|
/**
|
|
221
|
-
* - (BA-8) when this
|
|
222
|
-
* ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally
|
|
223
|
-
* (false = honest non-recovery, not a faked success).
|
|
223
|
+
* - (BA-8) when this
|
|
224
|
+
* leaf ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally
|
|
225
|
+
* passed (false = honest non-recovery, not a faked success). `temperatures` are the EFFECTIVE per-attempt temps
|
|
226
|
+
* (BA-10): a `null` marks an attempt the model ran at its DEFAULT because it rejected the requested temperature.
|
|
224
227
|
*/
|
|
225
228
|
refineLeaf?: {
|
|
226
229
|
iterations: number;
|
|
227
230
|
passed: boolean;
|
|
228
|
-
temperatures: number[];
|
|
231
|
+
temperatures: (number | null)[];
|
|
229
232
|
} | undefined;
|
|
230
233
|
model: string | null;
|
|
231
234
|
/**
|
|
@@ -353,9 +356,11 @@ export type Slice = {
|
|
|
353
356
|
* tier or at `maxDepth`) into a bounded generate→sense→regenerate loop instead of a single pass, so a failed
|
|
354
357
|
* slice can self-correct. `sensor` is a DETERMINISTIC close (test/compile/lint — NOT a model judge, R-S8) that
|
|
355
358
|
* returns a `Verdict`; on a non-pass its `critique` (the GAP, not the transcript) is fed FRESH into the next
|
|
356
|
-
* attempt (D6/A1 anti-anchoring) and the **retry temperature ESCALATES**
|
|
357
|
-
* `[0.2,0.7,1.0]`) — the live-validated
|
|
358
|
-
* (`poc/ba8-leaf-refine.mjs`: 0/5 → 2-3/5; flat temp recovers 0/5).
|
|
359
|
+
* attempt (D6/A1 anti-anchoring) and, on models that ACCEPT `temperature`, the **retry temperature ESCALATES**
|
|
360
|
+
* (`temperatures`, default `[0.2,0.7,1.0]`) — the live-validated lever that lets a weak model escape a
|
|
361
|
+
* repeat-the-same-mistake rut (`poc/ba8-leaf-refine.mjs`: 0/5 → 2-3/5; flat temp recovers 0/5). On a
|
|
362
|
+
* temperature-fixed model (BA-10) the provider drops the param, `receipts.refineLeaf.temperatures` records
|
|
363
|
+
* `null`, and the fed-back gap critique carries recovery alone. `maxIterations` defaults to
|
|
359
364
|
* `temperatures.length`; the REAL bound is bareguard (each attempt is gate-checked + metered). CARRIES DOWN the
|
|
360
365
|
* tree (preserved by `forChild`), so it engages at the leaves of a Family-A decomposition. Recovery is PARTIAL
|
|
361
366
|
* (a stubborn blind spot may persist) — `receipts.refineLeaf.passed` reports honestly. Does NOT apply to a node
|
|
@@ -420,9 +425,10 @@ export type Slice = {
|
|
|
420
425
|
* @property {boolean} incomplete
|
|
421
426
|
* @property {boolean} halted
|
|
422
427
|
* @property {object|null} tokens - The worker Loop's `metrics.tokens`.
|
|
423
|
-
* @property {{iterations: number, passed: boolean, temperatures: number[]}} [refineLeaf] - (BA-8) when this
|
|
424
|
-
* ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally
|
|
425
|
-
* (false = honest non-recovery, not a faked success).
|
|
428
|
+
* @property {{iterations: number, passed: boolean, temperatures: (number|null)[]}} [refineLeaf] - (BA-8) when this
|
|
429
|
+
* leaf ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally
|
|
430
|
+
* passed (false = honest non-recovery, not a faked success). `temperatures` are the EFFECTIVE per-attempt temps
|
|
431
|
+
* (BA-10): a `null` marks an attempt the model ran at its DEFAULT because it rejected the requested temperature.
|
|
426
432
|
* @property {string|null} model
|
|
427
433
|
* @property {string|null} [retrieval] - (§10 step 7) the retrieval mode this node ran (`scan`/`search`/`exact`),
|
|
428
434
|
* or null/absent for a plain reasoning node.
|
package/src/recurse.js
CHANGED
|
@@ -56,6 +56,10 @@ const DEFAULT_WORKER_BUDGET = 100;
|
|
|
56
56
|
// low temperature a weak model regenerates byte-identical wrong code and IGNORES even crisp deterministic
|
|
57
57
|
// feedback (0/5 recovery); recovery only appears once retries are given room to vary (0/5 → 2-3/5). So escalation
|
|
58
58
|
// is a DESIGN REQUIREMENT of the seam, not a tuning nicety. Overridable via `opts.refineLeaf.temperatures`.
|
|
59
|
+
// SCOPE (BA-10): this holds for models that ACCEPT `temperature`. On a temperature-fixed model (e.g.
|
|
60
|
+
// claude-sonnet-5 — the provider drops the param, `receipts.refineLeaf.temperatures` records `null`), the
|
|
61
|
+
// escalation lever is inert and the fed-back gap `critique` carries recovery alone (an empirical question the
|
|
62
|
+
// live run answers). The critique is the primary correction lever; temperature is a secondary diversity lever.
|
|
59
63
|
const DEFAULT_REFINE_TEMPS = [0.2, 0.7, 1.0];
|
|
60
64
|
|
|
61
65
|
/**
|
|
@@ -217,9 +221,11 @@ function auditSafeCtx(ctx, overrides = {}) {
|
|
|
217
221
|
* tier or at `maxDepth`) into a bounded generate→sense→regenerate loop instead of a single pass, so a failed
|
|
218
222
|
* slice can self-correct. `sensor` is a DETERMINISTIC close (test/compile/lint — NOT a model judge, R-S8) that
|
|
219
223
|
* returns a `Verdict`; on a non-pass its `critique` (the GAP, not the transcript) is fed FRESH into the next
|
|
220
|
-
* attempt (D6/A1 anti-anchoring) and the **retry temperature ESCALATES**
|
|
221
|
-
* `[0.2,0.7,1.0]`) — the live-validated
|
|
222
|
-
* (`poc/ba8-leaf-refine.mjs`: 0/5 → 2-3/5; flat temp recovers 0/5).
|
|
224
|
+
* attempt (D6/A1 anti-anchoring) and, on models that ACCEPT `temperature`, the **retry temperature ESCALATES**
|
|
225
|
+
* (`temperatures`, default `[0.2,0.7,1.0]`) — the live-validated lever that lets a weak model escape a
|
|
226
|
+
* repeat-the-same-mistake rut (`poc/ba8-leaf-refine.mjs`: 0/5 → 2-3/5; flat temp recovers 0/5). On a
|
|
227
|
+
* temperature-fixed model (BA-10) the provider drops the param, `receipts.refineLeaf.temperatures` records
|
|
228
|
+
* `null`, and the fed-back gap critique carries recovery alone. `maxIterations` defaults to
|
|
223
229
|
* `temperatures.length`; the REAL bound is bareguard (each attempt is gate-checked + metered). CARRIES DOWN the
|
|
224
230
|
* tree (preserved by `forChild`), so it engages at the leaves of a Family-A decomposition. Recovery is PARTIAL
|
|
225
231
|
* (a stubborn blind spot may persist) — `receipts.refineLeaf.passed` reports honestly. Does NOT apply to a node
|
|
@@ -285,9 +291,10 @@ function auditSafeCtx(ctx, overrides = {}) {
|
|
|
285
291
|
* @property {boolean} incomplete
|
|
286
292
|
* @property {boolean} halted
|
|
287
293
|
* @property {object|null} tokens - The worker Loop's `metrics.tokens`.
|
|
288
|
-
* @property {{iterations: number, passed: boolean, temperatures: number[]}} [refineLeaf] - (BA-8) when this
|
|
289
|
-
* ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally
|
|
290
|
-
* (false = honest non-recovery, not a faked success).
|
|
294
|
+
* @property {{iterations: number, passed: boolean, temperatures: (number|null)[]}} [refineLeaf] - (BA-8) when this
|
|
295
|
+
* leaf ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally
|
|
296
|
+
* passed (false = honest non-recovery, not a faked success). `temperatures` are the EFFECTIVE per-attempt temps
|
|
297
|
+
* (BA-10): a `null` marks an attempt the model ran at its DEFAULT because it rejected the requested temperature.
|
|
291
298
|
* @property {string|null} model
|
|
292
299
|
* @property {string|null} [retrieval] - (§10 step 7) the retrieval mode this node ran (`scan`/`search`/`exact`),
|
|
293
300
|
* or null/absent for a plain reasoning node.
|
|
@@ -603,6 +610,12 @@ async function recurseRefineLeaf(task, ctx, opts, state) {
|
|
|
603
610
|
tokensSum = tokensSum || {};
|
|
604
611
|
for (const [k, v] of Object.entries(t)) if (typeof v === 'number') tokensSum[k] = (tokensSum[k] || 0) + v;
|
|
605
612
|
};
|
|
613
|
+
// BA-10 honest receipt: the EFFECTIVE temperature per attempt. A model that rejects a non-default
|
|
614
|
+
// `temperature` (400, unsupported/deprecated) runs at its DEFAULT — the provider drops it and the Loop
|
|
615
|
+
// surfaces `temperatureDropped`. Recording the requested temp would claim a value the model ignored, so
|
|
616
|
+
// a dropped attempt is stored as `null` ("provider default"). Indexed by iteration (refine calls once each).
|
|
617
|
+
/** @type {(number|null)[]} */
|
|
618
|
+
const effectiveTemps = [];
|
|
606
619
|
// One attempt = a fresh leaf Loop (no spawn tool: a retry is a direct correction, not a re-decomposition) at the
|
|
607
620
|
// iteration's temperature, with the GAP fed forward as fresh feedback. A governance halt → throw so refine stops.
|
|
608
621
|
const attempt = async ({ iteration, critique }) => {
|
|
@@ -619,6 +632,10 @@ async function recurseRefineLeaf(task, ctx, opts, state) {
|
|
|
619
632
|
? `${base}\n\nYour previous attempt FAILED these checks:\n${critique}\n\nReturn a corrected result that passes ALL of them.`
|
|
620
633
|
: base;
|
|
621
634
|
const out = await loop.run([{ role: 'user', content: userText }], handleTools, { ctx: auditSafeCtx(ctx, { depth }), temperature });
|
|
635
|
+
// `temperatureDropped` is set on the Loop result only when the model rejected the requested temperature
|
|
636
|
+
// (BA-10); it's absent on the error/halt return shapes, so read it through a narrow cast.
|
|
637
|
+
const dropped = /** @type {{temperatureDropped?: boolean}} */ (out).temperatureDropped;
|
|
638
|
+
effectiveTemps[iteration] = dropped ? null : temperature;
|
|
622
639
|
accrueTokens(out.metrics ? out.metrics.tokens : null);
|
|
623
640
|
if (typeof out.error === 'string' && out.error.startsWith('halt:')) throw new HaltError('refine-leaf attempt halted', { rule: out.error.slice('halt:'.length) });
|
|
624
641
|
if (out.error) throw new Error(out.error); // a non-halt worker fault → honest incomplete
|
|
@@ -633,7 +650,10 @@ async function recurseRefineLeaf(task, ctx, opts, state) {
|
|
|
633
650
|
maxIterations,
|
|
634
651
|
});
|
|
635
652
|
node.tokens = tokensSum;
|
|
636
|
-
|
|
653
|
+
// `temperatures` = the EFFECTIVE temps (BA-10): a `null` marks an attempt whose requested temperature the
|
|
654
|
+
// model rejected and ran at its default — so the receipt never claims a value the model ignored. On a
|
|
655
|
+
// temperature-accepting model this equals the requested `temps.slice(0, iterations)` (byte-identical receipt).
|
|
656
|
+
node.refineLeaf = { iterations: outcome.iterations, passed: !!(outcome.verdict && outcome.verdict.pass), temperatures: effectiveTemps.slice(0, outcome.iterations) };
|
|
637
657
|
const result = outcome.result;
|
|
638
658
|
|
|
639
659
|
// Optional rubric layer on top of the deterministic sensor (RC-7): forced for critical, or a contract/override.
|
package/types/index.d.ts
CHANGED
|
@@ -80,6 +80,12 @@ export interface GenerateResult {
|
|
|
80
80
|
usage: Usage;
|
|
81
81
|
/** Model id the response was produced by; preferred over Provider.model for cost accounting. */
|
|
82
82
|
model?: string | null;
|
|
83
|
+
/**
|
|
84
|
+
* True when the requested `temperature` was rejected by the model (400, unsupported/deprecated) and
|
|
85
|
+
* the request was retried without it (BA-10). The response was produced at the model's DEFAULT
|
|
86
|
+
* temperature, not the one requested — callers reporting an effective temperature must honor this.
|
|
87
|
+
*/
|
|
88
|
+
temperatureDropped?: boolean;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
/** A conversation message in OpenAI chat format. */
|