agent-accelerator 0.6.0 → 0.6.6

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 (55) hide show
  1. package/README.md +62 -18
  2. package/package.json +1 -1
  3. package/src/agent/agent.ts +192 -695
  4. package/src/agent/architecture.md +19 -13
  5. package/src/agent/context.ts +1 -0
  6. package/src/agent/defaults.ts +88 -0
  7. package/src/agent/delegation.ts +8 -3
  8. package/src/agent/loop.ts +242 -226
  9. package/src/agent/named-sessions.ts +93 -0
  10. package/src/agent/options.ts +166 -0
  11. package/src/agent/run-control.ts +248 -0
  12. package/src/agent/subagents-tool.ts +14 -21
  13. package/src/agent/usage-limit.ts +92 -0
  14. package/src/agent/worker.ts +17 -3
  15. package/src/agent/workers.ts +13 -1
  16. package/src/architecture.md +1 -0
  17. package/src/index.ts +11 -11
  18. package/src/prompts/additional-context.md +2 -0
  19. package/src/prompts/architecture.md +19 -0
  20. package/src/prompts/prompts.ts +81 -0
  21. package/src/prompts/reasoning-directive.md +5 -0
  22. package/src/prompts/structured-output.md +2 -0
  23. package/src/prompts/tool-bridge.md +1 -0
  24. package/src/prompts/tool-interrupted.md +1 -0
  25. package/src/prompts/tool-not-found.md +1 -0
  26. package/src/prompts/tool-repeat-blocked.md +1 -0
  27. package/src/prompts/tool-schema-error.md +1 -0
  28. package/src/prompts/tool-truncated-arguments.md +1 -0
  29. package/src/prompts/worker-failed.md +1 -0
  30. package/src/prompts/worker-replied.md +2 -0
  31. package/src/providers/architecture.md +8 -8
  32. package/src/providers/google.ts +83 -13
  33. package/src/providers/openai-compat.ts +17 -7
  34. package/src/providers/openai.ts +10 -4
  35. package/src/providers/openrouter.ts +185 -18
  36. package/src/providers.ts +59 -9
  37. package/src/scheduler/architecture.md +7 -7
  38. package/src/scheduler/scheduler.ts +40 -0
  39. package/src/scheduler/tool.ts +5 -9
  40. package/src/session/journal.ts +1 -0
  41. package/src/session/store.ts +4 -0
  42. package/src/streaming/event-stream.ts +1 -1
  43. package/src/tools/architecture.md +7 -6
  44. package/src/tools/executor.ts +60 -15
  45. package/src/tools/tool-content.ts +32 -0
  46. package/src/types/agent.ts +40 -8
  47. package/src/types/core.ts +4 -0
  48. package/src/types/message.ts +7 -0
  49. package/src/types/model.ts +4 -1
  50. package/src/types/scheduler.ts +4 -4
  51. package/src/types/tool.ts +2 -0
  52. package/src/utils/errors.ts +3 -3
  53. package/src/utils/headers.ts +1 -1
  54. /package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +0 -0
  55. /package/src/{agent/subagents.md → prompts/subagents-tool.md} +0 -0
@@ -246,6 +246,14 @@ export function resolveWorkerCredentials(
246
246
  return { apiKeyToUse: undefined, baseUrlToUse: undefined, attemptedProvider };
247
247
  }
248
248
 
249
+ // Configs built by `buildDynamicWorkerConfig`: the Agent constructor checks it so SDK-built workers never take `configure()` or env defaults.
250
+ const workerConfigs = new WeakSet<object>();
251
+
252
+ /** True for a config made by `buildDynamicWorkerConfig` (a chat worker or a scheduled run), not by the developer. */
253
+ export function isWorkerConfig(config: object): boolean {
254
+ return workerConfigs.has(config);
255
+ }
256
+
249
257
  /** How many levels of workers may exist below an agent unless `dynamicSubagents.maxDepth` says otherwise: unlimited. */
250
258
  export const DEFAULT_MAX_DEPTH = Infinity;
251
259
 
@@ -256,7 +264,7 @@ export const DEFAULT_MAX_DEPTH = Infinity;
256
264
  * `persistent` makes a multi-turn worker that keeps its history (saved under `persistent.dir` when given, and
257
265
  * continued by `resume()` after a crash, never automatically). A persistent worker with levels left below it
258
266
  * (`maxDepth` of the parent, minus one for itself) gets its own `subagents` tool; its children can only be granted
259
- * the tools it was granted. One-shot workers never spawn. `dynamicSubagents.workerBudget` becomes each worker's own
267
+ * the tools it was granted. One-shot workers never spawn. `dynamicSubagents.workerBudget` becomes each chat worker's own
260
268
  * per-run `budget` (and is passed to its children).
261
269
  */
262
270
  export function buildDynamicWorkerConfig(
@@ -274,7 +282,7 @@ export function buildDynamicWorkerConfig(
274
282
  const { apiKeyToUse, baseUrlToUse } = resolveWorkerCredentials(parent, model);
275
283
  const dyn = parent.dynamicSubagents;
276
284
  const levelsBelow = (dyn?.maxDepth ?? DEFAULT_MAX_DEPTH) - 1;
277
- return {
285
+ const config: AgentConfig = {
278
286
  name: worker.name,
279
287
  description: worker.role || `Sub-agent ${worker.name}`,
280
288
  instructions: worker.instructions,
@@ -284,7 +292,11 @@ export function buildDynamicWorkerConfig(
284
292
  thinkingLevel: parent.dynamicSubagents?.thinkingLevel ?? parent.thinkingConfig?.level,
285
293
  sessionId: worker.sessionId,
286
294
  tools: worker.tools,
287
- ...(dyn?.workerBudget ? { budget: { ...dyn.workerBudget } } : {}),
295
+ // A chat worker (persistent) is capped by `workerBudget`; a one-shot scheduled run only by `scheduledActionBudget`.
296
+ ...(() => {
297
+ const cap = worker.persistent ? dyn?.workerBudget : dyn?.scheduledActionBudget;
298
+ return cap ? { budget: { ...cap } } : {};
299
+ })(),
288
300
  stateless: !worker.persistent,
289
301
  ...(worker.persistent?.dir ? { persist: { dir: worker.persistent.dir }, resume: "explicit" as const } : {}),
290
302
  ...(worker.persistent && levelsBelow >= 1
@@ -305,6 +317,8 @@ export function buildDynamicWorkerConfig(
305
317
  maxTurns: parent.maxTurns,
306
318
  ...parent.inheritableConfig(),
307
319
  };
320
+ workerConfigs.add(config);
321
+ return config;
308
322
  }
309
323
 
310
324
  // ---------------------------------------------------------------------------
@@ -23,7 +23,7 @@ import {
23
23
  type WorkerRunResult,
24
24
  } from "./delegation.ts";
25
25
  import { finishedTurns, historyUpTo, type Turn } from "./turns.ts";
26
- import { modelName, readUsage, type UsageNode } from "../session/usage.ts";
26
+ import { mergeUsage, modelName, readUsage, type UsageNode } from "../session/usage.ts";
27
27
 
28
28
  /** What `spawn` needs to start a worker. */
29
29
  export interface WorkerSpec {
@@ -157,6 +157,17 @@ function readSavedWorker(dir: string | undefined): {
157
157
  return { model: session?.session.model || "unknown", totals: readUsage(session?.session.usage), workers };
158
158
  }
159
159
 
160
+ /** What a saved session folder and every worker saved below it spent in total (read from disk): the starting point of a chat-wide usage cap after a restart; in: Agent.beginChat, out: usage totals. */
161
+ export function savedSpend(dir: string): ReturnType<typeof readUsage> {
162
+ const total = readUsage(undefined);
163
+ const add = (node: { totals: ReturnType<typeof readUsage>; workers: UsageNode[] }): void => {
164
+ mergeUsage(total, node.totals);
165
+ for (const w of node.workers) add(w);
166
+ };
167
+ add(readSavedWorker(dir));
168
+ return total;
169
+ }
170
+
160
171
  /** The workers of one parent `Agent` (`agent.workers`). */
161
172
  export class WorkerRegistry {
162
173
  private readonly entries = new Map<string, Entry>();
@@ -384,6 +395,7 @@ export class WorkerRegistry {
384
395
  })
385
396
  );
386
397
  (agent as unknown as { parentSessionId?: string }).parentSessionId = this.parent.sessionId;
398
+ agent.chat = this.parent.chat; // the worker's turns count into the chat-wide cap and are checked against it
387
399
  // A cut-off run was already loaded by the constructor; otherwise bring back what the worker said so far.
388
400
  const saved = entry.dir && agent.context.messages.length === 0 ? loadSessionDir(entry.dir)?.session : undefined;
389
401
  if (saved) agent.importSession(saved);
@@ -15,6 +15,7 @@ The whole SDK. `index.ts` is the only public entry (`main`/`module`/`types`); ev
15
15
  | `tools/` | `tool()`, schemas, the executor. |
16
16
  | `streaming/` | The event stream and the SSE parser. |
17
17
  | `session/` | Persistence (snapshot + journal), usage accounting, tool logs. |
18
+ | `prompts/` | The SDK's own prompts as `.md` files, `DEFAULT_PROMPTS`, `AgentConfig.prompts`. |
18
19
  | `scheduler/` | Scheduled actions. |
19
20
  | `types/` | Shared contracts (no logic except small helpers). |
20
21
  | `utils/` | Small shared helpers (durable writes, home, env, media, retry, safe I/O). |
package/src/index.ts CHANGED
@@ -31,6 +31,10 @@ export {
31
31
  } from "./tools/tool.ts";
32
32
  export type { CreateToolOptions } from "./tools/tool.ts";
33
33
  export { zodToJsonSchema, cleanJsonSchema } from "./tools/schema.ts";
34
+ export { toolContent } from "./tools/tool-content.ts";
35
+ export { DEFAULT_PROMPTS } from "./prompts/prompts.ts";
36
+ export type { PromptName, PromptOverrides } from "./prompts/prompts.ts";
37
+ export type { ToolContent, ToolContentPart } from "./tools/tool-content.ts";
34
38
  /**
35
39
  * @deprecated Internal executor surface. Prefer `Agent` runs; for direct
36
40
  * execution import from the future `/internal` entry.
@@ -60,17 +64,12 @@ export {
60
64
  export type { AgentAsToolTarget, SubAgentTrace } from "./agent/delegation.ts";
61
65
  export type { WorkerRegistry, WorkerSpec, WorkerInfo, WorkerReply, WorkerCallOptions } from "./agent/workers.ts";
62
66
 
63
- // Scheduled actions: dynamic sub-agents that run later and save their answer to a file
64
- export {
65
- createScheduledAction,
66
- listScheduledActions,
67
- updateScheduledAction,
68
- runScheduledAction,
69
- skipScheduledAction,
70
- deleteScheduledAction,
71
- } from "./scheduler/scheduler.ts";
67
+ // Scheduled actions: dynamic sub-agents that run later and save their answer to a file (use `agent.scheduled`)
68
+ export type { ScheduledActions } from "./scheduler/scheduler.ts";
72
69
  export { getDatetimeTool } from "./scheduler/tool.ts";
73
70
  export { setHome, resolveHome } from "./utils/home.ts";
71
+ export { configure } from "./agent/defaults.ts";
72
+ export type { AgentDefaults } from "./agent/defaults.ts";
74
73
  export type {
75
74
  ScheduledActionInput,
76
75
  ScheduledActionPatch,
@@ -116,7 +115,7 @@ export {
116
115
  type CatalogStatus,
117
116
  type RefreshCatalogOptions,
118
117
  } from "./models/catalog.ts";
119
- export { resolveEffectiveThinking } from "./agent/agent.ts";
118
+ export { resolveEffectiveThinking } from "./agent/options.ts";
120
119
  export { withRetries, isTransientError } from "./utils/retry.ts";
121
120
  export { toConciseProviderError, assertModalitiesSupported, assertNoVideoPartsOnResponses } from "./utils/errors.ts";
122
121
  // Native provider implementations.
@@ -223,7 +222,7 @@ export {
223
222
  } from "./types/errors.ts";
224
223
  export type { AgentAccelErrorCode, AgentAccelErrorContext } from "./types/errors.ts";
225
224
  // Normalized run limits (sub-agents inherit unless overridden)
226
- export type { AgentRunLimits } from "./agent/agent.ts";
225
+ export type { AgentRunLimits } from "./agent/options.ts";
227
226
 
228
227
  // Telemetry bus: warnings, anomalies and per-turn thinking traces (attach with `telemetryBus.onEvent`)
229
228
  export { telemetryBus } from "./telemetry.ts";
@@ -323,6 +322,7 @@ export type {
323
322
  ProviderContext,
324
323
  ProviderSteps,
325
324
  MediaResolution,
325
+ VideoProcessing,
326
326
  } from "./types/message.ts";
327
327
 
328
328
  export type {
@@ -0,0 +1,2 @@
1
+ [Additional Context]
2
+ {{context}}
@@ -0,0 +1,19 @@
1
+ # src/prompts/
2
+
3
+ The prompts the SDK itself puts in front of the model, as plain `.md` files, so they can be read, copied and replaced.
4
+
5
+ | File | What it is |
6
+ | :-------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `prompts.ts` | `DEFAULT_PROMPTS` (the shipped text, read once at load, final newline removed), `resolvePrompts` (defaults + `AgentConfig.prompts`, validated), `fillPrompt` (`{{name}}` markers). |
8
+ | `reasoning-directive.md`, `structured-output.md`, `additional-context.md`, `tool-bridge.md` | Text added to the system prompt, a user turn or a history gap. |
9
+ | `tool-truncated-arguments.md`, `tool-not-found.md`, `tool-schema-error.md`, `tool-repeat-blocked.md`, `tool-interrupted.md` | The refusals the executor and the loop send back as tool results. |
10
+ | `worker-replied.md`, `worker-failed.md` | The header of a `subagents` answer. |
11
+ | `subagents-tool.md`, `scheduled-actions-tool.md` | The manuals that are the descriptions of the `subagents` and `scheduled_actions` tools. |
12
+
13
+ ## Invariants
14
+
15
+ - One file per prompt; the name in `FILES` (`prompts.ts`) is the public name. Add a prompt by adding a file, a `FILES` entry, its use, a row in `docs/guides/prompts.md` and a pinned-text assertion in `test/prompts.test.ts`.
16
+ - The default text is pinned exactly by a test: reflowing a file (an editor or formatter) changes what the model sees and the prompt cache, so the test must fail.
17
+ - Every user of a prompt reads it from the Agent's resolved prompts (`agent.prompts`, `loop` config `prompts`, `ProviderRequestOptions.prompts`, executor `prompts`), never from `DEFAULT_PROMPTS` directly, except as the default of an optional parameter.
18
+ - Machine-readable parts stay in code: the `repeat_blocked` `errorCode`, the `[Structured Output]` marker line.
19
+ - The system prompt carries no delegation text (cache-stable): tool manuals are tool descriptions.
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The prompts the SDK itself puts in front of the model, kept as `.md` files in this folder so they can be read,
3
+ * copied and replaced. `DEFAULT_PROMPTS` holds the shipped text; `AgentConfig.prompts` overrides single entries per
4
+ * Agent. Keeping the original is recommended: it is what the SDK was tested with, and a good starting point (and the
5
+ * reference) for writing your own. `{{name}}` marks a value the SDK fills in at use time.
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ import { ConfigError } from "../types/errors.ts";
9
+
10
+ /** One entry per prompt file: its public name and the file that holds the default text. */
11
+ const FILES = {
12
+ reasoningDirective: "reasoning-directive.md",
13
+ structuredOutput: "structured-output.md",
14
+ additionalContext: "additional-context.md",
15
+ toolBridge: "tool-bridge.md",
16
+ toolTruncatedArguments: "tool-truncated-arguments.md",
17
+ toolNotFound: "tool-not-found.md",
18
+ toolSchemaError: "tool-schema-error.md",
19
+ toolRepeatBlocked: "tool-repeat-blocked.md",
20
+ toolInterrupted: "tool-interrupted.md",
21
+ workerReplied: "worker-replied.md",
22
+ workerFailed: "worker-failed.md",
23
+ subagentsTool: "subagents-tool.md",
24
+ scheduledActionsTool: "scheduled-actions-tool.md",
25
+ } as const;
26
+
27
+ /** The name of one SDK prompt (see `DEFAULT_PROMPTS`). */
28
+ export type PromptName = keyof typeof FILES;
29
+
30
+ /** The text of every prompt, by name. */
31
+ export type Prompts = Record<PromptName, string>;
32
+
33
+ /** Per-Agent replacements for single prompts (`AgentConfig.prompts`); every other prompt keeps its default. */
34
+ export type PromptOverrides = Partial<Prompts>;
35
+
36
+ // Reads one prompt file without its final newline; in: DEFAULT_PROMPTS at load, out: the default text.
37
+ function readPrompt(file: string): string {
38
+ return readFileSync(new URL(`./${file}`, import.meta.url), "utf8").replace(/\n$/, "");
39
+ }
40
+
41
+ /**
42
+ * The prompts as shipped. Placeholders (`{{name}}`) are filled in by the SDK: `reasoningDirective` none;
43
+ * `structuredOutput` `{{schema}}`; `additionalContext` `{{context}}`; `toolTruncatedArguments`, `toolRepeatBlocked`
44
+ * `{{tool}}`; `toolNotFound` `{{tool}}`, `{{suggestion}}`; `toolSchemaError` `{{tool}}`, `{{detail}}`, `{{keys}}`,
45
+ * `{{expected}}`; `toolInterrupted` `{{tool}}`, `{{id}}`; `workerReplied` `{{id}}`, `{{text}}`; `workerFailed` `{{id}}`,
46
+ * `{{reason}}`; `subagentsTool` `{{TOOLS}}`, `{{LIMIT}}`; `scheduledActionsTool` `{{TOOLS}}`.
47
+ */
48
+ export const DEFAULT_PROMPTS: Readonly<Prompts> = Object.freeze(
49
+ Object.fromEntries(Object.entries(FILES).map(([name, file]) => [name, readPrompt(file)])) as Prompts
50
+ );
51
+
52
+ /** Defaults with `overrides` applied. Throws a `ConfigError` for an unknown name or an empty or non-string text. */
53
+ export function resolvePrompts(overrides?: PromptOverrides): Prompts {
54
+ const out: Prompts = { ...DEFAULT_PROMPTS };
55
+ if (overrides === undefined) return out;
56
+ if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) {
57
+ throw new ConfigError(
58
+ '[Agent Accelerator] Invalid prompts: expected an object like { reasoningDirective: "..." } (see DEFAULT_PROMPTS for the names).'
59
+ );
60
+ }
61
+ for (const [name, text] of Object.entries(overrides)) {
62
+ if (text === undefined) continue;
63
+ if (!Object.hasOwn(FILES, name)) {
64
+ throw new ConfigError(
65
+ `[Agent Accelerator] Unknown prompt "${name}". Valid names: ${Object.keys(FILES).join(", ")}.`
66
+ );
67
+ }
68
+ if (typeof text !== "string" || !text.trim()) {
69
+ throw new ConfigError(`[Agent Accelerator] Invalid prompt "${name}": expected a non-empty string.`);
70
+ }
71
+ out[name as PromptName] = text;
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /** Fills `{{name}}` markers in a prompt from `values`; a marker with no value is left as written. */
77
+ export function fillPrompt(template: string, values: Record<string, string | number>): string {
78
+ return template.replace(/\{\{(\w+)\}\}/g, (whole, key: string) =>
79
+ Object.hasOwn(values, key) ? String(values[key]) : whole
80
+ );
81
+ }
@@ -0,0 +1,5 @@
1
+ [Reasoning Directive]
2
+
3
+ 1. Use internal reasoning strictly for private planning and step-by-step thinking.
4
+ 2. Never attempt to execute tools or output final user deliverables inside reasoning.
5
+ 3. Once your reasoning is complete, output your final response or function calls directly in the standard response output.
@@ -0,0 +1,2 @@
1
+ Return ONLY valid JSON matching this JSON Schema, with no markdown fences, no commentary, no extra text.
2
+ Schema: {{schema}}
@@ -0,0 +1 @@
1
+ I have processed the tool results.
@@ -0,0 +1 @@
1
+ Error: tool_call '{{tool}}' ({{id}}) has no result because the run was interrupted before it completed (recovered by ensureToolPairing). It may have partially executed: check the tool trace before repeating side-effecting work.
@@ -0,0 +1 @@
1
+ Error: Tool '{{tool}}' not found.{{suggestion}}
@@ -0,0 +1 @@
1
+ repeat_blocked: Tool '{{tool}}' was called again with identical arguments. The previous result is already available; reuse it or call the tool with different arguments.
@@ -0,0 +1 @@
1
+ Schema validation error for tool '{{tool}}': {{detail}} Received argument keys: [{{keys}}].{{expected}} Fix the arguments and call the tool again with corrected JSON. Do not explain the error, just retry the call.
@@ -0,0 +1 @@
1
+ Error: Tool '{{tool}}' arguments are truncated or incomplete and could not be parsed. Call the tool again with the complete JSON arguments. Do not explain the error, just retry the call.
@@ -0,0 +1 @@
1
+ [worker {{id}}] failed: {{reason}}
@@ -0,0 +1,2 @@
1
+ [worker {{id}}] replied:
2
+ {{text}}
@@ -2,14 +2,14 @@
2
2
 
3
3
  All HTTP and wire parsing. Canonical mappers (thinking, cache, tool choice, structured output) live in `../providers.ts`, never here.
4
4
 
5
- | File | Wire | Notes |
6
- | :----------------- | :--------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
7
- | `google.ts` | Google Interactions | Stateful chaining (TTL gate, self-heal on `not_found`), exact replay of raw `steps`, media consts at the top, Gemini 4 long decode as background interactions. |
8
- | `openai.ts` | OpenAI Responses | Stateless (`store:false`), full history every turn. |
9
- | `openrouter.ts` | OpenRouter Chat Completions | Every request streams (so it can be cancelled); `generate()` collects its own stream. |
10
- | `openai-compat.ts` | OpenAI-compatible Chat Completions | Serves every custom prefix; strict subset of fields; no default base URL. |
11
- | `registry.ts` | - | Routes `provider/model` to an adapter; the registry is immutable; `ModelProvider` helpers. |
12
- | `shared.ts` | - | Helpers every adapter uses: signals and timers, error payloads, ids, retry-after, SSE scaffold (`linkStreamAbort`, `withAbortTimer`, `readSseData`, `incompleteStreamCheck`, `failStream`). |
5
+ | File | Wire | Notes |
6
+ | :----------------- | :--------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `google.ts` | Google Interactions | Stateful chaining (TTL gate, self-heal on `not_found`), exact replay of raw `steps`, media consts at the top, Gemini 4 long decode as background interactions. The inline size checks (`assertGoogleInlineSizes`) exist but are not called yet. A video part's `processing` (`static`/`agentic`) is sent as the block's `processing` field. |
8
+ | `openai.ts` | OpenAI Responses | Stateless (`store:false`), full history every turn. |
9
+ | `openrouter.ts` | OpenRouter Chat Completions | Every request streams (so it can be cancelled); `generate()` collects its own stream. A turn's `reasoning_details` blocks are kept as `providerSteps` and sent back verbatim on the next request; parsed-PDF `file` annotations travel the same way, so a PDF is not parsed twice. Routing, `parallel_tool_calls`, `reasoning.exclude` and `user` come from `OPENROUTER_*` env vars (see the file header), never from AgentConfig. |
10
+ | `openai-compat.ts` | OpenAI-compatible Chat Completions | Serves every custom prefix; strict subset of fields; no default base URL. |
11
+ | `registry.ts` | - | Routes `provider/model` to an adapter; the registry is immutable; `ModelProvider` helpers. |
12
+ | `shared.ts` | - | Helpers every adapter uses: signals and timers, error payloads, ids, retry-after, SSE scaffold (`linkStreamAbort`, `withAbortTimer`, `readSseData`, `incompleteStreamCheck`, `failStream`). |
13
13
 
14
14
  ## Contract (`Provider`, in `../types/model.ts`)
15
15
 
@@ -29,7 +29,14 @@ import type {
29
29
  ProviderGenerateResult,
30
30
  ProviderRawData,
31
31
  } from "../types/model.ts";
32
- import type { ProviderContext, 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";
@@ -156,7 +163,8 @@ export const GOOGLE_INLINE_MIME_ALIASES: Readonly<Record<string, string>> = {
156
163
  "audio/wave": "audio/wav",
157
164
  "image/jpg": "image/jpeg",
158
165
  };
159
- // 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.
160
168
  /** Max inline payload per request (bytes). */
161
169
  export const GOOGLE_MAX_INLINE_REQUEST_BYTES = 100 * 1024 * 1024;
162
170
  /** Max inline PDF size (bytes). */
@@ -217,7 +225,8 @@ const INLINE_MIME: Record<MediaBlockType, readonly string[]> = {
217
225
  document: GOOGLE_DOCUMENT_MIME,
218
226
  };
219
227
 
220
- 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]+/;
221
230
  const YOUTUBE_URL = /^https:\/\/(www\.|m\.)?(youtube\.com|youtu\.be)\//i;
222
231
 
223
232
  // ---------------------------------------------------------------------------
@@ -230,6 +239,7 @@ type MediaBlock = {
230
239
  data?: string;
231
240
  uri?: string;
232
241
  resolution?: MediaResolution;
242
+ processing?: VideoProcessing;
233
243
  };
234
244
  type InteractionContentBlock = { type: "text"; text: string } | MediaBlock;
235
245
 
@@ -499,7 +509,7 @@ function uriBlock(type: MediaBlockType, value: string, mimeType?: string): Media
499
509
  const withMime = (mime?: string): MediaBlock => ({ type, uri: value, ...(mime ? { mime_type: mime } : {}) });
500
510
  if (type === "video" && YOUTUBE_URL.test(value)) return withMime(explicit);
501
511
  const mime = explicit ?? bareMime(inferMimeType(value, ""));
502
- if (value.startsWith(FILES_API_PREFIX) || value.startsWith("gs://")) return withMime(mime);
512
+ if (FILES_API_URL.test(value) || value.startsWith("gs://")) return withMime(mime);
503
513
  return value.startsWith("https://") && mime && (GOOGLE_URL_MIME as readonly string[]).includes(mime)
504
514
  ? withMime(mime)
505
515
  : undefined;
@@ -535,11 +545,63 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<InteractionCo
535
545
  const block = uri ?? (await inlineBlock(type, value, part.mimeType));
536
546
  // Per-item detail level (image and video blocks only, api-ref `MediaResolution`).
537
547
  const resolution = part.type === "image" || part.type === "video" ? part.resolution : undefined;
538
- 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 } : {}) });
539
555
  }
540
556
  return blocks;
541
557
  }
542
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
+
543
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.
544
606
  function replaySteps(steps: unknown[] | undefined): ProviderSteps | undefined {
545
607
  const kept = (steps ?? [])
@@ -575,13 +637,14 @@ function resultToText(result: unknown): string {
575
637
  return safeStringify(result);
576
638
  }
577
639
 
578
- // 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`.
579
- 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) : [];
580
643
  return {
581
644
  type: "function_result",
582
645
  name: part.name,
583
646
  call_id: part.id,
584
- result: [{ type: "text", text: resultToText(part.result) }],
647
+ result: [{ type: "text", text: resultToText(part.result) }, ...media],
585
648
  ...(part.isError ? { is_error: true } : {}),
586
649
  };
587
650
  }
@@ -625,7 +688,7 @@ async function latestTurnInput(
625
688
  if (!Array.isArray(m.content)) continue;
626
689
  for (const part of m.content) {
627
690
  if (part.type === "tool_result") {
628
- steps.push(functionResultStep(part));
691
+ steps.push(await functionResultStep(part));
629
692
  }
630
693
  }
631
694
  } else if (m.role === "user") {
@@ -715,7 +778,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
715
778
  if (!Array.isArray(m.content)) continue;
716
779
  for (const part of m.content) {
717
780
  if (part.type === "tool_result") {
718
- steps.push(functionResultStep(part));
781
+ steps.push(await functionResultStep(part));
719
782
  }
720
783
  }
721
784
  }
@@ -901,7 +964,11 @@ export class GoogleInteractionsProvider implements Provider {
901
964
  let effectiveSystemPrompt = context.systemPrompt;
902
965
  if (outputSpec && !modelSupportsStructuredOutput("google", modelId)) {
903
966
  outputEmulated = true;
904
- effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(context.systemPrompt, outputSpec);
967
+ effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(
968
+ context.systemPrompt,
969
+ outputSpec,
970
+ options?.prompts?.structuredOutput
971
+ );
905
972
  emitProviderWarning({
906
973
  provider: "google",
907
974
  capability: "structured output",
@@ -1108,6 +1175,8 @@ export class GoogleInteractionsProvider implements Provider {
1108
1175
  const sessionId = options?.sessionId;
1109
1176
  const tools = options?.tools as StandardToolDeclaration[] | undefined;
1110
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);
1111
1180
 
1112
1181
  const makeRawRequest = (b: InteractionRequestBody): ProviderRawData["request"] => ({
1113
1182
  url,
@@ -1220,6 +1289,8 @@ export class GoogleInteractionsProvider implements Provider {
1220
1289
  const sessionId = options?.sessionId;
1221
1290
  const tools = options?.tools as StandardToolDeclaration[] | undefined;
1222
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);
1223
1294
  evictSessionId = sessionId;
1224
1295
  usedChain = body.previous_interaction_id !== undefined;
1225
1296
 
@@ -1488,8 +1559,7 @@ export class GoogleInteractionsProvider implements Provider {
1488
1559
  // (JSON mocks) where no SSE frames were ever seen.
1489
1560
  const hasTerminal = completedBody !== undefined || sawFinishReason || sawUsage;
1490
1561
  // Trim provider trailing blank lines from finalized thinking (live
1491
- // deltas already emitted raw; display collapse happens in the
1492
- // wrapThinking presentation layer). Stateful bodies unaffected.
1562
+ // deltas already emitted raw). Stateful bodies unaffected.
1493
1563
  const cleanThinkingEarly = thinking.replace(/\n{3,}/g, "\n\n").trim();
1494
1564
  const ctEntry = Object.entries(responseHeaders).find(([k]) => k.toLowerCase() === "content-type");
1495
1565
  const isSSEPayload =
@@ -60,6 +60,7 @@ import { safeStringify } from "../utils/serialization.ts";
60
60
  import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
61
61
  import { withRetries } from "../utils/retry.ts";
62
62
  import { ConfigError, ValidationError } from "../types/errors.ts";
63
+ import { DEFAULT_PROMPTS } from "../prompts/prompts.ts";
63
64
  import {
64
65
  parseArguments,
65
66
  normalizeThinkingParts,
@@ -96,6 +97,8 @@ import {
96
97
  normalizeStructuredOutput,
97
98
  augmentSystemPromptWithStructuredOutput,
98
99
  warnIgnoredMediaResolution,
100
+ warnIgnoredVideoProcessing,
101
+ warnIgnoredToolMedia,
99
102
  } from "../providers.ts";
100
103
 
101
104
  // ---------------------------------------------------------------------------
@@ -402,7 +405,10 @@ function resultToOutput(result: unknown): string {
402
405
  * `google` (or untagged legacy). Signatures minted by other providers never
403
406
  * leak onto this transport.
404
407
  */
405
- async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessage[]> {
408
+ async function fullHistoryMessages(
409
+ context: ProviderContext,
410
+ bridge = DEFAULT_PROMPTS.toolBridge
411
+ ): Promise<ChatMessage[]> {
406
412
  const pairing = new Map<string, string>();
407
413
  for (const m of context.messages) {
408
414
  if (m.role === "assistant" && Array.isArray(m.content)) {
@@ -423,7 +429,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
423
429
  if (m.role === "system") continue;
424
430
  if (m.role === "user") {
425
431
  if (lastRole === "tool") {
426
- messages.push({ role: "assistant", content: "I have processed the tool results." });
432
+ messages.push({ role: "assistant", content: bridge });
427
433
  }
428
434
  if (typeof m.content === "string") {
429
435
  if (m.content) {
@@ -725,6 +731,8 @@ export class OpenAICompatibleChatProvider implements Provider {
725
731
  // Strict endpoints reject unknown body fields: implicit caching only.
726
732
  resolveCache(this.prefix, options?.cache, ["implicit"], `${this.prefix}/${modelId}`);
727
733
  warnIgnoredMediaResolution(this.prefix, context.messages, `${this.prefix}/${modelId}`);
734
+ warnIgnoredVideoProcessing(this.prefix, context.messages, `${this.prefix}/${modelId}`);
735
+ warnIgnoredToolMedia(this.prefix, context.messages, `${this.prefix}/${modelId}`);
728
736
 
729
737
  // Structured outputs: native `response_format` when supported, else
730
738
  // emulated via system prompt (strict endpoints would 400 on json_schema
@@ -741,7 +749,11 @@ export class OpenAICompatibleChatProvider implements Provider {
741
749
  outputEmulated = true;
742
750
  effectiveContext = {
743
751
  ...context,
744
- systemPrompt: augmentSystemPromptWithStructuredOutput(context.systemPrompt, outputSpec),
752
+ systemPrompt: augmentSystemPromptWithStructuredOutput(
753
+ context.systemPrompt,
754
+ outputSpec,
755
+ options?.prompts?.structuredOutput
756
+ ),
745
757
  };
746
758
  emitProviderWarning({
747
759
  provider: this.prefix,
@@ -752,7 +764,7 @@ export class OpenAICompatibleChatProvider implements Provider {
752
764
  fallback: "emulated JSON instruction + client validation (no response_format is sent)",
753
765
  });
754
766
  }
755
- const messages = await fullHistoryMessages(effectiveContext);
767
+ const messages = await fullHistoryMessages(effectiveContext, options?.prompts?.toolBridge);
756
768
  const body: ChatRequestBody = {
757
769
  model: modelId,
758
770
  messages,
@@ -762,9 +774,7 @@ export class OpenAICompatibleChatProvider implements Provider {
762
774
  if (compatTools) body.tools = compatTools;
763
775
  // Standard Chat Completions generation fields, only when the caller set them.
764
776
  Object.assign(body, mapGenerationParamsToChat(options));
765
- const toolChoice = mapToolChoiceToOpenAI(
766
- options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
767
- );
777
+ const toolChoice = mapToolChoiceToOpenAI(options?.toolChoice, this.prefix);
768
778
  if (toolChoice !== undefined) {
769
779
  // Chat Completions nests pinned names under `function`; the canonical
770
780
  // mapper emits the flat Responses shape, so normalize here.