agent-accelerator 0.5.1 → 0.5.3
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 +98 -8
- package/package.json +1 -1
- package/src/agent/agent.ts +283 -15
- package/src/agent/context.ts +7 -2
- package/src/agent/loop.ts +137 -12
- package/src/index.ts +4 -1
- package/src/models/catalog-cache.ts +3 -10
- package/src/providers/google.ts +378 -120
- package/src/providers/openai-compat.ts +2 -0
- package/src/providers/openai.ts +2 -0
- package/src/providers/openrouter.ts +2 -0
- package/src/providers.ts +16 -0
- package/src/scheduler/scheduler.ts +2 -0
- package/src/scheduler/store.ts +34 -21
- package/src/session/journal.ts +201 -0
- package/src/session/store.ts +181 -82
- package/src/session/tool-log.ts +62 -0
- package/src/telemetry-file.ts +4 -1
- package/src/tools/executor.ts +72 -15
- package/src/tools/tool.ts +0 -6
- package/src/types/agent.ts +26 -0
- package/src/types/message.ts +19 -0
- package/src/types/model.ts +5 -1
- package/src/types/response.ts +7 -0
- package/src/types/tool.ts +18 -6
- package/src/utils/durable.ts +218 -0
- package/src/utils/headers.ts +1 -1
- package/src/utils/retry.ts +17 -2
package/src/providers/google.ts
CHANGED
|
@@ -29,18 +29,19 @@ import type {
|
|
|
29
29
|
ProviderGenerateResult,
|
|
30
30
|
ProviderRawData,
|
|
31
31
|
} from "../types/model.ts";
|
|
32
|
-
import type { ProviderContext, Message, ContentPart } from "../types/message.ts";
|
|
32
|
+
import type { ProviderContext, Message, ContentPart, ProviderSteps, ToolResultPart, MediaResolution } from "../types/message.ts";
|
|
33
33
|
import type { StandardToolDeclaration, ToolCallRecord } from "../types/tool.ts";
|
|
34
34
|
import type { TokenUsage } from "../types/core.ts";
|
|
35
35
|
import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
36
36
|
import { SSEParser } from "../streaming/sse-parser.ts";
|
|
37
37
|
import { AgentResponse } from "../types/response.ts";
|
|
38
38
|
import { getApiKey, getEnv } from "../utils/env.ts";
|
|
39
|
-
import { normalizeMediaInput } from "../utils/media.ts";
|
|
39
|
+
import { normalizeMediaInput, inferMimeType } from "../utils/media.ts";
|
|
40
|
+
import { ConfigError, ValidationError, isAbortError } from "../types/errors.ts";
|
|
40
41
|
import { safeStringify } from "../utils/serialization.ts";
|
|
41
42
|
import { stripSchemaForGoogle } from "../tools/schema.ts";
|
|
42
43
|
import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
|
|
43
|
-
import { withRetries } from "../utils/retry.ts";
|
|
44
|
+
import { withRetries, sleep } from "../utils/retry.ts";
|
|
44
45
|
import { setHeaderCaseInsensitive, shouldEchoSignature } from "../utils/headers.ts";
|
|
45
46
|
import {
|
|
46
47
|
newToolCallId,
|
|
@@ -77,16 +78,102 @@ import {
|
|
|
77
78
|
emitProviderWarning,
|
|
78
79
|
} from "../providers.ts";
|
|
79
80
|
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Supported inputs (api-ref `interactions-api.md` enums, `docs/file-input-methods.md`)
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/** Image MIME types Google accepts inline. */
|
|
86
|
+
export const GOOGLE_IMAGE_MIME = ["image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff"] as const;
|
|
87
|
+
/** Audio MIME types Google accepts inline (audio is never fetched by URL). */
|
|
88
|
+
export const GOOGLE_AUDIO_MIME = ["audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac", "audio/mpeg", "audio/m4a", "audio/l16", "audio/opus", "audio/alaw", "audio/mulaw", "audio/webm"] as const;
|
|
89
|
+
/** Video MIME types Google accepts inline. */
|
|
90
|
+
export const GOOGLE_VIDEO_MIME = ["video/mp4", "video/mpeg", "video/mpg", "video/mov", "video/avi", "video/x-flv", "video/webm", "video/wmv", "video/3gpp"] as const;
|
|
91
|
+
/** Document MIME types Google accepts (the only two). */
|
|
92
|
+
export const GOOGLE_DOCUMENT_MIME = ["application/pdf", "text/csv"] as const;
|
|
93
|
+
/** MIME types Google fetches itself from a public `https://` URL. */
|
|
94
|
+
export const GOOGLE_URL_MIME = [
|
|
95
|
+
"text/html", "text/css", "text/plain", "text/xml", "text/csv", "text/rtf", "text/javascript",
|
|
96
|
+
"application/json", "application/pdf",
|
|
97
|
+
"image/bmp", "image/jpeg", "image/png", "image/webp",
|
|
98
|
+
"video/mp4", "video/mpeg", "video/quicktime", "video/avi", "video/x-flv", "video/mpg", "video/webm", "video/wmv", "video/3gpp",
|
|
99
|
+
] as const;
|
|
100
|
+
/** Common MIME spellings mapped to Google's inline names. */
|
|
101
|
+
export const GOOGLE_INLINE_MIME_ALIASES: Readonly<Record<string, string>> = {
|
|
102
|
+
"video/quicktime": "video/mov",
|
|
103
|
+
"audio/x-wav": "audio/wav",
|
|
104
|
+
"audio/wave": "audio/wav",
|
|
105
|
+
"image/jpg": "image/jpeg",
|
|
106
|
+
};
|
|
107
|
+
// Documented size limits. Not enforced yet: oversized inputs get Google's own error.
|
|
108
|
+
/** Max inline payload per request (bytes). */
|
|
109
|
+
export const GOOGLE_MAX_INLINE_REQUEST_BYTES = 100 * 1024 * 1024;
|
|
110
|
+
/** Max inline PDF size (bytes). */
|
|
111
|
+
export const GOOGLE_MAX_INLINE_PDF_BYTES = 50 * 1024 * 1024;
|
|
112
|
+
/** Max size of one file Google fetches by URL (bytes). */
|
|
113
|
+
export const GOOGLE_MAX_URL_FILE_BYTES = 15 * 1024 * 1024;
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Long decode (`docs/long-decode.md`, `docs/background-execution.md`)
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/** Output tokens one non-background request may produce; above it Gemini 4 runs as a background interaction (long decode). */
|
|
120
|
+
export const GOOGLE_MAX_SYNC_OUTPUT_TOKENS = 65_536;
|
|
121
|
+
// A background stream pauses between ~5K-token chunks; only this much silence counts as a dropped connection (docs: 15-20 min).
|
|
122
|
+
const BACKGROUND_STREAM_STALL_MS = 15 * 60_000;
|
|
123
|
+
// Reconnects in a row that bring no new event before a background stream gives up.
|
|
124
|
+
const BACKGROUND_MAX_RECONNECTS = 5;
|
|
125
|
+
|
|
126
|
+
// True when a request needs Gemini 4 long decode (background); any other model above the limit fails before the network; in: model id + maxOutputTokens, out: buildBody, isLongRunning.
|
|
127
|
+
function needsLongDecode(modelId: string, maxOutputTokens: number | undefined): boolean {
|
|
128
|
+
if (!maxOutputTokens || maxOutputTokens <= GOOGLE_MAX_SYNC_OUTPUT_TOKENS) return false;
|
|
129
|
+
if (/^gemini-4/i.test(modelId)) return true;
|
|
130
|
+
throw new ConfigError(
|
|
131
|
+
`[Agent Accelerator] [google/${modelId}] maxOutputTokens ${maxOutputTokens} is above Google's ${GOOGLE_MAX_SYNC_OUTPUT_TOKENS} limit; only Gemini 4 models can write more (long decode). Lower maxOutputTokens or use a gemini-4 model.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// True while a background interaction is still working; in: Interaction.status, out: polling and stream reconnects.
|
|
136
|
+
function isRunning(status: string | undefined): boolean {
|
|
137
|
+
return status === "queued" || status === "in_progress";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The two reader calls the stream loop uses (Bun ships two incompatible reader typings, so only these are typed).
|
|
141
|
+
type StreamReader = { read(): Promise<{ done: boolean; value?: Uint8Array }>; cancel(): Promise<void> };
|
|
142
|
+
|
|
143
|
+
// Reads the next chunk, or cancels the reader and rejects after `ms` of silence; in: a background stream reader, out: the stream read loop.
|
|
144
|
+
async function readWithin(reader: StreamReader, ms: number) {
|
|
145
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
146
|
+
const stalled = new Promise<never>((_, reject) => {
|
|
147
|
+
timer = setTimeout(() => {
|
|
148
|
+
void reader.cancel().catch(() => {});
|
|
149
|
+
reject(new Error("Google background stream stalled"));
|
|
150
|
+
}, ms);
|
|
151
|
+
});
|
|
152
|
+
try {
|
|
153
|
+
return await Promise.race([reader.read(), stalled]);
|
|
154
|
+
} finally {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type MediaBlockType = "image" | "audio" | "video" | "document";
|
|
160
|
+
|
|
161
|
+
const INLINE_MIME: Record<MediaBlockType, readonly string[]> = {
|
|
162
|
+
image: GOOGLE_IMAGE_MIME,
|
|
163
|
+
audio: GOOGLE_AUDIO_MIME,
|
|
164
|
+
video: GOOGLE_VIDEO_MIME,
|
|
165
|
+
document: GOOGLE_DOCUMENT_MIME,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const FILES_API_PREFIX = "https://generativelanguage.googleapis.com/v1beta/files/";
|
|
169
|
+
const YOUTUBE_URL = /^https:\/\/(www\.|m\.)?(youtube\.com|youtu\.be)\//i;
|
|
170
|
+
|
|
80
171
|
// ---------------------------------------------------------------------------
|
|
81
172
|
// Interactions wire shapes (subset used by this adapter)
|
|
82
173
|
// ---------------------------------------------------------------------------
|
|
83
174
|
|
|
84
|
-
type
|
|
85
|
-
|
|
86
|
-
| { type: "image"; mime_type: string; data?: string; uri?: string }
|
|
87
|
-
| { type: "audio"; mime_type: string; data?: string; uri?: string }
|
|
88
|
-
| { type: "video"; mime_type: string; data?: string; uri?: string }
|
|
89
|
-
| { type: "document"; mime_type: string; data?: string; uri?: string };
|
|
175
|
+
type MediaBlock = { type: MediaBlockType; mime_type?: string; data?: string; uri?: string; resolution?: MediaResolution };
|
|
176
|
+
type InteractionContentBlock = { type: "text"; text: string } | MediaBlock;
|
|
90
177
|
|
|
91
178
|
type InteractionInputStep =
|
|
92
179
|
| { type: "user_input"; content: string | InteractionContentBlock[] }
|
|
@@ -97,6 +184,7 @@ type InteractionInputStep =
|
|
|
97
184
|
name: string;
|
|
98
185
|
call_id: string;
|
|
99
186
|
result: InteractionContentBlock[];
|
|
187
|
+
is_error?: boolean;
|
|
100
188
|
}
|
|
101
189
|
| { type: string; [k: string]: unknown };
|
|
102
190
|
|
|
@@ -111,6 +199,7 @@ interface InteractionRequestBody {
|
|
|
111
199
|
store?: boolean;
|
|
112
200
|
previous_interaction_id?: string;
|
|
113
201
|
stream?: boolean;
|
|
202
|
+
background?: boolean;
|
|
114
203
|
}
|
|
115
204
|
|
|
116
205
|
interface InteractionStep {
|
|
@@ -147,6 +236,14 @@ interface InteractionObject {
|
|
|
147
236
|
[k: string]: unknown;
|
|
148
237
|
}
|
|
149
238
|
|
|
239
|
+
/** One parsed Interactions HTTP reply (create POST or poll GET). */
|
|
240
|
+
interface InteractionReply {
|
|
241
|
+
status: number;
|
|
242
|
+
statusText: string;
|
|
243
|
+
headers: Record<string, string>;
|
|
244
|
+
interaction: InteractionObject;
|
|
245
|
+
}
|
|
246
|
+
|
|
150
247
|
const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
151
248
|
|
|
152
249
|
/** Per-session interaction chaining (provider-generated ids only). */
|
|
@@ -321,48 +418,102 @@ function toGoogleToolChoice(
|
|
|
321
418
|
return { allowed_tools: { mode: norm.mode } };
|
|
322
419
|
}
|
|
323
420
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
421
|
+
type MediaValue = string | Uint8Array | ArrayBuffer;
|
|
422
|
+
|
|
423
|
+
// Lower-cases a MIME type and drops parameters (`text/csv; charset=utf-8` -> `text/csv`); in: part or response MIME, out: list checks.
|
|
424
|
+
function bareMime(mime: string | undefined): string | undefined {
|
|
425
|
+
return mime ? mime.split(";")[0]!.trim().toLowerCase() || undefined : undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Returns the `uri` block for a link Google fetches itself (YouTube, Files API, `gs://`, https URL of a URL-supported type), else undefined; in: a media part's string value, out: contentPartsToBlocks.
|
|
429
|
+
function uriBlock(type: MediaBlockType, value: string, mimeType?: string): MediaBlock | undefined {
|
|
430
|
+
const explicit = bareMime(mimeType);
|
|
431
|
+
const withMime = (mime?: string): MediaBlock => ({ type, uri: value, ...(mime ? { mime_type: mime } : {}) });
|
|
432
|
+
if (type === "video" && YOUTUBE_URL.test(value)) return withMime(explicit);
|
|
433
|
+
const mime = explicit ?? bareMime(inferMimeType(value, ""));
|
|
434
|
+
if (value.startsWith(FILES_API_PREFIX) || value.startsWith("gs://")) return withMime(mime);
|
|
435
|
+
return value.startsWith("https://") && mime && (GOOGLE_URL_MIME as readonly string[]).includes(mime) ? withMime(mime) : undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Inlines a media part as base64 under Google's MIME name, failing before the network on an unsupported type; in: any media value (bytes, data URL, path, http URL), out: contentPartsToBlocks.
|
|
439
|
+
async function inlineBlock(type: MediaBlockType, value: MediaValue, mimeType?: string): Promise<MediaBlock> {
|
|
440
|
+
const norm = await normalizeMediaInput(value, mimeType);
|
|
441
|
+
const raw = bareMime(norm.mimeType) ?? "";
|
|
442
|
+
const mime = GOOGLE_INLINE_MIME_ALIASES[raw] ?? raw;
|
|
443
|
+
if (!INLINE_MIME[type].includes(mime)) {
|
|
444
|
+
const what = type === "document" ? "Google reads only PDF and CSV documents" : `Google does not accept ${type} type "${mime}"`;
|
|
445
|
+
throw new ValidationError(
|
|
446
|
+
`[Agent Accelerator] ${what} (got "${mime}"; supported: ${INLINE_MIME[type].join(", ")}). Convert the file or pass the correct mimeType.`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return { type, mime_type: mime, data: norm.base64Data };
|
|
330
450
|
}
|
|
331
451
|
|
|
452
|
+
// Maps canonical text and media parts to Interactions content blocks (one builder for chained and full-history input); in: a message's parts, out: request `input`.
|
|
332
453
|
async function contentPartsToBlocks(parts: ContentPart[]): Promise<InteractionContentBlock[]> {
|
|
333
454
|
const blocks: InteractionContentBlock[] = [];
|
|
334
455
|
for (const part of parts) {
|
|
335
|
-
if (part.type === "text"
|
|
336
|
-
blocks.push({ type: "text", text: part.text });
|
|
337
|
-
|
|
338
|
-
part.type === "image" ||
|
|
339
|
-
part.type === "audio" ||
|
|
340
|
-
part.type === "video" ||
|
|
341
|
-
part.type === "file"
|
|
342
|
-
) {
|
|
343
|
-
const raw = (part as { image?: unknown; audio?: unknown; video?: unknown; file?: unknown }).image ??
|
|
344
|
-
(part as { audio?: unknown }).audio ??
|
|
345
|
-
(part as { video?: unknown }).video ??
|
|
346
|
-
(part as { file?: unknown }).file;
|
|
347
|
-
const norm = await normalizeMediaInput(
|
|
348
|
-
raw as string | Uint8Array | ArrayBuffer,
|
|
349
|
-
(part as { mimeType?: string }).mimeType
|
|
350
|
-
);
|
|
351
|
-
blocks.push({
|
|
352
|
-
type: mediaBlockType(part.type),
|
|
353
|
-
mime_type: norm.mimeType,
|
|
354
|
-
data: norm.base64Data,
|
|
355
|
-
} as InteractionContentBlock);
|
|
456
|
+
if (part.type === "text") {
|
|
457
|
+
if (part.text) blocks.push({ type: "text", text: part.text });
|
|
458
|
+
continue;
|
|
356
459
|
}
|
|
460
|
+
if (part.type !== "image" && part.type !== "audio" && part.type !== "video" && part.type !== "file") continue;
|
|
461
|
+
const type: MediaBlockType = part.type === "file" ? "document" : part.type;
|
|
462
|
+
const value = (part as unknown as Record<string, MediaValue>)[part.type]!;
|
|
463
|
+
const uri = typeof value === "string" ? uriBlock(type, value, part.mimeType) : undefined;
|
|
464
|
+
const block = uri ?? (await inlineBlock(type, value, part.mimeType));
|
|
465
|
+
// Per-item detail level (image and video blocks only, api-ref `MediaResolution`).
|
|
466
|
+
const resolution = part.type === "image" || part.type === "video" ? part.resolution : undefined;
|
|
467
|
+
blocks.push(resolution ? { ...block, resolution } : block);
|
|
357
468
|
}
|
|
358
469
|
return blocks;
|
|
359
470
|
}
|
|
360
471
|
|
|
472
|
+
// Keeps a turn's model-generated steps exactly as received (input echoes and empty signatures dropped); in: REST `steps` or assembled stream steps, out: ProviderGenerateResult.providerSteps.
|
|
473
|
+
function replaySteps(steps: unknown[] | undefined): ProviderSteps | undefined {
|
|
474
|
+
const kept = (steps ?? [])
|
|
475
|
+
.filter((s) => {
|
|
476
|
+
const type = (s as { type?: unknown } | null)?.type;
|
|
477
|
+
return typeof type === "string" && type !== "user_input" && type !== "function_result";
|
|
478
|
+
})
|
|
479
|
+
.map((s) => {
|
|
480
|
+
const { signature, ...rest } = s as Record<string, unknown>;
|
|
481
|
+
return signature === "" ? rest : s;
|
|
482
|
+
});
|
|
483
|
+
return kept.length > 0 ? { provider: "google", steps: kept } : undefined;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Appends streamed text to the last text block of a step's `content`/`summary`, or starts one; in: step.delta, out: the assembled stream step.
|
|
487
|
+
function appendBlockText(step: Record<string, unknown> | undefined, key: "content" | "summary", text: string): void {
|
|
488
|
+
if (!step) return;
|
|
489
|
+
const blocks = (Array.isArray(step[key]) ? step[key] : (step[key] = [])) as Array<{ type?: string; text?: string }>;
|
|
490
|
+
const last = blocks[blocks.length - 1];
|
|
491
|
+
if (last?.type === "text" && typeof last.text === "string") last.text += text;
|
|
492
|
+
else blocks.push({ type: "text", text });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Joins the `text` of content/summary blocks; in: a `step.start` step's `content` or `summary`, out: stream text/thinking.
|
|
496
|
+
function blockText(blocks: unknown): string {
|
|
497
|
+
if (!Array.isArray(blocks)) return "";
|
|
498
|
+
return blocks.map((b) => (typeof b?.text === "string" ? b.text : "")).join("");
|
|
499
|
+
}
|
|
500
|
+
|
|
361
501
|
function resultToText(result: unknown): string {
|
|
362
502
|
if (typeof result === "string") return result;
|
|
363
503
|
return safeStringify(result);
|
|
364
504
|
}
|
|
365
505
|
|
|
506
|
+
// Builds the documented `function_result` step (`is_error` only when the tool failed); in: a canonical tool_result part, out: chained or full-history `input`.
|
|
507
|
+
function functionResultStep(part: ToolResultPart): InteractionInputStep {
|
|
508
|
+
return {
|
|
509
|
+
type: "function_result",
|
|
510
|
+
name: part.name,
|
|
511
|
+
call_id: part.id,
|
|
512
|
+
result: [{ type: "text", text: resultToText(part.result) }],
|
|
513
|
+
...(part.isError ? { is_error: true } : {}),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
366
517
|
/**
|
|
367
518
|
* Latest-turn input for stateful chaining (the server already holds history):
|
|
368
519
|
* everything AFTER the last assistant message, in order. That is normally one
|
|
@@ -400,12 +551,7 @@ async function latestTurnInput(context: ProviderContext): Promise<string | Inter
|
|
|
400
551
|
if (!Array.isArray(m.content)) continue;
|
|
401
552
|
for (const part of m.content) {
|
|
402
553
|
if (part.type === "tool_result") {
|
|
403
|
-
steps.push(
|
|
404
|
-
type: "function_result",
|
|
405
|
-
name: part.name,
|
|
406
|
-
call_id: part.id,
|
|
407
|
-
result: [{ type: "text", text: resultToText(part.result) }],
|
|
408
|
-
});
|
|
554
|
+
steps.push(functionResultStep(part));
|
|
409
555
|
}
|
|
410
556
|
}
|
|
411
557
|
} else if (m.role === "user") {
|
|
@@ -420,7 +566,7 @@ async function latestTurnInput(context: ProviderContext): Promise<string | Inter
|
|
|
420
566
|
return steps;
|
|
421
567
|
}
|
|
422
568
|
|
|
423
|
-
|
|
569
|
+
// Rebuilds the whole conversation as Interactions steps; Google turns replay their stored raw steps verbatim, others are reconstructed; in: AgentContext messages, out: request `input` on reseed/switch turns.
|
|
424
570
|
async function fullHistorySteps(context: ProviderContext): Promise<InteractionInputStep[]> {
|
|
425
571
|
const steps: InteractionInputStep[] = [];
|
|
426
572
|
for (const m of context.messages) {
|
|
@@ -433,6 +579,11 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
433
579
|
if (blocks.length > 0) steps.push({ type: "user_input", content: blocks });
|
|
434
580
|
}
|
|
435
581
|
} else if (m.role === "assistant") {
|
|
582
|
+
// Exact replay: same bytes as the server returned (thought signatures included), best for implicit caching.
|
|
583
|
+
if (m.providerSteps?.provider === "google") {
|
|
584
|
+
steps.push(...(m.providerSteps.steps as InteractionInputStep[]));
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
436
587
|
if (typeof m.content === "string") {
|
|
437
588
|
if (m.content) {
|
|
438
589
|
steps.push({
|
|
@@ -443,6 +594,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
443
594
|
continue;
|
|
444
595
|
}
|
|
445
596
|
const texts: string[] = [];
|
|
597
|
+
const turnStart = steps.length;
|
|
446
598
|
for (const part of m.content) {
|
|
447
599
|
if (part.type === "thinking" && part.thinking) {
|
|
448
600
|
// Q-18: preserve namespaced signatures only — foreign-provider
|
|
@@ -475,7 +627,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
475
627
|
type: "model_output",
|
|
476
628
|
content: texts.map((t) => ({ type: "text", text: t })),
|
|
477
629
|
} as InteractionInputStep);
|
|
478
|
-
} else if (m.thoughtSignature && !steps.some((s) => s.type === "thought")) {
|
|
630
|
+
} else if (m.thoughtSignature && !steps.slice(turnStart).some((s) => s.type === "thought")) {
|
|
479
631
|
// Preserve a signature-only thought so stateless chaining keeps
|
|
480
632
|
// reasoning continuity even when no summary text was retained.
|
|
481
633
|
// Q-18: only for google/untagged signatures.
|
|
@@ -488,12 +640,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
488
640
|
if (!Array.isArray(m.content)) continue;
|
|
489
641
|
for (const part of m.content) {
|
|
490
642
|
if (part.type === "tool_result") {
|
|
491
|
-
steps.push(
|
|
492
|
-
type: "function_result",
|
|
493
|
-
name: part.name,
|
|
494
|
-
call_id: part.id,
|
|
495
|
-
result: [{ type: "text", text: resultToText(part.result) }],
|
|
496
|
-
});
|
|
643
|
+
steps.push(functionResultStep(part));
|
|
497
644
|
}
|
|
498
645
|
}
|
|
499
646
|
}
|
|
@@ -597,6 +744,7 @@ function parseInteraction(
|
|
|
597
744
|
provider: "google",
|
|
598
745
|
raw,
|
|
599
746
|
durationMs,
|
|
747
|
+
providerSteps: replaySteps(interaction.steps),
|
|
600
748
|
};
|
|
601
749
|
}
|
|
602
750
|
|
|
@@ -661,6 +809,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
661
809
|
model: modelId,
|
|
662
810
|
input: "",
|
|
663
811
|
...(stream ? { stream: true } : {}),
|
|
812
|
+
...(needsLongDecode(modelId, options?.maxOutputTokens) ? { background: true } : {}),
|
|
664
813
|
};
|
|
665
814
|
// Structured outputs: native `response_format` when supported, else
|
|
666
815
|
// emulated via system instruction (catalog `structured_output: false`
|
|
@@ -717,7 +866,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
717
866
|
// provider's turns, so send the full canonical history explicitly and
|
|
718
867
|
// statelessly, then resume chaining fresh on the next turn.
|
|
719
868
|
body.input = await fullHistorySteps(context);
|
|
720
|
-
|
|
869
|
+
// Background runs need storage (`store: false` is a 400); this turn then simply starts a fresh chain.
|
|
870
|
+
if (!body.background) body.store = false;
|
|
721
871
|
} else if (continuing) {
|
|
722
872
|
body.input = await latestTurnInput(context);
|
|
723
873
|
body.previous_interaction_id = chain!.interactionId;
|
|
@@ -735,7 +885,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
735
885
|
}
|
|
736
886
|
|
|
737
887
|
private requestInit(
|
|
738
|
-
|
|
888
|
+
method: "POST" | "GET",
|
|
889
|
+
body: InteractionRequestBody | undefined,
|
|
739
890
|
options: ProviderRequestOptions | undefined,
|
|
740
891
|
apiKey: string,
|
|
741
892
|
signal?: AbortSignal
|
|
@@ -746,19 +897,20 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
746
897
|
// `X-Goog-Api-Key` never duplicates the canonical header. Proxy flows
|
|
747
898
|
// (`apiKey === "unused"`) preserve custom auth instead of overwriting.
|
|
748
899
|
if (apiKey !== "unused") setHeaderCaseInsensitive(headers, "x-goog-api-key", apiKey);
|
|
749
|
-
return { method
|
|
900
|
+
return { method, headers, ...(body ? { body: JSON.stringify(body) } : {}), signal };
|
|
750
901
|
}
|
|
751
902
|
|
|
752
903
|
private async doFetch(
|
|
753
904
|
url: string,
|
|
754
|
-
|
|
905
|
+
method: "POST" | "GET",
|
|
906
|
+
body: InteractionRequestBody | undefined,
|
|
755
907
|
options: ProviderRequestOptions | undefined,
|
|
756
908
|
apiKey: string,
|
|
757
909
|
signal?: AbortSignal
|
|
758
910
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
759
911
|
const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
|
|
760
912
|
try {
|
|
761
|
-
const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, effective));
|
|
913
|
+
const res = await (options?.fetch ?? fetch)(url, this.requestInit(method, body, options, apiKey, effective));
|
|
762
914
|
const text = await res.text();
|
|
763
915
|
const headers: Record<string, string> = {};
|
|
764
916
|
res.headers.forEach((v, k) => {
|
|
@@ -799,6 +951,45 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
799
951
|
throw toConciseProviderError(err, "google", modelId);
|
|
800
952
|
}
|
|
801
953
|
|
|
954
|
+
/** Poll delays (ms) for background interactions: before the first poll, then between polls. */
|
|
955
|
+
pollDelaysMs: [number, number] = [2_000, 5_000];
|
|
956
|
+
|
|
957
|
+
/** True for Gemini 4 long decode (`maxOutputTokens` above 65,536): the request runs in the background for up to 24 h. */
|
|
958
|
+
isLongRunning(model: string | ModelSpec, options?: ProviderRequestOptions): boolean {
|
|
959
|
+
return needsLongDecode(cleanModelId(model), options?.maxOutputTokens);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// One Interactions HTTP call parsed into its interaction (errors mapped concisely); in: create POST or poll GET, out: generate.
|
|
963
|
+
private async fetchInteraction(
|
|
964
|
+
url: string,
|
|
965
|
+
method: "POST" | "GET",
|
|
966
|
+
body: InteractionRequestBody | undefined,
|
|
967
|
+
options: ProviderRequestOptions | undefined,
|
|
968
|
+
apiKey: string,
|
|
969
|
+
modelId: string
|
|
970
|
+
): Promise<InteractionReply> {
|
|
971
|
+
const res = await this.doFetch(url, method, body, options, apiKey, options?.signal);
|
|
972
|
+
this.throwIfError(res.status, res.statusText, url, res.text, modelId, res.headers);
|
|
973
|
+
try {
|
|
974
|
+
return { status: res.status, statusText: res.statusText, headers: res.headers, interaction: JSON.parse(res.text) as InteractionObject };
|
|
975
|
+
} catch {
|
|
976
|
+
throw toConciseProviderError(
|
|
977
|
+
Object.assign(new Error("Invalid JSON response from Google Interactions API"), {
|
|
978
|
+
statusCode: res.status,
|
|
979
|
+
responseBody: res.text.slice(0, 500),
|
|
980
|
+
url,
|
|
981
|
+
}),
|
|
982
|
+
"google",
|
|
983
|
+
modelId
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Fire-and-forget cancel so an aborted background run stops billing; in: an aborted generate/stream, out: POST {id}/cancel.
|
|
989
|
+
private cancelInteraction(baseUrl: string, id: string, options: ProviderRequestOptions | undefined, apiKey: string): void {
|
|
990
|
+
void this.doFetch(`${baseUrl}/interactions/${id}/cancel`, "POST", undefined, options, apiKey).catch(() => {});
|
|
991
|
+
}
|
|
992
|
+
|
|
802
993
|
async generate(
|
|
803
994
|
model: string | ModelSpec,
|
|
804
995
|
context: ProviderContext,
|
|
@@ -835,24 +1026,33 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
835
1026
|
body: b,
|
|
836
1027
|
});
|
|
837
1028
|
let rawRequest = makeRawRequest(body);
|
|
1029
|
+
const retryOptions = {
|
|
1030
|
+
...retryOptionsFrom(options),
|
|
1031
|
+
signal: options?.signal,
|
|
1032
|
+
label: { providerId: "google", modelId: clean },
|
|
1033
|
+
};
|
|
838
1034
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1035
|
+
// Polls a background interaction until it leaves queued/in_progress; an abort also cancels it server-side.
|
|
1036
|
+
const waitForCompletion = async (reply: InteractionReply): Promise<InteractionReply> => {
|
|
1037
|
+
const id = reply.interaction.id;
|
|
1038
|
+
if (!id) return reply;
|
|
843
1039
|
try {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
"google",
|
|
853
|
-
clean
|
|
854
|
-
);
|
|
1040
|
+
for (let poll = 0; isRunning(reply.interaction.status); poll++) {
|
|
1041
|
+
await sleep(this.pollDelaysMs[poll === 0 ? 0 : 1], options?.signal);
|
|
1042
|
+
reply = await withRetries(() => this.fetchInteraction(`${url}/${id}`, "GET", undefined, options, apiKey, clean), retryOptions);
|
|
1043
|
+
}
|
|
1044
|
+
return reply;
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
if (options?.signal?.aborted || isAbortError(err)) this.cancelInteraction(baseUrl, id, options, apiKey);
|
|
1047
|
+
throw err;
|
|
855
1048
|
}
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
// Create (retried as a whole), wait if it runs in the background, then record the chain and map the result.
|
|
1052
|
+
const run = async (): Promise<ProviderGenerateResult> => {
|
|
1053
|
+
let reply = await withRetries(() => this.fetchInteraction(url, "POST", body, options, apiKey, clean), retryOptions);
|
|
1054
|
+
if (body.background) reply = await waitForCompletion(reply);
|
|
1055
|
+
const { interaction } = reply;
|
|
856
1056
|
if (interaction.id && sessionId && body.store !== false) {
|
|
857
1057
|
setChain(sessionId, { interactionId: interaction.id, model: clean, at: Date.now() });
|
|
858
1058
|
} else if (body.store === false && sessionId) {
|
|
@@ -864,19 +1064,14 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
864
1064
|
interaction,
|
|
865
1065
|
clean,
|
|
866
1066
|
Date.now() - startTime,
|
|
867
|
-
{ request: rawRequest, response: { status:
|
|
1067
|
+
{ request: rawRequest, response: { status: reply.status, statusText: reply.statusText, headers: reply.headers, body: interaction } }
|
|
868
1068
|
);
|
|
869
1069
|
checkTurnTokens(sessionId, parsed.usage);
|
|
870
1070
|
return parsed;
|
|
871
1071
|
};
|
|
872
1072
|
|
|
873
|
-
const retryOptions = {
|
|
874
|
-
...retryOptionsFrom(options),
|
|
875
|
-
signal: options?.signal,
|
|
876
|
-
label: { providerId: "google", modelId: clean },
|
|
877
|
-
};
|
|
878
1073
|
try {
|
|
879
|
-
return await
|
|
1074
|
+
return await run();
|
|
880
1075
|
} catch (err) {
|
|
881
1076
|
if (err instanceof Error && (err as { name?: string }).name === "AbortError") throw err;
|
|
882
1077
|
// Reactive self-heal (Rule 2): a chained pointer the server no longer
|
|
@@ -887,7 +1082,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
887
1082
|
clearInteractionChains(sessionId);
|
|
888
1083
|
body = await this.buildBody(clean, context, options, sessionId, tools, false);
|
|
889
1084
|
rawRequest = makeRawRequest(body);
|
|
890
|
-
return await
|
|
1085
|
+
return await run();
|
|
891
1086
|
}
|
|
892
1087
|
throw err;
|
|
893
1088
|
}
|
|
@@ -925,6 +1120,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
925
1120
|
// visible in catch).
|
|
926
1121
|
let evictSessionId: string | undefined;
|
|
927
1122
|
let usedChain = false;
|
|
1123
|
+
// Set once a background interaction id is known: an abort then cancels it server-side.
|
|
1124
|
+
let cancelBackground = (): void => {};
|
|
928
1125
|
try {
|
|
929
1126
|
// Hard generation floor (mirrors generate): no network on legacy ids.
|
|
930
1127
|
assertSupportedGeneration(clean);
|
|
@@ -958,30 +1155,32 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
958
1155
|
};
|
|
959
1156
|
|
|
960
1157
|
const streamTimeoutMs = timeoutFor(options as { timeoutMs?: number } | undefined);
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1158
|
+
// Opens one SSE connection (the create POST, or a background resume GET). Transient failures
|
|
1159
|
+
// (429/5xx/network) are retried before the first byte; a live stream is never retried.
|
|
1160
|
+
const open = (target: string, method: "POST" | "GET"): Promise<Response> =>
|
|
1161
|
+
openStreamResponse({
|
|
1162
|
+
fetchOnce: async () => {
|
|
1163
|
+
const timer =
|
|
1164
|
+
streamTimeoutMs && streamTimeoutMs > 0
|
|
1165
|
+
? setTimeout(() => {
|
|
1166
|
+
try {
|
|
1167
|
+
linked.abort();
|
|
1168
|
+
} catch {}
|
|
1169
|
+
}, streamTimeoutMs)
|
|
1170
|
+
: undefined;
|
|
1171
|
+
try {
|
|
1172
|
+
return await (options?.fetch ?? fetch)(target, this.requestInit(method, method === "POST" ? body : undefined, options, apiKey, linked.signal));
|
|
1173
|
+
} finally {
|
|
1174
|
+
if (timer) clearTimeout(timer);
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
onHttpError: (r, text) => this.throwIfError(r.status, r.statusText, target, text, clean, headersToRecord(r.headers)),
|
|
1178
|
+
noBodyError: () => toConciseProviderError(new Error("Google streaming response had no body"), "google", clean),
|
|
1179
|
+
retry: retryOptionsFrom(options),
|
|
1180
|
+
signal: linked.signal,
|
|
1181
|
+
label: { providerId: "google", modelId: clean },
|
|
1182
|
+
});
|
|
1183
|
+
const res = await open(url, "POST");
|
|
985
1184
|
const responseHeaders: Record<string, string> = {};
|
|
986
1185
|
res.headers.forEach((v, k) => {
|
|
987
1186
|
responseHeaders[k] = v;
|
|
@@ -989,13 +1188,14 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
989
1188
|
const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
990
1189
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
991
1190
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
const decoder = new TextDecoder();
|
|
1191
|
+
let reader: StreamReader | undefined;
|
|
1192
|
+
let lastEventId: string | undefined;
|
|
995
1193
|
let text = "";
|
|
996
1194
|
let thinking = "";
|
|
997
1195
|
let signature: string | undefined;
|
|
998
1196
|
const calls = new Map<number, { id: string; name: string; startArgs: string; deltaArgs: string }>();
|
|
1197
|
+
// Every step as the server would return it (start + deltas), for exact history replay.
|
|
1198
|
+
const steps = new Map<number, Record<string, unknown>>();
|
|
999
1199
|
let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
1000
1200
|
let finishReason = "stop";
|
|
1001
1201
|
let sawFinishReason = false;
|
|
@@ -1010,11 +1210,14 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1010
1210
|
() => {
|
|
1011
1211
|
aborted = true;
|
|
1012
1212
|
try {
|
|
1013
|
-
void reader
|
|
1213
|
+
void reader?.cancel();
|
|
1014
1214
|
} catch {}
|
|
1015
1215
|
},
|
|
1016
1216
|
{ once: true }
|
|
1017
1217
|
);
|
|
1218
|
+
cancelBackground = () => {
|
|
1219
|
+
if (body.background && responseId) this.cancelInteraction(baseUrl, responseId, options, apiKey);
|
|
1220
|
+
};
|
|
1018
1221
|
|
|
1019
1222
|
const handleMessage = (event: string | undefined, data: string): void => {
|
|
1020
1223
|
if (!data) return;
|
|
@@ -1031,6 +1234,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1031
1234
|
return;
|
|
1032
1235
|
}
|
|
1033
1236
|
const type = (msg["event_type"] as string) || event;
|
|
1237
|
+
if (typeof msg["event_id"] === "string") lastEventId = msg["event_id"] as string;
|
|
1034
1238
|
if (type === "error") {
|
|
1035
1239
|
// SSE error event (e.g. rate_limit_exceeded mid-stream). Must
|
|
1036
1240
|
// surface — silently ending with empty text + zero usage hides it.
|
|
@@ -1050,16 +1254,20 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1050
1254
|
const delta = (msg["delta"] as Record<string, unknown>) ?? {};
|
|
1051
1255
|
const dType = delta["type"] as string;
|
|
1052
1256
|
if (dType === "text" && typeof delta["text"] === "string") {
|
|
1257
|
+
appendBlockText(steps.get(index), "content", delta["text"] as string);
|
|
1053
1258
|
text += delta["text"] as string;
|
|
1054
1259
|
eventStream.push({ type: "text_delta", delta: delta["text"] as string, partialText: text });
|
|
1055
1260
|
} else if (dType === "thought_summary") {
|
|
1056
1261
|
const content = delta["content"] as { type?: string; text?: string } | undefined;
|
|
1057
1262
|
const t = content?.text ?? (typeof delta["text"] === "string" ? (delta["text"] as string) : "");
|
|
1058
1263
|
if (t) {
|
|
1264
|
+
appendBlockText(steps.get(index), "summary", t);
|
|
1059
1265
|
thinking += t;
|
|
1060
1266
|
eventStream.push({ type: "thinking_delta", thinkingDelta: t, partialThinking: thinking });
|
|
1061
1267
|
}
|
|
1062
1268
|
} else if (dType === "thought_signature" && typeof delta["signature"] === "string") {
|
|
1269
|
+
const s = steps.get(index);
|
|
1270
|
+
if (s) s["signature"] = delta["signature"];
|
|
1063
1271
|
// Wire truth: signature arrives as its own delta (no text event).
|
|
1064
1272
|
if (!signature) signature = delta["signature"] as string;
|
|
1065
1273
|
} else if (dType === "arguments" || dType === "arguments_delta") {
|
|
@@ -1073,7 +1281,21 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1073
1281
|
} else if (type === "step.start") {
|
|
1074
1282
|
const index = (msg["index"] as number) ?? 0;
|
|
1075
1283
|
const step = (msg["step"] as Record<string, unknown>) ?? {};
|
|
1076
|
-
|
|
1284
|
+
steps.set(index, step);
|
|
1285
|
+
if (step["type"] === "model_output") {
|
|
1286
|
+
const t = blockText(step["content"]);
|
|
1287
|
+
if (t) {
|
|
1288
|
+
text += t;
|
|
1289
|
+
eventStream.push({ type: "text_delta", delta: t, partialText: text });
|
|
1290
|
+
}
|
|
1291
|
+
} else if (step["type"] === "thought") {
|
|
1292
|
+
const t = blockText(step["summary"]);
|
|
1293
|
+
if (t) {
|
|
1294
|
+
thinking += t;
|
|
1295
|
+
eventStream.push({ type: "thinking_delta", thinkingDelta: t, partialThinking: thinking });
|
|
1296
|
+
}
|
|
1297
|
+
if (!signature && typeof step["signature"] === "string" && step["signature"]) signature = step["signature"];
|
|
1298
|
+
} else if (step["type"] === "function_call") {
|
|
1077
1299
|
const args = step["arguments"];
|
|
1078
1300
|
const startArgs = typeof args === "string" ? args : args ? JSON.stringify(args) : "";
|
|
1079
1301
|
calls.set(index, {
|
|
@@ -1083,6 +1305,13 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1083
1305
|
deltaArgs: "",
|
|
1084
1306
|
});
|
|
1085
1307
|
}
|
|
1308
|
+
} else if (type === "step.stop" || type === "interaction.status_update") {
|
|
1309
|
+
// Cumulative usage so far: keeps usage when the stream is cut before `interaction.completed`.
|
|
1310
|
+
const stepUsage = msg["usage"] as InteractionObject["usage"] | undefined;
|
|
1311
|
+
if (stepUsage) {
|
|
1312
|
+
usage = mapUsage(stepUsage);
|
|
1313
|
+
sawUsage = true;
|
|
1314
|
+
}
|
|
1086
1315
|
} else if (type === "interaction.completed") {
|
|
1087
1316
|
const interaction = (msg["interaction"] as InteractionObject) ?? {};
|
|
1088
1317
|
// Failed/cancelled interactions must surface loudly — never
|
|
@@ -1100,30 +1329,57 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1100
1329
|
}
|
|
1101
1330
|
};
|
|
1102
1331
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1332
|
+
// Reads one connection to its end. A background connection that drops or stalls just ends
|
|
1333
|
+
// (the caller reconnects); provider error events from handleMessage always throw.
|
|
1334
|
+
const readAll = async (response: Response): Promise<void> => {
|
|
1335
|
+
const parser = new SSEParser();
|
|
1336
|
+
const decoder = new TextDecoder();
|
|
1337
|
+
const current = (reader = response.body!.getReader()); // openStreamResponse guarantees a body
|
|
1338
|
+
try {
|
|
1339
|
+
while (true) {
|
|
1340
|
+
if (linked.signal.aborted || eventStream.isCancelled()) {
|
|
1341
|
+
try {
|
|
1342
|
+
await current.cancel();
|
|
1343
|
+
} catch {}
|
|
1344
|
+
throw Object.assign(new Error("Stream aborted"), { name: "AbortError" });
|
|
1345
|
+
}
|
|
1346
|
+
let chunk: { done: boolean; value?: Uint8Array };
|
|
1347
|
+
try {
|
|
1348
|
+
chunk = body.background ? await readWithin(current, BACKGROUND_STREAM_STALL_MS) : await current.read();
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
if (!body.background || aborted || isAbortError(err)) throw err;
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
if (chunk.done) break;
|
|
1354
|
+
for (const m of parser.feed(decoder.decode(chunk.value, { stream: true }))) handleMessage(m.event, m.data);
|
|
1355
|
+
}
|
|
1356
|
+
for (const m of parser.flush()) handleMessage(m.event, m.data);
|
|
1357
|
+
} finally {
|
|
1105
1358
|
try {
|
|
1106
|
-
|
|
1359
|
+
current.releaseLock();
|
|
1107
1360
|
} catch {}
|
|
1108
|
-
throw Object.assign(new Error("Stream aborted"), { name: "AbortError" });
|
|
1109
1361
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
await readAll(res);
|
|
1365
|
+
// Long decode: a background stream may drop; resume after the last event until the interaction ends.
|
|
1366
|
+
for (let misses = 0; body.background && completedBody === undefined && !aborted; ) {
|
|
1367
|
+
if (!responseId || ++misses > BACKGROUND_MAX_RECONNECTS) throw incompleteStreamError("google", clean);
|
|
1368
|
+
const before = framesSeen;
|
|
1369
|
+
const resume = `${baseUrl}/interactions/${responseId}?stream=true${lastEventId ? `&last_event_id=${encodeURIComponent(lastEventId)}` : ""}`;
|
|
1370
|
+
await readAll(await open(resume, "GET"));
|
|
1371
|
+
if (framesSeen > before) misses = 0;
|
|
1114
1372
|
}
|
|
1115
|
-
for (const m of parser.flush()) handleMessage(m.event, m.data);
|
|
1116
|
-
try {
|
|
1117
|
-
reader.releaseLock();
|
|
1118
|
-
} catch {}
|
|
1119
1373
|
|
|
1120
1374
|
if (linked.signal.aborted || eventStream.isCancelled() || aborted) {
|
|
1121
1375
|
throw Object.assign(new Error("Stream aborted"), { name: "AbortError" });
|
|
1122
1376
|
}
|
|
1123
1377
|
|
|
1124
1378
|
const toolCalls: ToolCallRecord[] = [];
|
|
1125
|
-
for (const c of calls
|
|
1379
|
+
for (const [index, c] of calls) {
|
|
1126
1380
|
const args = parseStreamedToolArguments(c.startArgs, c.deltaArgs);
|
|
1381
|
+
const step = steps.get(index);
|
|
1382
|
+
if (step) step["arguments"] = args;
|
|
1127
1383
|
const record: ToolCallRecord = {
|
|
1128
1384
|
id: c.id,
|
|
1129
1385
|
name: c.name,
|
|
@@ -1178,6 +1434,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1178
1434
|
response: { ...responseMeta, body: completedBody, badFrames } as unknown as ProviderRawData["response"],
|
|
1179
1435
|
},
|
|
1180
1436
|
durationMs: Date.now() - startTime,
|
|
1437
|
+
providerSteps: replaySteps([...steps.entries()].sort(([a], [b]) => a - b).map(([, s]) => s)),
|
|
1181
1438
|
});
|
|
1182
1439
|
eventStream.push({ type: "done", delta: "", usage, finishReason, responseId });
|
|
1183
1440
|
eventStream.end(finalResponse);
|
|
@@ -1188,6 +1445,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1188
1445
|
eventStream.isCancelled() ||
|
|
1189
1446
|
(raw as { name?: string }).name === "AbortError";
|
|
1190
1447
|
if (isAbort) {
|
|
1448
|
+
cancelBackground();
|
|
1191
1449
|
eventStream.fail(
|
|
1192
1450
|
Object.assign(new Error("Stream aborted"), { name: "AbortError" })
|
|
1193
1451
|
);
|