agent-accelerator 0.5.9 → 0.6.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.
Files changed (85) hide show
  1. package/README.md +64 -16
  2. package/examples/prompts/SYSTEM_PROMPT.md +2 -0
  3. package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
  4. package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
  5. package/package.json +4 -1
  6. package/src/agent/agent.ts +447 -172
  7. package/src/agent/architecture.md +16 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +9 -3
  10. package/src/agent/defaults.ts +88 -0
  11. package/src/agent/delegation.ts +86 -28
  12. package/src/agent/loop.ts +474 -374
  13. package/src/agent/subagent.ts +1 -3
  14. package/src/agent/subagents-tool.ts +50 -19
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/usage-limit.ts +92 -0
  17. package/src/agent/worker.ts +73 -30
  18. package/src/agent/workers.ts +126 -21
  19. package/src/architecture.md +17 -15
  20. package/src/index.ts +43 -19
  21. package/src/models/architecture.md +4 -3
  22. package/src/models/catalog-cache.ts +6 -5
  23. package/src/models/catalog.ts +39 -30
  24. package/src/prompts/additional-context.md +2 -0
  25. package/src/prompts/architecture.md +19 -0
  26. package/src/prompts/prompts.ts +81 -0
  27. package/src/prompts/reasoning-directive.md +5 -0
  28. package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +4 -0
  29. package/src/prompts/structured-output.md +2 -0
  30. package/src/{agent/subagents.md → prompts/subagents-tool.md} +3 -0
  31. package/src/prompts/tool-bridge.md +1 -0
  32. package/src/prompts/tool-interrupted.md +1 -0
  33. package/src/prompts/tool-not-found.md +1 -0
  34. package/src/prompts/tool-repeat-blocked.md +1 -0
  35. package/src/prompts/tool-schema-error.md +1 -0
  36. package/src/prompts/tool-truncated-arguments.md +1 -0
  37. package/src/prompts/worker-failed.md +1 -0
  38. package/src/prompts/worker-replied.md +2 -0
  39. package/src/providers/architecture.md +10 -8
  40. package/src/providers/google.ts +228 -69
  41. package/src/providers/openai-compat.ts +74 -56
  42. package/src/providers/openai.ts +64 -41
  43. package/src/providers/openrouter.ts +270 -48
  44. package/src/providers/registry.ts +6 -21
  45. package/src/providers/shared.ts +87 -33
  46. package/src/providers.ts +115 -124
  47. package/src/scheduler/architecture.md +8 -7
  48. package/src/scheduler/scheduler.ts +226 -60
  49. package/src/scheduler/store.ts +23 -10
  50. package/src/scheduler/time.ts +17 -3
  51. package/src/scheduler/tool.ts +38 -16
  52. package/src/session/architecture.md +7 -6
  53. package/src/session/journal.ts +39 -12
  54. package/src/session/store.ts +126 -61
  55. package/src/session/usage.ts +58 -9
  56. package/src/streaming/architecture.md +4 -3
  57. package/src/streaming/event-stream.ts +26 -12
  58. package/src/telemetry.ts +6 -2
  59. package/src/tools/architecture.md +9 -7
  60. package/src/tools/executor.ts +109 -62
  61. package/src/tools/schema.ts +10 -6
  62. package/src/tools/subagent-result.ts +6 -1
  63. package/src/tools/tool-content.ts +32 -0
  64. package/src/tools/tool.ts +9 -11
  65. package/src/types/agent.ts +37 -22
  66. package/src/types/architecture.md +11 -10
  67. package/src/types/core.ts +5 -9
  68. package/src/types/errors.ts +26 -4
  69. package/src/types/message.ts +7 -0
  70. package/src/types/model.ts +4 -5
  71. package/src/types/scheduler.ts +4 -4
  72. package/src/types/tool.ts +14 -2
  73. package/src/update-models.ts +5 -5
  74. package/src/utils/architecture.md +16 -15
  75. package/src/utils/cache.ts +1 -3
  76. package/src/utils/durable.ts +17 -5
  77. package/src/utils/env.ts +9 -7
  78. package/src/utils/errors.ts +9 -17
  79. package/src/utils/headers.ts +3 -10
  80. package/src/utils/media.ts +23 -8
  81. package/src/utils/raw-capture.ts +9 -2
  82. package/src/utils/retry.ts +15 -3
  83. package/src/utils/safe-io.ts +29 -7
  84. package/src/utils/serialization.ts +1 -4
  85. package/src/utils/session.ts +1 -2
@@ -29,7 +29,14 @@ import type {
29
29
  ProviderGenerateResult,
30
30
  ProviderRawData,
31
31
  } from "../types/model.ts";
32
- import type { ProviderContext, Message, ContentPart, ProviderSteps, ToolResultPart, MediaResolution } from "../types/message.ts";
32
+ import type {
33
+ ProviderContext,
34
+ ContentPart,
35
+ ProviderSteps,
36
+ ToolResultPart,
37
+ MediaResolution,
38
+ VideoProcessing,
39
+ } from "../types/message.ts";
33
40
  import type { StandardToolDeclaration, ToolCallRecord } from "../types/tool.ts";
34
41
  import type { TokenUsage } from "../types/core.ts";
35
42
  import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
@@ -53,7 +60,6 @@ import {
53
60
  combinedSignal,
54
61
  timeoutFor,
55
62
  hasAuthHeader,
56
- clampToolCallId,
57
63
  openStreamResponse,
58
64
  retryOptionsFrom,
59
65
  headersToRecord,
@@ -85,19 +91,70 @@ import {
85
91
  // ---------------------------------------------------------------------------
86
92
 
87
93
  /** Image MIME types Google accepts inline. */
88
- export const GOOGLE_IMAGE_MIME = ["image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff"] as const;
94
+ export const GOOGLE_IMAGE_MIME = [
95
+ "image/png",
96
+ "image/jpeg",
97
+ "image/webp",
98
+ "image/heic",
99
+ "image/heif",
100
+ "image/gif",
101
+ "image/bmp",
102
+ "image/tiff",
103
+ ] as const;
89
104
  /** Audio MIME types Google accepts inline (audio is never fetched by URL). */
90
- 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;
105
+ export const GOOGLE_AUDIO_MIME = [
106
+ "audio/wav",
107
+ "audio/mp3",
108
+ "audio/aiff",
109
+ "audio/aac",
110
+ "audio/ogg",
111
+ "audio/flac",
112
+ "audio/mpeg",
113
+ "audio/m4a",
114
+ "audio/l16",
115
+ "audio/opus",
116
+ "audio/alaw",
117
+ "audio/mulaw",
118
+ "audio/webm",
119
+ ] as const;
91
120
  /** Video MIME types Google accepts inline. */
92
- 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;
121
+ export const GOOGLE_VIDEO_MIME = [
122
+ "video/mp4",
123
+ "video/mpeg",
124
+ "video/mpg",
125
+ "video/mov",
126
+ "video/avi",
127
+ "video/x-flv",
128
+ "video/webm",
129
+ "video/wmv",
130
+ "video/3gpp",
131
+ ] as const;
93
132
  /** Document MIME types Google accepts (the only two). */
94
133
  export const GOOGLE_DOCUMENT_MIME = ["application/pdf", "text/csv"] as const;
95
134
  /** MIME types Google fetches itself from a public `https://` URL. */
96
135
  export const GOOGLE_URL_MIME = [
97
- "text/html", "text/css", "text/plain", "text/xml", "text/csv", "text/rtf", "text/javascript",
98
- "application/json", "application/pdf",
99
- "image/bmp", "image/jpeg", "image/png", "image/webp",
100
- "video/mp4", "video/mpeg", "video/quicktime", "video/avi", "video/x-flv", "video/mpg", "video/webm", "video/wmv", "video/3gpp",
136
+ "text/html",
137
+ "text/css",
138
+ "text/plain",
139
+ "text/xml",
140
+ "text/csv",
141
+ "text/rtf",
142
+ "text/javascript",
143
+ "application/json",
144
+ "application/pdf",
145
+ "image/bmp",
146
+ "image/jpeg",
147
+ "image/png",
148
+ "image/webp",
149
+ "video/mp4",
150
+ "video/mpeg",
151
+ "video/quicktime",
152
+ "video/avi",
153
+ "video/x-flv",
154
+ "video/mpg",
155
+ "video/webm",
156
+ "video/wmv",
157
+ "video/3gpp",
101
158
  ] as const;
102
159
  /** Common MIME spellings mapped to Google's inline names. */
103
160
  export const GOOGLE_INLINE_MIME_ALIASES: Readonly<Record<string, string>> = {
@@ -106,7 +163,8 @@ export const GOOGLE_INLINE_MIME_ALIASES: Readonly<Record<string, string>> = {
106
163
  "audio/wave": "audio/wav",
107
164
  "image/jpg": "image/jpeg",
108
165
  };
109
- // Documented size limits. Not enforced yet: oversized inputs get Google's own error.
166
+ // Documented size limits (file-input-methods.md). `assertGoogleInlineSizes` below checks them, but it is NOT called yet:
167
+ // the owner is not sure how Files API uploads should be handled, so oversized inputs still get Google's own error for now.
110
168
  /** Max inline payload per request (bytes). */
111
169
  export const GOOGLE_MAX_INLINE_REQUEST_BYTES = 100 * 1024 * 1024;
112
170
  /** Max inline PDF size (bytes). */
@@ -167,14 +225,22 @@ const INLINE_MIME: Record<MediaBlockType, readonly string[]> = {
167
225
  document: GOOGLE_DOCUMENT_MIME,
168
226
  };
169
227
 
170
- const FILES_API_PREFIX = "https://generativelanguage.googleapis.com/v1beta/files/";
228
+ // A Google Files API link on either API version (`/v1/files/<id>` or `/v1beta/files/<id>`); the host is exact, so no other URL counts.
229
+ const FILES_API_URL = /^https:\/\/generativelanguage\.googleapis\.com\/v1(?:beta)?\/files\/[^/\s]+/;
171
230
  const YOUTUBE_URL = /^https:\/\/(www\.|m\.)?(youtube\.com|youtu\.be)\//i;
172
231
 
173
232
  // ---------------------------------------------------------------------------
174
233
  // Interactions wire shapes (subset used by this adapter)
175
234
  // ---------------------------------------------------------------------------
176
235
 
177
- type MediaBlock = { type: MediaBlockType; mime_type?: string; data?: string; uri?: string; resolution?: MediaResolution };
236
+ type MediaBlock = {
237
+ type: MediaBlockType;
238
+ mime_type?: string;
239
+ data?: string;
240
+ uri?: string;
241
+ resolution?: MediaResolution;
242
+ processing?: VideoProcessing;
243
+ };
178
244
  type InteractionContentBlock = { type: "text"; text: string } | MediaBlock;
179
245
 
180
246
  type InteractionInputStep =
@@ -317,7 +383,10 @@ function liveChainFor(sessionId: string | undefined, options?: ProviderRequestOp
317
383
  * below the previous turn's total (Rule 3: a caller dropped the chain without
318
384
  * rehydrating history). Never throws; observability-only.
319
385
  */
320
- function checkTurnTokens(sessionId: string | undefined, usage: { inputTokens: number; outputTokens: number; cachedTokens?: number }): void {
386
+ function checkTurnTokens(
387
+ sessionId: string | undefined,
388
+ usage: { inputTokens: number; outputTokens: number; cachedTokens?: number }
389
+ ): void {
321
390
  try {
322
391
  if (!sessionId) return;
323
392
  const total = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
@@ -409,9 +478,7 @@ function toGoogleTools(tools?: StandardToolDeclaration[]): Array<Record<string,
409
478
  type: "function",
410
479
  name: t.name,
411
480
  description: t.description,
412
- parameters: stripSchemaForGoogle(
413
- (t.parameters || { type: "object", properties: {} }) as Record<string, unknown>
414
- ),
481
+ parameters: stripSchemaForGoogle((t.parameters || { type: "object", properties: {} }) as Record<string, unknown>),
415
482
  }));
416
483
  }
417
484
 
@@ -442,8 +509,10 @@ function uriBlock(type: MediaBlockType, value: string, mimeType?: string): Media
442
509
  const withMime = (mime?: string): MediaBlock => ({ type, uri: value, ...(mime ? { mime_type: mime } : {}) });
443
510
  if (type === "video" && YOUTUBE_URL.test(value)) return withMime(explicit);
444
511
  const mime = explicit ?? bareMime(inferMimeType(value, ""));
445
- if (value.startsWith(FILES_API_PREFIX) || value.startsWith("gs://")) return withMime(mime);
446
- return value.startsWith("https://") && mime && (GOOGLE_URL_MIME as readonly string[]).includes(mime) ? withMime(mime) : undefined;
512
+ if (FILES_API_URL.test(value) || value.startsWith("gs://")) return withMime(mime);
513
+ return value.startsWith("https://") && mime && (GOOGLE_URL_MIME as readonly string[]).includes(mime)
514
+ ? withMime(mime)
515
+ : undefined;
447
516
  }
448
517
 
449
518
  // 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.
@@ -452,7 +521,8 @@ async function inlineBlock(type: MediaBlockType, value: MediaValue, mimeType?: s
452
521
  const raw = bareMime(norm.mimeType) ?? "";
453
522
  const mime = GOOGLE_INLINE_MIME_ALIASES[raw] ?? raw;
454
523
  if (!INLINE_MIME[type].includes(mime)) {
455
- const what = type === "document" ? "Google reads only PDF and CSV documents" : `Google does not accept ${type} type "${mime}"`;
524
+ const what =
525
+ type === "document" ? "Google reads only PDF and CSV documents" : `Google does not accept ${type} type "${mime}"`;
456
526
  throw new ValidationError(
457
527
  `[Agent Accelerator] ${what} (got "${mime}"; supported: ${INLINE_MIME[type].join(", ")}). Convert the file or pass the correct mimeType.`
458
528
  );
@@ -475,11 +545,63 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<InteractionCo
475
545
  const block = uri ?? (await inlineBlock(type, value, part.mimeType));
476
546
  // Per-item detail level (image and video blocks only, api-ref `MediaResolution`).
477
547
  const resolution = part.type === "image" || part.type === "video" ? part.resolution : undefined;
478
- blocks.push(resolution ? { ...block, resolution } : block);
548
+ // Per-video processing mode (`static` | `agentic`, api-ref `MediaProcessing`): the model may then add `processing_call` / `processing_result` steps, which the raw-steps replay keeps like any other step.
549
+ const processing = part.type === "video" ? part.processing : undefined;
550
+ if (processing !== undefined && processing !== "static" && processing !== "agentic")
551
+ throw new ValidationError(
552
+ `[Agent Accelerator] Invalid video processing ${JSON.stringify(processing)}: expected "static" or "agentic".`
553
+ );
554
+ blocks.push({ ...block, ...(resolution ? { resolution } : {}), ...(processing ? { processing } : {}) });
479
555
  }
480
556
  return blocks;
481
557
  }
482
558
 
559
+ // Decoded size in bytes of a base64 string, without decoding it; in: assertGoogleInlineSizes, out: a byte count.
560
+ function base64Bytes(b64: string): number {
561
+ const clean = b64.replace(/\s/g, "");
562
+ const padding = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
563
+ return Math.max(0, Math.floor((clean.length * 3) / 4) - padding);
564
+ }
565
+
566
+ // Every inline media block (`data` + `mime_type`) found anywhere in a request `input`, including blocks inside steps and function results; in: assertGoogleInlineSizes, out: the blocks.
567
+ function inlineBlocks(
568
+ value: unknown,
569
+ out: Array<{ mime?: string; bytes: number }> = []
570
+ ): Array<{ mime?: string; bytes: number }> {
571
+ if (Array.isArray(value)) {
572
+ for (const v of value) inlineBlocks(v, out);
573
+ } else if (value && typeof value === "object") {
574
+ const o = value as Record<string, unknown>;
575
+ if (typeof o["data"] === "string" && typeof o["mime_type"] === "string")
576
+ out.push({ mime: bareMime(o["mime_type"]), bytes: base64Bytes(o["data"]) });
577
+ else for (const v of Object.values(o)) inlineBlocks(v, out);
578
+ }
579
+ return out;
580
+ }
581
+
582
+ /**
583
+ * Fails before the network when the inline media of one request is over Google's documented limits: 100 MB in total and
584
+ * 50 MB for each PDF (larger files go through the Files API). Counts decoded bytes, over the whole `input` (the full
585
+ * history on a reseed). NOT called by the adapter yet (see the call sites in `generate` and `stream`).
586
+ */
587
+ export function assertGoogleInlineSizes(input: unknown, modelId: string): void {
588
+ const blocks = inlineBlocks(input);
589
+ const mb = (n: number): string => `${(n / (1024 * 1024)).toFixed(1)} MB`;
590
+ for (const b of blocks) {
591
+ if (b.mime === "application/pdf" && b.bytes > GOOGLE_MAX_INLINE_PDF_BYTES) {
592
+ throw new ValidationError(
593
+ `[Agent Accelerator] [google/${modelId}] A PDF of ${mb(b.bytes)} is over Google's ${mb(GOOGLE_MAX_INLINE_PDF_BYTES)} limit for inline PDFs. Upload it with the Files API and pass its link instead.`
594
+ );
595
+ }
596
+ }
597
+ const total = blocks.reduce((n, b) => n + b.bytes, 0);
598
+ if (total > GOOGLE_MAX_INLINE_REQUEST_BYTES) {
599
+ throw new ValidationError(
600
+ `[Agent Accelerator] [google/${modelId}] The inline media of this request is ${mb(total)}, over Google's ${mb(GOOGLE_MAX_INLINE_REQUEST_BYTES)} limit per request. Upload the large files with the Files API and pass their links instead.`
601
+ );
602
+ }
603
+ }
604
+
483
605
  // 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.
484
606
  function replaySteps(steps: unknown[] | undefined): ProviderSteps | undefined {
485
607
  const kept = (steps ?? [])
@@ -515,13 +637,14 @@ function resultToText(result: unknown): string {
515
637
  return safeStringify(result);
516
638
  }
517
639
 
518
- // 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`.
519
- function functionResultStep(part: ToolResultPart): InteractionInputStep {
640
+ // Builds the documented `function_result` step (`is_error` only when the tool failed); text first, then the media the tool returned with `toolContent` (Gemini 3 accepts image and document blocks in `result`); in: a canonical tool_result part, out: chained or full-history `input`.
641
+ async function functionResultStep(part: ToolResultPart): Promise<InteractionInputStep> {
642
+ const media = part.parts && part.parts.length > 0 ? await contentPartsToBlocks(part.parts) : [];
520
643
  return {
521
644
  type: "function_result",
522
645
  name: part.name,
523
646
  call_id: part.id,
524
- result: [{ type: "text", text: resultToText(part.result) }],
647
+ result: [{ type: "text", text: resultToText(part.result) }, ...media],
525
648
  ...(part.isError ? { is_error: true } : {}),
526
649
  };
527
650
  }
@@ -534,7 +657,9 @@ function functionResultStep(part: ToolResultPart): InteractionInputStep {
534
657
  * behind them. Sending only the last message would drop the tool results and
535
658
  * leave the chained interaction waiting on its function calls.
536
659
  */
537
- async function latestTurnInput(context: ProviderContext): Promise<string | InteractionContentBlock[] | InteractionInputStep[]> {
660
+ async function latestTurnInput(
661
+ context: ProviderContext
662
+ ): Promise<string | InteractionContentBlock[] | InteractionInputStep[]> {
538
663
  const messages = context.messages;
539
664
  let lastAssistant = -1;
540
665
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -563,7 +688,7 @@ async function latestTurnInput(context: ProviderContext): Promise<string | Inter
563
688
  if (!Array.isArray(m.content)) continue;
564
689
  for (const part of m.content) {
565
690
  if (part.type === "tool_result") {
566
- steps.push(functionResultStep(part));
691
+ steps.push(await functionResultStep(part));
567
692
  }
568
693
  }
569
694
  } else if (m.role === "user") {
@@ -617,10 +742,11 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
617
742
  const sig = shouldEchoSignature(partProvider ?? msgProvider, "google")
618
743
  ? part.thoughtSignature || m.thoughtSignature || ""
619
744
  : "";
620
- const thoughtStep: { type: "thought"; signature?: string; summary: Array<{ type: string; text?: string }> } = {
621
- type: "thought",
622
- summary: [{ type: "text", text: part.thinking }],
623
- };
745
+ const thoughtStep: { type: "thought"; signature?: string; summary: Array<{ type: string; text?: string }> } =
746
+ {
747
+ type: "thought",
748
+ summary: [{ type: "text", text: part.thinking }],
749
+ };
624
750
  if (sig) thoughtStep.signature = sig;
625
751
  steps.push(thoughtStep);
626
752
  } else if (part.type === "tool_call") {
@@ -652,7 +778,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
652
778
  if (!Array.isArray(m.content)) continue;
653
779
  for (const part of m.content) {
654
780
  if (part.type === "tool_result") {
655
- steps.push(functionResultStep(part));
781
+ steps.push(await functionResultStep(part));
656
782
  }
657
783
  }
658
784
  }
@@ -733,11 +859,7 @@ function parseInteraction(
733
859
  // truncated marker); absent args mean a zero-argument call.
734
860
  const rawArgs: unknown = step.arguments;
735
861
  const args =
736
- rawArgs && typeof rawArgs === "object"
737
- ? rawArgs
738
- : typeof rawArgs === "string"
739
- ? parseArguments(rawArgs)
740
- : {};
862
+ rawArgs && typeof rawArgs === "object" ? rawArgs : typeof rawArgs === "string" ? parseArguments(rawArgs) : {};
741
863
  toolCalls.push({
742
864
  id: step.id || newToolCallId(),
743
865
  name: step.name || "unknown",
@@ -815,12 +937,11 @@ export class GoogleInteractionsProvider implements Provider {
815
937
  // only signatures return and canonical `thinking` would always be empty.
816
938
  // Enabled whenever thinking is active — including the default/dynamic
817
939
  // case where no explicit level is sent — and off only when disabled.
818
- const thinkingActive =
819
- options?.thinking?.enabled !== false && (options?.thinking?.level ?? "dynamic") !== "none";
940
+ const thinkingActive = options?.thinking?.enabled !== false && (options?.thinking?.level ?? "dynamic") !== "none";
820
941
  if (thinkingActive) generationConfig["thinking_summaries"] = "auto";
821
942
  if (toolChoice) generationConfig["tool_choice"] = toolChoice;
822
- // max_output_tokens only; stop and seed warn+drop.
823
- Object.assign(generationConfig, mapGenerationParamsToGoogle(options, `google/${modelId}`));
943
+ // max_output_tokens only.
944
+ Object.assign(generationConfig, mapGenerationParamsToGoogle(options));
824
945
 
825
946
  const body: InteractionRequestBody = {
826
947
  model: modelId,
@@ -834,19 +955,26 @@ export class GoogleInteractionsProvider implements Provider {
834
955
  let outputEmulated = false;
835
956
  let outputSpec: import("../types/model.ts").StructuredOutputSpec | undefined;
836
957
  try {
837
- outputSpec = normalizeStructuredOutput(options?.output ?? (options as { outputSchema?: unknown })?.outputSchema);
958
+ outputSpec = normalizeStructuredOutput(
959
+ options?.output ?? (options as { outputSchema?: unknown })?.outputSchema
960
+ );
838
961
  } catch {
839
962
  outputSpec = undefined;
840
963
  }
841
964
  let effectiveSystemPrompt = context.systemPrompt;
842
965
  if (outputSpec && !modelSupportsStructuredOutput("google", modelId)) {
843
966
  outputEmulated = true;
844
- effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(context.systemPrompt, outputSpec);
967
+ effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(
968
+ context.systemPrompt,
969
+ outputSpec,
970
+ options?.prompts?.structuredOutput
971
+ );
845
972
  emitProviderWarning({
846
973
  provider: "google",
847
974
  capability: "structured output",
848
975
  requested: `${outputSpec.name} (google/${modelId})`,
849
- reason: "the catalog reports structured_output: false for this model, so native response_format is unsupported.",
976
+ reason:
977
+ "the catalog reports structured_output: false for this model, so native response_format is unsupported.",
850
978
  fallback: "emulated JSON instruction + client validation (no response_format is sent)",
851
979
  });
852
980
  }
@@ -927,7 +1055,10 @@ export class GoogleInteractionsProvider implements Provider {
927
1055
  apiKey: string,
928
1056
  signal?: AbortSignal
929
1057
  ): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
930
- const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
1058
+ const { signal: effective, cleanup } = combinedSignal(
1059
+ signal,
1060
+ timeoutFor(options as { timeoutMs?: number } | undefined)
1061
+ );
931
1062
  try {
932
1063
  const res = await (options?.fetch ?? fetch)(url, this.requestInit(method, body, options, apiKey, effective));
933
1064
  const text = await res.text();
@@ -944,7 +1075,6 @@ export class GoogleInteractionsProvider implements Provider {
944
1075
  // Throws a concise provider error (status, code, Retry-After, key hint) for a non-2xx reply; in: every Google HTTP call, out: nothing or a thrown error.
945
1076
  private throwIfError(
946
1077
  status: number,
947
- statusText: string,
948
1078
  url: string,
949
1079
  bodyText: string,
950
1080
  modelId: string,
@@ -989,9 +1119,14 @@ export class GoogleInteractionsProvider implements Provider {
989
1119
  modelId: string
990
1120
  ): Promise<InteractionReply> {
991
1121
  const res = await this.doFetch(url, method, body, options, apiKey, options?.signal);
992
- this.throwIfError(res.status, res.statusText, url, res.text, modelId, res.headers);
1122
+ this.throwIfError(res.status, url, res.text, modelId, res.headers);
993
1123
  try {
994
- return { status: res.status, statusText: res.statusText, headers: res.headers, interaction: JSON.parse(res.text) as InteractionObject };
1124
+ return {
1125
+ status: res.status,
1126
+ statusText: res.statusText,
1127
+ headers: res.headers,
1128
+ interaction: JSON.parse(res.text) as InteractionObject,
1129
+ };
995
1130
  } catch {
996
1131
  throw toConciseProviderError(
997
1132
  Object.assign(new Error("Invalid JSON response from Google Interactions API"), {
@@ -1006,7 +1141,12 @@ export class GoogleInteractionsProvider implements Provider {
1006
1141
  }
1007
1142
 
1008
1143
  // Fire-and-forget cancel so an aborted background run stops billing; in: an aborted generate/stream, out: POST {id}/cancel.
1009
- private cancelInteraction(baseUrl: string, id: string, options: ProviderRequestOptions | undefined, apiKey: string): void {
1144
+ private cancelInteraction(
1145
+ baseUrl: string,
1146
+ id: string,
1147
+ options: ProviderRequestOptions | undefined,
1148
+ apiKey: string
1149
+ ): void {
1010
1150
  void this.doFetch(`${baseUrl}/interactions/${id}/cancel`, "POST", undefined, options, apiKey).catch(() => {});
1011
1151
  }
1012
1152
 
@@ -1035,6 +1175,8 @@ export class GoogleInteractionsProvider implements Provider {
1035
1175
  const sessionId = options?.sessionId;
1036
1176
  const tools = options?.tools as StandardToolDeclaration[] | undefined;
1037
1177
  let body = await this.buildBody(clean, context, options, sessionId, tools, false);
1178
+ // Size limits are implemented but switched off for now (the owner is unsure about Files API handling yet):
1179
+ // assertGoogleInlineSizes(body.input, clean);
1038
1180
 
1039
1181
  const makeRawRequest = (b: InteractionRequestBody): ProviderRawData["request"] => ({
1040
1182
  url,
@@ -1060,7 +1202,10 @@ export class GoogleInteractionsProvider implements Provider {
1060
1202
  try {
1061
1203
  for (let poll = 0; isRunning(reply.interaction.status); poll++) {
1062
1204
  await sleep(this.pollDelaysMs[poll === 0 ? 0 : 1], options?.signal);
1063
- reply = await withRetries(() => this.fetchInteraction(`${url}/${id}`, "GET", undefined, options, apiKey, clean), retryOptions);
1205
+ reply = await withRetries(
1206
+ () => this.fetchInteraction(`${url}/${id}`, "GET", undefined, options, apiKey, clean),
1207
+ retryOptions
1208
+ );
1064
1209
  }
1065
1210
  return reply;
1066
1211
  } catch (err) {
@@ -1071,7 +1216,10 @@ export class GoogleInteractionsProvider implements Provider {
1071
1216
 
1072
1217
  // Create (retried as a whole), wait if it runs in the background, then record the chain and map the result.
1073
1218
  const run = async (): Promise<ProviderGenerateResult> => {
1074
- let reply = await withRetries(() => this.fetchInteraction(url, "POST", body, options, apiKey, clean), retryOptions);
1219
+ let reply = await withRetries(
1220
+ () => this.fetchInteraction(url, "POST", body, options, apiKey, clean),
1221
+ retryOptions
1222
+ );
1075
1223
  if (body.background) reply = await waitForCompletion(reply);
1076
1224
  const { interaction } = reply;
1077
1225
  if (interaction.id && sessionId && body.store !== false) {
@@ -1081,12 +1229,10 @@ export class GoogleInteractionsProvider implements Provider {
1081
1229
  lastTurnTotals.delete(sessionId);
1082
1230
  }
1083
1231
  noteProviderTurn(sessionId, "google");
1084
- const parsed = parseInteraction(
1085
- interaction,
1086
- clean,
1087
- Date.now() - startTime,
1088
- { request: rawRequest, response: { status: reply.status, statusText: reply.statusText, headers: reply.headers, body: interaction } }
1089
- );
1232
+ const parsed = parseInteraction(interaction, clean, Date.now() - startTime, {
1233
+ request: rawRequest,
1234
+ response: { status: reply.status, statusText: reply.statusText, headers: reply.headers, body: interaction },
1235
+ });
1090
1236
  checkTurnTokens(sessionId, parsed.usage);
1091
1237
  return parsed;
1092
1238
  };
@@ -1143,6 +1289,8 @@ export class GoogleInteractionsProvider implements Provider {
1143
1289
  const sessionId = options?.sessionId;
1144
1290
  const tools = options?.tools as StandardToolDeclaration[] | undefined;
1145
1291
  const body = await this.buildBody(clean, context, options, sessionId, tools, true);
1292
+ // Size limits are implemented but switched off for now (the owner is unsure about Files API handling yet):
1293
+ // assertGoogleInlineSizes(body.input, clean);
1146
1294
  evictSessionId = sessionId;
1147
1295
  usedChain = body.previous_interaction_id !== undefined;
1148
1296
 
@@ -1165,9 +1313,16 @@ export class GoogleInteractionsProvider implements Provider {
1165
1313
  // (429/5xx/network) are retried before the first byte; a live stream is never retried.
1166
1314
  const open = (target: string, method: "POST" | "GET"): Promise<Response> =>
1167
1315
  openStreamResponse({
1168
- fetchOnce: () => withAbortTimer(linked, streamTimeoutMs, () => (options?.fetch ?? fetch)(target, this.requestInit(method, method === "POST" ? body : undefined, options, apiKey, linked.signal))),
1169
- onHttpError: (r, text) => this.throwIfError(r.status, r.statusText, target, text, clean, headersToRecord(r.headers)),
1170
- noBodyError: () => toConciseProviderError(new Error("Google streaming response had no body"), "google", clean),
1316
+ fetchOnce: () =>
1317
+ withAbortTimer(linked, streamTimeoutMs, () =>
1318
+ (options?.fetch ?? fetch)(
1319
+ target,
1320
+ this.requestInit(method, method === "POST" ? body : undefined, options, apiKey, linked.signal)
1321
+ )
1322
+ ),
1323
+ onHttpError: (r, text) => this.throwIfError(r.status, target, text, clean, headersToRecord(r.headers)),
1324
+ noBodyError: () =>
1325
+ toConciseProviderError(new Error("Google streaming response had no body"), "google", clean),
1171
1326
  retry: retryOptionsFrom(options),
1172
1327
  signal: linked.signal,
1173
1328
  label: { providerId: "google", modelId: clean },
@@ -1177,7 +1332,11 @@ export class GoogleInteractionsProvider implements Provider {
1177
1332
  res.headers.forEach((v, k) => {
1178
1333
  responseHeaders[k] = v;
1179
1334
  });
1180
- const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
1335
+ const responseMeta: Record<string, unknown> & {
1336
+ status: number;
1337
+ statusText: string;
1338
+ headers: Record<string, string>;
1339
+ } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
1181
1340
  eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
1182
1341
 
1183
1342
  let reader: StreamReader | undefined;
@@ -1286,7 +1445,8 @@ export class GoogleInteractionsProvider implements Provider {
1286
1445
  thinking += t;
1287
1446
  eventStream.push({ type: "thinking_delta", thinkingDelta: t, partialThinking: thinking });
1288
1447
  }
1289
- if (!signature && typeof step["signature"] === "string" && step["signature"]) signature = step["signature"];
1448
+ if (!signature && typeof step["signature"] === "string" && step["signature"])
1449
+ signature = step["signature"];
1290
1450
  } else if (step["type"] === "function_call") {
1291
1451
  const args = step["arguments"];
1292
1452
  const startArgs = typeof args === "string" ? args : args ? JSON.stringify(args) : "";
@@ -1343,7 +1503,8 @@ export class GoogleInteractionsProvider implements Provider {
1343
1503
  break;
1344
1504
  }
1345
1505
  if (chunk.done) break;
1346
- for (const m of parser.feed(decoder.decode(chunk.value, { stream: true }))) handleMessage(m.event, m.data);
1506
+ for (const m of parser.feed(decoder.decode(chunk.value, { stream: true })))
1507
+ handleMessage(m.event, m.data);
1347
1508
  }
1348
1509
  for (const m of parser.flush()) handleMessage(m.event, m.data);
1349
1510
  } finally {
@@ -1383,7 +1544,8 @@ export class GoogleInteractionsProvider implements Provider {
1383
1544
  }
1384
1545
 
1385
1546
  if (sessionId) {
1386
- if (responseId && body.store !== false) setChain(sessionId, { interactionId: responseId, model: clean, at: Date.now() });
1547
+ if (responseId && body.store !== false)
1548
+ setChain(sessionId, { interactionId: responseId, model: clean, at: Date.now() });
1387
1549
  else if (body.store === false) {
1388
1550
  interactionChains.delete(sessionId);
1389
1551
  lastTurnTotals.delete(sessionId);
@@ -1402,7 +1564,8 @@ export class GoogleInteractionsProvider implements Provider {
1402
1564
  const cleanThinkingEarly = thinking.replace(/\n{3,}/g, "\n\n").trim();
1403
1565
  const ctEntry = Object.entries(responseHeaders).find(([k]) => k.toLowerCase() === "content-type");
1404
1566
  const isSSEPayload =
1405
- (typeof ctEntry?.[1] === "string" && ((ctEntry[1] as string).includes("event-stream") || (ctEntry[1] as string).startsWith("text/"))) ||
1567
+ (typeof ctEntry?.[1] === "string" &&
1568
+ ((ctEntry[1] as string).includes("event-stream") || (ctEntry[1] as string).startsWith("text/"))) ||
1406
1569
  framesSeen > 0 ||
1407
1570
  badFrames > 0;
1408
1571
  if (!hasTerminal && !text && !cleanThinkingEarly && toolCalls.length === 0 && isSSEPayload) {
@@ -1433,14 +1596,10 @@ export class GoogleInteractionsProvider implements Provider {
1433
1596
  } catch (err: unknown) {
1434
1597
  const raw = err instanceof Error ? err : new Error(String(err));
1435
1598
  const isAbort =
1436
- linked.signal.aborted ||
1437
- eventStream.isCancelled() ||
1438
- (raw as { name?: string }).name === "AbortError";
1599
+ linked.signal.aborted || eventStream.isCancelled() || (raw as { name?: string }).name === "AbortError";
1439
1600
  if (isAbort) {
1440
1601
  cancelBackground();
1441
- eventStream.fail(
1442
- Object.assign(new Error("Stream aborted"), { name: "AbortError" })
1443
- );
1602
+ eventStream.fail(Object.assign(new Error("Stream aborted"), { name: "AbortError" }));
1444
1603
  } else if (evictSessionId && usedChain && isNotFoundChainError(raw)) {
1445
1604
  // Reactive self-heal, fail-clean variant (Rule 2): the `start`
1446
1605
  // event is already delivered, so a transparent in-stream retry