@oh-my-pi/pi-agent-core 18.2.4 → 18.2.5
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/CHANGELOG.md +6 -0
- package/dist/types/append-only-context.d.ts +0 -4
- package/package.json +8 -8
- package/src/agent-loop.ts +30 -4
- package/src/append-only-context.ts +122 -1
- package/src/proxy.ts +49 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.2.5] - 2026-09-17
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Improved agent performance by reducing redundant tool-schema processing during repeated model calls and optimizing streamed tool-call argument parsing.
|
|
10
|
+
|
|
5
11
|
## [18.2.1] - 2026-09-15
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -41,10 +41,6 @@ export declare class StablePrefix {
|
|
|
41
41
|
get fingerprint(): string;
|
|
42
42
|
get version(): number;
|
|
43
43
|
get built(): boolean;
|
|
44
|
-
/**
|
|
45
|
-
* Build or rebuild from live context.
|
|
46
|
-
* Returns `true` if the prefix actually changed (cache miss imminent).
|
|
47
|
-
*/
|
|
48
44
|
build(context: AgentContext, options: BuildOptions): boolean;
|
|
49
45
|
/** Force rebuild on the next `build()` call. */
|
|
50
46
|
invalidate(): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-agent-core",
|
|
4
|
-
"version": "18.2.
|
|
4
|
+
"version": "18.2.5",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -35,16 +35,16 @@
|
|
|
35
35
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "18.2.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "18.2.
|
|
40
|
-
"@oh-my-pi/pi-natives": "18.2.
|
|
41
|
-
"@oh-my-pi/pi-utils": "18.2.
|
|
42
|
-
"@oh-my-pi/pi-wire": "18.2.
|
|
43
|
-
"@oh-my-pi/snapcompact": "18.2.
|
|
38
|
+
"@oh-my-pi/pi-ai": "18.2.5",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "18.2.5",
|
|
40
|
+
"@oh-my-pi/pi-natives": "18.2.5",
|
|
41
|
+
"@oh-my-pi/pi-utils": "18.2.5",
|
|
42
|
+
"@oh-my-pi/pi-wire": "18.2.5",
|
|
43
|
+
"@oh-my-pi/snapcompact": "18.2.5",
|
|
44
44
|
"@opentelemetry/api": "^1.9.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@oh-my-pi/omptype": "18.2.
|
|
47
|
+
"@oh-my-pi/omptype": "18.2.5",
|
|
48
48
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
49
49
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
50
50
|
"@types/bun": "^1.3.14"
|
package/src/agent-loop.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
getStreamingPartialJson,
|
|
38
38
|
kCursorExecResolved,
|
|
39
39
|
} from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
40
|
+
import { stamp } from "@oh-my-pi/pi-ai/utils/schema/stamps";
|
|
40
41
|
import {
|
|
41
42
|
createHarmonyAuditEvent,
|
|
42
43
|
detectHarmonyLeakInAssistantMessage,
|
|
@@ -886,6 +887,29 @@ export function normalizeMessagesForProvider(
|
|
|
886
887
|
const INTENT_FIELD_DESCRIPTION = "concise intent";
|
|
887
888
|
const INTENT_SCHEMA_UNION_KEYS = ["anyOf", "oneOf"] as const;
|
|
888
889
|
|
|
890
|
+
// Memoize injection per input schema identity: normalizeTools runs on every
|
|
891
|
+
// model call and injectIntentIntoSchema mints a fresh root object each time,
|
|
892
|
+
// which defeats the stamp-keyed schema memos downstream (toolWireSchema,
|
|
893
|
+
// stripSchemaDescriptions, tryEnforceStrictSchema each deep-clone + re-walk
|
|
894
|
+
// the whole catalog per request). The injected object is shared across
|
|
895
|
+
// requests — the same profile as the intent-off path, where parameters IS the
|
|
896
|
+
// shared memoized wire schema (see schema-immutability.test.ts). One stamp
|
|
897
|
+
// key per (mode, describeIntent) variant; index 0 = bare, 1 = described.
|
|
898
|
+
const INTENT_STAMPS = {
|
|
899
|
+
require: [Symbol("intent:require"), Symbol("intent:require:described")],
|
|
900
|
+
optional: [Symbol("intent:optional"), Symbol("intent:optional:described")],
|
|
901
|
+
} as const;
|
|
902
|
+
|
|
903
|
+
function memoizedInjectIntentIntoSchema(
|
|
904
|
+
schema: Record<string, unknown>,
|
|
905
|
+
mode: "require" | "optional",
|
|
906
|
+
describeIntent: boolean,
|
|
907
|
+
): unknown {
|
|
908
|
+
return stamp(schema, INTENT_STAMPS[mode][describeIntent ? 1 : 0], host =>
|
|
909
|
+
injectIntentIntoSchema(host, mode, describeIntent),
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
889
913
|
function injectIntentIntoSchema(
|
|
890
914
|
schema: unknown,
|
|
891
915
|
mode: "require" | "optional" = "require",
|
|
@@ -965,12 +989,14 @@ export function normalizeTools(tools: AgentContext["tools"], options: NormalizeT
|
|
|
965
989
|
// re-inject `i` (without its hint, which `describeIntent: false` omits) so
|
|
966
990
|
// intent tracing keeps the field while no descriptions ride the wire.
|
|
967
991
|
if (pruneDescriptions) {
|
|
968
|
-
|
|
969
|
-
|
|
992
|
+
const stripped = stripSchemaDescriptions(toolWireSchema(t));
|
|
993
|
+
const parameters = (
|
|
994
|
+
doInjectIntent ? memoizedInjectIntentIntoSchema(stripped, intentMode, false) : stripped
|
|
995
|
+
) as TSchema;
|
|
970
996
|
return { ...t, parameters, description: "" };
|
|
971
997
|
}
|
|
972
|
-
|
|
973
|
-
|
|
998
|
+
const wire = toolWireSchema(t);
|
|
999
|
+
const parameters = (doInjectIntent ? memoizedInjectIntentIntoSchema(wire, intentMode, true) : wire) as TSchema;
|
|
974
1000
|
const description = t.description ?? "";
|
|
975
1001
|
const examplesBlock = renderToolExamples({ ...t, parameters }, doInjectIntent ? INTENT_FIELD : undefined);
|
|
976
1002
|
const finalDescription = examplesBlock ? `${description}\n\n${examplesBlock}` : description;
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* message delta is a cache miss each turn.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import type { AgentTool } from "./types";
|
|
17
18
|
import type { Context, Message, Tool } from "@oh-my-pi/pi-ai";
|
|
19
|
+
import { toolWireSchema } from "@oh-my-pi/pi-ai";
|
|
18
20
|
import { normalizeTools } from "./agent-loop";
|
|
19
21
|
import { messageEstimateVersion } from "./compaction/message-cache";
|
|
20
22
|
import type { AgentContext, AgentMessage } from "./types";
|
|
@@ -23,6 +25,52 @@ import type { AgentContext, AgentMessage } from "./types";
|
|
|
23
25
|
// StablePrefix (formerly ImmutablePrefix)
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Cheap per-tool wire-identity key: every field normalizeTools and
|
|
30
|
+
* computeFingerprint read. The resolved parameters contribute by object
|
|
31
|
+
* identity — the wire-schema memo is stamp-keyed on the parameters object,
|
|
32
|
+
* so identity equals normalized bytes (modulo intent injection, a pure
|
|
33
|
+
* function of the mode flags compared separately). `customFormat` and
|
|
34
|
+
* `examples` contribute by reference: both are treated as immutable config
|
|
35
|
+
* (a registry swap replaces the object), matching how the snapshot consumes
|
|
36
|
+
* them. `intent` functions are compared by reference — same closure means
|
|
37
|
+
* same mode resolution.
|
|
38
|
+
*/
|
|
39
|
+
function toolKeyForPrefix(tool: AgentTool): string {
|
|
40
|
+
const params = toolWireSchema(tool);
|
|
41
|
+
const customFormat = tool.customFormat;
|
|
42
|
+
const examples = (tool as { examples?: unknown }).examples;
|
|
43
|
+
// Identity strings for objects (Map-assigned ids, no string-build cost
|
|
44
|
+
// on the hot path beyond the first sighting per object).
|
|
45
|
+
return [
|
|
46
|
+
tool.name ?? "",
|
|
47
|
+
tool.description ?? "",
|
|
48
|
+
String(tool.strict ?? ""),
|
|
49
|
+
tool.customWireName ?? "",
|
|
50
|
+
typeof tool.intent === "function" ? `fn:${objectId(tool.intent)}` : `mode:${tool.intent ?? "require"}`,
|
|
51
|
+
objectId(params),
|
|
52
|
+
objectId(customFormat),
|
|
53
|
+
objectId(examples),
|
|
54
|
+
].join("\u0000");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const objectIds = new WeakMap<object, number>();
|
|
58
|
+
let nextObjectId = 1;
|
|
59
|
+
|
|
60
|
+
function objectId(obj: unknown): number {
|
|
61
|
+
if (obj === null || obj === undefined || (typeof obj !== "object" && typeof obj !== "function")) {
|
|
62
|
+
// Primitives contribute by value below via String(); the id path is
|
|
63
|
+
// only for reference-identity comparison of config objects.
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
let id = objectIds.get(obj);
|
|
67
|
+
if (id === undefined) {
|
|
68
|
+
id = nextObjectId++;
|
|
69
|
+
objectIds.set(obj, id);
|
|
70
|
+
}
|
|
71
|
+
return id;
|
|
72
|
+
}
|
|
73
|
+
|
|
26
74
|
/** Frozen system prompt + tool spec snapshot. */
|
|
27
75
|
export interface StablePrefixSnapshot {
|
|
28
76
|
systemPrompt: string[];
|
|
@@ -63,10 +111,42 @@ export class StablePrefix {
|
|
|
63
111
|
/**
|
|
64
112
|
* Build or rebuild from live context.
|
|
65
113
|
* Returns `true` if the prefix actually changed (cache miss imminent).
|
|
114
|
+
*
|
|
115
|
+
* Steady-state fast path: when the live prompt reference is unchanged
|
|
116
|
+
* AND every tool resolves to the same normalized parameters identity as
|
|
117
|
+
* last build, the fingerprint cannot have changed, so the full snapshot
|
|
118
|
+
* + stringify is skipped. Comparing resolved parameters (not the tool
|
|
119
|
+
* container) is load-bearing: tools like ReadTool expose `parameters` as
|
|
120
|
+
* a getter over live settings (`skillful`, `memory.backend`), so the
|
|
121
|
+
* schema can swap under stable tool references when a setting toggles.
|
|
122
|
+
* Any other in-place mutation must go through `invalidate()`.
|
|
66
123
|
*/
|
|
124
|
+
#lastPrompt: readonly string[] | undefined;
|
|
125
|
+
// Joined prompt bytes snapshot: the prompt array is caller-owned and
|
|
126
|
+
// mutable in place (Agent.setSystemPrompt stores the caller's array;
|
|
127
|
+
// anyone holding it can push/splice), so reference equality alone cannot
|
|
128
|
+
// prove the bytes are unchanged. Compared by value on the fast path.
|
|
129
|
+
#lastPromptText: string | undefined;
|
|
130
|
+
// Per-tool wire-identity snapshot: every field normalizeTools and
|
|
131
|
+
// computeFingerprint read (name, description, resolved parameters,
|
|
132
|
+
// strict, customFormat, customWireName, intent mode, examples
|
|
133
|
+
// reference). A registry swap that keeps name/description/parameters
|
|
134
|
+
// but changes any of these must miss the fast path.
|
|
135
|
+
#lastToolKey: readonly string[] | undefined;
|
|
136
|
+
#lastIntentTracing: boolean | undefined;
|
|
137
|
+
#lastPruneToolDescriptions: boolean | undefined;
|
|
138
|
+
|
|
67
139
|
build(context: AgentContext, options: BuildOptions): boolean {
|
|
140
|
+
const prev = this.#snapshot;
|
|
141
|
+
if (prev !== null && this.#fastPathHit(context, options)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
68
144
|
const snapshot = takeSnapshot(context, options);
|
|
69
|
-
|
|
145
|
+
this.#recordFastPathKey(context, options, snapshot.tools);
|
|
146
|
+
if (prev && prev.fingerprint === snapshot.fingerprint) {
|
|
147
|
+
// Identity changed but bytes did not (e.g. equivalent rebuild):
|
|
148
|
+
// keep serving the cached snapshot so downstream memo identity
|
|
149
|
+
// (stamp-keyed schema caches) stays stable.
|
|
70
150
|
return false;
|
|
71
151
|
}
|
|
72
152
|
this.#snapshot = snapshot;
|
|
@@ -74,9 +154,50 @@ export class StablePrefix {
|
|
|
74
154
|
return true;
|
|
75
155
|
}
|
|
76
156
|
|
|
157
|
+
/** True when the cheap key matches: prompt + per-tool resolved identity. */
|
|
158
|
+
#fastPathHit(context: AgentContext, options: BuildOptions): boolean {
|
|
159
|
+
if (
|
|
160
|
+
this.#lastIntentTracing !== options.intentTracing ||
|
|
161
|
+
this.#lastPruneToolDescriptions !== options.pruneToolDescriptions
|
|
162
|
+
) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
// Prompt by reference first (steady state), then by joined bytes so
|
|
166
|
+
// an in-place push/splice of the same array still misses.
|
|
167
|
+
if (this.#lastPrompt !== context.systemPrompt) return false;
|
|
168
|
+
if (this.#lastPromptText !== undefined) {
|
|
169
|
+
const text = context.systemPrompt.join("\u0000");
|
|
170
|
+
if (text !== this.#lastPromptText) return false;
|
|
171
|
+
}
|
|
172
|
+
const tools = context.tools ?? [];
|
|
173
|
+
if (this.#lastToolKey === undefined || this.#lastToolKey.length !== tools.length) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
for (let i = 0; i < tools.length; i++) {
|
|
177
|
+
if (this.#lastToolKey[i] !== toolKeyForPrefix(tools[i]!)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#recordFastPathKey(context: AgentContext, options: BuildOptions, normalized: Tool[]): void {
|
|
185
|
+
this.#lastPrompt = context.systemPrompt;
|
|
186
|
+
this.#lastPromptText = context.systemPrompt.join("\u0000");
|
|
187
|
+
this.#lastIntentTracing = options.intentTracing;
|
|
188
|
+
this.#lastPruneToolDescriptions = options.pruneToolDescriptions;
|
|
189
|
+
this.#lastToolKey = (context.tools ?? []).map(tool => toolKeyForPrefix(tool));
|
|
190
|
+
void normalized;
|
|
191
|
+
}
|
|
192
|
+
|
|
77
193
|
/** Force rebuild on the next `build()` call. */
|
|
78
194
|
invalidate(): void {
|
|
79
195
|
this.#snapshot = null;
|
|
196
|
+
this.#lastPrompt = undefined;
|
|
197
|
+
this.#lastPromptText = undefined;
|
|
198
|
+
this.#lastToolKey = undefined;
|
|
199
|
+
this.#lastIntentTracing = undefined;
|
|
200
|
+
this.#lastPruneToolDescriptions = undefined;
|
|
80
201
|
}
|
|
81
202
|
|
|
82
203
|
/**
|
package/src/proxy.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
type StreamingPartialJsonCarrier,
|
|
21
21
|
setStreamingPartialJson,
|
|
22
22
|
} from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
23
|
-
import { parseStreamingJson, readSseJson } from "@oh-my-pi/pi-utils";
|
|
23
|
+
import { parseStreamingJson, parseStreamingJsonThrottled, readSseJson } from "@oh-my-pi/pi-utils";
|
|
24
24
|
|
|
25
25
|
// Event stream adapter for proxy SSE events
|
|
26
26
|
export class ProxyMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
|
|
@@ -117,6 +117,10 @@ export function streamProxy(model: Model, context: Context, options: ProxyStream
|
|
|
117
117
|
};
|
|
118
118
|
|
|
119
119
|
let response: Response | null = null;
|
|
120
|
+
// Declared outside try so the disconnect path (catch) can finalize
|
|
121
|
+
// still-buffered tool-call arguments before emitting the error event.
|
|
122
|
+
const partialJsonByIndex = new Map<number, string>();
|
|
123
|
+
const parsedLenByIndex = new Map<number, number>();
|
|
120
124
|
const abortHandler = () => {
|
|
121
125
|
const body = response?.body;
|
|
122
126
|
if (body) {
|
|
@@ -165,12 +169,11 @@ export function streamProxy(model: Model, context: Context, options: ProxyStream
|
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
let sawTerminalEvent = false;
|
|
168
|
-
const partialJsonByIndex = new Map<number, string>();
|
|
169
172
|
for await (const event of readSseJson<ProxyAssistantMessageEvent>(
|
|
170
173
|
response.body as ReadableStream<Uint8Array>,
|
|
171
174
|
options.signal,
|
|
172
175
|
)) {
|
|
173
|
-
const parsedEvent = processProxyEvent(event, partial, partialJsonByIndex);
|
|
176
|
+
const parsedEvent = processProxyEvent(event, partial, partialJsonByIndex, parsedLenByIndex);
|
|
174
177
|
if (parsedEvent) {
|
|
175
178
|
if (parsedEvent.type === "done" || parsedEvent.type === "error") {
|
|
176
179
|
sawTerminalEvent = true;
|
|
@@ -193,6 +196,10 @@ export function streamProxy(model: Model, context: Context, options: ProxyStream
|
|
|
193
196
|
const reason = options.signal?.aborted ? "aborted" : "error";
|
|
194
197
|
partial.stopReason = reason;
|
|
195
198
|
partial.errorMessage = errorMessage;
|
|
199
|
+
// Disconnect/abort path: no terminal event ran, so trailing
|
|
200
|
+
// throttled deltas never reached content.arguments. Finalize
|
|
201
|
+
// them here so the persisted/retried transcript is complete.
|
|
202
|
+
finalizeBufferedArguments(partial, partialJsonByIndex);
|
|
196
203
|
scrubPartialJson(partial);
|
|
197
204
|
stream.push({
|
|
198
205
|
type: "error",
|
|
@@ -235,6 +242,7 @@ function processProxyEvent(
|
|
|
235
242
|
proxyEvent: ProxyAssistantMessageEvent,
|
|
236
243
|
partial: AssistantMessage,
|
|
237
244
|
partialJsonByIndex: Map<number, string>,
|
|
245
|
+
parsedLenByIndex: Map<number, number>,
|
|
238
246
|
): AssistantMessageEvent | undefined {
|
|
239
247
|
switch (proxyEvent.type) {
|
|
240
248
|
case "start":
|
|
@@ -335,13 +343,22 @@ function processProxyEvent(
|
|
|
335
343
|
[kStreamingPartialJson]: "",
|
|
336
344
|
} as ToolCall & StreamingPartialJsonCarrier;
|
|
337
345
|
partialJsonByIndex.set(proxyEvent.contentIndex, "");
|
|
346
|
+
parsedLenByIndex.set(proxyEvent.contentIndex, 0);
|
|
338
347
|
return { type: "toolcall_start", contentIndex: proxyEvent.contentIndex, partial };
|
|
339
348
|
case "toolcall_delta": {
|
|
340
349
|
const content = partial.content[proxyEvent.contentIndex];
|
|
341
350
|
if (content?.type === "toolCall") {
|
|
342
351
|
const acc = (partialJsonByIndex.get(proxyEvent.contentIndex) ?? "") + proxyEvent.delta;
|
|
343
352
|
partialJsonByIndex.set(proxyEvent.contentIndex, acc);
|
|
344
|
-
|
|
353
|
+
// Geometric throttle (same contract as native providers): the
|
|
354
|
+
// authoritative parse lands at toolcall_end; mid-stream
|
|
355
|
+
// previews refresh at most ~3% late on large buffers.
|
|
356
|
+
const lastLen = parsedLenByIndex.get(proxyEvent.contentIndex) ?? 0;
|
|
357
|
+
const parsed = parseStreamingJsonThrottled(acc, lastLen);
|
|
358
|
+
if (parsed !== null) {
|
|
359
|
+
content.arguments = parsed.value || {};
|
|
360
|
+
parsedLenByIndex.set(proxyEvent.contentIndex, parsed.parsedLen);
|
|
361
|
+
}
|
|
345
362
|
setStreamingPartialJson(content, acc);
|
|
346
363
|
partial.content[proxyEvent.contentIndex] = { ...content }; // Trigger reactivity
|
|
347
364
|
return {
|
|
@@ -357,7 +374,13 @@ function processProxyEvent(
|
|
|
357
374
|
case "toolcall_end": {
|
|
358
375
|
const content = partial.content[proxyEvent.contentIndex];
|
|
359
376
|
if (content?.type === "toolCall") {
|
|
377
|
+
// Authoritative final parse (mirrors native providers): the
|
|
378
|
+
// throttle may have skipped trailing deltas, so the last
|
|
379
|
+
// parsed arguments can lag the accumulated buffer.
|
|
380
|
+
const acc = partialJsonByIndex.get(proxyEvent.contentIndex);
|
|
381
|
+
if (acc !== undefined && acc.length > 0) content.arguments = parseStreamingJson(acc) || {};
|
|
360
382
|
partialJsonByIndex.delete(proxyEvent.contentIndex);
|
|
383
|
+
parsedLenByIndex.delete(proxyEvent.contentIndex);
|
|
361
384
|
clearStreamingPartialJson(content);
|
|
362
385
|
return {
|
|
363
386
|
type: "toolcall_end",
|
|
@@ -373,7 +396,10 @@ function processProxyEvent(
|
|
|
373
396
|
partial.stopReason = proxyEvent.reason;
|
|
374
397
|
partial.usage = proxyEvent.usage;
|
|
375
398
|
if (proxyEvent.content !== undefined) partial.content = proxyEvent.content;
|
|
399
|
+
else finalizeBufferedArguments(partial, partialJsonByIndex);
|
|
376
400
|
scrubPartialJson(partial);
|
|
401
|
+
partialJsonByIndex.clear();
|
|
402
|
+
parsedLenByIndex.clear();
|
|
377
403
|
return { type: "done", reason: proxyEvent.reason, message: partial };
|
|
378
404
|
|
|
379
405
|
case "error":
|
|
@@ -381,7 +407,26 @@ function processProxyEvent(
|
|
|
381
407
|
partial.errorMessage = proxyEvent.errorMessage;
|
|
382
408
|
partial.usage = proxyEvent.usage;
|
|
383
409
|
if (proxyEvent.content !== undefined) partial.content = proxyEvent.content;
|
|
410
|
+
else finalizeBufferedArguments(partial, partialJsonByIndex);
|
|
384
411
|
scrubPartialJson(partial);
|
|
412
|
+
partialJsonByIndex.clear();
|
|
413
|
+
parsedLenByIndex.clear();
|
|
385
414
|
return { type: "error", reason: proxyEvent.reason, error: partial };
|
|
386
415
|
}
|
|
387
416
|
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Authoritatively parse every still-buffered tool-call argument accumulator
|
|
420
|
+
* into its content block. Terminal `done`/`error` events without a preceding
|
|
421
|
+
* `toolcall_end` (and with `content` omitted) finalize the partial message as
|
|
422
|
+
*-is; without this, trailing deltas below the throttle gate never reach
|
|
423
|
+
* `content.arguments` and the finalized call carries stale arguments.
|
|
424
|
+
*/
|
|
425
|
+
function finalizeBufferedArguments(partial: AssistantMessage, partialJsonByIndex: Map<number, string>): void {
|
|
426
|
+
if (partialJsonByIndex.size === 0) return;
|
|
427
|
+
for (const [index, acc] of partialJsonByIndex) {
|
|
428
|
+
if (acc.length === 0) continue;
|
|
429
|
+
const content = partial.content[index];
|
|
430
|
+
if (content?.type === "toolCall") content.arguments = parseStreamingJson(acc) || {};
|
|
431
|
+
}
|
|
432
|
+
}
|