@animalabs/membrane 0.5.80 → 0.5.82
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/dist/cache-wire-receipt.d.ts +13 -0
- package/dist/cache-wire-receipt.d.ts.map +1 -0
- package/dist/cache-wire-receipt.js +108 -0
- package/dist/cache-wire-receipt.js.map +1 -0
- package/dist/context/index.d.ts +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +1 -1
- package/dist/context/index.js.map +1 -1
- package/dist/context/process.d.ts +3 -1
- package/dist/context/process.d.ts.map +1 -1
- package/dist/context/process.js +230 -49
- package/dist/context/process.js.map +1 -1
- package/dist/context/types.d.ts +30 -0
- package/dist/context/types.d.ts.map +1 -1
- package/dist/context/types.js +16 -0
- package/dist/context/types.js.map +1 -1
- package/dist/floating-cache-marker.test.js +21 -0
- package/dist/floating-cache-marker.test.js.map +1 -1
- package/dist/formatters/anthropic-xml.d.ts +2 -0
- package/dist/formatters/anthropic-xml.d.ts.map +1 -1
- package/dist/formatters/anthropic-xml.js +64 -23
- package/dist/formatters/anthropic-xml.js.map +1 -1
- package/dist/formatters/native.d.ts.map +1 -1
- package/dist/formatters/native.js +16 -3
- package/dist/formatters/native.js.map +1 -1
- package/dist/formatters/normalize-tool-pairs.d.ts +7 -0
- package/dist/formatters/normalize-tool-pairs.d.ts.map +1 -1
- package/dist/formatters/normalize-tool-pairs.js +45 -0
- package/dist/formatters/normalize-tool-pairs.js.map +1 -1
- package/dist/formatters/types.d.ts +31 -0
- package/dist/formatters/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/membrane.d.ts +137 -8
- package/dist/membrane.d.ts.map +1 -1
- package/dist/membrane.js +823 -310
- package/dist/membrane.js.map +1 -1
- package/dist/providers/anthropic.d.ts +37 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +142 -25
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/bedrock.d.ts +8 -0
- package/dist/providers/bedrock.d.ts.map +1 -1
- package/dist/providers/bedrock.js +24 -1
- package/dist/providers/bedrock.js.map +1 -1
- package/dist/providers/gemini.d.ts +11 -0
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +104 -82
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/mock.d.ts +2 -0
- package/dist/providers/mock.d.ts.map +1 -1
- package/dist/providers/mock.js +2 -0
- package/dist/providers/mock.js.map +1 -1
- package/dist/providers/openai-compatible.d.ts +7 -0
- package/dist/providers/openai-compatible.d.ts.map +1 -1
- package/dist/providers/openai-compatible.js +99 -48
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/providers/openai-completions.d.ts +5 -0
- package/dist/providers/openai-completions.d.ts.map +1 -1
- package/dist/providers/openai-completions.js +107 -44
- package/dist/providers/openai-completions.js.map +1 -1
- package/dist/providers/openai-responses-api.d.ts +16 -0
- package/dist/providers/openai-responses-api.d.ts.map +1 -1
- package/dist/providers/openai-responses-api.js +38 -10
- package/dist/providers/openai-responses-api.js.map +1 -1
- package/dist/providers/openai-responses.d.ts +2 -0
- package/dist/providers/openai-responses.d.ts.map +1 -1
- package/dist/providers/openai-responses.js +8 -1
- package/dist/providers/openai-responses.js.map +1 -1
- package/dist/providers/openai.d.ts +7 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +92 -44
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +9 -0
- package/dist/providers/openrouter.d.ts.map +1 -1
- package/dist/providers/openrouter.js +118 -62
- package/dist/providers/openrouter.js.map +1 -1
- package/dist/providers/utils.d.ts +74 -2
- package/dist/providers/utils.d.ts.map +1 -1
- package/dist/providers/utils.js +184 -3
- package/dist/providers/utils.js.map +1 -1
- package/dist/registry/default-pricing.d.ts +22 -0
- package/dist/registry/default-pricing.d.ts.map +1 -1
- package/dist/registry/default-pricing.js +162 -28
- package/dist/registry/default-pricing.js.map +1 -1
- package/dist/types/content.d.ts +13 -0
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/content.js.map +1 -1
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +26 -0
- package/dist/types/errors.js.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/provider.d.ts +50 -0
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/request.d.ts +6 -0
- package/dist/types/request.d.ts.map +1 -1
- package/dist/types/response.d.ts +85 -4
- package/dist/types/response.d.ts.map +1 -1
- package/dist/types/response.js.map +1 -1
- package/dist/types/streaming.d.ts +6 -1
- package/dist/types/streaming.d.ts.map +1 -1
- package/dist/utils/cache-marker-budget.d.ts +91 -0
- package/dist/utils/cache-marker-budget.d.ts.map +1 -0
- package/dist/utils/cache-marker-budget.js +208 -0
- package/dist/utils/cache-marker-budget.js.map +1 -0
- package/dist/utils/cost.d.ts +22 -0
- package/dist/utils/cost.d.ts.map +1 -1
- package/dist/utils/cost.js +55 -0
- package/dist/utils/cost.js.map +1 -1
- package/dist/utils/thinking-carriers.d.ts +80 -0
- package/dist/utils/thinking-carriers.d.ts.map +1 -0
- package/dist/utils/thinking-carriers.js +115 -0
- package/dist/utils/thinking-carriers.js.map +1 -0
- package/dist/utils/tool-parser.d.ts +24 -0
- package/dist/utils/tool-parser.d.ts.map +1 -1
- package/dist/utils/tool-parser.js +367 -161
- package/dist/utils/tool-parser.js.map +1 -1
- package/dist/utils/usage.d.ts +71 -0
- package/dist/utils/usage.d.ts.map +1 -0
- package/dist/utils/usage.js +161 -0
- package/dist/utils/usage.js.map +1 -0
- package/dist/yielding-stream.d.ts +8 -0
- package/dist/yielding-stream.d.ts.map +1 -1
- package/dist/yielding-stream.js +49 -3
- package/dist/yielding-stream.js.map +1 -1
- package/package.json +1 -1
- package/src/cache-wire-receipt.ts +125 -0
- package/src/context/index.ts +1 -0
- package/src/context/process.ts +305 -59
- package/src/context/types.ts +38 -0
- package/src/floating-cache-marker.test.ts +24 -0
- package/src/formatters/anthropic-xml.ts +66 -23
- package/src/formatters/native.ts +23 -2
- package/src/formatters/normalize-tool-pairs.ts +49 -0
- package/src/formatters/types.ts +35 -0
- package/src/index.ts +6 -0
- package/src/membrane.ts +974 -313
- package/src/providers/anthropic.ts +173 -19
- package/src/providers/bedrock.ts +25 -1
- package/src/providers/gemini.ts +125 -87
- package/src/providers/index.ts +1 -0
- package/src/providers/mock.ts +3 -0
- package/src/providers/openai-compatible.ts +102 -45
- package/src/providers/openai-completions.ts +112 -46
- package/src/providers/openai-responses-api.ts +45 -15
- package/src/providers/openai-responses.ts +8 -1
- package/src/providers/openai.ts +95 -42
- package/src/providers/openrouter.ts +125 -63
- package/src/providers/utils.ts +227 -6
- package/src/registry/default-pricing.ts +165 -28
- package/src/types/content.ts +14 -0
- package/src/types/errors.ts +28 -0
- package/src/types/index.ts +6 -0
- package/src/types/provider.ts +55 -0
- package/src/types/request.ts +7 -0
- package/src/types/response.ts +109 -8
- package/src/types/streaming.ts +8 -2
- package/src/utils/cache-marker-budget.ts +237 -0
- package/src/utils/cost.ts +64 -0
- package/src/utils/thinking-carriers.ts +125 -0
- package/src/utils/tool-parser.ts +473 -174
- package/src/utils/usage.ts +184 -0
- package/src/yielding-stream.ts +54 -3
package/src/utils/tool-parser.ts
CHANGED
|
@@ -41,110 +41,241 @@ function parseParamValue(value: string): unknown {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Parse the parameters of one invoke body. A self-closing invoke has no body
|
|
46
|
+
* and therefore no parameters.
|
|
47
|
+
*/
|
|
48
|
+
function parseInvokeParameters(invokeBody: string | undefined): Record<string, unknown> {
|
|
49
|
+
const input: Record<string, unknown> = {};
|
|
50
|
+
if (invokeBody === undefined) return input;
|
|
51
|
+
|
|
52
|
+
PARAMETER_REGEX.lastIndex = 0;
|
|
53
|
+
let paramMatch: RegExpExecArray | null;
|
|
54
|
+
while ((paramMatch = PARAMETER_REGEX.exec(invokeBody)) !== null) {
|
|
55
|
+
input[paramMatch[2] ?? ''] = parseParamValue(paramMatch[3] ?? '');
|
|
56
|
+
}
|
|
57
|
+
return input;
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
// ============================================================================
|
|
45
61
|
// Tool Call Parsing
|
|
46
62
|
// ============================================================================
|
|
47
63
|
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
64
|
+
// Invoke tags, both forms in ONE alternation so a block that mixes them keeps
|
|
65
|
+
// document order (two sequential passes appended full-then-self-closing).
|
|
66
|
+
// The name may be single- or double-quoted and whitespace may precede the
|
|
67
|
+
// closing angle bracket; the quote character is backreferenced so the opposite
|
|
68
|
+
// quote stays legal inside the name.
|
|
69
|
+
// The name is at least one character and cannot contain its own quote, so a
|
|
70
|
+
// nameless invoke and a stray second attribute both fail to match rather than
|
|
71
|
+
// yielding a garbage tool name — a block that parses to no invokes is reported.
|
|
72
|
+
// Groups: 1 = antml prefix, 2 = quote char, 3 = name, 4 = body (full form only).
|
|
73
|
+
const INVOKE_REGEX =
|
|
74
|
+
/<(antml:)?invoke\s+name=(["'])((?:(?!\2).)+)\2\s*(?:\/>|>([\s\S]*?)<\/(antml:)?invoke>)/g;
|
|
75
|
+
|
|
76
|
+
const INVOKE_NAME_GROUP = 3;
|
|
77
|
+
const INVOKE_BODY_GROUP = 4;
|
|
78
|
+
const INVOKE_CLOSER_PREFIX_GROUP = 5;
|
|
79
|
+
|
|
80
|
+
// Same pattern without /g, for the re-anchor read that runs INSIDE the outer
|
|
81
|
+
// iteration: sharing the /g instance would clobber its lastIndex. Built from
|
|
82
|
+
// the one source so the two can never drift apart.
|
|
83
|
+
const INVOKE_REANCHOR_REGEX = new RegExp(INVOKE_REGEX.source);
|
|
84
|
+
|
|
85
|
+
// An invoke OPENER on its own — the containment test one level below the
|
|
86
|
+
// block fix, since an opener that never closed is invisible to INVOKE_REGEX.
|
|
87
|
+
const INVOKE_OPEN_REGEX = /<(antml:)?invoke\s+name=/g;
|
|
88
|
+
|
|
89
|
+
const INVOKE_CLOSE_TAG = '</invoke>';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The invokes one block's inner content dispatches, plus the heads it refused.
|
|
93
|
+
*
|
|
94
|
+
* The full-form alternative of INVOKE_REGEX is lazy, so an invoke the model
|
|
95
|
+
* left OPEN pairs with the NEXT invoke's closing tag: the head matched, carried
|
|
96
|
+
* the inner call's parameters as its own, and the inner call never parsed at
|
|
97
|
+
* all — the same block-splice disease one level down. A match whose BODY holds
|
|
98
|
+
* another opener is therefore refused as a dispatchable invoke and re-anchored
|
|
99
|
+
* to the innermost opener inside it, which is where the live call begins. The
|
|
100
|
+
* re-anchored text holds no further opener by construction, so this terminates.
|
|
101
|
+
*/
|
|
102
|
+
interface ParsedInvokes {
|
|
103
|
+
calls: Array<{ name: string; input: Record<string, unknown> }>;
|
|
104
|
+
unclosedHeads: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Offsets of every invoke opener inside one matched invoke body. */
|
|
108
|
+
function invokeOpenerOffsets(invokeBody: string): number[] {
|
|
109
|
+
const offsets: number[] = [];
|
|
110
|
+
INVOKE_OPEN_REGEX.lastIndex = 0;
|
|
111
|
+
let openerMatch: RegExpExecArray | null;
|
|
112
|
+
while ((openerMatch = INVOKE_OPEN_REGEX.exec(invokeBody)) !== null) {
|
|
113
|
+
offsets.push(openerMatch.index);
|
|
114
|
+
}
|
|
115
|
+
return offsets;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function collectInvokes(innerContent: string): ParsedInvokes {
|
|
119
|
+
const calls: Array<{ name: string; input: Record<string, unknown> }> = [];
|
|
120
|
+
let unclosedHeads = 0;
|
|
121
|
+
|
|
122
|
+
INVOKE_REGEX.lastIndex = 0;
|
|
123
|
+
let invokeMatch: RegExpExecArray | null;
|
|
124
|
+
while ((invokeMatch = INVOKE_REGEX.exec(innerContent)) !== null) {
|
|
125
|
+
const invokeBody = invokeMatch[INVOKE_BODY_GROUP];
|
|
126
|
+
const swallowedOpenerOffsets = invokeBody === undefined ? [] : invokeOpenerOffsets(invokeBody);
|
|
127
|
+
|
|
128
|
+
if (swallowedOpenerOffsets.length === 0) {
|
|
129
|
+
calls.push({
|
|
130
|
+
name: invokeMatch[INVOKE_NAME_GROUP] ?? '',
|
|
131
|
+
input: parseInvokeParameters(invokeBody),
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
51
135
|
|
|
52
|
-
//
|
|
53
|
-
|
|
136
|
+
// Every opener in the body is a head that never closed, and so is the
|
|
137
|
+
// matched head itself; only the innermost one owns the closing tag.
|
|
138
|
+
unclosedHeads += swallowedOpenerOffsets.length;
|
|
139
|
+
|
|
140
|
+
const fullMatch = invokeMatch[0];
|
|
141
|
+
const closerLength =
|
|
142
|
+
INVOKE_CLOSE_TAG.length + (invokeMatch[INVOKE_CLOSER_PREFIX_GROUP]?.length ?? 0);
|
|
143
|
+
const bodyOffset = fullMatch.length - closerLength - invokeBody!.length;
|
|
144
|
+
const innermostOffset = swallowedOpenerOffsets[swallowedOpenerOffsets.length - 1]!;
|
|
145
|
+
const reanchoredMatch = INVOKE_REANCHOR_REGEX.exec(fullMatch.slice(bodyOffset + innermostOffset));
|
|
146
|
+
|
|
147
|
+
// A re-anchored head that still does not parse — a nameless invoke, say —
|
|
148
|
+
// is refused like any other: counted above, dispatched never.
|
|
149
|
+
if (reanchoredMatch) {
|
|
150
|
+
calls.push({
|
|
151
|
+
name: reanchoredMatch[INVOKE_NAME_GROUP] ?? '',
|
|
152
|
+
input: parseInvokeParameters(reanchoredMatch[INVOKE_BODY_GROUP]),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
54
156
|
|
|
55
|
-
|
|
56
|
-
|
|
157
|
+
return { calls, unclosedHeads };
|
|
158
|
+
}
|
|
57
159
|
|
|
58
160
|
// Parameter tags
|
|
59
161
|
const PARAMETER_REGEX = /<(antml:)?parameter\s+name="([^"]+)">([\s\S]*?)<\/(antml:)?parameter>/g;
|
|
60
162
|
|
|
61
|
-
|
|
62
|
-
const FUNCTION_RESULTS_START = /<(antml:)?function_results>/;
|
|
163
|
+
const FUNCTION_CALLS_OPEN_REGEX = /<(antml:)?function_calls>/g;
|
|
63
164
|
|
|
64
165
|
/**
|
|
65
|
-
*
|
|
166
|
+
* One <function_calls> block, resolved to the span its calls may be read from.
|
|
66
167
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
168
|
+
* The block regexes are flat: first opener to first closer, with no nesting
|
|
169
|
+
* check. When a max_tokens truncation leaves an unclosed block in the persisted
|
|
170
|
+
* turn, the next round's closer splices onto that stale opener and the match
|
|
171
|
+
* spans two rounds — so the stale invoke pairs with the NEW round's `</invoke>`
|
|
172
|
+
* and one call is dispatched bearing the stale tool's name and everything
|
|
173
|
+
* between as its argument, including intervening user text.
|
|
174
|
+
*
|
|
175
|
+
* A match whose inner content holds another opener is therefore REJECTED as a
|
|
176
|
+
* block: no dispatch ever crosses an opener. The span is then re-anchored to
|
|
177
|
+
* the innermost (last) opener inside it, which is where the live block actually
|
|
178
|
+
* begins, so the real call still runs and the stale half falls out as ordinary
|
|
179
|
+
* preceding text. Re-anchoring terminates: the re-anchored inner content holds
|
|
180
|
+
* no opener by construction.
|
|
69
181
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
while ((blockMatch = FUNCTION_CALLS_REGEX.exec(text)) !== null) {
|
|
79
|
-
// Check if this block already has results after it
|
|
80
|
-
const afterPos = blockMatch.index + blockMatch[0].length;
|
|
81
|
-
const textAfter = text.slice(afterPos, afterPos + 100); // Check next 100 chars
|
|
82
|
-
|
|
83
|
-
if (!FUNCTION_RESULTS_START.test(textAfter.trimStart())) {
|
|
84
|
-
// This block hasn't been executed yet - store it
|
|
85
|
-
// Need to capture all properties since exec returns are reused
|
|
86
|
-
lastUnexecutedMatch = {
|
|
87
|
-
...blockMatch,
|
|
88
|
-
index: blockMatch.index,
|
|
89
|
-
input: blockMatch.input,
|
|
90
|
-
} as RegExpExecArray;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
182
|
+
interface ResolvedToolBlock {
|
|
183
|
+
start: number;
|
|
184
|
+
end: number;
|
|
185
|
+
innerContent: string;
|
|
186
|
+
fullMatch: string;
|
|
187
|
+
wasSpliced: boolean;
|
|
188
|
+
}
|
|
93
189
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
190
|
+
function resolveToolBlock(text: string, blockMatch: RegExpExecArray): ResolvedToolBlock {
|
|
191
|
+
const end = blockMatch.index + blockMatch[0].length;
|
|
192
|
+
const innerContent = blockMatch[2] ?? '';
|
|
193
|
+
const innerStart = blockMatch.index + blockMatch[0].indexOf('>') + 1;
|
|
97
194
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
195
|
+
FUNCTION_CALLS_OPEN_REGEX.lastIndex = 0;
|
|
196
|
+
let innermostOpener: RegExpExecArray | null = null;
|
|
197
|
+
let nestedMatch: RegExpExecArray | null;
|
|
198
|
+
while ((nestedMatch = FUNCTION_CALLS_OPEN_REGEX.exec(innerContent)) !== null) {
|
|
199
|
+
innermostOpener = nestedMatch;
|
|
200
|
+
}
|
|
101
201
|
|
|
102
|
-
|
|
103
|
-
|
|
202
|
+
if (!innermostOpener) {
|
|
203
|
+
return {
|
|
204
|
+
start: blockMatch.index,
|
|
205
|
+
end,
|
|
206
|
+
innerContent,
|
|
207
|
+
fullMatch: blockMatch[0],
|
|
208
|
+
wasSpliced: false,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
104
211
|
|
|
105
|
-
const
|
|
212
|
+
const reanchoredStart = innerStart + innermostOpener.index;
|
|
213
|
+
return {
|
|
214
|
+
start: reanchoredStart,
|
|
215
|
+
end,
|
|
216
|
+
innerContent: innerContent.slice(innermostOpener.index + innermostOpener[0].length),
|
|
217
|
+
fullMatch: text.slice(reanchoredStart, end),
|
|
218
|
+
wasSpliced: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
106
221
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
222
|
+
// Openers that mark a block as already executed, when one is the very next
|
|
223
|
+
// token after it
|
|
224
|
+
const FUNCTION_RESULTS_START_ANCHORED = /^<(antml:)?function_results>/;
|
|
110
225
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const invokeContent = invokeMatch[3] ?? ''; // Group 3 is the content
|
|
226
|
+
// Longest opener the anchored test can match, so the slice it reads is bounded
|
|
227
|
+
const FUNCTION_RESULTS_OPENER_MAX_LENGTH = '<function_results>'.length + 'antml:'.length;
|
|
114
228
|
|
|
115
|
-
|
|
116
|
-
const input: Record<string, unknown> = {};
|
|
117
|
-
PARAMETER_REGEX.lastIndex = 0;
|
|
118
|
-
let paramMatch: RegExpExecArray | null;
|
|
229
|
+
const SINGLE_WHITESPACE_REGEX = /\s/;
|
|
119
230
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
231
|
+
/**
|
|
232
|
+
* Has this block already been executed? True only when the next NON-WHITESPACE
|
|
233
|
+
* token after it is a function_results opener. The previous test — "a
|
|
234
|
+
* function_results opener appears anywhere in the next 100 characters" — was
|
|
235
|
+
* wrong in both directions: padding past 100 characters re-selected a block
|
|
236
|
+
* that had already run, and a results block belonging to some later exchange
|
|
237
|
+
* marked a live call as spent.
|
|
238
|
+
*/
|
|
239
|
+
function isFollowedByResults(text: string, afterPos: number): boolean {
|
|
240
|
+
let scan = afterPos;
|
|
241
|
+
while (scan < text.length && SINGLE_WHITESPACE_REGEX.test(text[scan]!)) scan++;
|
|
242
|
+
return FUNCTION_RESULTS_START_ANCHORED.test(
|
|
243
|
+
text.slice(scan, scan + FUNCTION_RESULTS_OPENER_MAX_LENGTH)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
123
246
|
|
|
124
|
-
|
|
125
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Parse tool calls from text containing XML function_calls blocks
|
|
249
|
+
*
|
|
250
|
+
* Uses "last-unexecuted-block" logic: finds the last function_calls block
|
|
251
|
+
* that doesn't have function_results immediately following it.
|
|
252
|
+
*/
|
|
253
|
+
export function parseToolCalls(text: string): ParsedToolCalls | null {
|
|
254
|
+
// Pick the last unexecuted block among those that survive containment: a
|
|
255
|
+
// block quoted inside thinking or echoed in a tool result is content, and
|
|
256
|
+
// dispatching from it runs a call the model never made.
|
|
257
|
+
let lastUnexecutedBlock: ResolvedToolBlock | null = null;
|
|
258
|
+
|
|
259
|
+
for (const block of collectLiveToolBlocks(text)) {
|
|
260
|
+
if (!isFollowedByResults(text, block.end)) {
|
|
261
|
+
lastUnexecutedBlock = block;
|
|
126
262
|
}
|
|
263
|
+
}
|
|
127
264
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
name: toolName,
|
|
131
|
-
input,
|
|
132
|
-
});
|
|
265
|
+
if (!lastUnexecutedBlock) {
|
|
266
|
+
return null;
|
|
133
267
|
}
|
|
134
268
|
|
|
135
|
-
|
|
136
|
-
INVOKE_REGEX_SELF_CLOSE.lastIndex = 0;
|
|
137
|
-
let selfCloseMatch: RegExpExecArray | null;
|
|
269
|
+
const { innerContent, fullMatch } = lastUnexecutedBlock;
|
|
138
270
|
|
|
139
|
-
|
|
140
|
-
|
|
271
|
+
const beforeText = text.slice(0, lastUnexecutedBlock.start);
|
|
272
|
+
const afterText = text.slice(lastUnexecutedBlock.end);
|
|
141
273
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
274
|
+
const calls: ToolCall[] = collectInvokes(innerContent).calls.map((invoke) => ({
|
|
275
|
+
id: generateToolId(),
|
|
276
|
+
name: invoke.name,
|
|
277
|
+
input: invoke.input,
|
|
278
|
+
}));
|
|
148
279
|
|
|
149
280
|
return {
|
|
150
281
|
calls,
|
|
@@ -336,6 +467,142 @@ export function formatToolDefinitions(tools: ToolDefinitionForPrompt[]): string
|
|
|
336
467
|
// Regex for matching thinking blocks (both plain and antml: prefixed)
|
|
337
468
|
const THINKING_BLOCK_REGEX = /<(antml:)?thinking>([\s\S]*?)<\/(antml:)?thinking>/g;
|
|
338
469
|
|
|
470
|
+
/**
|
|
471
|
+
* One span the block sweeps found, before anything about it is registered.
|
|
472
|
+
*
|
|
473
|
+
* The sweeps are independent and their spans overlap: a function_calls block
|
|
474
|
+
* quoted inside a thinking block is found by both. Containment decides which
|
|
475
|
+
* spans are real, and it has to decide FIRST — every side effect downstream
|
|
476
|
+
* (call sites, legacy-result claims, diagnostic counts) is scoped to the
|
|
477
|
+
* survivors.
|
|
478
|
+
*/
|
|
479
|
+
type CandidateSpan =
|
|
480
|
+
| { kind: 'thinking'; start: number; end: number; thinking: string }
|
|
481
|
+
| { kind: 'calls'; start: number; end: number; resolvedBlock: ResolvedToolBlock }
|
|
482
|
+
| { kind: 'results'; start: number; end: number; innerContent: string; rawXml: string };
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Keep the outermost spans, in document order. RETAINED SPANS NEVER OVERLAP:
|
|
486
|
+
* a span whose START lies inside one already kept is refused, whether it ends
|
|
487
|
+
* inside that container or crosses out past it.
|
|
488
|
+
*
|
|
489
|
+
* Sorted by start with the outermost first on a tie, a span that begins before
|
|
490
|
+
* the furthest retained end begins INSIDE a container, and everything a
|
|
491
|
+
* container encloses is its content — the model wrote it, it did not call it.
|
|
492
|
+
* Testing only `end` let a crossing span through: a call opening inside a
|
|
493
|
+
* thinking block or a tool result and closing after it was retained alongside
|
|
494
|
+
* its container, so the quoted call dispatched and the overlapping source text
|
|
495
|
+
* was emitted twice — once as thinking content, once as a real tool_use — while
|
|
496
|
+
* the text cursor walked past the inner span, printing the container's own
|
|
497
|
+
* closing tag as visible model text. A dangling closer left downstream by such
|
|
498
|
+
* a refusal is what it looks like: ordinary text.
|
|
499
|
+
*
|
|
500
|
+
* The bound is unchanged: only a CLOSED container is visible here, so the
|
|
501
|
+
* streaming stop-sequence path, where the text is cut at `</function_calls>`
|
|
502
|
+
* before the enclosing `<thinking>` closes, is still outside this rule.
|
|
503
|
+
*/
|
|
504
|
+
function retainOutermostSpans(spans: CandidateSpan[]): CandidateSpan[] {
|
|
505
|
+
const sorted = [...spans].sort((a, b) => a.start - b.start || b.end - a.end);
|
|
506
|
+
const retained: CandidateSpan[] = [];
|
|
507
|
+
let furthestRetainedEnd = -1;
|
|
508
|
+
for (const span of sorted) {
|
|
509
|
+
if (span.start < furthestRetainedEnd) continue;
|
|
510
|
+
retained.push(span);
|
|
511
|
+
furthestRetainedEnd = span.end;
|
|
512
|
+
}
|
|
513
|
+
return retained;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* The text left once every retained span is cut out — the document's own
|
|
518
|
+
* structural level.
|
|
519
|
+
*
|
|
520
|
+
* Structural diagnostics read this rather than the raw text, because a tool
|
|
521
|
+
* tag QUOTED inside a thinking block or a tool result is content, not
|
|
522
|
+
* structure: counting it made a model that merely described an unclosed block
|
|
523
|
+
* indistinguishable from one whose block was truncated mid-write.
|
|
524
|
+
*/
|
|
525
|
+
/**
|
|
526
|
+
* Every span the three block sweeps find, unfiltered and unregistered.
|
|
527
|
+
*
|
|
528
|
+
* Both parse entry points read the document through this one census, so the
|
|
529
|
+
* dispatch path and the block path can never disagree about which blocks are
|
|
530
|
+
* real.
|
|
531
|
+
*/
|
|
532
|
+
function collectCandidateSpans(text: string): CandidateSpan[] {
|
|
533
|
+
const spans: CandidateSpan[] = [];
|
|
534
|
+
|
|
535
|
+
THINKING_BLOCK_REGEX.lastIndex = 0;
|
|
536
|
+
let thinkingMatch: RegExpExecArray | null;
|
|
537
|
+
while ((thinkingMatch = THINKING_BLOCK_REGEX.exec(text)) !== null) {
|
|
538
|
+
spans.push({
|
|
539
|
+
kind: 'thinking',
|
|
540
|
+
start: thinkingMatch.index,
|
|
541
|
+
end: thinkingMatch.index + thinkingMatch[0].length,
|
|
542
|
+
thinking: thinkingMatch[2] ?? '',
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
FUNCTION_BLOCK_WITH_CONTENT_REGEX.lastIndex = 0;
|
|
547
|
+
let funcMatch: RegExpExecArray | null;
|
|
548
|
+
while ((funcMatch = FUNCTION_BLOCK_WITH_CONTENT_REGEX.exec(text)) !== null) {
|
|
549
|
+
// A spliced match (a stale opener joined to a later round's closer) is
|
|
550
|
+
// rejected as a block and re-anchored to its innermost opener; see
|
|
551
|
+
// resolveToolBlock. Containment reads the RESOLVED span, which is where
|
|
552
|
+
// the live block actually begins.
|
|
553
|
+
const resolvedBlock = resolveToolBlock(text, funcMatch);
|
|
554
|
+
spans.push({
|
|
555
|
+
kind: 'calls',
|
|
556
|
+
start: resolvedBlock.start,
|
|
557
|
+
end: resolvedBlock.end,
|
|
558
|
+
resolvedBlock,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
FUNCTION_RESULTS_BLOCK_REGEX.lastIndex = 0;
|
|
563
|
+
let resultsMatch: RegExpExecArray | null;
|
|
564
|
+
while ((resultsMatch = FUNCTION_RESULTS_BLOCK_REGEX.exec(text)) !== null) {
|
|
565
|
+
spans.push({
|
|
566
|
+
kind: 'results',
|
|
567
|
+
start: resultsMatch.index,
|
|
568
|
+
end: resultsMatch.index + resultsMatch[0].length,
|
|
569
|
+
innerContent: resultsMatch[2] ?? '',
|
|
570
|
+
// Verbatim document text the harness placed — carried for exact replay
|
|
571
|
+
// on the prefill path (membrane#36).
|
|
572
|
+
rawXml: resultsMatch[0],
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return spans;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* The function_calls blocks that are structure rather than content: those that
|
|
581
|
+
* survive containment, in document order.
|
|
582
|
+
*
|
|
583
|
+
* A block quoted inside a <thinking> block or echoed inside a tool result is
|
|
584
|
+
* text the model wrote ABOUT a call, not a call. Selecting from the raw sweep
|
|
585
|
+
* dispatched it — a model that named a tool and explicitly declined to use it
|
|
586
|
+
* had it executed anyway.
|
|
587
|
+
*/
|
|
588
|
+
function collectLiveToolBlocks(text: string): ResolvedToolBlock[] {
|
|
589
|
+
const live: ResolvedToolBlock[] = [];
|
|
590
|
+
for (const span of retainOutermostSpans(collectCandidateSpans(text))) {
|
|
591
|
+
if (span.kind === 'calls') live.push(span.resolvedBlock);
|
|
592
|
+
}
|
|
593
|
+
return live;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function textOutsideSpans(text: string, spans: CandidateSpan[]): string {
|
|
597
|
+
let residue = '';
|
|
598
|
+
let cursor = 0;
|
|
599
|
+
for (const span of spans) {
|
|
600
|
+
if (span.start > cursor) residue += text.slice(cursor, span.start);
|
|
601
|
+
cursor = Math.max(cursor, span.end);
|
|
602
|
+
}
|
|
603
|
+
return residue + text.slice(cursor);
|
|
604
|
+
}
|
|
605
|
+
|
|
339
606
|
// Regex for matching function_calls blocks with their content
|
|
340
607
|
const FUNCTION_BLOCK_WITH_CONTENT_REGEX = /<(antml:)?function_calls>([\s\S]*?)<\/(antml:)?function_calls>/g;
|
|
341
608
|
|
|
@@ -369,6 +636,30 @@ export function parseAccumulatedIntoBlocks(
|
|
|
369
636
|
blocks: ContentBlock[];
|
|
370
637
|
toolCalls: ToolCall[];
|
|
371
638
|
toolResults: ToolResult[];
|
|
639
|
+
/**
|
|
640
|
+
* The text ends inside a tool block — an opener with no closer, or a cut
|
|
641
|
+
* mid-tag. The membrane loop does not resume on a length stop, so this is
|
|
642
|
+
* what a max_tokens truncation leaves behind, and persisting it bare lets the
|
|
643
|
+
* next round's closer splice onto the stale opener.
|
|
644
|
+
*/
|
|
645
|
+
unclosedToolBlock: boolean;
|
|
646
|
+
/**
|
|
647
|
+
* function_calls blocks that yielded no invokes at all. Always a defect —
|
|
648
|
+
* never how a well-formed block ends.
|
|
649
|
+
*/
|
|
650
|
+
emptyToolBlocks: number;
|
|
651
|
+
/**
|
|
652
|
+
* Block matches that spanned another opener and were re-anchored to the
|
|
653
|
+
* innermost one. Each is a truncated block that was persisted bare and is
|
|
654
|
+
* being repaired at read time.
|
|
655
|
+
*/
|
|
656
|
+
splicedToolBlocks: number;
|
|
657
|
+
/**
|
|
658
|
+
* `<invoke>` heads that were left open and swallowed a later invoke, so the
|
|
659
|
+
* match was refused and re-anchored to the call it absorbed. Nothing was
|
|
660
|
+
* dispatched under the head's name, and the head itself never ran.
|
|
661
|
+
*/
|
|
662
|
+
unclosedInvokeHeads: number;
|
|
372
663
|
} {
|
|
373
664
|
// If we're starting inside a block from prefill, prepend a synthetic opening tag
|
|
374
665
|
// so the regex can match the closing tag properly
|
|
@@ -383,12 +674,17 @@ export function parseAccumulatedIntoBlocks(
|
|
|
383
674
|
const blocks: ContentBlock[] = [];
|
|
384
675
|
const toolCalls: ToolCall[] = [];
|
|
385
676
|
const toolResults: ToolResult[] = [];
|
|
677
|
+
let emptyToolBlocks = 0;
|
|
678
|
+
let splicedToolBlocks = 0;
|
|
679
|
+
let unclosedInvokeHeads = 0;
|
|
386
680
|
|
|
387
681
|
// Track positions of all special blocks to extract plain text between them
|
|
388
682
|
type BlockPosition = {
|
|
389
683
|
start: number;
|
|
390
684
|
end: number;
|
|
391
685
|
block: ContentBlock | ContentBlock[];
|
|
686
|
+
calls?: ToolCall[];
|
|
687
|
+
results?: ToolResult[];
|
|
392
688
|
};
|
|
393
689
|
const positions: BlockPosition[] = [];
|
|
394
690
|
|
|
@@ -407,98 +703,91 @@ export function parseAccumulatedIntoBlocks(
|
|
|
407
703
|
return generateToolId();
|
|
408
704
|
};
|
|
409
705
|
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// Parse parameters
|
|
444
|
-
PARAMETER_REGEX.lastIndex = 0;
|
|
445
|
-
let paramMatch: RegExpExecArray | null;
|
|
446
|
-
while ((paramMatch = PARAMETER_REGEX.exec(invokeContent)) !== null) {
|
|
447
|
-
const paramName = paramMatch[2] ?? '';
|
|
448
|
-
const paramValue = paramMatch[3] ?? '';
|
|
449
|
-
// Parse value with special handling for large integers
|
|
450
|
-
input[paramName] = parseParamValue(paramValue);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const id = generateToolId();
|
|
454
|
-
const toolCall: ToolCall = { id, name: toolName, input };
|
|
455
|
-
toolCalls.push(toolCall);
|
|
456
|
-
callSites.push({ id, name: toolName, pos: funcMatch.index });
|
|
457
|
-
blockToolCalls.push({
|
|
458
|
-
type: 'tool_use',
|
|
459
|
-
id,
|
|
460
|
-
name: toolName,
|
|
461
|
-
input,
|
|
462
|
-
rawXml,
|
|
706
|
+
// ── Pass 1: collect every candidate span, with NO side effects ───────────
|
|
707
|
+
//
|
|
708
|
+
// The three sweeps overlap by construction (a function_calls block quoted
|
|
709
|
+
// inside a thinking block is found by both), so nothing may be registered —
|
|
710
|
+
// no callSites entry, no legacy-result claim, no diagnostic count — until
|
|
711
|
+
// containment has decided which spans are real. Registering first and
|
|
712
|
+
// filtering after produced a call site that a legacy result could claim and
|
|
713
|
+
// that the filter then deleted, leaving a tool_result addressed to a
|
|
714
|
+
// tool_use no longer in the response.
|
|
715
|
+
const candidateSpans = collectCandidateSpans(processedText);
|
|
716
|
+
|
|
717
|
+
// ── Pass 2: containment ──────────────────────────────────────────────────
|
|
718
|
+
//
|
|
719
|
+
// Sorted by start, outermost span first on a tie: a span that BEGINS before
|
|
720
|
+
// the furthest retained end begins inside a container, so it is content of
|
|
721
|
+
// that container, not a sibling of it — retained spans never overlap, and a
|
|
722
|
+
// span that crosses out past its container is refused with the rest of the
|
|
723
|
+
// container's content. Zero-invoke and zero-result spans take part as
|
|
724
|
+
// containers even though they contribute no block of their own — whether a
|
|
725
|
+
// span holds anything parseable says nothing about whether it encloses the
|
|
726
|
+
// text beneath it.
|
|
727
|
+
const survivingSpans = retainOutermostSpans(candidateSpans);
|
|
728
|
+
|
|
729
|
+
// ── Pass 3: register the survivors, in document order ────────────────────
|
|
730
|
+
//
|
|
731
|
+
// One ordered walk, so every call site preceding a results block is already
|
|
732
|
+
// registered when that block claims — and no filtered span ever was.
|
|
733
|
+
for (const span of survivingSpans) {
|
|
734
|
+
if (span.kind === 'thinking') {
|
|
735
|
+
positions.push({
|
|
736
|
+
start: span.start,
|
|
737
|
+
end: span.end,
|
|
738
|
+
block: { type: 'thinking', thinking: span.thinking },
|
|
463
739
|
});
|
|
740
|
+
continue;
|
|
464
741
|
}
|
|
465
742
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
743
|
+
if (span.kind === 'calls') {
|
|
744
|
+
const resolvedBlock = span.resolvedBlock;
|
|
745
|
+
if (resolvedBlock.wasSpliced) splicedToolBlocks++;
|
|
746
|
+
// Verbatim document text of the whole block — carried on each parsed
|
|
747
|
+
// tool_use so prefill replay reproduces the generation exactly instead
|
|
748
|
+
// of synthesizing a paraphrase (membrane#36).
|
|
749
|
+
const rawXml = resolvedBlock.fullMatch;
|
|
750
|
+
const blockToolCalls: ContentBlock[] = [];
|
|
751
|
+
const blockCalls: ToolCall[] = [];
|
|
752
|
+
|
|
753
|
+
// Parse invoke tags in this block (both forms, in document order); an
|
|
754
|
+
// invoke left open is refused and re-anchored to the call it swallowed.
|
|
755
|
+
const parsedInvokes = collectInvokes(resolvedBlock.innerContent);
|
|
756
|
+
unclosedInvokeHeads += parsedInvokes.unclosedHeads;
|
|
757
|
+
for (const invoke of parsedInvokes.calls) {
|
|
758
|
+
const toolName = invoke.name;
|
|
759
|
+
const input = invoke.input;
|
|
760
|
+
|
|
761
|
+
const id = generateToolId();
|
|
762
|
+
blockCalls.push({ id, name: toolName, input });
|
|
763
|
+
callSites.push({ id, name: toolName, pos: resolvedBlock.start });
|
|
764
|
+
blockToolCalls.push({
|
|
765
|
+
type: 'tool_use',
|
|
766
|
+
id,
|
|
767
|
+
name: toolName,
|
|
768
|
+
input,
|
|
769
|
+
rawXml,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
483
772
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
773
|
+
if (blockToolCalls.length === 0) {
|
|
774
|
+
emptyToolBlocks++;
|
|
775
|
+
} else {
|
|
776
|
+
positions.push({
|
|
777
|
+
start: resolvedBlock.start,
|
|
778
|
+
end: resolvedBlock.end,
|
|
779
|
+
block: blockToolCalls,
|
|
780
|
+
calls: blockCalls,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
continue;
|
|
490
784
|
}
|
|
491
|
-
}
|
|
492
785
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
while ((resultsMatch = FUNCTION_RESULTS_BLOCK_REGEX.exec(processedText)) !== null) {
|
|
497
|
-
const innerContent = resultsMatch[2] ?? '';
|
|
498
|
-
// Verbatim document text the harness placed — carried for exact replay
|
|
499
|
-
// on the prefill path (membrane#36).
|
|
500
|
-
const rawXml = resultsMatch[0];
|
|
786
|
+
const innerContent = span.innerContent;
|
|
787
|
+
const rawXml = span.rawXml;
|
|
788
|
+
const resultsStart = span.start;
|
|
501
789
|
const blockResults: ContentBlock[] = [];
|
|
790
|
+
const blockResultValues: ToolResult[] = [];
|
|
502
791
|
|
|
503
792
|
// Parse result tags
|
|
504
793
|
RESULT_REGEX.lastIndex = 0;
|
|
@@ -507,8 +796,7 @@ export function parseAccumulatedIntoBlocks(
|
|
|
507
796
|
const toolUseId = resultMatch[1] ?? '';
|
|
508
797
|
const content = unescapeXml(resultMatch[2] ?? '');
|
|
509
798
|
pairedCallIds.add(toolUseId);
|
|
510
|
-
|
|
511
|
-
toolResults.push(result);
|
|
799
|
+
blockResultValues.push({ toolUseId, content, isError: false });
|
|
512
800
|
blockResults.push({
|
|
513
801
|
type: 'tool_result',
|
|
514
802
|
toolUseId,
|
|
@@ -525,8 +813,7 @@ export function parseAccumulatedIntoBlocks(
|
|
|
525
813
|
const toolUseId = errorMatch[1] ?? '';
|
|
526
814
|
const content = unescapeXml(errorMatch[2] ?? '');
|
|
527
815
|
pairedCallIds.add(toolUseId);
|
|
528
|
-
|
|
529
|
-
toolResults.push(result);
|
|
816
|
+
blockResultValues.push({ toolUseId, content, isError: true });
|
|
530
817
|
blockResults.push({
|
|
531
818
|
type: 'tool_result',
|
|
532
819
|
toolUseId,
|
|
@@ -543,8 +830,8 @@ export function parseAccumulatedIntoBlocks(
|
|
|
543
830
|
while ((legacyResultMatch = LEGACY_RESULT_REGEX.exec(innerContent)) !== null) {
|
|
544
831
|
const toolName = legacyResultMatch[1]?.trim() || undefined;
|
|
545
832
|
const content = unescapeXml(legacyResultMatch[2] ?? '');
|
|
546
|
-
const toolUseId = claimCall(
|
|
547
|
-
|
|
833
|
+
const toolUseId = claimCall(resultsStart, toolName);
|
|
834
|
+
blockResultValues.push({ toolUseId, toolName, content, isError: false });
|
|
548
835
|
blockResults.push({
|
|
549
836
|
type: 'tool_result',
|
|
550
837
|
toolUseId,
|
|
@@ -558,8 +845,8 @@ export function parseAccumulatedIntoBlocks(
|
|
|
558
845
|
let legacyErrorMatch: RegExpExecArray | null;
|
|
559
846
|
while ((legacyErrorMatch = LEGACY_ERROR_REGEX.exec(innerContent)) !== null) {
|
|
560
847
|
const content = unescapeXml(legacyErrorMatch[1] ?? '');
|
|
561
|
-
const toolUseId = claimCall(
|
|
562
|
-
|
|
848
|
+
const toolUseId = claimCall(resultsStart);
|
|
849
|
+
blockResultValues.push({ toolUseId, content, isError: true });
|
|
563
850
|
blockResults.push({
|
|
564
851
|
type: 'tool_result',
|
|
565
852
|
toolUseId,
|
|
@@ -571,15 +858,19 @@ export function parseAccumulatedIntoBlocks(
|
|
|
571
858
|
|
|
572
859
|
if (blockResults.length > 0) {
|
|
573
860
|
positions.push({
|
|
574
|
-
start:
|
|
575
|
-
end:
|
|
861
|
+
start: span.start,
|
|
862
|
+
end: span.end,
|
|
576
863
|
block: blockResults,
|
|
864
|
+
results: blockResultValues,
|
|
577
865
|
});
|
|
578
866
|
}
|
|
579
867
|
}
|
|
580
868
|
|
|
581
|
-
//
|
|
582
|
-
|
|
869
|
+
// Survivors are already in document order, so positions are too.
|
|
870
|
+
for (const pos of positions) {
|
|
871
|
+
if (pos.calls) toolCalls.push(...pos.calls);
|
|
872
|
+
if (pos.results) toolResults.push(...pos.results);
|
|
873
|
+
}
|
|
583
874
|
|
|
584
875
|
// Build final blocks array, inserting text blocks between special blocks
|
|
585
876
|
// Use processedText for slicing since positions are relative to it
|
|
@@ -600,7 +891,7 @@ export function parseAccumulatedIntoBlocks(
|
|
|
600
891
|
blocks.push(pos.block);
|
|
601
892
|
}
|
|
602
893
|
|
|
603
|
-
lastEnd = pos.end;
|
|
894
|
+
lastEnd = Math.max(lastEnd, pos.end);
|
|
604
895
|
}
|
|
605
896
|
|
|
606
897
|
// Add any remaining text after the last special block
|
|
@@ -613,7 +904,15 @@ export function parseAccumulatedIntoBlocks(
|
|
|
613
904
|
}
|
|
614
905
|
}
|
|
615
906
|
|
|
616
|
-
return {
|
|
907
|
+
return {
|
|
908
|
+
blocks,
|
|
909
|
+
toolCalls,
|
|
910
|
+
toolResults,
|
|
911
|
+
unclosedToolBlock: endsWithPartialToolBlock(textOutsideSpans(processedText, survivingSpans)),
|
|
912
|
+
emptyToolBlocks,
|
|
913
|
+
splicedToolBlocks,
|
|
914
|
+
unclosedInvokeHeads,
|
|
915
|
+
};
|
|
617
916
|
}
|
|
618
917
|
|
|
619
918
|
// ============================================================================
|