bare-agent 0.51.0 → 0.52.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 +1 -1
- package/bareagent.context.md +2 -2
- package/package.json +1 -1
- package/primitives.json +1 -1
- package/src/loop.d.ts +4 -2
- package/src/loop.js +30 -20
- package/src/provider-anthropic.d.ts +6 -7
- package/src/provider-anthropic.js +16 -11
- package/src/provider-clipipe.d.ts +11 -2
- package/src/provider-clipipe.js +22 -10
- package/src/provider-gemini.d.ts +6 -7
- package/src/provider-gemini.js +16 -11
- package/src/provider-ollama.d.ts +6 -7
- package/src/provider-ollama.js +16 -11
- package/src/provider-openai.d.ts +8 -9
- package/src/provider-openai.js +24 -17
- package/src/provider-temperature.d.ts +2 -2
- package/src/provider-temperature.js +5 -3
- package/src/thinking-mode.d.ts +75 -26
- package/src/thinking-mode.js +130 -71
- package/types/index.d.ts +16 -6
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Some things an LLM can't judge about its own work. Building a harness or an exte
|
|
|
33
33
|
|
|
34
34
|
## Fast gut check — Jev
|
|
35
35
|
|
|
36
|
-
An LLM call is slow, deliberate thinking (System 2). Jev, from TypeSafe, is the fast gut check (System 1): a classifier that returns a typed decision with a calibrated probability —
|
|
36
|
+
An LLM call is slow, deliberate thinking (System 2). Jev, from TypeSafe, is the fast gut check (System 1): a classifier that returns a typed decision with a calibrated probability — far faster and cheaper than an LLM call. bare-agent's `JevProvider` covers all three shapes — **yes/no**, **pick-one**, **score** — so your automation calls it like any other step: act when it's confident, escalate when it isn't. Injection-hardened by default; also usable as the `jev` check in the Evaluator.
|
|
37
37
|
|
|
38
38
|
## Break big tasks down — RLM
|
|
39
39
|
|
package/bareagent.context.md
CHANGED
|
@@ -513,11 +513,11 @@ new OpenAIProvider({ baseUrl: 'https://api.deepseek.com', model: 'deepseek-flash
|
|
|
513
513
|
|
|
514
514
|
> **Fallback caveat:** per-call options (incl. `thinking`) are forwarded to EVERY provider in a `FallbackProvider` chain. For a mixed-vendor chain set `thinking` on each provider's constructor, not per-call.
|
|
515
515
|
|
|
516
|
-
**Normalized `thinkingMode: 'on' | 'off'`.** Say "on"/"off"; bare-agent sends the right field for the provider/model (`AnthropicProvider`, `OpenAIProvider` incl. DeepSeek, `GeminiProvider`, `OllamaProvider`): constructor option, overridable per call (`generate(..., { thinkingMode })` / `loop.run(msgs, tools, { thinkingMode })`), `null` suppresses. It is a NEW field — the raw `thinking` option above is untouched, and setting both is a `ValidationError`. It tries the known request shapes (a 400
|
|
516
|
+
**Normalized `thinkingMode: 'on' | 'off'`.** Say "on"/"off"; bare-agent sends the right field for the provider/model (`AnthropicProvider`, `OpenAIProvider` incl. DeepSeek, `GeminiProvider`, `OllamaProvider`): constructor option, overridable per call (`generate(..., { thinkingMode })` / `loop.run(msgs, tools, { thinkingMode })`), `null` suppresses. It is a NEW field — the raw `thinking` option above is untouched, and setting both is a `ValidationError`. It tries the known request shapes (a client validation error — 400, or 422 — walks the ladder; the winning shape is cached) and **always runs the call** — callers need not care about providers. After the shapes, ONE plain request (no thinking field) decides: it succeeds ⇒ the setting was the problem and the result says `unsupported` (warns once); it fails too ⇒ a real error, thrown as the plain request's own (never a `ValidationError`). 5xx/429/timeouts/401/403/404 propagate at once with no ladder. A model measured as not switchable (`claude-opus-5-5` off, `gemini-3.1-pro-preview` off) goes out once WITHOUT a thinking field. "Unsupported" is never cached, so an unknown model that can't switch pays its refused shape requests on each call. Every reply carries `result.thinkingStatus`: `'applied'` (accepted; for `'off'`, zero thinking seen), `'unsupported'` (this provider/model can't do it — the call ran without our field), `'ignored'` (`'off'` accepted but the model thought anyway — warns once, result kept), or `'unknown'` (accepted, signal unreadable; e.g. OpenAI without `reasoning_tokens`). `result.thinkingApplied` is kept and derived (`true`/`false`/`false`/`null`). On `loop.run()` the WORST status across rounds wins (ignored > unsupported > unknown > applied). Still a `ValidationError`: a bad value, or raw `thinking` + `thinkingMode` together. Anthropic `'on'` + a non-default `temperature` (the API rejects it with thinking enabled): the temperature is dropped and retried once, `temperatureDropped: true`. `'on'` is never flagged (adaptive models may skip thinking). `thinkingSupport(provider, model, { baseUrl }?)` from `bare-agent/providers` (`baseUrl` makes an OpenAI-wire DeepSeek host answer with DeepSeek shapes, same detection as the request path) tells you, offline, what is known. DeepSeek `'on'` + a forced `toolChoice` can't think, so it is sent with `thinking:{type:'disabled'}` and reports `'unsupported'`; CLIPipe has no mechanism and runs unchanged, `'unsupported'`. In a `FallbackProvider` a not-switchable member just runs (no fall-through). Ollama's `think` is not live-verified.
|
|
517
517
|
|
|
518
518
|
```js
|
|
519
519
|
const p = new AnthropicProvider({ apiKey, model: 'claude-sonnet-5', thinkingMode: 'off' });
|
|
520
|
-
const r = await p.generate(msgs, [], {}); // r.thinkingApplied === true
|
|
520
|
+
const r = await p.generate(msgs, [], {}); // r.thinkingStatus === 'applied' (r.thinkingApplied === true)
|
|
521
521
|
await p.generate(msgs, [], { thinkingMode: 'on' }); // per-call override
|
|
522
522
|
```
|
|
523
523
|
|
package/package.json
CHANGED
package/primitives.json
CHANGED
|
@@ -447,7 +447,7 @@
|
|
|
447
447
|
"category": "providers",
|
|
448
448
|
"when": "you need to know, before building a request, whether a provider/model can have thinking turned on or off and which request field does it — pure lookup, no network",
|
|
449
449
|
"import": "import { thinkingSupport } from 'bare-agent/providers'",
|
|
450
|
-
"signature": "thinkingSupport(provider: 'anthropic'|'openai'|'deepseek'|'gemini'|'ollama'|'clipipe'|string, model?: string) => { switchable: boolean|'unverified', off: string|null, on: string|null, note: string }",
|
|
450
|
+
"signature": "thinkingSupport(provider: 'anthropic'|'openai'|'deepseek'|'gemini'|'ollama'|'clipipe'|string, model?: string, opts?: { baseUrl?: string }) => { switchable: boolean|'unverified', off: string|null, on: string|null, note: string }",
|
|
451
451
|
"fails": "returns switchable:false for clipipe and unknown providers (never throws); 'unverified' means not measured, not unsupported; Ollama's shapes are not live-verified.",
|
|
452
452
|
"example": "thinkingSupport('anthropic', 'claude-opus-5-5'); // { switchable: false, off: null, on: \"thinking:{type:'adaptive'}\", note: '...' }"
|
|
453
453
|
},
|
package/src/loop.d.ts
CHANGED
|
@@ -167,7 +167,7 @@ export class Loop {
|
|
|
167
167
|
* thunk is re-evaluated each round (D4/eval-assist F2) so a tool set that grows mid-run — e.g. a skill
|
|
168
168
|
* unlocking its tools — is offered on the next round; a static array is resolved once at wire time.
|
|
169
169
|
* @param {Record<string, any>} [options={}] - Per-run overrides (system, temperature, ctx, etc.).
|
|
170
|
-
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, malformedToolCall?: GenerateResult['malformedToolCall'], temperatureDropped?: boolean, thinkingApplied?: boolean|null}>}
|
|
170
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, malformedToolCall?: GenerateResult['malformedToolCall'], temperatureDropped?: boolean, thinkingApplied?: boolean|null, thinkingStatus?: 'applied'|'unsupported'|'ignored'|'unknown'}>}
|
|
171
171
|
* On halt the returned `error` is `halt:<rule>` (or `halt:unknown` if the
|
|
172
172
|
* thrown HaltError carried no `rule`), and `msgs` is sanitized so any
|
|
173
173
|
* dangling assistant `tool_calls` from the halted round are paired with
|
|
@@ -208,6 +208,7 @@ export class Loop {
|
|
|
208
208
|
malformedToolCall?: GenerateResult["malformedToolCall"];
|
|
209
209
|
temperatureDropped?: boolean;
|
|
210
210
|
thinkingApplied?: boolean | null;
|
|
211
|
+
thinkingStatus?: "applied" | "unsupported" | "ignored" | "unknown";
|
|
211
212
|
}>;
|
|
212
213
|
_warnedTruncated: boolean | undefined;
|
|
213
214
|
/**
|
|
@@ -236,7 +237,7 @@ export class Loop {
|
|
|
236
237
|
* @param {string} text - User message.
|
|
237
238
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
238
239
|
* @param {Record<string, any>} [options={}] - Per-run overrides.
|
|
239
|
-
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, malformedToolCall?: GenerateResult['malformedToolCall'], temperatureDropped?: boolean, thinkingApplied?: boolean|null}>}
|
|
240
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, malformedToolCall?: GenerateResult['malformedToolCall'], temperatureDropped?: boolean, thinkingApplied?: boolean|null, thinkingStatus?: 'applied'|'unsupported'|'ignored'|'unknown'}>}
|
|
240
241
|
*/
|
|
241
242
|
chat(text: string, tools?: ToolDef[], options?: Record<string, any>): Promise<{
|
|
242
243
|
text: string;
|
|
@@ -251,6 +252,7 @@ export class Loop {
|
|
|
251
252
|
malformedToolCall?: GenerateResult["malformedToolCall"];
|
|
252
253
|
temperatureDropped?: boolean;
|
|
253
254
|
thinkingApplied?: boolean | null;
|
|
255
|
+
thinkingStatus?: "applied" | "unsupported" | "ignored" | "unknown";
|
|
254
256
|
}>;
|
|
255
257
|
/**
|
|
256
258
|
* Request a clean stop. The current tool call finishes; the loop then exits at the next boundary and
|
package/src/loop.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const { ToolError, HaltError } = require('./errors');
|
|
4
4
|
const { classifyStopReason } = require('./provider-stop-reason');
|
|
5
|
+
const { appliedOf } = require('./thinking-mode');
|
|
6
|
+
|
|
7
|
+
// Worst-wins precedence for the run's `thinkingStatus`. Null-prototype so a provider-supplied
|
|
8
|
+
// status string like 'constructor' cannot match an inherited key.
|
|
9
|
+
/** @type {Record<string, number>} */
|
|
10
|
+
const THINKING_STATUS_RANK = Object.assign(Object.create(null), { applied: 0, unknown: 1, unsupported: 2, ignored: 3 });
|
|
5
11
|
|
|
6
12
|
/** @typedef {import('../types').Provider} Provider */
|
|
7
13
|
/** @typedef {import('../types').Message} Message */
|
|
@@ -455,7 +461,7 @@ class Loop {
|
|
|
455
461
|
* thunk is re-evaluated each round (D4/eval-assist F2) so a tool set that grows mid-run — e.g. a skill
|
|
456
462
|
* unlocking its tools — is offered on the next round; a static array is resolved once at wire time.
|
|
457
463
|
* @param {Record<string, any>} [options={}] - Per-run overrides (system, temperature, ctx, etc.).
|
|
458
|
-
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, malformedToolCall?: GenerateResult['malformedToolCall'], temperatureDropped?: boolean, thinkingApplied?: boolean|null}>}
|
|
464
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, malformedToolCall?: GenerateResult['malformedToolCall'], temperatureDropped?: boolean, thinkingApplied?: boolean|null, thinkingStatus?: 'applied'|'unsupported'|'ignored'|'unknown'}>}
|
|
459
465
|
* On halt the returned `error` is `halt:<rule>` (or `halt:unknown` if the
|
|
460
466
|
* thrown HaltError carried no `rule`), and `msgs` is sanitized so any
|
|
461
467
|
* dangling assistant `tool_calls` from the halted round are paired with
|
|
@@ -565,10 +571,12 @@ class Loop {
|
|
|
565
571
|
// unsupported/deprecated) and retried without it. Surfaced on the result so an upstream receipt
|
|
566
572
|
// (recurse's refineLeaf) can report the EFFECTIVE temperature rather than the ignored request.
|
|
567
573
|
let temperatureDropped = false;
|
|
568
|
-
// Normalized thinkingMode (src/thinking-mode.js): the run's `
|
|
569
|
-
// (
|
|
570
|
-
|
|
571
|
-
|
|
574
|
+
// Normalized thinkingMode (src/thinking-mode.js): the run's `thinkingStatus` is the WORST status across
|
|
575
|
+
// rounds (ignored > unsupported > unknown > applied); `thinkingApplied` is derived from it (applied⇒true,
|
|
576
|
+
// ignored/unsupported⇒false, unknown⇒null). undefined ⇒ no round carried either ⇒ both keys absent.
|
|
577
|
+
/** @type {'applied'|'unsupported'|'ignored'|'unknown'|undefined} */
|
|
578
|
+
let worstThinkingStatus;
|
|
579
|
+
const thinkingFields = () => worstThinkingStatus === undefined ? undefined : { thinkingStatus: worstThinkingStatus, thinkingApplied: appliedOf(worstThinkingStatus) };
|
|
572
580
|
// BA-11: consecutive policy-deny counter (reset by any tool call that PASSES policy). When it reaches
|
|
573
581
|
// this.maxConsecutiveDenials the run short-circuits cleanly — see the deny block below.
|
|
574
582
|
let consecutiveDenials = 0;
|
|
@@ -601,7 +609,7 @@ class Loop {
|
|
|
601
609
|
sealDanglingToolCalls(msgs, `[halted:${stuckTag}]`);
|
|
602
610
|
this._reportError('stuck', new Error(`tool "${tc.name}" failed ${identicalErrors} times with identical arguments`), { rule: stuckTag, attempts: identicalErrors });
|
|
603
611
|
this._safeEmit({ type: 'loop:done', data: { text: lastText, stuck: true, rule: stuckTag, cost: totalCost } });
|
|
604
|
-
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: stuckTag, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...(
|
|
612
|
+
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: stuckTag, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...thinkingFields() };
|
|
605
613
|
}
|
|
606
614
|
return null;
|
|
607
615
|
};
|
|
@@ -848,7 +856,7 @@ class Loop {
|
|
|
848
856
|
this._reportError('provider', err, { round });
|
|
849
857
|
if (this.throwOnError) throw err;
|
|
850
858
|
// BA-5: a mid-run provider failure must not erase the work of the rounds that succeeded.
|
|
851
|
-
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: err.message, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...(
|
|
859
|
+
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: err.message, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...thinkingFields() };
|
|
852
860
|
}
|
|
853
861
|
|
|
854
862
|
lastUsage = result.usage || lastUsage;
|
|
@@ -865,10 +873,12 @@ class Loop {
|
|
|
865
873
|
lastMalformedToolCall = (result.malformedToolCall && typeof result.malformedToolCall === 'object')
|
|
866
874
|
? result.malformedToolCall : null;
|
|
867
875
|
if (result.temperatureDropped) temperatureDropped = true;
|
|
868
|
-
//
|
|
869
|
-
//
|
|
870
|
-
|
|
871
|
-
|
|
876
|
+
// Worst-status-wins across rounds; a round carrying only the legacy `thinkingApplied` (a custom provider)
|
|
877
|
+
// is mapped true→applied, false→ignored, null→unknown.
|
|
878
|
+
{
|
|
879
|
+
const st = THINKING_STATUS_RANK[result.thinkingStatus] !== undefined ? result.thinkingStatus
|
|
880
|
+
: result.thinkingApplied === true ? 'applied' : result.thinkingApplied === false ? 'ignored' : result.thinkingApplied === null ? 'unknown' : undefined;
|
|
881
|
+
if (st !== undefined && (worstThinkingStatus === undefined || THINKING_STATUS_RANK[st] > THINKING_STATUS_RANK[worstThinkingStatus])) worstThinkingStatus = st;
|
|
872
882
|
}
|
|
873
883
|
// Publish the latest measured usage to ctx (non-enumerable, fail-open) so a transcript-bound seam —
|
|
874
884
|
// e.g. F2 stash auto-compaction — can read EXACT provider-counted `inputTokens` to gauge context
|
|
@@ -953,7 +963,7 @@ class Loop {
|
|
|
953
963
|
sealDanglingToolCalls(msgs, `[halted:${session.error}]`);
|
|
954
964
|
this._reportError('session', new Error(`provider session terminated: ${session.error}`), { rule: session.error, sessionTurns: session.turns ?? null });
|
|
955
965
|
this._safeEmit({ type: 'loop:done', data: { text: lastText, rule: session.error, sessionTurns: session.turns ?? null, cost: totalCost } });
|
|
956
|
-
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: session.error, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...(
|
|
966
|
+
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: session.error, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...thinkingFields() };
|
|
957
967
|
}
|
|
958
968
|
|
|
959
969
|
// BA-13: classify this round's terminal signal against the neutral stop-reason vocabulary. BA-6
|
|
@@ -1018,7 +1028,7 @@ class Loop {
|
|
|
1018
1028
|
msgs.push({ role: 'assistant', content: result.text });
|
|
1019
1029
|
}
|
|
1020
1030
|
this._safeEmit({ type: 'loop:done', data: { text: lastText, ...(terminal === 'truncated' && { truncated: true }), terminal, stopReason: lastStopReason, droppedToolCalls: dropped, cost: totalCost } });
|
|
1021
|
-
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: errorTag, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...(
|
|
1031
|
+
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: errorTag, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...thinkingFields() };
|
|
1022
1032
|
}
|
|
1023
1033
|
|
|
1024
1034
|
// No tool calls — LLM gave a final text response
|
|
@@ -1038,7 +1048,7 @@ class Loop {
|
|
|
1038
1048
|
try { await flush(msgs, ctx); }
|
|
1039
1049
|
catch (err) { if (err instanceof HaltError) throw err; this._reportError('trim-flush', err, { round }); }
|
|
1040
1050
|
}
|
|
1041
|
-
return { text: result.text, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...(
|
|
1051
|
+
return { text: result.text, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...thinkingFields() };
|
|
1042
1052
|
}
|
|
1043
1053
|
|
|
1044
1054
|
// Execute tool calls
|
|
@@ -1147,7 +1157,7 @@ class Loop {
|
|
|
1147
1157
|
sealDanglingToolCalls(msgs, `[halted:${denyTag}]`);
|
|
1148
1158
|
this._reportError('denied', new Error(`policy denied ${consecutiveDenials} consecutive tool calls (${tc.name})`), { rule: denyTag, denials: consecutiveDenials });
|
|
1149
1159
|
this._safeEmit({ type: 'loop:done', data: { text: lastText, denied: true, rule: denyTag, cost: totalCost } });
|
|
1150
|
-
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: denyTag, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...(
|
|
1160
|
+
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: denyTag, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...thinkingFields() };
|
|
1151
1161
|
}
|
|
1152
1162
|
continue;
|
|
1153
1163
|
}
|
|
@@ -1221,7 +1231,7 @@ class Loop {
|
|
|
1221
1231
|
// BA-5: the rule tag survives on `error`; so does the work. A halt is how a bounded attempt is
|
|
1222
1232
|
// SUPPOSED to end — the caller reads `error` to know it was bounded and `text` to learn from it.
|
|
1223
1233
|
this._safeEmit({ type: 'loop:done', data: { text: lastText, halted: true, rule, cost: totalCost } });
|
|
1224
|
-
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: `halt:${rule}`, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...(
|
|
1234
|
+
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: `halt:${rule}`, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...thinkingFields() };
|
|
1225
1235
|
}
|
|
1226
1236
|
throw err;
|
|
1227
1237
|
}
|
|
@@ -1251,20 +1261,20 @@ class Loop {
|
|
|
1251
1261
|
const rule = err.rule || 'unknown';
|
|
1252
1262
|
this._reportError('halt', err, { rule, reason: err.decision?.reason ?? null });
|
|
1253
1263
|
this._safeEmit({ type: 'loop:done', data: { text: lastText, halted: true, rule, cost: totalCost } });
|
|
1254
|
-
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: `halt:${rule}`, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...(
|
|
1264
|
+
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: `halt:${rule}`, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...thinkingFields() };
|
|
1255
1265
|
}
|
|
1256
1266
|
this._reportError('trim-flush', err, { phase: 'stop' });
|
|
1257
1267
|
}
|
|
1258
1268
|
}
|
|
1259
1269
|
this._safeEmit({ type: 'loop:done', data: { text: lastText, stopped: true, cost: totalCost } });
|
|
1260
|
-
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...(
|
|
1270
|
+
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...thinkingFields() };
|
|
1261
1271
|
}
|
|
1262
1272
|
|
|
1263
1273
|
// Hard safety limit — should never fire under normal usage; bareguard's
|
|
1264
1274
|
// limits.maxTurns (or the LLM's natural completion) ends the loop first.
|
|
1265
1275
|
const warning = `[Loop] hit internal safety limit of ${HARD_ROUND_LIMIT} rounds. Wire bareguard for proper governance — see bare-agent/bareguard.`;
|
|
1266
1276
|
this._safeEmit({ type: 'loop:done', data: { text: lastText, warning, cost: totalCost } });
|
|
1267
|
-
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: warning, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...(
|
|
1277
|
+
return { text: lastText, toolCalls: [], usage: lastUsage, cost: totalCost, error: warning, stopReason: lastStopReason, model: lastModel, msgs, metrics: finalizeMetrics(), ...(lastMalformedToolCall && { malformedToolCall: lastMalformedToolCall }), ...(temperatureDropped && { temperatureDropped: true }), ...thinkingFields() };
|
|
1268
1278
|
}
|
|
1269
1279
|
|
|
1270
1280
|
/**
|
|
@@ -1337,7 +1347,7 @@ class Loop {
|
|
|
1337
1347
|
* @param {string} text - User message.
|
|
1338
1348
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
1339
1349
|
* @param {Record<string, any>} [options={}] - Per-run overrides.
|
|
1340
|
-
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, malformedToolCall?: GenerateResult['malformedToolCall'], temperatureDropped?: boolean, thinkingApplied?: boolean|null}>}
|
|
1350
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, stopReason: string|null, model: string|null, msgs: Message[], metrics: RunMetrics, malformedToolCall?: GenerateResult['malformedToolCall'], temperatureDropped?: boolean, thinkingApplied?: boolean|null, thinkingStatus?: 'applied'|'unsupported'|'ignored'|'unknown'}>}
|
|
1341
1351
|
*/
|
|
1342
1352
|
async chat(text, tools = [], options = {}) {
|
|
1343
1353
|
this._history.push({ role: 'user', content: text });
|
|
@@ -31,8 +31,8 @@ export type AnthropicOptions = {
|
|
|
31
31
|
*/
|
|
32
32
|
thinking?: any;
|
|
33
33
|
/**
|
|
34
|
-
* - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right Anthropic `thinking` request field for the provider/model,
|
|
35
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
34
|
+
* - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right Anthropic `thinking` request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
35
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
36
36
|
*/
|
|
37
37
|
thinkingMode?: "on" | "off" | null | undefined;
|
|
38
38
|
/**
|
|
@@ -63,8 +63,8 @@ export type AnthropicOptions = {
|
|
|
63
63
|
* chain set `thinking` on each provider's CONSTRUCTOR instead of per-call.
|
|
64
64
|
*
|
|
65
65
|
* **MEASURED CAVEAT — this option does not "turn thinking on".** On `claude-sonnet-5` adaptive thinking is ALREADY the default: sending this changed the observed thinking rate not at all (2/10 rounds with it vs 3/10 without — `poc/ba7-adaptive-default.mjs`). Its real use is pinning the mode and reaching `display`/`effort`. The change that mattered is that thinking blocks are now PRESERVED and replayed (see `Message.providerBlocks`), which happens whether or not you ever set this.
|
|
66
|
-
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right Anthropic `thinking` request field for the provider/model,
|
|
67
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
66
|
+
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right Anthropic `thinking` request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
67
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
68
68
|
* @property {boolean} [exposeErrorBody=false] - Attach the full upstream response to `err.body` on HTTP errors (off by default to avoid leaking unexpected fields through error logs; `err.message` still carries the API error).
|
|
69
69
|
* @property {number} [timeoutMs=600000] - BA-18: request/idle timeout in ms. A silent or never-answering socket (dropped by the server, or a response that never starts) was otherwise bounded only by the OS TCP timeout (~2h) — a hang, not an error, so every retry policy above it was inert. Bounds on socket INACTIVITY (the timer resets on activity, so a slow-but-streaming response is not killed); on trip, `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`, `context.bound: 'idle'`) that a wired `Retry` (`Loop({ retry })`) retries. Default 10 min sits above any single non-streaming completion; `0` or `Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
70
70
|
* @property {number} [deadlineMs=0] - BA-19: TOTAL call-duration deadline in ms, beside `timeoutMs`. The idle bound resets on any socket activity, so a response that trickles a byte forever (a "zombie stream") never trips it and hangs the caller for hours. This is an absolute, non-resetting wall-clock ceiling; on trip, `generate()` rejects with a TERMINAL `TimeoutError` (`code: 'EDEADLINE'`, `context.bound: 'deadline'`, `retryable: false` — a hard ceiling meant to STOP, not re-spend). DISABLED by default (a deliberately long single call is legitimate); `0`/`Infinity` disable. When both are set with `timeoutMs < deadlineMs`, a silent socket trips the idle bound first. Overridable per call via `generate(..., { deadlineMs })`.
|
|
@@ -100,9 +100,8 @@ export class AnthropicProvider {
|
|
|
100
100
|
* @throws {Error} `[AnthropicProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
101
101
|
*/
|
|
102
102
|
generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
|
|
103
|
-
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
104
|
-
_warnTemperatureDropped(): void;
|
|
105
|
-
_warnedTempDropped: boolean | undefined;
|
|
103
|
+
/** One-time warning (per reason) that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
104
|
+
_warnTemperatureDropped(reason?: string): void;
|
|
106
105
|
/**
|
|
107
106
|
* BA-7: the provider-native blocks to replay at the FRONT of an assistant turn's content, or `[]`.
|
|
108
107
|
*
|
|
@@ -7,7 +7,7 @@ const { requestWithTemperatureFallback } = require('./provider-temperature');
|
|
|
7
7
|
const { normalizeStopReason } = require('./provider-stop-reason');
|
|
8
8
|
const { resolveTimeoutMs, applyRequestBounds, guardResponseSettles } = require('./provider-http');
|
|
9
9
|
const { hasUsageSignal } = require('./provider-usage');
|
|
10
|
-
const { normalizeThinkingMode, resolveThinkingMode, requestWithThinkingMode,
|
|
10
|
+
const { normalizeThinkingMode, resolveThinkingMode, requestWithThinkingMode, assessThinking } = require('./thinking-mode');
|
|
11
11
|
|
|
12
12
|
// BA-24: the raw Anthropic usage field names. Presence of any (even value 0) means the API reported a
|
|
13
13
|
// usage signal → build the object; absence of all means no signal → surface null (unpriceable).
|
|
@@ -35,8 +35,8 @@ function isLoopbackHost(hostname) {
|
|
|
35
35
|
* chain set `thinking` on each provider's CONSTRUCTOR instead of per-call.
|
|
36
36
|
*
|
|
37
37
|
* **MEASURED CAVEAT — this option does not "turn thinking on".** On `claude-sonnet-5` adaptive thinking is ALREADY the default: sending this changed the observed thinking rate not at all (2/10 rounds with it vs 3/10 without — `poc/ba7-adaptive-default.mjs`). Its real use is pinning the mode and reaching `display`/`effort`. The change that mattered is that thinking blocks are now PRESERVED and replayed (see `Message.providerBlocks`), which happens whether or not you ever set this.
|
|
38
|
-
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right Anthropic `thinking` request field for the provider/model,
|
|
39
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
38
|
+
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right Anthropic `thinking` request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
39
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
40
40
|
* @property {boolean} [exposeErrorBody=false] - Attach the full upstream response to `err.body` on HTTP errors (off by default to avoid leaking unexpected fields through error logs; `err.message` still carries the API error).
|
|
41
41
|
* @property {number} [timeoutMs=600000] - BA-18: request/idle timeout in ms. A silent or never-answering socket (dropped by the server, or a response that never starts) was otherwise bounded only by the OS TCP timeout (~2h) — a hang, not an error, so every retry policy above it was inert. Bounds on socket INACTIVITY (the timer resets on activity, so a slow-but-streaming response is not killed); on trip, `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`, `context.bound: 'idle'`) that a wired `Retry` (`Loop({ retry })`) retries. Default 10 min sits above any single non-streaming completion; `0` or `Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
42
42
|
* @property {number} [deadlineMs=0] - BA-19: TOTAL call-duration deadline in ms, beside `timeoutMs`. The idle bound resets on any socket activity, so a response that trickles a byte forever (a "zombie stream") never trips it and hangs the caller for hours. This is an absolute, non-resetting wall-clock ceiling; on trip, `generate()` rejects with a TERMINAL `TimeoutError` (`code: 'EDEADLINE'`, `context.bound: 'deadline'`, `retryable: false` — a hard ceiling meant to STOP, not re-spend). DISABLED by default (a deliberately long single call is legitimate); `0`/`Infinity` disable. When both are set with `timeoutMs < deadlineMs`, a silent socket trips the idle bound first. Overridable per call via `generate(..., { deadlineMs })`.
|
|
@@ -159,6 +159,8 @@ class AnthropicProvider {
|
|
|
159
159
|
const thinking = options.thinking !== undefined ? options.thinking : this.thinking;
|
|
160
160
|
if (thinking) body.thinking = thinking;
|
|
161
161
|
const thinkingMode = resolveThinkingMode({ label: 'AnthropicProvider', instanceMode: this.thinkingMode, options, instanceThinking: this.thinking, hasRawThinking: true });
|
|
162
|
+
/** @type {{ unsupported: boolean }} */
|
|
163
|
+
const thinkingTrack = { unsupported: false };
|
|
162
164
|
if (tools.length > 0) {
|
|
163
165
|
body.tools = tools.map(t => ({
|
|
164
166
|
name: t.name,
|
|
@@ -174,12 +176,12 @@ class AnthropicProvider {
|
|
|
174
176
|
const deadlineMs = resolveTimeoutMs(this.deadlineMs, options.deadlineMs, 0, 'deadlineMs');
|
|
175
177
|
const { data, temperatureDropped } = await requestWithTemperatureFallback({
|
|
176
178
|
request: () => thinkingMode
|
|
177
|
-
? requestWithThinkingMode({ kind: 'anthropic', mode: thinkingMode, model: this.model,
|
|
179
|
+
? requestWithThinkingMode({ kind: 'anthropic', mode: thinkingMode, model: this.model, body, cache: this._thinkingShapes, track: thinkingTrack, request: (b) => this._request(b, timeoutMs, deadlineMs) })
|
|
178
180
|
: this._request(body, timeoutMs, deadlineMs),
|
|
179
181
|
hadTemperature: () => body.temperature != null,
|
|
180
182
|
stripTemperature: () => { delete body.temperature; },
|
|
181
183
|
thinkingOn: thinkingMode === 'on',
|
|
182
|
-
warnOnce: () => this._warnTemperatureDropped(),
|
|
184
|
+
warnOnce: (reason) => this._warnTemperatureDropped(reason),
|
|
183
185
|
});
|
|
184
186
|
|
|
185
187
|
let text = '';
|
|
@@ -226,15 +228,18 @@ class AnthropicProvider {
|
|
|
226
228
|
}
|
|
227
229
|
: null,
|
|
228
230
|
...(temperatureDropped && { temperatureDropped: true }),
|
|
229
|
-
...(thinkingMode &&
|
|
231
|
+
...(thinkingMode && assessThinking({ kind: 'anthropic', mode: thinkingMode, data, unsupported: thinkingTrack.unsupported, owner: this, label: 'AnthropicProvider', model: this.model })),
|
|
230
232
|
};
|
|
231
233
|
}
|
|
232
234
|
|
|
233
|
-
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
234
|
-
_warnTemperatureDropped() {
|
|
235
|
-
|
|
236
|
-
this
|
|
237
|
-
|
|
235
|
+
/** One-time warning (per reason) that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
236
|
+
_warnTemperatureDropped(reason = 'unsupported') {
|
|
237
|
+
const flag = reason === 'thinking' ? '_warnedTempThinking' : '_warnedTempDropped';
|
|
238
|
+
if (this[flag]) return;
|
|
239
|
+
this[flag] = true;
|
|
240
|
+
console.warn(reason === 'thinking'
|
|
241
|
+
? `[AnthropicProvider] '${this.model}': 'temperature' must be 1 while thinking is on — retrying without it. Further drops from this provider instance are silent.`
|
|
242
|
+
: `[AnthropicProvider] '${this.model}' rejected a non-default 'temperature' (unsupported/deprecated) — retrying without it. Further drops from this provider instance are silent.`);
|
|
238
243
|
}
|
|
239
244
|
|
|
240
245
|
/**
|
|
@@ -20,7 +20,7 @@ export type CLIPipeOptions = {
|
|
|
20
20
|
*/
|
|
21
21
|
env?: Record<string, string> | undefined;
|
|
22
22
|
/**
|
|
23
|
-
* -
|
|
23
|
+
* - Accepted ('on'|'off', else `ValidationError`) but CLIPipe has NO thinking switch: the call runs unchanged and the result carries `thinkingStatus:'unsupported'` (+ `thinkingApplied:false`; warns once per instance). Overridable per call; `null` suppresses an instance default.
|
|
24
24
|
*/
|
|
25
25
|
thinkingMode?: any;
|
|
26
26
|
/**
|
|
@@ -133,6 +133,7 @@ export class CLIPipeProvider {
|
|
|
133
133
|
* const provider = new CLIPipeProvider({ command: 'claude' });
|
|
134
134
|
*/
|
|
135
135
|
constructor(options?: CLIPipeOptions);
|
|
136
|
+
thinkingMode: "on" | "off" | null;
|
|
136
137
|
command: string;
|
|
137
138
|
args: string[];
|
|
138
139
|
cwd: string | undefined;
|
|
@@ -176,7 +177,7 @@ export class CLIPipeProvider {
|
|
|
176
177
|
* failure for a genuinely tool-incapable model lives in the tool-mode capability probe.
|
|
177
178
|
* @param {Message[]} messages - Conversation messages in OpenAI format.
|
|
178
179
|
* @param {ToolDef[]} [tools=[]] - Caller tools. Honored only in tool mode (`toolProtocol` set).
|
|
179
|
-
* @param {Record<string, any>} [options={}] - Unused, except `thinkingMode` (
|
|
180
|
+
* @param {Record<string, any>} [options={}] - Unused, except `thinkingMode` (validated; CLIPipe has no thinking switch, so the call runs unchanged with `thinkingStatus:'unsupported'`).
|
|
180
181
|
* @returns {Promise<GenerateResult>}
|
|
181
182
|
* @throws {Error} `[CLIPipeProvider] failed to spawn "cmd": ...` — when the command cannot be found or executed.
|
|
182
183
|
* @throws {Error} `[CLIPipeProvider] process exited with code N: ...` — on non-zero exit.
|
|
@@ -184,6 +185,14 @@ export class CLIPipeProvider {
|
|
|
184
185
|
* @throws {Error} `[CLIPipeProvider] process produced no output` — when stdout is empty.
|
|
185
186
|
*/
|
|
186
187
|
generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
|
|
188
|
+
/**
|
|
189
|
+
* The pre-thinkingMode generate body: routes to native / emulation / plain-text.
|
|
190
|
+
* @param {Message[]} messages
|
|
191
|
+
* @param {ToolDef[]} tools
|
|
192
|
+
* @param {Record<string, any>} options
|
|
193
|
+
* @returns {Promise<GenerateResult>}
|
|
194
|
+
*/
|
|
195
|
+
_generateInner(messages: Message[], tools: ToolDef[], options: Record<string, any>): Promise<GenerateResult>;
|
|
187
196
|
_warnedNoProtocol: boolean | undefined;
|
|
188
197
|
/**
|
|
189
198
|
* Tool mode (v0.32.0) — one turn of schema-validated tool emulation. Renders the Loop's
|
package/src/provider-clipipe.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { spawn } = require('child_process');
|
|
4
|
-
const { ProviderError, HaltError
|
|
4
|
+
const { ProviderError, HaltError } = require('./errors');
|
|
5
|
+
const { normalizeThinkingMode, resolveThinkingMode, warnThinkingStatus } = require('./thinking-mode');
|
|
5
6
|
const { buildToolSystemPrompt, renderTranscript, resolveToolProtocol, mapClaudeMeta } = require('./provider-clipipe-tools');
|
|
6
7
|
const { createBridge, resolveSessionError, runSession } = require('./provider-clipipe-mcp');
|
|
7
8
|
|
|
@@ -21,7 +22,7 @@ const { createBridge, resolveSessionError, runSession } = require('./provider-cl
|
|
|
21
22
|
* @property {string[]} [args=[]] - Arguments to pass to the command.
|
|
22
23
|
* @property {string} [cwd] - Working directory for the child process.
|
|
23
24
|
* @property {Record<string, string>} [env] - Environment variables for the child process.
|
|
24
|
-
* @property {any} [thinkingMode] -
|
|
25
|
+
* @property {any} [thinkingMode] - Accepted ('on'|'off', else `ValidationError`) but CLIPipe has NO thinking switch: the call runs unchanged and the result carries `thinkingStatus:'unsupported'` (+ `thinkingApplied:false`; warns once per instance). Overridable per call; `null` suppresses an instance default.
|
|
25
26
|
* @property {number} [timeout=30000] - Timeout in milliseconds.
|
|
26
27
|
* @property {string} [systemPromptFlag] - CLI flag for system prompt (e.g. '--system'). When set, system messages are extracted and passed via this flag instead of stdin.
|
|
27
28
|
* @property {(chunk: string) => void} [onChunk] - Called with each stdout chunk as it streams.
|
|
@@ -108,10 +109,6 @@ function subtractUsage(total, streamed) {
|
|
|
108
109
|
return out;
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
const CLIPIPE_NO_THINKING_MODE = () => new ValidationError(
|
|
112
|
-
'[CLIPipeProvider] thinkingMode is not supported — a CLI subprocess has no thinking switch bare-agent can set. Remove thinkingMode (see thinkingSupport(\'clipipe\')).',
|
|
113
|
-
{ context: { lib: 'bare-agent', provider: 'clipipe' } });
|
|
114
|
-
|
|
115
112
|
class CLIPipeProvider {
|
|
116
113
|
/**
|
|
117
114
|
* Provider that pipes prompts to a CLI command via stdin and reads stdout.
|
|
@@ -124,8 +121,9 @@ class CLIPipeProvider {
|
|
|
124
121
|
*/
|
|
125
122
|
constructor(options = {}) {
|
|
126
123
|
if (!options.command) throw new Error('[CLIPipeProvider] requires command');
|
|
127
|
-
// thinkingMode has NO mechanism here
|
|
128
|
-
|
|
124
|
+
// thinkingMode has NO mechanism here: validated (a bad value is a caller mistake), then every call
|
|
125
|
+
// runs unchanged and reports thinkingStatus 'unsupported'.
|
|
126
|
+
this.thinkingMode = normalizeThinkingMode(options.thinkingMode, 'CLIPipeProvider');
|
|
129
127
|
this.command = options.command;
|
|
130
128
|
this.args = options.args || [];
|
|
131
129
|
this.cwd = options.cwd || undefined;
|
|
@@ -184,7 +182,7 @@ class CLIPipeProvider {
|
|
|
184
182
|
* failure for a genuinely tool-incapable model lives in the tool-mode capability probe.
|
|
185
183
|
* @param {Message[]} messages - Conversation messages in OpenAI format.
|
|
186
184
|
* @param {ToolDef[]} [tools=[]] - Caller tools. Honored only in tool mode (`toolProtocol` set).
|
|
187
|
-
* @param {Record<string, any>} [options={}] - Unused, except `thinkingMode` (
|
|
185
|
+
* @param {Record<string, any>} [options={}] - Unused, except `thinkingMode` (validated; CLIPipe has no thinking switch, so the call runs unchanged with `thinkingStatus:'unsupported'`).
|
|
188
186
|
* @returns {Promise<GenerateResult>}
|
|
189
187
|
* @throws {Error} `[CLIPipeProvider] failed to spawn "cmd": ...` — when the command cannot be found or executed.
|
|
190
188
|
* @throws {Error} `[CLIPipeProvider] process exited with code N: ...` — on non-zero exit.
|
|
@@ -192,7 +190,21 @@ class CLIPipeProvider {
|
|
|
192
190
|
* @throws {Error} `[CLIPipeProvider] process produced no output` — when stdout is empty.
|
|
193
191
|
*/
|
|
194
192
|
async generate(messages, tools = [], options = {}) {
|
|
195
|
-
|
|
193
|
+
const mode = resolveThinkingMode({ label: 'CLIPipeProvider', instanceMode: this.thinkingMode, options: options || {} });
|
|
194
|
+
const result = await this._generateInner(messages, tools, options);
|
|
195
|
+
if (!mode) return result;
|
|
196
|
+
warnThinkingStatus({ owner: this, label: 'CLIPipeProvider', model: result.model || this.command, mode, status: 'unsupported' });
|
|
197
|
+
return { ...result, thinkingStatus: 'unsupported', thinkingApplied: false };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The pre-thinkingMode generate body: routes to native / emulation / plain-text.
|
|
202
|
+
* @param {Message[]} messages
|
|
203
|
+
* @param {ToolDef[]} tools
|
|
204
|
+
* @param {Record<string, any>} options
|
|
205
|
+
* @returns {Promise<GenerateResult>}
|
|
206
|
+
*/
|
|
207
|
+
async _generateInner(messages, tools, options) {
|
|
196
208
|
if (Array.isArray(tools) && tools.length > 0) {
|
|
197
209
|
if (this.nativeTools) return this._generateWithMcp(messages, tools, options);
|
|
198
210
|
if (this.toolProtocol) return this._generateWithTools(messages, tools);
|
package/src/provider-gemini.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ export type GeminiOptions = {
|
|
|
16
16
|
*/
|
|
17
17
|
baseUrl?: string | undefined;
|
|
18
18
|
/**
|
|
19
|
-
* - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `generationConfig.thinkingConfig.thinkingBudget` request field for the provider/model,
|
|
20
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
19
|
+
* - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `generationConfig.thinkingConfig.thinkingBudget` request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
20
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
21
21
|
*/
|
|
22
22
|
thinkingMode?: "on" | "off" | null | undefined;
|
|
23
23
|
/**
|
|
@@ -38,8 +38,8 @@ export type GeminiOptions = {
|
|
|
38
38
|
* @property {string} [apiKey] - Google AI Studio (Gemini) API key.
|
|
39
39
|
* @property {string} [model='gemini-2.5-flash'] - Model ID.
|
|
40
40
|
* @property {string} [baseUrl='https://generativelanguage.googleapis.com/v1beta'] - API base (override for proxies; posts to `${baseUrl}/models/${model}:generateContent`).
|
|
41
|
-
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `generationConfig.thinkingConfig.thinkingBudget` request field for the provider/model,
|
|
42
|
-
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model —
|
|
41
|
+
* @property {'on'|'off'|null} [thinkingMode] - Normalized thinking switch: `'on'` or `'off'`, and bare-agent sends the right `generationConfig.thinkingConfig.thinkingBudget` request field for the provider/model, tries the next known shape when a shape request fails with a 400/422 (not the temperature path), and the call ALWAYS RUNS: if every shape fails that way, ONE plain request without a thinking field decides — plain OK ⇒ `thinkingStatus:'unsupported'`, plain fails ⇒ that plain request's error is thrown as-is; other failures propagate after one request; a model measured as not switchable is sent plain directly. Only the winning shape is cached per instance+model ('unsupported' is never cached); warns once per status. Every reply is checked: `GenerateResult.thinkingStatus` is `'applied'`/`'unsupported'`/`'ignored'`/`'unknown'` (`'ignored'` = 'off' was accepted but the reply shows thinking anyway — warns once, result kept) and the legacy `thinkingApplied` is derived (`true`/`false`/`null`). Unset ⇒ request body byte-identical to before. Overridable per call via `generate(..., { thinkingMode })`; `null` suppresses an instance default. Any value but 'on'/'off' ⇒ `ValidationError`. A NEW field: it does not change the raw `thinking` option; setting both (after per-call override) ⇒ `ValidationError`. See `src/thinking-mode.js` / `thinkingSupport()`.
|
|
42
|
+
* Per-call `thinkingMode` reaches EVERY provider in a `FallbackProvider` chain — correct for a normalized field (each provider maps it to its own shape; one that cannot — CLIPipe, a non-switchable model — just runs without it and reports `thinkingStatus:'unsupported'`, so the first member answers and the chain does not fall through).
|
|
43
43
|
* @property {boolean} [exposeErrorBody=false] - Attach the full upstream response to `err.body` on HTTP errors (off by default; `err.message` still carries the API error).
|
|
44
44
|
* @property {number} [timeoutMs=600000] - BA-18: request/idle timeout in ms. Bounds a silent or never-answering socket on inactivity so `generate()` rejects with a retryable `TimeoutError` (`code: 'ETIMEDOUT'`, `context.bound: 'idle'`) instead of hanging until the OS TCP timeout (~2h). `0`/`Infinity` disables it (pre-BA-18 behaviour). Overridable per call via `generate(..., { timeoutMs })`.
|
|
45
45
|
* @property {number} [deadlineMs=0] - BA-19: TOTAL call-duration deadline in ms, beside `timeoutMs`. The idle bound resets on any socket activity, so a response that trickles a byte forever never trips it and hangs for hours. This is an absolute, non-resetting ceiling; on trip, `generate()` rejects with a TERMINAL `TimeoutError` (`code: 'EDEADLINE'`, `context.bound: 'deadline'`, `retryable: false`). DISABLED by default; `0`/`Infinity` disable. Overridable per call via `generate(..., { deadlineMs })`.
|
|
@@ -78,9 +78,8 @@ export class GeminiProvider {
|
|
|
78
78
|
* @throws {Error} `[GeminiProvider] ...` — on HTTP errors (4xx/5xx) or invalid JSON response.
|
|
79
79
|
*/
|
|
80
80
|
generate(messages: Message[], tools?: ToolDef[], options?: Record<string, any>): Promise<GenerateResult>;
|
|
81
|
-
/** One-time warning that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
82
|
-
_warnTemperatureDropped(): void;
|
|
83
|
-
_warnedTempDropped: boolean | undefined;
|
|
81
|
+
/** One-time warning (per reason) that this model rejected `temperature` and the request was retried without it (BA-10). */
|
|
82
|
+
_warnTemperatureDropped(reason?: string): void;
|
|
84
83
|
/**
|
|
85
84
|
* Normalize Gemini `usageMetadata` to the neutral {@link Usage} shape. Like OpenAI, `promptTokenCount`
|
|
86
85
|
* INCLUDES the cached tokens (`cachedContentTokenCount`), so subtract for the uncached remainder
|