bare-agent 0.23.0 → 0.25.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 +3 -3
- package/bareagent.context.md +6 -2
- package/package.json +1 -1
- package/src/loop.d.ts +22 -2
- package/src/loop.js +45 -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 +35 -13
- package/src/recurse.js +48 -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
|
|
|
@@ -119,9 +119,9 @@ console.log(result.count, result.matchedIds); // a code-derived count + the id
|
|
|
119
119
|
|
|
120
120
|
> **⚠️ Cost is open by design — wire a cap.** `recurse()` adds no intrinsic total-work limit. On the model-driven default a node can spawn up to ~100 children per level, each recursing to `maxDepth` (default 3), so **token / $ spend compounds and is bounded only by your gate** — not by recurse. Run it **with bareguard** (`ctx.policy`, which enforces depth/budget/call caps) **or with some token/USD cap** for any non-trivial or untrusted task; ungoverned, a weak model that over-decomposes *will* burn tokens. For a hard local brake without a gate, set `maxDepth: 1` (flat, no nesting). The forced modes (`mode:'fanout'` / `'partition'`) are bounded by a deterministic count + concurrency cap; the open path is the model-driven default.
|
|
121
121
|
|
|
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')`
|
|
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. A plain deny stays advisory (the model can pivot to an allowed tool), but the Loop short-circuits a *spin* — `maxConsecutiveDenials` consecutive denials of the same action (default 3) stop the run with `error:'denied:<tool>'` instead of burning the budget to the cap; under `recurse` that surfaces as `{ incomplete, blocker:'governance-deny' }`. `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.25.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
|
|
|
@@ -379,6 +379,8 @@ if (result.error?.startsWith('halt:')) {
|
|
|
379
379
|
|
|
380
380
|
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.
|
|
381
381
|
|
|
382
|
+
**Deny-spin short-circuit (`maxConsecutiveDenials`, default 3, v0.25+).** A *non-halt* deny (a `policy` verdict that isn't `true` — e.g. a `humanChannel: deny`, an allowlist miss, a `content`/`fs.writeScope` block) is **advisory**: it's fed back to the model as a tool result so the model can pivot to a different allowed tool. But a model that keeps retrying the *same* denied action would otherwise spin every round until your `budget.maxCostUsd` finally halts it — burning the whole cap with no progress (this bit a coding agent whose write kept tripping `content.askPatterns`). The Loop now counts **consecutive** denials (any allowed call resets the streak, preserving the pivot) and short-circuits at `maxConsecutiveDenials` with `result.error === 'denied:<tool>'` (a clean return, transcript sealed — never a throw). Check `result.error?.startsWith('denied:')` to distinguish a governance block from a completed run; set `maxConsecutiveDenials: 0` (or `Infinity`) on `new Loop({...})` to restore the pure-advisory behavior. Under `recurse`, a short-circuited worker returns a **labeled** `{ incomplete: true, blocker: 'governance-deny' }` (and `receipts.blocker`) so you can widen scope / re-gate / escalate rather than read it as a model failure.
|
|
383
|
+
|
|
382
384
|
Legacy `wrapTool` / `wrapTools` are retained as deprecation shims (one-shot console warning, removal in 1.0). Migration: replace `wrapTools(tools)` at `loop.run()` with `filterTools(tools)` once upfront + `onLlmResult` / `onToolResult` on `new Loop({...})` to pick up LLM-cost recording and `_ctx` threading.
|
|
383
385
|
|
|
384
386
|
**`actionTranslator` for bash/fs primitive activation (v0.10.1+).** Bareguard's `bashCheck` / `fsCheck` / `netCheck` only fire when `action.type === 'bash'` / `'read'` / `'write'` / `'fetch'`. The default action shape is `{type: toolName, args, _ctx}` which matches `tools.denylist` / `tools.allowlist` but does NOT activate those primitives. Adopters who want both pass `wireGate(gate, { actionTranslator })`. Since bareguard 0.4.1+, the primitives read fields from either flat (`action.cmd`) or nested (`action.args.cmd` / `.command`) shapes, so you can pass args through verbatim:
|
|
@@ -673,7 +675,7 @@ const out = await recurse('Audit auth.js, billing.js, gateway.js for authz bugs'
|
|
|
673
675
|
|
|
674
676
|
**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
677
|
|
|
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.
|
|
678
|
+
**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
679
|
|
|
678
680
|
```javascript
|
|
679
681
|
const out = await recurse('Fix the failing function in calc.js', ctx, {
|
|
@@ -741,6 +743,8 @@ new CLIPipe({ command: 'ollama', args: ['run', 'llama3.2'] })
|
|
|
741
743
|
|
|
742
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`.
|
|
743
745
|
|
|
746
|
+
**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
|
+
|
|
744
748
|
**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
749
|
|
|
746
750
|
**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
|
@@ -52,6 +52,16 @@ export type LoopOptions = {
|
|
|
52
52
|
*/
|
|
53
53
|
onLlmResult?: Function | undefined;
|
|
54
54
|
onToolResult?: Function | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* - BA-11 safety net (default 3). Short-circuit the run when
|
|
57
|
+
* `policy` denies this many tool calls IN A ROW with no allowed call in between — a governance deny is
|
|
58
|
+
* not a recoverable tool error, so a model that keeps retrying variants of a denied action would
|
|
59
|
+
* otherwise burn the budget to the cap without progress (probe-16: 16 calls, sensor never reached). Any
|
|
60
|
+
* tool call that PASSES policy resets the streak, preserving allowlist-safe pivoting (deny X → allow Y).
|
|
61
|
+
* The run returns cleanly with `error: 'denied:<tool>'` (mirrors the halt return; never throws even under
|
|
62
|
+
* throwOnError). Set `0` or `Infinity` to disable (restores pre-BA-11 advisory-deny behavior).
|
|
63
|
+
*/
|
|
64
|
+
maxConsecutiveDenials?: number | undefined;
|
|
55
65
|
/**
|
|
56
66
|
* - Removed in v0.8; presence throws a migration error.
|
|
57
67
|
*/
|
|
@@ -79,6 +89,7 @@ export class Loop {
|
|
|
79
89
|
throwOnError: boolean;
|
|
80
90
|
store: import("../types").Store | null;
|
|
81
91
|
policy: Function | null;
|
|
92
|
+
maxConsecutiveDenials: number;
|
|
82
93
|
assemble: Function | null;
|
|
83
94
|
trim: Function | null;
|
|
84
95
|
onLlmResult: Function | null;
|
|
@@ -111,7 +122,7 @@ export class Loop {
|
|
|
111
122
|
* thunk is re-evaluated each round (D4/eval-assist F2) so a tool set that grows mid-run — e.g. a skill
|
|
112
123
|
* unlocking its tools — is offered on the next round; a static array is resolved once at wire time.
|
|
113
124
|
* @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}>}
|
|
125
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics, temperatureDropped?: boolean}>}
|
|
115
126
|
* On halt the returned `error` is `halt:<rule>` (or `halt:unknown` if the
|
|
116
127
|
* thrown HaltError carried no `rule`), and `msgs` is sanitized so any
|
|
117
128
|
* dangling assistant `tool_calls` from the halted round are paired with
|
|
@@ -129,6 +140,7 @@ export class Loop {
|
|
|
129
140
|
error: string | null;
|
|
130
141
|
msgs: Message[];
|
|
131
142
|
metrics: RunMetrics;
|
|
143
|
+
temperatureDropped?: boolean;
|
|
132
144
|
}>;
|
|
133
145
|
/**
|
|
134
146
|
* Health check — validates provider, store, and tools without throwing.
|
|
@@ -156,7 +168,7 @@ export class Loop {
|
|
|
156
168
|
* @param {string} text - User message.
|
|
157
169
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
158
170
|
* @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}>}
|
|
171
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics, temperatureDropped?: boolean}>}
|
|
160
172
|
*/
|
|
161
173
|
chat(text: string, tools?: ToolDef[], options?: Record<string, any>): Promise<{
|
|
162
174
|
text: string;
|
|
@@ -166,6 +178,7 @@ export class Loop {
|
|
|
166
178
|
error: string | null;
|
|
167
179
|
msgs: Message[];
|
|
168
180
|
metrics: RunMetrics;
|
|
181
|
+
temperatureDropped?: boolean;
|
|
169
182
|
}>;
|
|
170
183
|
stop(): void;
|
|
171
184
|
}
|
|
@@ -226,6 +239,13 @@ export function estimateCost(model: string | null, usage: Usage | null): number
|
|
|
226
239
|
* gate.record (via wireGate). `event.kind` discriminates the source: `'turn'` for a main-loop round,
|
|
227
240
|
* `'summarize'` for an out-of-band `ctx.summarize` call (R-C6). Both count against the budget.
|
|
228
241
|
* @property {Function} [onToolResult]
|
|
242
|
+
* @property {number} [maxConsecutiveDenials] - BA-11 safety net (default 3). Short-circuit the run when
|
|
243
|
+
* `policy` denies this many tool calls IN A ROW with no allowed call in between — a governance deny is
|
|
244
|
+
* not a recoverable tool error, so a model that keeps retrying variants of a denied action would
|
|
245
|
+
* otherwise burn the budget to the cap without progress (probe-16: 16 calls, sensor never reached). Any
|
|
246
|
+
* tool call that PASSES policy resets the streak, preserving allowlist-safe pivoting (deny X → allow Y).
|
|
247
|
+
* The run returns cleanly with `error: 'denied:<tool>'` (mirrors the halt return; never throws even under
|
|
248
|
+
* throwOnError). Set `0` or `Infinity` to disable (restores pre-BA-11 advisory-deny behavior).
|
|
229
249
|
* @property {number} [maxRounds] - Removed in v0.8; presence throws a migration error.
|
|
230
250
|
*/
|
|
231
251
|
/** @type {Record<string, {in: number, out: number, cacheReadMult?: number, cacheWriteMult?: number}>} */
|
package/src/loop.js
CHANGED
|
@@ -49,6 +49,13 @@ const { ToolError, HaltError } = require('./errors');
|
|
|
49
49
|
* gate.record (via wireGate). `event.kind` discriminates the source: `'turn'` for a main-loop round,
|
|
50
50
|
* `'summarize'` for an out-of-band `ctx.summarize` call (R-C6). Both count against the budget.
|
|
51
51
|
* @property {Function} [onToolResult]
|
|
52
|
+
* @property {number} [maxConsecutiveDenials] - BA-11 safety net (default 3). Short-circuit the run when
|
|
53
|
+
* `policy` denies this many tool calls IN A ROW with no allowed call in between — a governance deny is
|
|
54
|
+
* not a recoverable tool error, so a model that keeps retrying variants of a denied action would
|
|
55
|
+
* otherwise burn the budget to the cap without progress (probe-16: 16 calls, sensor never reached). Any
|
|
56
|
+
* tool call that PASSES policy resets the streak, preserving allowlist-safe pivoting (deny X → allow Y).
|
|
57
|
+
* The run returns cleanly with `error: 'denied:<tool>'` (mirrors the halt return; never throws even under
|
|
58
|
+
* throwOnError). Set `0` or `Infinity` to disable (restores pre-BA-11 advisory-deny behavior).
|
|
52
59
|
* @property {number} [maxRounds] - Removed in v0.8; presence throws a migration error.
|
|
53
60
|
*/
|
|
54
61
|
|
|
@@ -212,6 +219,13 @@ class Loop {
|
|
|
212
219
|
throw new Error('[Loop] options.policy must be a function (toolName, args, ctx) => true | string');
|
|
213
220
|
}
|
|
214
221
|
this.policy = options.policy || null;
|
|
222
|
+
// BA-11 deny-spin guard. Default 3; 0/Infinity/non-finite disables (restores advisory-deny behavior).
|
|
223
|
+
// Validated only if provided so an explicit 0 is honored as "off" (Infinity also disables).
|
|
224
|
+
if (options.maxConsecutiveDenials != null
|
|
225
|
+
&& (typeof options.maxConsecutiveDenials !== 'number' || options.maxConsecutiveDenials < 0 || Number.isNaN(options.maxConsecutiveDenials))) {
|
|
226
|
+
throw new Error('[Loop] options.maxConsecutiveDenials must be a non-negative number (0 or Infinity disables)');
|
|
227
|
+
}
|
|
228
|
+
this.maxConsecutiveDenials = options.maxConsecutiveDenials != null ? options.maxConsecutiveDenials : 3;
|
|
215
229
|
if (options.assemble != null && typeof options.assemble !== 'function') {
|
|
216
230
|
throw new Error('[Loop] options.assemble must be a function (msgs, info) => msgs');
|
|
217
231
|
}
|
|
@@ -294,7 +308,7 @@ class Loop {
|
|
|
294
308
|
* thunk is re-evaluated each round (D4/eval-assist F2) so a tool set that grows mid-run — e.g. a skill
|
|
295
309
|
* unlocking its tools — is offered on the next round; a static array is resolved once at wire time.
|
|
296
310
|
* @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}>}
|
|
311
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics, temperatureDropped?: boolean}>}
|
|
298
312
|
* On halt the returned `error` is `halt:<rule>` (or `halt:unknown` if the
|
|
299
313
|
* thrown HaltError carried no `rule`), and `msgs` is sanitized so any
|
|
300
314
|
* dangling assistant `tool_calls` from the halted round are paired with
|
|
@@ -356,6 +370,13 @@ class Loop {
|
|
|
356
370
|
|
|
357
371
|
let lastUsage = { inputTokens: 0, outputTokens: 0 };
|
|
358
372
|
let totalCost = 0;
|
|
373
|
+
// BA-10: sticky across rounds — true if ANY round's `temperature` was dropped by the model (400,
|
|
374
|
+
// unsupported/deprecated) and retried without it. Surfaced on the result so an upstream receipt
|
|
375
|
+
// (recurse's refineLeaf) can report the EFFECTIVE temperature rather than the ignored request.
|
|
376
|
+
let temperatureDropped = false;
|
|
377
|
+
// BA-11: consecutive policy-deny counter (reset by any tool call that PASSES policy). When it reaches
|
|
378
|
+
// this.maxConsecutiveDenials the run short-circuits cleanly — see the deny block below.
|
|
379
|
+
let consecutiveDenials = 0;
|
|
359
380
|
|
|
360
381
|
// The meter (Feature 3): bareagent is the canonical run counter. Accumulates across rounds and is
|
|
361
382
|
// returned as `result.metrics`. `tokens` is CUMULATIVE over all four tiers (fixes the last-round-only
|
|
@@ -583,6 +604,7 @@ class Loop {
|
|
|
583
604
|
}
|
|
584
605
|
|
|
585
606
|
lastUsage = result.usage || lastUsage;
|
|
607
|
+
if (result.temperatureDropped) temperatureDropped = true;
|
|
586
608
|
// Publish the latest measured usage to ctx (non-enumerable, fail-open) so a transcript-bound seam —
|
|
587
609
|
// e.g. F2 stash auto-compaction — can read EXACT provider-counted `inputTokens` to gauge context
|
|
588
610
|
// pressure on the NEXT round's trim. Symmetric with lending ctx.summarize; the Loop stays unaware of
|
|
@@ -642,7 +664,7 @@ class Loop {
|
|
|
642
664
|
try { await flush(msgs, ctx); }
|
|
643
665
|
catch (err) { if (err instanceof HaltError) throw err; this._reportError('trim-flush', err, { round }); }
|
|
644
666
|
}
|
|
645
|
-
return { text: result.text, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, msgs, metrics: finalizeMetrics() };
|
|
667
|
+
return { text: result.text, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, msgs, metrics: finalizeMetrics(), ...(temperatureDropped && { temperatureDropped: true }) };
|
|
646
668
|
}
|
|
647
669
|
|
|
648
670
|
// Execute tool calls
|
|
@@ -726,10 +748,30 @@ class Loop {
|
|
|
726
748
|
: `[Loop] Tool "${tc.name}" denied by policy`;
|
|
727
749
|
msgs.push({ role: 'tool', tool_call_id: tc.id, content: reason });
|
|
728
750
|
this._safeEmit({ type: 'loop:tool_result', data: { tool: tc.name, denied: true, reason } });
|
|
751
|
+
// BA-11: a governance deny is not a recoverable tool error. Count consecutive denials; when the
|
|
752
|
+
// model keeps retrying denied actions (proven live: 8 in a row before giving up) short-circuit the
|
|
753
|
+
// run rather than let it burn the budget to the cap. The streak resets on any ALLOWED tool call
|
|
754
|
+
// (below), so a legit deny-then-pivot (deny X → allow Y) never trips this. 0/Infinity disables.
|
|
755
|
+
consecutiveDenials += 1;
|
|
756
|
+
if (this.maxConsecutiveDenials > 0 && Number.isFinite(this.maxConsecutiveDenials)
|
|
757
|
+
&& consecutiveDenials >= this.maxConsecutiveDenials) {
|
|
758
|
+
const denyTag = `denied:${tc.name}`;
|
|
759
|
+
// Pair any still-dangling tool_calls from this round so the returned transcript stays
|
|
760
|
+
// provider-valid (same seal the halt path uses), then exit cleanly — no throw even under
|
|
761
|
+
// throwOnError, mirroring the governance-halt contract.
|
|
762
|
+
sealDanglingToolCalls(msgs, denyTag);
|
|
763
|
+
this._reportError('denied', new Error(`policy denied ${consecutiveDenials} consecutive tool calls (${tc.name})`), { rule: denyTag, denials: consecutiveDenials });
|
|
764
|
+
this._safeEmit({ type: 'loop:done', data: { text: '', denied: true, rule: denyTag, cost: totalCost } });
|
|
765
|
+
return { text: '', toolCalls: [], usage: lastUsage, cost: totalCost, error: denyTag, msgs, metrics: finalizeMetrics() };
|
|
766
|
+
}
|
|
729
767
|
continue;
|
|
730
768
|
}
|
|
731
769
|
}
|
|
732
770
|
|
|
771
|
+
// BA-11: reaching here means this tool call PASSED policy (or there is no policy) — progress, so the
|
|
772
|
+
// consecutive-deny streak resets. A single deny followed by an allowed call never trips the guard.
|
|
773
|
+
consecutiveDenials = 0;
|
|
774
|
+
|
|
733
775
|
const toolStartedAt = Date.now();
|
|
734
776
|
let toolResult;
|
|
735
777
|
let toolError;
|
|
@@ -863,7 +905,7 @@ class Loop {
|
|
|
863
905
|
* @param {string} text - User message.
|
|
864
906
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
865
907
|
* @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}>}
|
|
908
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics, temperatureDropped?: boolean}>}
|
|
867
909
|
*/
|
|
868
910
|
async chat(text, tools = [], options = {}) {
|
|
869
911
|
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
|
|
@@ -213,19 +215,25 @@ export type RecurseNode = {
|
|
|
213
215
|
verdict: Verdict | null;
|
|
214
216
|
incomplete: boolean;
|
|
215
217
|
halted: boolean;
|
|
218
|
+
/**
|
|
219
|
+
* - (BA-11) set to `'governance-deny'` when this node stopped because its Loop
|
|
220
|
+
* short-circuited a consecutive-policy-deny spin (not a model failure). Mirrors `RecurseResult.blocker`.
|
|
221
|
+
*/
|
|
222
|
+
blocker?: string | undefined;
|
|
216
223
|
/**
|
|
217
224
|
* - The worker Loop's `metrics.tokens`.
|
|
218
225
|
*/
|
|
219
226
|
tokens: object | null;
|
|
220
227
|
/**
|
|
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).
|
|
228
|
+
* - (BA-8) when this
|
|
229
|
+
* leaf ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally
|
|
230
|
+
* passed (false = honest non-recovery, not a faked success). `temperatures` are the EFFECTIVE per-attempt temps
|
|
231
|
+
* (BA-10): a `null` marks an attempt the model ran at its DEFAULT because it rejected the requested temperature.
|
|
224
232
|
*/
|
|
225
233
|
refineLeaf?: {
|
|
226
234
|
iterations: number;
|
|
227
235
|
passed: boolean;
|
|
228
|
-
temperatures: number[];
|
|
236
|
+
temperatures: (number | null)[];
|
|
229
237
|
} | undefined;
|
|
230
238
|
model: string | null;
|
|
231
239
|
/**
|
|
@@ -284,6 +292,12 @@ export type RecurseResult = {
|
|
|
284
292
|
* back incomplete (§9 scenario 1) — the anti-survivor-sum signal, not a quiet undercount.
|
|
285
293
|
*/
|
|
286
294
|
missingSlices?: string[] | undefined;
|
|
295
|
+
/**
|
|
296
|
+
* - Present when `incomplete` for a specific, actionable reason. `'governance-deny'`
|
|
297
|
+
* (BA-11): the worker's Loop short-circuited after N consecutive policy denials rather than burn to the
|
|
298
|
+
* budget cap — the caller can widen scope / re-gate / escalate instead of reading it as a model failure.
|
|
299
|
+
*/
|
|
300
|
+
blocker?: string | undefined;
|
|
287
301
|
/**
|
|
288
302
|
* - The audit node for this call (RC-10).
|
|
289
303
|
*/
|
|
@@ -353,9 +367,11 @@ export type Slice = {
|
|
|
353
367
|
* tier or at `maxDepth`) into a bounded generate→sense→regenerate loop instead of a single pass, so a failed
|
|
354
368
|
* slice can self-correct. `sensor` is a DETERMINISTIC close (test/compile/lint — NOT a model judge, R-S8) that
|
|
355
369
|
* 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).
|
|
370
|
+
* attempt (D6/A1 anti-anchoring) and, on models that ACCEPT `temperature`, the **retry temperature ESCALATES**
|
|
371
|
+
* (`temperatures`, default `[0.2,0.7,1.0]`) — the live-validated lever that lets a weak model escape a
|
|
372
|
+
* repeat-the-same-mistake rut (`poc/ba8-leaf-refine.mjs`: 0/5 → 2-3/5; flat temp recovers 0/5). On a
|
|
373
|
+
* temperature-fixed model (BA-10) the provider drops the param, `receipts.refineLeaf.temperatures` records
|
|
374
|
+
* `null`, and the fed-back gap critique carries recovery alone. `maxIterations` defaults to
|
|
359
375
|
* `temperatures.length`; the REAL bound is bareguard (each attempt is gate-checked + metered). CARRIES DOWN the
|
|
360
376
|
* tree (preserved by `forChild`), so it engages at the leaves of a Family-A decomposition. Recovery is PARTIAL
|
|
361
377
|
* (a stubborn blind spot may persist) — `receipts.refineLeaf.passed` reports honestly. Does NOT apply to a node
|
|
@@ -419,10 +435,13 @@ export type Slice = {
|
|
|
419
435
|
* @property {Verdict|null} verdict
|
|
420
436
|
* @property {boolean} incomplete
|
|
421
437
|
* @property {boolean} halted
|
|
438
|
+
* @property {string} [blocker] - (BA-11) set to `'governance-deny'` when this node stopped because its Loop
|
|
439
|
+
* short-circuited a consecutive-policy-deny spin (not a model failure). Mirrors `RecurseResult.blocker`.
|
|
422
440
|
* @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).
|
|
441
|
+
* @property {{iterations: number, passed: boolean, temperatures: (number|null)[]}} [refineLeaf] - (BA-8) when this
|
|
442
|
+
* leaf ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally
|
|
443
|
+
* passed (false = honest non-recovery, not a faked success). `temperatures` are the EFFECTIVE per-attempt temps
|
|
444
|
+
* (BA-10): a `null` marks an attempt the model ran at its DEFAULT because it rejected the requested temperature.
|
|
426
445
|
* @property {string|null} model
|
|
427
446
|
* @property {string|null} [retrieval] - (§10 step 7) the retrieval mode this node ran (`scan`/`search`/`exact`),
|
|
428
447
|
* or null/absent for a plain reasoning node.
|
|
@@ -443,6 +462,9 @@ export type Slice = {
|
|
|
443
462
|
* @property {any} [best] - The best partial answer when `incomplete` (RC-9).
|
|
444
463
|
* @property {string[]} [missingSlices] - When `incomplete` because a child failed: the sub-task(s) that came
|
|
445
464
|
* back incomplete (§9 scenario 1) — the anti-survivor-sum signal, not a quiet undercount.
|
|
465
|
+
* @property {string} [blocker] - Present when `incomplete` for a specific, actionable reason. `'governance-deny'`
|
|
466
|
+
* (BA-11): the worker's Loop short-circuited after N consecutive policy denials rather than burn to the
|
|
467
|
+
* budget cap — the caller can widen scope / re-gate / escalate instead of reading it as a model failure.
|
|
446
468
|
* @property {RecurseNode} receipts - The audit node for this call (RC-10).
|
|
447
469
|
*/
|
|
448
470
|
/**
|
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
|
|
@@ -284,10 +290,13 @@ function auditSafeCtx(ctx, overrides = {}) {
|
|
|
284
290
|
* @property {Verdict|null} verdict
|
|
285
291
|
* @property {boolean} incomplete
|
|
286
292
|
* @property {boolean} halted
|
|
293
|
+
* @property {string} [blocker] - (BA-11) set to `'governance-deny'` when this node stopped because its Loop
|
|
294
|
+
* short-circuited a consecutive-policy-deny spin (not a model failure). Mirrors `RecurseResult.blocker`.
|
|
287
295
|
* @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).
|
|
296
|
+
* @property {{iterations: number, passed: boolean, temperatures: (number|null)[]}} [refineLeaf] - (BA-8) when this
|
|
297
|
+
* leaf ran as a bounded refine loop: how many attempts it took and whether the deterministic sensor finally
|
|
298
|
+
* passed (false = honest non-recovery, not a faked success). `temperatures` are the EFFECTIVE per-attempt temps
|
|
299
|
+
* (BA-10): a `null` marks an attempt the model ran at its DEFAULT because it rejected the requested temperature.
|
|
291
300
|
* @property {string|null} model
|
|
292
301
|
* @property {string|null} [retrieval] - (§10 step 7) the retrieval mode this node ran (`scan`/`search`/`exact`),
|
|
293
302
|
* or null/absent for a plain reasoning node.
|
|
@@ -309,6 +318,9 @@ function auditSafeCtx(ctx, overrides = {}) {
|
|
|
309
318
|
* @property {any} [best] - The best partial answer when `incomplete` (RC-9).
|
|
310
319
|
* @property {string[]} [missingSlices] - When `incomplete` because a child failed: the sub-task(s) that came
|
|
311
320
|
* back incomplete (§9 scenario 1) — the anti-survivor-sum signal, not a quiet undercount.
|
|
321
|
+
* @property {string} [blocker] - Present when `incomplete` for a specific, actionable reason. `'governance-deny'`
|
|
322
|
+
* (BA-11): the worker's Loop short-circuited after N consecutive policy denials rather than burn to the
|
|
323
|
+
* budget cap — the caller can widen scope / re-gate / escalate instead of reading it as a model failure.
|
|
312
324
|
* @property {RecurseNode} receipts - The audit node for this call (RC-10).
|
|
313
325
|
*/
|
|
314
326
|
|
|
@@ -503,6 +515,15 @@ async function recurse(task, ctx = {}, opts = {}) {
|
|
|
503
515
|
node.incomplete = true;
|
|
504
516
|
return { incomplete: true, best: out.text || null, receipts: node };
|
|
505
517
|
}
|
|
518
|
+
// BA-11: a deny-spin short-circuit. The Loop stopped the worker after N consecutive governance denials
|
|
519
|
+
// (a governance deny is not a recoverable tool error — retrying variants would burn to the budget cap;
|
|
520
|
+
// probe-16: 16 calls, sensor never reached → incomplete). Surface it as a clean, LABELED incomplete so a
|
|
521
|
+
// caller can tell a governance block apart from a model failure and act (widen scope, re-gate, escalate).
|
|
522
|
+
if (typeof out.error === 'string' && out.error.startsWith('denied:')) {
|
|
523
|
+
node.incomplete = true;
|
|
524
|
+
node.blocker = 'governance-deny';
|
|
525
|
+
return { incomplete: true, best: out.text || null, blocker: 'governance-deny', receipts: node };
|
|
526
|
+
}
|
|
506
527
|
if (out.error) {
|
|
507
528
|
node.incomplete = true;
|
|
508
529
|
return { incomplete: true, best: out.text || null, receipts: node };
|
|
@@ -603,6 +624,12 @@ async function recurseRefineLeaf(task, ctx, opts, state) {
|
|
|
603
624
|
tokensSum = tokensSum || {};
|
|
604
625
|
for (const [k, v] of Object.entries(t)) if (typeof v === 'number') tokensSum[k] = (tokensSum[k] || 0) + v;
|
|
605
626
|
};
|
|
627
|
+
// BA-10 honest receipt: the EFFECTIVE temperature per attempt. A model that rejects a non-default
|
|
628
|
+
// `temperature` (400, unsupported/deprecated) runs at its DEFAULT — the provider drops it and the Loop
|
|
629
|
+
// surfaces `temperatureDropped`. Recording the requested temp would claim a value the model ignored, so
|
|
630
|
+
// a dropped attempt is stored as `null` ("provider default"). Indexed by iteration (refine calls once each).
|
|
631
|
+
/** @type {(number|null)[]} */
|
|
632
|
+
const effectiveTemps = [];
|
|
606
633
|
// One attempt = a fresh leaf Loop (no spawn tool: a retry is a direct correction, not a re-decomposition) at the
|
|
607
634
|
// iteration's temperature, with the GAP fed forward as fresh feedback. A governance halt → throw so refine stops.
|
|
608
635
|
const attempt = async ({ iteration, critique }) => {
|
|
@@ -619,6 +646,10 @@ async function recurseRefineLeaf(task, ctx, opts, state) {
|
|
|
619
646
|
? `${base}\n\nYour previous attempt FAILED these checks:\n${critique}\n\nReturn a corrected result that passes ALL of them.`
|
|
620
647
|
: base;
|
|
621
648
|
const out = await loop.run([{ role: 'user', content: userText }], handleTools, { ctx: auditSafeCtx(ctx, { depth }), temperature });
|
|
649
|
+
// `temperatureDropped` is set on the Loop result only when the model rejected the requested temperature
|
|
650
|
+
// (BA-10); it's absent on the error/halt return shapes, so read it through a narrow cast.
|
|
651
|
+
const dropped = /** @type {{temperatureDropped?: boolean}} */ (out).temperatureDropped;
|
|
652
|
+
effectiveTemps[iteration] = dropped ? null : temperature;
|
|
622
653
|
accrueTokens(out.metrics ? out.metrics.tokens : null);
|
|
623
654
|
if (typeof out.error === 'string' && out.error.startsWith('halt:')) throw new HaltError('refine-leaf attempt halted', { rule: out.error.slice('halt:'.length) });
|
|
624
655
|
if (out.error) throw new Error(out.error); // a non-halt worker fault → honest incomplete
|
|
@@ -633,7 +664,10 @@ async function recurseRefineLeaf(task, ctx, opts, state) {
|
|
|
633
664
|
maxIterations,
|
|
634
665
|
});
|
|
635
666
|
node.tokens = tokensSum;
|
|
636
|
-
|
|
667
|
+
// `temperatures` = the EFFECTIVE temps (BA-10): a `null` marks an attempt whose requested temperature the
|
|
668
|
+
// model rejected and ran at its default — so the receipt never claims a value the model ignored. On a
|
|
669
|
+
// temperature-accepting model this equals the requested `temps.slice(0, iterations)` (byte-identical receipt).
|
|
670
|
+
node.refineLeaf = { iterations: outcome.iterations, passed: !!(outcome.verdict && outcome.verdict.pass), temperatures: effectiveTemps.slice(0, outcome.iterations) };
|
|
637
671
|
const result = outcome.result;
|
|
638
672
|
|
|
639
673
|
// Optional rubric layer on top of the deterministic sensor (RC-7): forced for critical, or a contract/override.
|
|
@@ -653,6 +687,13 @@ async function recurseRefineLeaf(task, ctx, opts, state) {
|
|
|
653
687
|
node.incomplete = true;
|
|
654
688
|
return { incomplete: true, best: null, receipts: node };
|
|
655
689
|
}
|
|
690
|
+
// BA-11: a deny-spin inside a refine attempt (the Loop short-circuited after N consecutive governance
|
|
691
|
+
// denials, rethrown at recurse.js as `denied:<tool>`) is a LABELED governance block, not a model fault.
|
|
692
|
+
if (typeof err?.message === 'string' && err.message.startsWith('denied:')) {
|
|
693
|
+
node.incomplete = true;
|
|
694
|
+
node.blocker = 'governance-deny';
|
|
695
|
+
return { incomplete: true, best: null, blocker: 'governance-deny', receipts: node };
|
|
696
|
+
}
|
|
656
697
|
node.incomplete = true;
|
|
657
698
|
return { incomplete: true, best: null, receipts: node };
|
|
658
699
|
}
|
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. */
|