veryfront 0.1.209 → 0.1.211

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.
@@ -198,6 +198,97 @@ export function captureStreamedToolCallInput(
198
198
  };
199
199
  }
200
200
 
201
+ /**
202
+ * A streamed tool call is "incomplete" when the provider stream terminated
203
+ * (abort, stall, timeout, transport error) before the SDK emitted the
204
+ * finalizing `tool-call` event that sets `inputAvailable: true`. In that state
205
+ * `arguments` only holds partial JSON fragments from `tool-input-delta` events,
206
+ * so the tool call is NOT a committed model choice and must not be parsed or
207
+ * executed. This is semantically distinct from a parse failure on a finalized
208
+ * tool call (`inputAvailable: true` but malformed JSON — which only happens on
209
+ * genuine provider bugs) and needs to be reported as a stream-termination
210
+ * error rather than a tool-argument error.
211
+ */
212
+ export function isStreamedToolCallIncomplete(
213
+ toolCall: Pick<StreamingToolCall, "inputAvailable">,
214
+ ): boolean {
215
+ return toolCall.inputAvailable !== true;
216
+ }
217
+
218
+ /**
219
+ * Classification of a streamed tool call when we reach end-of-stream and need
220
+ * to persist it into the assistant message. Three distinct cases, each with
221
+ * different semantics downstream:
222
+ *
223
+ * - `complete`: provider emitted the finalizing `tool-call` event and the
224
+ * arguments parsed cleanly. Execute the tool normally.
225
+ * - `parse-error`: provider emitted the finalizing `tool-call` event but the
226
+ * arguments are not valid JSON. This is a provider/SDK bug; record it as a
227
+ * tool-argument error so the step can recover.
228
+ * - `incomplete`: stream terminated before the finalizing event fired. The
229
+ * model never committed this tool use; record it as a stream-termination
230
+ * error so the parent (e.g. child-fork watchdog) can decide whether to
231
+ * retry the step cleanly instead of seeing a malformed tool call.
232
+ */
233
+ export type StreamedToolCallMaterialization =
234
+ | { readonly kind: "complete"; readonly part: MessagePart }
235
+ | {
236
+ readonly kind: "parse-error";
237
+ readonly part: MessagePart;
238
+ readonly parseError: string;
239
+ }
240
+ | {
241
+ readonly kind: "incomplete";
242
+ readonly part: MessagePart;
243
+ readonly partialArgumentsLength: number;
244
+ readonly partialArgumentsPreview: string;
245
+ };
246
+
247
+ /**
248
+ * Classify and build the persisted `MessagePart` for a single streamed tool
249
+ * call. Pure function — no logging, no SSE, no memory. Callers decide what to
250
+ * do with the result so this stays unit-testable.
251
+ *
252
+ * The resulting `part` is always pushed into the assistant message so the
253
+ * conversation history is transparent: even incomplete tool calls leave a
254
+ * visible trace with their partial `inputText`. What differs is the caller's
255
+ * error-surfacing behavior (log warning, SSE event, tool-result error).
256
+ */
257
+ export function materializeStreamedToolCall(
258
+ tc: StreamingToolCall,
259
+ ): StreamedToolCallMaterialization {
260
+ const basePart: MessagePart = {
261
+ type: `tool-${tc.name}`,
262
+ toolCallId: tc.id,
263
+ toolName: tc.name,
264
+ args: {},
265
+ ...(tc.arguments.length > 0 ? { inputText: tc.arguments } : {}),
266
+ };
267
+
268
+ if (isStreamedToolCallIncomplete(tc)) {
269
+ return {
270
+ kind: "incomplete",
271
+ part: basePart,
272
+ partialArgumentsLength: tc.arguments.length,
273
+ partialArgumentsPreview: tc.arguments.slice(0, 200),
274
+ };
275
+ }
276
+
277
+ const capturedInput = captureStreamedToolCallInput(tc);
278
+ const part: MessagePart = {
279
+ type: `tool-${tc.name}`,
280
+ toolCallId: tc.id,
281
+ toolName: tc.name,
282
+ args: capturedInput.args,
283
+ ...(capturedInput.inputText ? { inputText: capturedInput.inputText } : {}),
284
+ };
285
+
286
+ if (capturedInput.parseError) {
287
+ return { kind: "parse-error", part, parseError: capturedInput.parseError };
288
+ }
289
+ return { kind: "complete", part };
290
+ }
291
+
201
292
  function isToolResultPart(part: MessagePart): part is ToolResultPart {
202
293
  return part.type === "tool-result" && "result" in part;
203
294
  }
@@ -961,20 +1052,35 @@ export class AgentRuntime {
961
1052
  if (state.accumulatedText) streamParts.push({ type: "text", text: state.accumulatedText });
962
1053
 
963
1054
  for (const tc of state.toolCalls.values()) {
964
- const capturedInput = captureStreamedToolCallInput(tc);
965
- if (capturedInput.parseError) {
1055
+ const materialized = materializeStreamedToolCall(tc);
1056
+ streamParts.push(materialized.part);
1057
+
1058
+ if (materialized.kind === "incomplete") {
1059
+ // Stream terminated before the provider emitted the finalizing
1060
+ // `tool-call` event for this block. The model never committed this
1061
+ // tool use. Surface the failure via SSE so the live client can
1062
+ // react, and leave the partial fragment under `inputText` in the
1063
+ // persisted part above so the history is replayable and transparent.
1064
+ logger.warn("Streamed tool call terminated before tool-call event", {
1065
+ toolCallId: tc.id,
1066
+ toolName: tc.name,
1067
+ partialArgumentsLength: materialized.partialArgumentsLength,
1068
+ partialArgumentsPreview: materialized.partialArgumentsPreview,
1069
+ });
1070
+ const dynamicIncomplete = isDynamicTool(tc.name);
1071
+ sendSSE(controller, encoder, {
1072
+ type: "tool-input-error",
1073
+ toolCallId: tc.id,
1074
+ errorText: `Stream terminated before tool-call event fired for "${tc.name}". ` +
1075
+ `Received ${materialized.partialArgumentsLength} chars of partial tool-input deltas.`,
1076
+ ...(dynamicIncomplete ? { dynamic: true } : {}),
1077
+ });
1078
+ } else if (materialized.kind === "parse-error") {
966
1079
  logger.warn("Failed to parse streamed tool arguments", {
967
1080
  toolCallId: tc.id,
968
- error: capturedInput.parseError,
1081
+ error: materialized.parseError,
969
1082
  });
970
1083
  }
971
- streamParts.push({
972
- type: `tool-${tc.name}`,
973
- toolCallId: tc.id,
974
- toolName: tc.name,
975
- args: capturedInput.args,
976
- ...(capturedInput.inputText ? { inputText: capturedInput.inputText } : {}),
977
- });
978
1084
  }
979
1085
 
980
1086
  const assistantMessage: Message = {
@@ -1033,6 +1139,30 @@ export class AgentRuntime {
1033
1139
 
1034
1140
  for (const tc of streamedToolCalls) {
1035
1141
  throwIfAborted(abortSignal);
1142
+ if (isStreamedToolCallIncomplete(tc)) {
1143
+ // Stream ended before the provider finalized this tool call. We
1144
+ // cannot execute it — record a distinct stream-termination error
1145
+ // (not a tool-argument parse error) so the parent step and any
1146
+ // upstream orchestrator (e.g. the child-fork watchdog) see a
1147
+ // completed step with a clearly-labelled failure and can recover.
1148
+ const incompleteToolCall: ToolCall = {
1149
+ id: tc.id,
1150
+ name: tc.name,
1151
+ args: {},
1152
+ ...(tc.arguments.length > 0 ? { inputText: tc.arguments } : {}),
1153
+ status: "pending",
1154
+ };
1155
+ await this.recordToolError(
1156
+ incompleteToolCall,
1157
+ `Stream terminated before tool-call event fired for "${tc.name}". ` +
1158
+ `Received ${tc.arguments.length} chars of partial tool-input deltas.`,
1159
+ controller,
1160
+ encoder,
1161
+ currentMessages,
1162
+ toolCalls,
1163
+ );
1164
+ continue;
1165
+ }
1036
1166
  const capturedInput = captureStreamedToolCallInput(tc);
1037
1167
  const toolCall: ToolCall = {
1038
1168
  id: tc.id,
@@ -1,3 +1,3 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.209";
3
+ export const VERSION = "0.1.211";