bare-agent 0.26.0 → 0.27.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 +7 -1
- package/bareagent.context.md +47 -2
- package/package.json +3 -3
- package/src/bareguard-adapter.d.ts +2 -2
- package/src/bareguard-adapter.js +1 -1
- package/src/loop.d.ts +40 -0
- package/src/loop.js +185 -15
- package/src/provider-anthropic.d.ts +32 -2
- package/src/provider-anthropic.js +106 -4
- package/src/provider-clipipe.js +47 -21
- package/src/provider-gemini.js +10 -0
- package/src/provider-ollama.d.ts +1 -1
- package/src/provider-ollama.js +32 -9
- package/src/provider-openai.js +14 -5
- package/src/provider-stop-reason.d.ts +26 -0
- package/src/provider-stop-reason.js +148 -0
- package/src/recurse-synthesize.js +11 -2
- package/tools/shell.d.ts +13 -2
- package/tools/shell.js +28 -8
- package/types/index.d.ts +46 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BA-6 — normalize each provider's native finish-reason field to one neutral vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* Every provider tells you WHY generation ended. Before this, bare-agent read the field on none of
|
|
5
|
+
* them (`grep -rn 'stop_reason\|finish_reason\|done_reason' src/` → zero hits), so a round the API
|
|
6
|
+
* CUT OFF at the token cap was indistinguishable from one the model chose to end — the Loop's rule is
|
|
7
|
+
* "no tool calls ⇒ final answer", and a truncation has no tool calls, so it returned as a clean finish
|
|
8
|
+
* with `error: null`. A truncation was laundered into a completion.
|
|
9
|
+
*
|
|
10
|
+
* The neutral vocabulary (what the Loop is allowed to reason about):
|
|
11
|
+
*
|
|
12
|
+
* 'end_turn' the model finished of its own accord — the ONLY clean finish
|
|
13
|
+
* 'max_tokens' CUT OFF at the output cap. NOT a finish. Load-bearing (see below).
|
|
14
|
+
* 'tool_use' stopped to call a tool, and the call is COMPLETE
|
|
15
|
+
* 'stop_sequence' hit a caller-supplied stop string — a legitimate finish
|
|
16
|
+
* 'refusal' declined on safety grounds (Anthropic `refusal`, OpenAI `content_filter`, …)
|
|
17
|
+
* 'pause_turn' server-side tool loop paused; the caller is expected to RESUME, not to error
|
|
18
|
+
* 'context_exceeded' ran out of CONTEXT WINDOW (distinct from running out of output budget)
|
|
19
|
+
* null provider didn't say / we don't recognize it
|
|
20
|
+
*
|
|
21
|
+
* `null` is the safe default and it is deliberate: an unmapped or absent value reproduces the
|
|
22
|
+
* pre-BA-6 behavior exactly. A wrong guess therefore degrades to the status quo rather than inventing
|
|
23
|
+
* a false truncation error on a healthy run. Unknown-but-present values pass through verbatim (a
|
|
24
|
+
* caller can still see them) but the Loop only ever ACTS on the values above.
|
|
25
|
+
*
|
|
26
|
+
* ── Why 'max_tokens' vs 'tool_use' is the load-bearing distinction (measured, not assumed) ──
|
|
27
|
+
*
|
|
28
|
+
* `poc/ba6-stop-reason-mapping.mjs`, real API, claude-sonnet-5 + gpt-4o-mini:
|
|
29
|
+
*
|
|
30
|
+
* a COMPLETE tool call ALWAYS arrives tagged 'tool_use' — never 'max_tokens'.
|
|
31
|
+
*
|
|
32
|
+
* Anthropic returned `stop_reason: "tool_use"` with intact arguments even at a tight 1024-token cap;
|
|
33
|
+
* OpenAI refuses outright (HTTP 400) rather than emit a tool call it could not finish. Neither ever
|
|
34
|
+
* handed back a COMPLETE tool call tagged as truncated.
|
|
35
|
+
*
|
|
36
|
+
* The converse is the dangerous case, and it is not hypothetical — it is the BA-4 file-zeroing bug one
|
|
37
|
+
* layer up: a round tagged 'max_tokens' that CARRIES a tool call carries a tool call that was cut off
|
|
38
|
+
* mid-generation, whose arguments are missing keys. That is precisely how a `claude-haiku-4-5` worker
|
|
39
|
+
* emptied a 1789-line file — it hit the output cap mid-`shell_write`, the `content` argument never
|
|
40
|
+
* arrived, and the truncated call was executed as if whole. So the Loop must NEVER execute the tool
|
|
41
|
+
* calls of a 'max_tokens' round. Refusing costs nothing legitimate (complete calls come back
|
|
42
|
+
* 'tool_use') and closes the data-loss path at the protocol layer, for every tool, not just
|
|
43
|
+
* `shell_write`.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** Anthropic `stop_reason` → neutral. */
|
|
47
|
+
const ANTHROPIC = {
|
|
48
|
+
end_turn: 'end_turn',
|
|
49
|
+
max_tokens: 'max_tokens',
|
|
50
|
+
tool_use: 'tool_use',
|
|
51
|
+
stop_sequence: 'stop_sequence',
|
|
52
|
+
refusal: 'refusal',
|
|
53
|
+
pause_turn: 'pause_turn',
|
|
54
|
+
model_context_window_exceeded: 'context_exceeded',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** OpenAI (and OpenAI-compatible) `finish_reason` → neutral. */
|
|
58
|
+
const OPENAI = {
|
|
59
|
+
stop: 'end_turn',
|
|
60
|
+
length: 'max_tokens',
|
|
61
|
+
tool_calls: 'tool_use',
|
|
62
|
+
function_call: 'tool_use',
|
|
63
|
+
content_filter: 'refusal',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gemini `finishReason` → neutral. Gemini does NOT tag a function call specially — a complete tool call
|
|
68
|
+
* comes back as `STOP` with a `functionCall` part (measured live) — so there is no `tool_use` row here
|
|
69
|
+
* by design. `normalizeStopReason` derives it from `hasToolCalls` instead; see the note there.
|
|
70
|
+
*/
|
|
71
|
+
const GEMINI = {
|
|
72
|
+
STOP: 'end_turn',
|
|
73
|
+
MAX_TOKENS: 'max_tokens',
|
|
74
|
+
SAFETY: 'refusal',
|
|
75
|
+
RECITATION: 'refusal',
|
|
76
|
+
BLOCKLIST: 'refusal',
|
|
77
|
+
PROHIBITED_CONTENT: 'refusal',
|
|
78
|
+
SPII: 'refusal',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Ollama `done_reason` → neutral. `load`/`unload` are lifecycle values, not completions — they map to
|
|
83
|
+
* null (unknown) rather than being forced into the vocabulary.
|
|
84
|
+
*/
|
|
85
|
+
const OLLAMA = {
|
|
86
|
+
stop: 'end_turn',
|
|
87
|
+
length: 'max_tokens',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const TABLES = {
|
|
91
|
+
anthropic: ANTHROPIC,
|
|
92
|
+
openai: OPENAI,
|
|
93
|
+
gemini: GEMINI,
|
|
94
|
+
ollama: OLLAMA,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Map a provider's native finish-reason value onto the neutral vocabulary.
|
|
99
|
+
*
|
|
100
|
+
* @param {string|null|undefined} raw - the provider's native value (`stop_reason` / `finish_reason` /
|
|
101
|
+
* `finishReason` / `done_reason`). Absent or non-string ⇒ `null` (pre-BA-6 behavior).
|
|
102
|
+
* @param {'anthropic'|'openai'|'gemini'|'ollama'} provider - which table to read.
|
|
103
|
+
* @param {{hasToolCalls?: boolean}} [ctx] - what the round actually CARRIED. See below: two providers
|
|
104
|
+
* cannot express "stopped to call a tool" in their finish-reason field at all, so the round's own
|
|
105
|
+
* content is the only place that fact exists.
|
|
106
|
+
* @returns {string|null} a neutral value, an unrecognized value passed through verbatim, or `null`.
|
|
107
|
+
*/
|
|
108
|
+
function normalizeStopReason(raw, provider, ctx = {}) {
|
|
109
|
+
if (typeof raw !== 'string' || raw === '') return null;
|
|
110
|
+
const table = TABLES[provider];
|
|
111
|
+
if (!table) return raw;
|
|
112
|
+
// An unrecognized-but-present value passes through: the caller can still SEE it, and the Loop only
|
|
113
|
+
// acts on the known vocabulary — so a new upstream value can never be mistaken for a truncation.
|
|
114
|
+
const mapped = table[raw] || raw;
|
|
115
|
+
|
|
116
|
+
// GEMINI AND OLLAMA HAVE NO `tool_use` FINISH REASON (both measured live: Gemini returns
|
|
117
|
+
// `finishReason: STOP` and Ollama `done_reason: 'stop'` on a round that emitted a complete function
|
|
118
|
+
// call). Reported verbatim, a round that stopped TO CALL A TOOL would come back as `end_turn` — "the
|
|
119
|
+
// model finished of its own accord" — on 2 of 5 providers and `tool_use` on the other 3.
|
|
120
|
+
//
|
|
121
|
+
// That is the BA-6 defect class in miniature: a round that is NOT a finish, reporting as a finish.
|
|
122
|
+
// An adopter branching on `stopReason === 'end_turn'` would be right on Anthropic/OpenAI and wrong
|
|
123
|
+
// on Gemini/Ollama. So derive it from what the round CARRIED — the model did stop to call a tool,
|
|
124
|
+
// and that is a report, not an invention.
|
|
125
|
+
//
|
|
126
|
+
// Narrow on purpose: only ever promotes `end_turn` → `tool_use`. It cannot touch `max_tokens` (a
|
|
127
|
+
// truncated round carrying a half-generated call must stay TRUNCATED — that is BA-4's mechanism),
|
|
128
|
+
// nor `refusal`/`pause_turn`/`context_exceeded`, nor an unrecognized passthrough value.
|
|
129
|
+
if (mapped === 'end_turn' && ctx.hasToolCalls === true) return 'tool_use';
|
|
130
|
+
return mapped;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Did this round get CUT OFF at the output-token cap?
|
|
135
|
+
*
|
|
136
|
+
* The one predicate the Loop acts on. Deliberately narrow: `context_exceeded`, `refusal` and
|
|
137
|
+
* `pause_turn` are all "not a normal finish" but they are NOT output-cap truncations and must not be
|
|
138
|
+
* folded in here — `pause_turn` in particular is a RESUMABLE state, and erroring on it would break
|
|
139
|
+
* server-side tool flows that are working exactly as designed.
|
|
140
|
+
*
|
|
141
|
+
* @param {string|null|undefined} stopReason - a NEUTRAL value (post-{@link normalizeStopReason}).
|
|
142
|
+
* @returns {boolean}
|
|
143
|
+
*/
|
|
144
|
+
function isTruncated(stopReason) {
|
|
145
|
+
return stopReason === 'max_tokens';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { normalizeStopReason, isTruncated };
|
|
@@ -62,8 +62,17 @@ async function mergeReduce(task, results, opts) {
|
|
|
62
62
|
if (typeof out.error === 'string' && out.error.startsWith('halt:')) {
|
|
63
63
|
throw new HaltError('[synthesize] merge halted by governance', { rule: out.error.slice('halt:'.length) });
|
|
64
64
|
}
|
|
65
|
-
// A non-halt fault (e.g. provider error) is non-fatal here — fall
|
|
66
|
-
// losing the partials entirely. recurse still reports honest
|
|
65
|
+
// A non-halt fault (e.g. provider error, deny short-circuit, the hard round limit) is non-fatal here — fall
|
|
66
|
+
// back to the LOSSLESS concat rather than losing the partials entirely. recurse still reports honest
|
|
67
|
+
// completeness via its own paths.
|
|
68
|
+
//
|
|
69
|
+
// Branch on `out.error`, NOT on the falsiness of `out.text` (BA-5). Since the Loop now preserves the text a
|
|
70
|
+
// bounded run produced, a faulted merge returns its PARTIAL, aborted prose — so `out.text || concat` would
|
|
71
|
+
// silently ship that fragment as the synthesized answer and every child result would be lost. Proven
|
|
72
|
+
// reachable: the merge Loop registers no tools, so a hallucinated tool call is fed back as
|
|
73
|
+
// `[Loop] Unknown tool` and the round loop CONTINUES — round 1 emits prose, round 2 dies, and the fallback
|
|
74
|
+
// never fires. `error` is the sole success signal; text-falsiness never was one.
|
|
75
|
+
if (out.error) return concatReduce(results);
|
|
67
76
|
return out.text || concatReduce(results);
|
|
68
77
|
}
|
|
69
78
|
|
package/tools/shell.d.ts
CHANGED
|
@@ -77,12 +77,23 @@ declare function _grepCore({ pattern, path: rawPath, recursive, maxMatches, flag
|
|
|
77
77
|
* writes through the shell is impractical: redirection is a shell metachar that an argv/bash allowlist denies).
|
|
78
78
|
* Creates parent directories. Caps size as a sanity ceiling. NO shell — so it gates cleanly through bareguard's
|
|
79
79
|
* fs primitive when the adopter translates `shell_write` → `{ type:'write', path }` (see createShellTools doc).
|
|
80
|
-
*
|
|
80
|
+
*
|
|
81
|
+
* `content` is REQUIRED and must be a string (BA-4). It used to default to `''`, which made the ordinary
|
|
82
|
+
* failure mode of a long generation — the model hits its output-token cap and the tool call arrives with
|
|
83
|
+
* `content` absent — silently truncate the target to zero bytes and report `"wrote 0 bytes"` as SUCCESS.
|
|
84
|
+
* No policy can catch that: a 0-byte write is a legal write, and bareguard's fs primitive judges
|
|
85
|
+
* `{type:'write', path}` without ever inspecting the body. It is a missing precondition in the primitive,
|
|
86
|
+
* not a governance gap. An explicit `content: ''` still empties the file — the caller meant it.
|
|
87
|
+
* `content` is typed REQUIRED so the generated `.d.ts` states the real contract — a library caller that omits
|
|
88
|
+
* it is a type error, not a runtime surprise. The guard below still runs, because the tool-execute boundary
|
|
89
|
+
* feeds this UNTRUSTED model-authored args (that is the boundary BA-4 was breached at, and where types buy
|
|
90
|
+
* nothing).
|
|
91
|
+
* @param {{path: string, content: string, append?: boolean, maxBytes?: number}} args
|
|
81
92
|
* @returns {Promise<string>}
|
|
82
93
|
*/
|
|
83
94
|
declare function writeFile({ path: rawPath, content, append, maxBytes }: {
|
|
84
95
|
path: string;
|
|
85
|
-
content
|
|
96
|
+
content: string;
|
|
86
97
|
append?: boolean;
|
|
87
98
|
maxBytes?: number;
|
|
88
99
|
}): Promise<string>;
|
package/tools/shell.js
CHANGED
|
@@ -90,23 +90,40 @@ async function readEntry(rawPath, maxBytes) {
|
|
|
90
90
|
* writes through the shell is impractical: redirection is a shell metachar that an argv/bash allowlist denies).
|
|
91
91
|
* Creates parent directories. Caps size as a sanity ceiling. NO shell — so it gates cleanly through bareguard's
|
|
92
92
|
* fs primitive when the adopter translates `shell_write` → `{ type:'write', path }` (see createShellTools doc).
|
|
93
|
-
*
|
|
93
|
+
*
|
|
94
|
+
* `content` is REQUIRED and must be a string (BA-4). It used to default to `''`, which made the ordinary
|
|
95
|
+
* failure mode of a long generation — the model hits its output-token cap and the tool call arrives with
|
|
96
|
+
* `content` absent — silently truncate the target to zero bytes and report `"wrote 0 bytes"` as SUCCESS.
|
|
97
|
+
* No policy can catch that: a 0-byte write is a legal write, and bareguard's fs primitive judges
|
|
98
|
+
* `{type:'write', path}` without ever inspecting the body. It is a missing precondition in the primitive,
|
|
99
|
+
* not a governance gap. An explicit `content: ''` still empties the file — the caller meant it.
|
|
100
|
+
* `content` is typed REQUIRED so the generated `.d.ts` states the real contract — a library caller that omits
|
|
101
|
+
* it is a type error, not a runtime surprise. The guard below still runs, because the tool-execute boundary
|
|
102
|
+
* feeds this UNTRUSTED model-authored args (that is the boundary BA-4 was breached at, and where types buy
|
|
103
|
+
* nothing).
|
|
104
|
+
* @param {{path: string, content: string, append?: boolean, maxBytes?: number}} args
|
|
94
105
|
* @returns {Promise<string>}
|
|
95
106
|
*/
|
|
96
|
-
async function writeFile({ path: rawPath, content
|
|
107
|
+
async function writeFile({ path: rawPath, content, append = false, maxBytes }) {
|
|
97
108
|
if (typeof rawPath !== 'string' || rawPath.length === 0) {
|
|
98
109
|
throw new Error('shell_write requires a non-empty "path" string');
|
|
99
110
|
}
|
|
100
|
-
|
|
111
|
+
if (typeof content !== 'string') {
|
|
112
|
+
throw new Error(
|
|
113
|
+
'shell_write requires a "content" string (pass content:"" to deliberately empty the file). '
|
|
114
|
+
+ `Got ${content === undefined ? 'no content argument' : `content of type ${content === null ? 'null' : typeof content}`}`
|
|
115
|
+
+ ' — refusing to write, the file is unchanged. If your output was cut short, retry with the full content.',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
101
118
|
const cap = maxBytes || DEFAULT_WRITE_MAX_BYTES;
|
|
102
|
-
const bytes = Buffer.byteLength(
|
|
119
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
103
120
|
if (bytes > cap) {
|
|
104
121
|
throw new Error(`shell_write content is ${bytes} bytes, over the ${cap}-byte cap (pass maxBytes to raise it)`);
|
|
105
122
|
}
|
|
106
123
|
const resolved = path.resolve(expandHome(rawPath));
|
|
107
124
|
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
108
|
-
if (append) await fs.appendFile(resolved,
|
|
109
|
-
else await fs.writeFile(resolved,
|
|
125
|
+
if (append) await fs.appendFile(resolved, content, 'utf8');
|
|
126
|
+
else await fs.writeFile(resolved, content, 'utf8');
|
|
110
127
|
return `${append ? 'appended' : 'wrote'} ${bytes} bytes to ${resolved}`;
|
|
111
128
|
}
|
|
112
129
|
|
|
@@ -425,13 +442,16 @@ function createShellTools() {
|
|
|
425
442
|
type: 'object',
|
|
426
443
|
properties: {
|
|
427
444
|
path: { type: 'string', description: 'Target file path. ~ expands to home. Parent dirs are created.' },
|
|
428
|
-
content: { type: 'string', description: 'The full text to write (UTF-8).' },
|
|
445
|
+
content: { type: 'string', description: 'The full text to write (UTF-8). Required — a call without it is REJECTED, not treated as an empty write. Pass "" only to deliberately empty the file.' },
|
|
429
446
|
append: { type: 'boolean', description: 'Append to the file instead of overwriting it (default false).' },
|
|
430
447
|
maxBytes: { type: 'integer', description: 'Reject a write larger than this many bytes (default 5242880).' },
|
|
431
448
|
},
|
|
432
449
|
required: ['path', 'content'],
|
|
433
450
|
},
|
|
434
|
-
|
|
451
|
+
// The args are model-authored and UNTRUSTED — `content` may be absent (an output-token-capped
|
|
452
|
+
// generation), so the boundary type stays loose and `writeFile` enforces the contract at runtime (BA-4).
|
|
453
|
+
execute: async (/** @type {{path: string, content?: string, append?: boolean, maxBytes?: number}} */ args) =>
|
|
454
|
+
writeFile(/** @type {any} */ (args)),
|
|
435
455
|
},
|
|
436
456
|
{
|
|
437
457
|
name: 'shell_run',
|
package/types/index.d.ts
CHANGED
|
@@ -80,12 +80,39 @@ 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
|
+
* Why generation ended, normalized across providers (BA-6). Before this, no provider read its native
|
|
85
|
+
* finish-reason field, so a round the API CUT OFF at the token cap was indistinguishable from one the
|
|
86
|
+
* model chose to end — and the Loop, whose rule is "no tool calls ⇒ final answer", returned the
|
|
87
|
+
* truncation as a clean finish with `error: null`.
|
|
88
|
+
*
|
|
89
|
+
* - `'end_turn'` — the model finished on its own. The only clean finish.
|
|
90
|
+
* - `'max_tokens'` — CUT OFF at the output cap. The Loop returns `error: 'truncated:max_tokens'`
|
|
91
|
+
* (preserving the partial text) and REFUSES to execute any tool call the round carries: a complete
|
|
92
|
+
* call always arrives as `'tool_use'`, so one riding a `'max_tokens'` round was cut off
|
|
93
|
+
* mid-generation with arguments missing — the BA-4 file-zeroing mechanism.
|
|
94
|
+
* - `'tool_use'` — stopped to call a tool, and the call is COMPLETE.
|
|
95
|
+
* - `'stop_sequence'` / `'refusal'` / `'pause_turn'` / `'context_exceeded'` — reported, not acted on.
|
|
96
|
+
* - `null` — the provider didn't report one (e.g. CLIPipe) or the value is unrecognized. Reproduces
|
|
97
|
+
* pre-BA-6 behavior exactly, so an unmapped provider degrades to the status quo.
|
|
98
|
+
*/
|
|
99
|
+
stopReason?: string | null;
|
|
83
100
|
/**
|
|
84
101
|
* True when the requested `temperature` was rejected by the model (400, unsupported/deprecated) and
|
|
85
102
|
* the request was retried without it (BA-10). The response was produced at the model's DEFAULT
|
|
86
103
|
* temperature, not the one requested — callers reporting an effective temperature must honor this.
|
|
87
104
|
*/
|
|
88
105
|
temperatureDropped?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* BA-7 — provider-native content blocks the normalized `{text, toolCalls}` shape cannot express
|
|
108
|
+
* (Anthropic `thinking` / `redacted_thinking`), captured opaquely so the Loop can put them on the
|
|
109
|
+
* transcript and the provider can replay them on the next round.
|
|
110
|
+
*
|
|
111
|
+
* Present only when the response actually carried such blocks — so a provider that returns none
|
|
112
|
+
* leaves both the result and the resulting message byte-identical to pre-BA-7. See `Message.providerBlocks`
|
|
113
|
+
* for the replay contract (the provider/model tag is enforced; a signature is model-bound).
|
|
114
|
+
*/
|
|
115
|
+
providerBlocks?: { provider: string; model: string; blocks: any[] };
|
|
89
116
|
/**
|
|
90
117
|
* Authoritative per-call cost in USD, reported by the provider itself — e.g. CLIPipeProvider
|
|
91
118
|
* `parse:'claude-json'` surfacing the claude CLI's own `total_cost_usd`, a real price with no local
|
|
@@ -102,6 +129,25 @@ export interface Message {
|
|
|
102
129
|
content?: string | null;
|
|
103
130
|
tool_calls?: any[];
|
|
104
131
|
tool_call_id?: string;
|
|
132
|
+
/**
|
|
133
|
+
* BA-7 — provider-native content blocks that this OpenAI-shaped message cannot express, carried
|
|
134
|
+
* verbatim so they can be replayed to the provider that issued them.
|
|
135
|
+
*
|
|
136
|
+
* Today this is Anthropic `thinking` / `redacted_thinking`. Anthropic's contract is that such
|
|
137
|
+
* blocks are echoed back UNCHANGED (`signature` included) when continuing a tool-use conversation;
|
|
138
|
+
* before BA-7 there was no field on this type that could hold one, so they were silently dropped.
|
|
139
|
+
*
|
|
140
|
+
* OPAQUE by design — the Loop never reads `blocks`, and the provider re-emits their bytes rather
|
|
141
|
+
* than re-serializing a parsed shape (a `redacted_thinking` block cannot survive a round-trip
|
|
142
|
+
* through parsed fields). The `provider`/`model` tag is enforced on replay: a thinking signature is
|
|
143
|
+
* bound to the model that produced it, so a mismatch drops the blocks and degrades to the lossy
|
|
144
|
+
* pre-BA-7 request rather than risking a 400.
|
|
145
|
+
*
|
|
146
|
+
* The normalized `content` / `tool_calls` remain the source of truth: only blocks that have no
|
|
147
|
+
* normalized representation live here, so an `assemble`/`trim` seam that rewrites this message is
|
|
148
|
+
* never silently undone by a stale cached copy of its text.
|
|
149
|
+
*/
|
|
150
|
+
providerBlocks?: { provider: string; model: string; blocks: any[] };
|
|
105
151
|
[key: string]: any;
|
|
106
152
|
}
|
|
107
153
|
|