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.
- package/README.md +62 -18
- package/package.json +1 -1
- package/src/agent/agent.ts +192 -695
- package/src/agent/architecture.md +19 -13
- package/src/agent/context.ts +1 -0
- package/src/agent/defaults.ts +88 -0
- package/src/agent/delegation.ts +8 -3
- package/src/agent/loop.ts +242 -226
- package/src/agent/named-sessions.ts +93 -0
- package/src/agent/options.ts +166 -0
- package/src/agent/run-control.ts +248 -0
- package/src/agent/subagents-tool.ts +14 -21
- package/src/agent/usage-limit.ts +92 -0
- package/src/agent/worker.ts +17 -3
- package/src/agent/workers.ts +13 -1
- package/src/architecture.md +1 -0
- package/src/index.ts +11 -11
- package/src/prompts/additional-context.md +2 -0
- package/src/prompts/architecture.md +19 -0
- package/src/prompts/prompts.ts +81 -0
- package/src/prompts/reasoning-directive.md +5 -0
- package/src/prompts/structured-output.md +2 -0
- package/src/prompts/tool-bridge.md +1 -0
- package/src/prompts/tool-interrupted.md +1 -0
- package/src/prompts/tool-not-found.md +1 -0
- package/src/prompts/tool-repeat-blocked.md +1 -0
- package/src/prompts/tool-schema-error.md +1 -0
- package/src/prompts/tool-truncated-arguments.md +1 -0
- package/src/prompts/worker-failed.md +1 -0
- package/src/prompts/worker-replied.md +2 -0
- package/src/providers/architecture.md +8 -8
- package/src/providers/google.ts +83 -13
- package/src/providers/openai-compat.ts +17 -7
- package/src/providers/openai.ts +10 -4
- package/src/providers/openrouter.ts +185 -18
- package/src/providers.ts +59 -9
- package/src/scheduler/architecture.md +7 -7
- package/src/scheduler/scheduler.ts +40 -0
- package/src/scheduler/tool.ts +5 -9
- package/src/session/journal.ts +1 -0
- package/src/session/store.ts +4 -0
- package/src/streaming/event-stream.ts +1 -1
- package/src/tools/architecture.md +7 -6
- package/src/tools/executor.ts +60 -15
- package/src/tools/tool-content.ts +32 -0
- package/src/types/agent.ts +40 -8
- package/src/types/core.ts +4 -0
- package/src/types/message.ts +7 -0
- package/src/types/model.ts +4 -1
- package/src/types/scheduler.ts +4 -4
- package/src/types/tool.ts +2 -0
- package/src/utils/errors.ts +3 -3
- package/src/utils/headers.ts +1 -1
- /package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +0 -0
- /package/src/{agent/subagents.md → prompts/subagents-tool.md} +0 -0
package/src/agent/worker.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|
package/src/agent/workers.ts
CHANGED
|
@@ -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);
|
package/src/architecture.md
CHANGED
|
@@ -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/
|
|
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/
|
|
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,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 @@
|
|
|
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}}
|
|
@@ -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
|
|
package/src/providers/google.ts
CHANGED
|
@@ -29,7 +29,14 @@ import type {
|
|
|
29
29
|
ProviderGenerateResult,
|
|
30
30
|
ProviderRawData,
|
|
31
31
|
} from "../types/model.ts";
|
|
32
|
-
import type {
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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:
|
|
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(
|
|
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.
|