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/providers/openai.ts
CHANGED
|
@@ -91,6 +91,8 @@ import {
|
|
|
91
91
|
augmentSystemPromptWithStructuredOutput,
|
|
92
92
|
emitProviderWarning,
|
|
93
93
|
warnIgnoredMediaResolution,
|
|
94
|
+
warnIgnoredVideoProcessing,
|
|
95
|
+
warnIgnoredToolMedia,
|
|
94
96
|
} from "../providers.ts";
|
|
95
97
|
|
|
96
98
|
// ---------------------------------------------------------------------------
|
|
@@ -551,6 +553,8 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
551
553
|
// `12h` maps to `prompt_cache_retention: "24h"`; everything else stays on implicit prompt_cache_key affinity.
|
|
552
554
|
const cache = resolveCache("openai", options?.cache, ["implicit", "12h"], `openai/${modelId}`);
|
|
553
555
|
warnIgnoredMediaResolution("openai", context.messages, `openai/${modelId}`);
|
|
556
|
+
warnIgnoredVideoProcessing("openai", context.messages, `openai/${modelId}`);
|
|
557
|
+
warnIgnoredToolMedia("openai", context.messages, `openai/${modelId}`);
|
|
554
558
|
|
|
555
559
|
// Structured outputs: native `text.format` when supported, else emulated
|
|
556
560
|
// via instructions (catalog `structured_output: false` would 400).
|
|
@@ -564,7 +568,11 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
564
568
|
}
|
|
565
569
|
if (outputSpec && !modelSupportsStructuredOutput("openai", modelId)) {
|
|
566
570
|
outputEmulated = true;
|
|
567
|
-
effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(
|
|
571
|
+
effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(
|
|
572
|
+
context.systemPrompt,
|
|
573
|
+
outputSpec,
|
|
574
|
+
options?.prompts?.structuredOutput
|
|
575
|
+
);
|
|
568
576
|
emitProviderWarning({
|
|
569
577
|
provider: "openai",
|
|
570
578
|
capability: "structured output",
|
|
@@ -586,9 +594,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
586
594
|
if (effectiveSystemPrompt) body.instructions = effectiveSystemPrompt;
|
|
587
595
|
const aiTools = toOpenAITools(tools);
|
|
588
596
|
if (aiTools) body.tools = aiTools;
|
|
589
|
-
const toolChoice = mapToolChoiceToOpenAI(
|
|
590
|
-
options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
591
|
-
);
|
|
597
|
+
const toolChoice = mapToolChoiceToOpenAI(options?.toolChoice);
|
|
592
598
|
if (toolChoice !== undefined) body.tool_choice = toolChoice;
|
|
593
599
|
if (effort) body.reasoning = { effort };
|
|
594
600
|
if (serviceTier) body.service_tier = serviceTier;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* never in the canonical layer (`src/providers.ts`).
|
|
7
7
|
*
|
|
8
8
|
* Wire contract: `references/testings/openrouter/CONTRACT.md` (docs + live
|
|
9
|
-
* captures
|
|
9
|
+
* captures, 2026-09-24). Capability matrix:
|
|
10
10
|
* `references/testings/openrouter/CAPABILITY-MATRIX.md`.
|
|
11
11
|
*
|
|
12
12
|
* This is the STABLE transport. The beta Responses skin
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
* - IDs are provider-generated (`gen-…` generations, `call_…`/uuid tool ids)
|
|
25
25
|
* and echoed verbatim (`tool_call_id`). The `call_${random}` fallback in
|
|
26
26
|
* parsers is a local canonical correlation id only; it is never sent.
|
|
27
|
-
* - Prior-turn reasoning is
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* - Prior-turn reasoning is resent as `reasoning_details` exactly as returned (kept on the assistant message as
|
|
28
|
+
* `providerSteps`), which tool turns need (reasoning-tokens.md, "Preserving Reasoning"). Plain `reasoning` text alone is
|
|
29
|
+
* never resent. Parsed-PDF `file` annotations ride the same way and go back as `annotations`, so the router does not parse
|
|
30
|
+
* the same PDF again (pdf-inputs.md, "Skip Parsing Costs").
|
|
30
31
|
*/
|
|
31
32
|
import type {
|
|
32
33
|
Provider,
|
|
@@ -47,6 +48,7 @@ import { normalizeMediaInput } from "../utils/media.ts";
|
|
|
47
48
|
import { safeStringify } from "../utils/serialization.ts";
|
|
48
49
|
import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
|
|
49
50
|
import { ConfigError, isAbortError } from "../types/errors.ts";
|
|
51
|
+
import { DEFAULT_PROMPTS } from "../prompts/prompts.ts";
|
|
50
52
|
import {
|
|
51
53
|
newToolCallId,
|
|
52
54
|
redactedHeaders,
|
|
@@ -84,6 +86,8 @@ import {
|
|
|
84
86
|
augmentSystemPromptWithStructuredOutput,
|
|
85
87
|
emitProviderWarning,
|
|
86
88
|
warnIgnoredMediaResolution,
|
|
89
|
+
warnIgnoredVideoProcessing,
|
|
90
|
+
warnIgnoredToolMedia,
|
|
87
91
|
} from "../providers.ts";
|
|
88
92
|
|
|
89
93
|
// ---------------------------------------------------------------------------
|
|
@@ -108,6 +112,8 @@ type ChatMessage =
|
|
|
108
112
|
type: "function";
|
|
109
113
|
function: { name: string; arguments: string };
|
|
110
114
|
}>;
|
|
115
|
+
reasoning_details?: unknown[];
|
|
116
|
+
annotations?: unknown[];
|
|
111
117
|
name?: string;
|
|
112
118
|
}
|
|
113
119
|
| { role: "tool"; content: string; tool_call_id: string; name?: string }
|
|
@@ -118,7 +124,10 @@ interface ChatRequestBody {
|
|
|
118
124
|
messages: ChatMessage[];
|
|
119
125
|
tools?: Array<Record<string, unknown>>;
|
|
120
126
|
tool_choice?: string | { type: "function"; function: { name: string } };
|
|
121
|
-
reasoning?: { effort
|
|
127
|
+
reasoning?: { effort?: string; exclude?: boolean };
|
|
128
|
+
provider?: Record<string, unknown>;
|
|
129
|
+
parallel_tool_calls?: boolean;
|
|
130
|
+
user?: string;
|
|
122
131
|
service_tier?: string;
|
|
123
132
|
session_id?: string;
|
|
124
133
|
cache_control?: { type: "ephemeral"; ttl: "5m" | "1h" };
|
|
@@ -135,8 +144,13 @@ interface ChatUsage {
|
|
|
135
144
|
prompt_tokens?: number;
|
|
136
145
|
completion_tokens?: number;
|
|
137
146
|
total_tokens?: number;
|
|
138
|
-
prompt_tokens_details?: {
|
|
139
|
-
|
|
147
|
+
prompt_tokens_details?: {
|
|
148
|
+
cached_tokens?: number;
|
|
149
|
+
cache_write_tokens?: number;
|
|
150
|
+
audio_tokens?: number;
|
|
151
|
+
video_tokens?: number;
|
|
152
|
+
};
|
|
153
|
+
completion_tokens_details?: { reasoning_tokens?: number; audio_tokens?: number; image_tokens?: number };
|
|
140
154
|
cost?: number;
|
|
141
155
|
[k: string]: unknown;
|
|
142
156
|
}
|
|
@@ -188,6 +202,71 @@ function midStreamStatus(code: unknown, errorType: unknown): number | undefined
|
|
|
188
202
|
return typeof errorType === "string" ? RETRYABLE_ERROR_TYPE_STATUS[errorType] : undefined;
|
|
189
203
|
}
|
|
190
204
|
|
|
205
|
+
// OpenRouter options that come from env vars, never from AgentConfig (provider routing, `parallel_tool_calls`, `reasoning.exclude`, `user`). A per-Agent `env` wins over the process env; nothing is sent when no variable is set; a bad value is a ConfigError before any network call. NOT supported on purpose (the owner does not want them for now): `reasoning.max_tokens` and `trace`; in: buildBody, out: the extra body fields.
|
|
206
|
+
const ROUTING_LISTS = { OPENROUTER_ORDER: "order", OPENROUTER_ONLY: "only", OPENROUTER_IGNORE: "ignore" } as const;
|
|
207
|
+
const ROUTING_FLAGS = {
|
|
208
|
+
OPENROUTER_ALLOW_FALLBACKS: "allow_fallbacks",
|
|
209
|
+
OPENROUTER_REQUIRE_PARAMETERS: "require_parameters",
|
|
210
|
+
OPENROUTER_ZDR: "zdr",
|
|
211
|
+
} as const;
|
|
212
|
+
function openRouterEnvOptions(options?: ProviderRequestOptions): {
|
|
213
|
+
provider?: Record<string, unknown>;
|
|
214
|
+
parallel_tool_calls?: boolean;
|
|
215
|
+
reasoning_exclude?: boolean;
|
|
216
|
+
user?: string;
|
|
217
|
+
} {
|
|
218
|
+
const read = (name: string): string | undefined => {
|
|
219
|
+
const own = options?.env?.[name]?.trim();
|
|
220
|
+
return own ? own : getEnv(name);
|
|
221
|
+
};
|
|
222
|
+
const bad = (name: string, value: string, expected: string): never => {
|
|
223
|
+
throw new ConfigError(`[Agent Accelerator] Invalid ${name} ${JSON.stringify(value)}: expected ${expected}.`);
|
|
224
|
+
};
|
|
225
|
+
const flag = (name: string): boolean | undefined => {
|
|
226
|
+
const v = read(name);
|
|
227
|
+
if (v === undefined) return undefined;
|
|
228
|
+
const t = v.toLowerCase();
|
|
229
|
+
if (t === "true" || t === "1") return true;
|
|
230
|
+
if (t === "false" || t === "0") return false;
|
|
231
|
+
return bad(name, v, "true or false");
|
|
232
|
+
};
|
|
233
|
+
const provider: Record<string, unknown> = {};
|
|
234
|
+
for (const [name, field] of Object.entries(ROUTING_LISTS)) {
|
|
235
|
+
const v = read(name);
|
|
236
|
+
if (v === undefined) continue;
|
|
237
|
+
const list = v
|
|
238
|
+
.split(",")
|
|
239
|
+
.map((x) => x.trim())
|
|
240
|
+
.filter(Boolean);
|
|
241
|
+
if (list.length === 0) bad(name, v, "a comma-separated list of provider slugs");
|
|
242
|
+
provider[field] = list;
|
|
243
|
+
}
|
|
244
|
+
for (const [name, field] of Object.entries(ROUTING_FLAGS)) {
|
|
245
|
+
const v = flag(name);
|
|
246
|
+
if (v !== undefined) provider[field] = v;
|
|
247
|
+
}
|
|
248
|
+
const collection = read("OPENROUTER_DATA_COLLECTION");
|
|
249
|
+
if (collection !== undefined) {
|
|
250
|
+
if (collection !== "allow" && collection !== "deny") bad("OPENROUTER_DATA_COLLECTION", collection, "allow or deny");
|
|
251
|
+
provider["data_collection"] = collection;
|
|
252
|
+
}
|
|
253
|
+
const sort = read("OPENROUTER_SORT");
|
|
254
|
+
if (sort !== undefined) {
|
|
255
|
+
if (sort !== "price" && sort !== "throughput" && sort !== "latency")
|
|
256
|
+
bad("OPENROUTER_SORT", sort, "price, throughput or latency");
|
|
257
|
+
provider["sort"] = sort;
|
|
258
|
+
}
|
|
259
|
+
const parallel = flag("OPENROUTER_PARALLEL_TOOL_CALLS");
|
|
260
|
+
const exclude = flag("OPENROUTER_REASONING_EXCLUDE");
|
|
261
|
+
const user = read("OPENROUTER_USER");
|
|
262
|
+
return {
|
|
263
|
+
...(Object.keys(provider).length > 0 ? { provider } : {}),
|
|
264
|
+
...(parallel !== undefined ? { parallel_tool_calls: parallel } : {}),
|
|
265
|
+
...(exclude !== undefined ? { reasoning_exclude: exclude } : {}),
|
|
266
|
+
...(user ? { user } : {}),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
191
270
|
// Canonical tool declarations as Chat Completions function tools; in: options.tools, out: request `tools`.
|
|
192
271
|
function toOpenRouterTools(tools?: StandardToolDeclaration[]): Array<Record<string, unknown>> | undefined {
|
|
193
272
|
if (!tools || tools.length === 0) return undefined;
|
|
@@ -311,6 +390,48 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
|
|
|
311
390
|
return blocks;
|
|
312
391
|
}
|
|
313
392
|
|
|
393
|
+
// Folds one streamed `reasoning_details` chunk into the turn's blocks: chunks with the same index (else id) are one block whose text, summary, data and signature are concatenated or filled in, in order; in: the stream handler, out: the blocks replayed on the next request.
|
|
394
|
+
function mergeReasoningDetails(blocks: Array<Record<string, unknown>>, chunk: unknown): void {
|
|
395
|
+
if (!chunk || typeof chunk !== "object" || Array.isArray(chunk)) return;
|
|
396
|
+
const c = chunk as Record<string, unknown>;
|
|
397
|
+
const same = blocks.find((b) =>
|
|
398
|
+
typeof c["index"] === "number" ? b["index"] === c["index"] : c["id"] != null && b["id"] === c["id"]
|
|
399
|
+
);
|
|
400
|
+
if (!same) {
|
|
401
|
+
blocks.push({ ...c });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
for (const [k, v] of Object.entries(c)) {
|
|
405
|
+
if (v === null || v === undefined) continue;
|
|
406
|
+
if ((k === "text" || k === "summary" || k === "data") && typeof v === "string" && typeof same[k] === "string")
|
|
407
|
+
same[k] = (same[k] as string) + v;
|
|
408
|
+
else same[k] = v;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Adds a streamed `file` annotation (a parsed PDF) once per `file.hash`; in: the stream handler, out: the annotations replayed on the next request.
|
|
413
|
+
function mergeAnnotation(blocks: Array<Record<string, unknown>>, chunk: unknown): void {
|
|
414
|
+
if (!chunk || typeof chunk !== "object" || (chunk as { type?: unknown }).type !== "file") return;
|
|
415
|
+
const hash = (chunk as { file?: { hash?: unknown } }).file?.hash;
|
|
416
|
+
if (typeof hash === "string" && blocks.some((b) => (b as { file?: { hash?: unknown } }).file?.hash === hash)) return;
|
|
417
|
+
blocks.push(chunk as Record<string, unknown>);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// The reasoning blocks and the parsed-PDF annotations of an assistant turn as OpenRouter returned them (both live in `providerSteps`, told apart by `type: "file"`); in: a stored assistant message, out: fullHistoryMessages.
|
|
421
|
+
function storedReplay(m: { providerSteps?: { provider: string; steps: unknown[] } }): {
|
|
422
|
+
reasoning_details?: unknown[];
|
|
423
|
+
annotations?: unknown[];
|
|
424
|
+
} {
|
|
425
|
+
if (m.providerSteps?.provider !== "openrouter") return {};
|
|
426
|
+
const isFile = (x: unknown): boolean => (x as { type?: unknown } | null)?.type === "file";
|
|
427
|
+
const annotations = m.providerSteps.steps.filter(isFile);
|
|
428
|
+
const details = m.providerSteps.steps.filter((x) => !isFile(x));
|
|
429
|
+
return {
|
|
430
|
+
...(details.length > 0 ? { reasoning_details: details } : {}),
|
|
431
|
+
...(annotations.length > 0 ? { annotations } : {}),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
314
435
|
// A tool result as the text of a tool message; in: fullHistoryMessages, out: a string.
|
|
315
436
|
function resultToOutput(result: unknown): string {
|
|
316
437
|
if (typeof result === "string") return result;
|
|
@@ -323,7 +444,10 @@ function resultToOutput(result: unknown): string {
|
|
|
323
444
|
* carries everything. Assistant items carry no provider `id`/`status`
|
|
324
445
|
* requirements (unlike the discontinued Responses skin).
|
|
325
446
|
*/
|
|
326
|
-
async function fullHistoryMessages(
|
|
447
|
+
async function fullHistoryMessages(
|
|
448
|
+
context: ProviderContext,
|
|
449
|
+
bridge = DEFAULT_PROMPTS.toolBridge
|
|
450
|
+
): Promise<ChatMessage[]> {
|
|
327
451
|
// Map assistant tool_call ids so tool results pair even though the
|
|
328
452
|
// executor keys results by the call id.
|
|
329
453
|
const pairing = new Map<string, string>();
|
|
@@ -354,7 +478,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
|
|
|
354
478
|
// Strict routes reject `user` directly after `tool`: bridge with a
|
|
355
479
|
// synthetic assistant turn (pi api/openai-completions.ts:1233).
|
|
356
480
|
if (lastRole === "tool") {
|
|
357
|
-
messages.push({ role: "assistant", content:
|
|
481
|
+
messages.push({ role: "assistant", content: bridge });
|
|
358
482
|
}
|
|
359
483
|
if (typeof m.content === "string") {
|
|
360
484
|
if (m.content) {
|
|
@@ -369,9 +493,10 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
|
|
|
369
493
|
}
|
|
370
494
|
}
|
|
371
495
|
} else if (m.role === "assistant") {
|
|
496
|
+
const replay = storedReplay(m);
|
|
372
497
|
if (typeof m.content === "string") {
|
|
373
498
|
if (m.content) {
|
|
374
|
-
messages.push({ role: "assistant", content: m.content });
|
|
499
|
+
messages.push({ role: "assistant", content: m.content, ...replay });
|
|
375
500
|
}
|
|
376
501
|
continue;
|
|
377
502
|
}
|
|
@@ -387,8 +512,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
|
|
|
387
512
|
} else if (part.type === "text" && part.text) {
|
|
388
513
|
texts.push(part.text);
|
|
389
514
|
}
|
|
390
|
-
//
|
|
391
|
-
// tool calls only (documented decision; avoids strict-route 400s).
|
|
515
|
+
// Reasoning is not rebuilt from text: the stored `reasoning_details` blocks are replayed verbatim below.
|
|
392
516
|
}
|
|
393
517
|
// Skip empty assistant turns (no text + no tool calls): strict
|
|
394
518
|
// routes reject them, and they carry no signal (pi drop rule).
|
|
@@ -397,6 +521,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
|
|
|
397
521
|
role: "assistant",
|
|
398
522
|
content: texts.length > 0 ? texts.join("\n") : null,
|
|
399
523
|
...(calls.length > 0 ? { tool_calls: calls } : {}),
|
|
524
|
+
...replay,
|
|
400
525
|
});
|
|
401
526
|
lastRole = "assistant";
|
|
402
527
|
} else if (m.role === "tool") {
|
|
@@ -448,6 +573,25 @@ function mapUsage(raw?: ChatUsage): TokenUsage {
|
|
|
448
573
|
cacheWriteTokens: raw?.prompt_tokens_details?.cache_write_tokens ?? 0,
|
|
449
574
|
thinkingTokens: raw?.completion_tokens_details?.reasoning_tokens ?? 0,
|
|
450
575
|
};
|
|
576
|
+
// Media tokens are listed only when the provider reported more than zero of them.
|
|
577
|
+
const count = (n: unknown): number | undefined =>
|
|
578
|
+
typeof n === "number" && Number.isFinite(n) && n > 0 ? n : undefined;
|
|
579
|
+
const inDet = {
|
|
580
|
+
audioTokens: count(raw?.prompt_tokens_details?.audio_tokens),
|
|
581
|
+
videoTokens: count(raw?.prompt_tokens_details?.video_tokens),
|
|
582
|
+
};
|
|
583
|
+
const outDet = {
|
|
584
|
+
audioTokens: count(raw?.completion_tokens_details?.audio_tokens),
|
|
585
|
+
imageTokens: count(raw?.completion_tokens_details?.image_tokens),
|
|
586
|
+
};
|
|
587
|
+
const keep = (d: Record<string, number | undefined>): Record<string, number> | undefined => {
|
|
588
|
+
const kept = Object.fromEntries(Object.entries(d).filter(([, v]) => v !== undefined)) as Record<string, number>;
|
|
589
|
+
return Object.keys(kept).length > 0 ? kept : undefined;
|
|
590
|
+
};
|
|
591
|
+
const inKept = keep(inDet);
|
|
592
|
+
const outKept = keep(outDet);
|
|
593
|
+
if (inKept) usage.inputDetails = inKept;
|
|
594
|
+
if (outKept) usage.outputDetails = outKept;
|
|
451
595
|
if (typeof raw?.cost === "number" && raw.cost >= 0) {
|
|
452
596
|
// an explicit 0 (a free model, a response-cache hit) is a known cost, not a missing one
|
|
453
597
|
usage.cost = { totalCost: raw.cost };
|
|
@@ -502,6 +646,8 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
502
646
|
`openrouter/${modelId}`
|
|
503
647
|
);
|
|
504
648
|
warnIgnoredMediaResolution("openrouter", context.messages, `openrouter/${modelId}`, ["video"]);
|
|
649
|
+
warnIgnoredVideoProcessing("openrouter", context.messages, `openrouter/${modelId}`);
|
|
650
|
+
warnIgnoredToolMedia("openrouter", context.messages, `openrouter/${modelId}`);
|
|
505
651
|
|
|
506
652
|
// Structured outputs: native `response_format` when supported, else
|
|
507
653
|
// emulated via system-prompt instruction + client validation (catalog
|
|
@@ -519,7 +665,11 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
519
665
|
outputEmulated = true;
|
|
520
666
|
effectiveContext = {
|
|
521
667
|
...context,
|
|
522
|
-
systemPrompt: augmentSystemPromptWithStructuredOutput(
|
|
668
|
+
systemPrompt: augmentSystemPromptWithStructuredOutput(
|
|
669
|
+
context.systemPrompt,
|
|
670
|
+
outputSpec,
|
|
671
|
+
options?.prompts?.structuredOutput
|
|
672
|
+
),
|
|
523
673
|
};
|
|
524
674
|
emitProviderWarning({
|
|
525
675
|
provider: "openrouter",
|
|
@@ -530,7 +680,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
530
680
|
fallback: "emulated JSON instruction + client validation (no response_format is sent)",
|
|
531
681
|
});
|
|
532
682
|
}
|
|
533
|
-
const messages = await fullHistoryMessages(effectiveContext);
|
|
683
|
+
const messages = await fullHistoryMessages(effectiveContext, options?.prompts?.toolBridge);
|
|
534
684
|
const body: ChatRequestBody = {
|
|
535
685
|
model: modelId,
|
|
536
686
|
messages,
|
|
@@ -538,11 +688,17 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
538
688
|
};
|
|
539
689
|
const orTools = toOpenRouterTools(tools);
|
|
540
690
|
if (orTools) body.tools = orTools;
|
|
541
|
-
const toolChoice = mapToolChoiceToOpenRouterChat(
|
|
542
|
-
options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
543
|
-
);
|
|
691
|
+
const toolChoice = mapToolChoiceToOpenRouterChat(options?.toolChoice);
|
|
544
692
|
if (toolChoice !== undefined) body.tool_choice = toolChoice;
|
|
545
|
-
|
|
693
|
+
const env = openRouterEnvOptions(options);
|
|
694
|
+
if (effort || env.reasoning_exclude !== undefined)
|
|
695
|
+
body.reasoning = {
|
|
696
|
+
...(effort ? { effort } : {}),
|
|
697
|
+
...(env.reasoning_exclude !== undefined ? { exclude: env.reasoning_exclude } : {}),
|
|
698
|
+
};
|
|
699
|
+
if (env.provider) body.provider = env.provider;
|
|
700
|
+
if (env.parallel_tool_calls !== undefined && orTools) body.parallel_tool_calls = env.parallel_tool_calls;
|
|
701
|
+
if (env.user) body.user = env.user;
|
|
546
702
|
if (serviceTier) body.service_tier = serviceTier;
|
|
547
703
|
if (cache === "5m" || cache === "1h") body.cache_control = { type: "ephemeral", ttl: cache };
|
|
548
704
|
// Documented Chat Completions generation controls (only those the caller set).
|
|
@@ -653,6 +809,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
653
809
|
provider: "openrouter",
|
|
654
810
|
raw: res.raw,
|
|
655
811
|
durationMs: res.durationMs,
|
|
812
|
+
providerSteps: res.providerSteps,
|
|
656
813
|
};
|
|
657
814
|
} catch (err) {
|
|
658
815
|
// A stream reports its own generic abort; the caller's reason (`interrupt(new Error("..."))`) is what the run should carry.
|
|
@@ -748,6 +905,10 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
748
905
|
// Index of the most recent tool-call delta: unindexed chunks continue
|
|
749
906
|
// it (falling back to calls.size would fork a phantom call per chunk).
|
|
750
907
|
let lastToolIndex = 0;
|
|
908
|
+
// The turn's reasoning blocks exactly as returned, replayed on the next request (tool turns need them).
|
|
909
|
+
const reasoningBlocks: Array<Record<string, unknown>> = [];
|
|
910
|
+
// Parsed PDFs (`file` annotations) of the turn: sent back so OpenRouter does not parse the same file again.
|
|
911
|
+
const annotationBlocks: Array<Record<string, unknown>> = [];
|
|
751
912
|
let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
752
913
|
let finishReason = "stop";
|
|
753
914
|
let sawFinishReason = false;
|
|
@@ -812,6 +973,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
812
973
|
const details = delta["reasoning_details"];
|
|
813
974
|
if (Array.isArray(details)) {
|
|
814
975
|
for (const block of details) {
|
|
976
|
+
mergeReasoningDetails(reasoningBlocks, block);
|
|
815
977
|
const t = (block as { text?: string })?.text;
|
|
816
978
|
if (typeof t === "string" && t) {
|
|
817
979
|
thinking += t;
|
|
@@ -833,6 +995,8 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
833
995
|
partialThinking: thinking,
|
|
834
996
|
});
|
|
835
997
|
}
|
|
998
|
+
if (Array.isArray(delta["annotations"]))
|
|
999
|
+
for (const a of delta["annotations"]) mergeAnnotation(annotationBlocks, a);
|
|
836
1000
|
// Tool-call deltas accumulate per choice index.
|
|
837
1001
|
const deltaCalls = delta["tool_calls"];
|
|
838
1002
|
if (Array.isArray(deltaCalls)) {
|
|
@@ -942,6 +1106,9 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
942
1106
|
response: { ...responseMeta, body: completedBody, badFrames } as unknown as ProviderRawData["response"],
|
|
943
1107
|
},
|
|
944
1108
|
durationMs: Date.now() - startTime,
|
|
1109
|
+
...(reasoningBlocks.length + annotationBlocks.length > 0
|
|
1110
|
+
? { providerSteps: { provider: "openrouter", steps: [...reasoningBlocks, ...annotationBlocks] } }
|
|
1111
|
+
: {}),
|
|
945
1112
|
});
|
|
946
1113
|
eventStream.push({ type: "done", delta: "", usage, finishReason, responseId });
|
|
947
1114
|
eventStream.end(finalResponse);
|
package/src/providers.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { zodToJsonSchema } from "./tools/schema.ts";
|
|
|
16
16
|
import { ValidationError } from "./types/errors.ts";
|
|
17
17
|
import { TRUNCATED_ARGS_KEY } from "./types/tool.ts";
|
|
18
18
|
import { telemetryBus } from "./telemetry.ts";
|
|
19
|
+
import { DEFAULT_PROMPTS, fillPrompt } from "./prompts/prompts.ts";
|
|
19
20
|
|
|
20
21
|
/** Classification of a canonical capability on a given provider. */
|
|
21
22
|
export type ProviderCapabilityStatus = "NATIVE" | "TRANSLATED" | "AUTOMATIC" | "EMULATED" | "UNSUPPORTED";
|
|
@@ -232,6 +233,47 @@ export function warnIgnoredMediaResolution(
|
|
|
232
233
|
});
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
/** Warns once per provider/model that a video's `processing` mode is ignored (only Google has one); in: an adapter's request messages, out: telemetry bus only. */
|
|
237
|
+
export function warnIgnoredVideoProcessing(provider: string, messages: Message[], modelRef: string): void {
|
|
238
|
+
const used = messages.some(
|
|
239
|
+
(m) => Array.isArray(m.content) && m.content.some((p) => p.type === "video" && p.processing)
|
|
240
|
+
);
|
|
241
|
+
if (!used) return;
|
|
242
|
+
emitProviderWarning({
|
|
243
|
+
provider,
|
|
244
|
+
capability: "video processing",
|
|
245
|
+
requested: modelRef,
|
|
246
|
+
reason: "only Google reads a video in a chosen processing mode (static or agentic).",
|
|
247
|
+
fallback: "the provider's default video handling",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Warns once per provider/model that media a tool returned with `toolContent` is not sent (only Google takes media inside a function result); the model still gets the tool's text; in: an adapter's request messages, out: telemetry bus only. */
|
|
252
|
+
export function warnIgnoredToolMedia(provider: string, messages: Message[], modelRef: string): void {
|
|
253
|
+
const used = messages.some(
|
|
254
|
+
(m) => Array.isArray(m.content) && m.content.some((p) => p.type === "tool_result" && p.parts && p.parts.length > 0)
|
|
255
|
+
);
|
|
256
|
+
if (!used) return;
|
|
257
|
+
emitProviderWarning({
|
|
258
|
+
provider,
|
|
259
|
+
capability: "media in tool results",
|
|
260
|
+
requested: modelRef,
|
|
261
|
+
reason: "only Google accepts images, audio, video or files inside a function result.",
|
|
262
|
+
fallback: "the tool's text only (the media is not sent)",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Warns once that the `validated` tool choice (Google only) is dropped and the provider default (auto) is used; in: the OpenRouter and OpenAI tool-choice mappers (compat goes through the OpenAI one), out: a provider_warning on the bus.
|
|
267
|
+
function warnValidatedToolChoice(provider: string): void {
|
|
268
|
+
emitProviderWarning({
|
|
269
|
+
provider,
|
|
270
|
+
capability: "tool choice",
|
|
271
|
+
requested: "validated",
|
|
272
|
+
reason: "only Google has a validated tool-choice mode.",
|
|
273
|
+
fallback: "automatic tool choice",
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
235
277
|
/** OpenRouter Chat Completions `tool_choice` wire values. */
|
|
236
278
|
export type OpenRouterChatToolChoice = "auto" | "none" | "required" | { type: "function"; function: { name: string } };
|
|
237
279
|
|
|
@@ -243,9 +285,10 @@ export type OpenRouterChatToolChoice = "auto" | "none" | "required" | { type: "f
|
|
|
243
285
|
* (`{type:function,name}`): completions nests the name under `function`.
|
|
244
286
|
*/
|
|
245
287
|
export function mapToolChoiceToOpenRouterChat(
|
|
246
|
-
choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
288
|
+
choice: "auto" | "none" | "required" | "validated" | { type: "function"; function: { name: string } } | undefined
|
|
247
289
|
): OpenRouterChatToolChoice | undefined {
|
|
248
290
|
if (!choice || choice === "auto") return undefined;
|
|
291
|
+
if (choice === "validated") return (warnValidatedToolChoice("openrouter"), undefined);
|
|
249
292
|
if (choice === "none" || choice === "required") return choice;
|
|
250
293
|
const name = choice.function?.name;
|
|
251
294
|
if (name) return { type: "function", function: { name } };
|
|
@@ -292,9 +335,11 @@ export type OpenAIToolChoice = "auto" | "none" | "required" | { type: "function"
|
|
|
292
335
|
* variants are out of scope and never emitted.
|
|
293
336
|
*/
|
|
294
337
|
export function mapToolChoiceToOpenAI(
|
|
295
|
-
choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
338
|
+
choice: "auto" | "none" | "required" | "validated" | { type: "function"; function: { name: string } } | undefined,
|
|
339
|
+
provider = "openai"
|
|
296
340
|
): OpenAIToolChoice | undefined {
|
|
297
341
|
if (!choice || choice === "auto") return undefined;
|
|
342
|
+
if (choice === "validated") return (warnValidatedToolChoice(provider), undefined);
|
|
298
343
|
if (choice === "none" || choice === "required") return choice;
|
|
299
344
|
const name = choice.function?.name;
|
|
300
345
|
if (name) return { type: "function", name };
|
|
@@ -302,7 +347,7 @@ export function mapToolChoiceToOpenAI(
|
|
|
302
347
|
}
|
|
303
348
|
|
|
304
349
|
/** Normalized tool-choice mode shared by provider adapters. */
|
|
305
|
-
export type CanonicalToolChoiceMode = "auto" | "any" | "none";
|
|
350
|
+
export type CanonicalToolChoiceMode = "auto" | "any" | "none" | "validated";
|
|
306
351
|
|
|
307
352
|
/**
|
|
308
353
|
* Normalizes canonical toolChoice into a provider-neutral mode + optional
|
|
@@ -310,13 +355,14 @@ export type CanonicalToolChoiceMode = "auto" | "any" | "none";
|
|
|
310
355
|
* `{ allowed_tools: { mode, tools? } }` in its own adapter.
|
|
311
356
|
*/
|
|
312
357
|
export function normalizeToolChoice(
|
|
313
|
-
choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined,
|
|
358
|
+
choice: "auto" | "none" | "required" | "validated" | { type: "function"; function: { name: string } } | undefined,
|
|
314
359
|
availableToolNames: string[]
|
|
315
360
|
): { mode: CanonicalToolChoiceMode; tools?: string[] } | undefined {
|
|
316
361
|
if (!choice) return undefined;
|
|
317
362
|
if (typeof choice === "string") {
|
|
318
363
|
if (choice === "auto") return undefined; // server default
|
|
319
364
|
if (choice === "none") return { mode: "none" };
|
|
365
|
+
if (choice === "validated") return { mode: "validated" };
|
|
320
366
|
if (choice === "required") {
|
|
321
367
|
return availableToolNames.length > 0 ? { mode: "any", tools: availableToolNames } : { mode: "any" };
|
|
322
368
|
}
|
|
@@ -654,8 +700,11 @@ export function mapOutputToCompatChat(
|
|
|
654
700
|
*
|
|
655
701
|
* @example `augmentSystemPromptWithStructuredOutput(sys, spec)`
|
|
656
702
|
*/
|
|
657
|
-
export function buildStructuredOutputInstruction(
|
|
658
|
-
|
|
703
|
+
export function buildStructuredOutputInstruction(
|
|
704
|
+
spec: StructuredOutputSpec,
|
|
705
|
+
template = DEFAULT_PROMPTS.structuredOutput
|
|
706
|
+
): string {
|
|
707
|
+
return fillPrompt(template, { schema: JSON.stringify(spec.schema) });
|
|
659
708
|
}
|
|
660
709
|
|
|
661
710
|
/**
|
|
@@ -665,11 +714,12 @@ export function buildStructuredOutputInstruction(spec: StructuredOutputSpec): st
|
|
|
665
714
|
*/
|
|
666
715
|
export function augmentSystemPromptWithStructuredOutput(
|
|
667
716
|
systemPrompt: string | undefined,
|
|
668
|
-
spec: StructuredOutputSpec | undefined
|
|
717
|
+
spec: StructuredOutputSpec | undefined,
|
|
718
|
+
template?: string
|
|
669
719
|
): string | undefined {
|
|
670
720
|
if (!spec) return systemPrompt;
|
|
671
|
-
const instruction = `[Structured Output]\n${buildStructuredOutputInstruction(spec)}`;
|
|
721
|
+
const instruction = `[Structured Output]\n${buildStructuredOutputInstruction(spec, template)}`;
|
|
672
722
|
if (!systemPrompt) return instruction;
|
|
673
|
-
if (systemPrompt.includes(
|
|
723
|
+
if (systemPrompt.includes(instruction)) return systemPrompt;
|
|
674
724
|
return `${systemPrompt}\n\n${instruction}`;
|
|
675
725
|
}
|
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Scheduled actions: a worker that runs later (once or repeating) and saves its answer to a file. Off unless `dynamicSubagents.allowScheduledActions` is set.
|
|
4
4
|
|
|
5
|
-
| File
|
|
6
|
-
|
|
|
7
|
-
| `scheduler.ts`
|
|
8
|
-
| `store.ts`
|
|
9
|
-
| `time.ts`
|
|
10
|
-
| `tool.ts`
|
|
5
|
+
| File | What it is |
|
|
6
|
+
| :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| `scheduler.ts` | One `Scheduler` per home folder: a 1 s unref'd clock that runs only while something is pending, startup sweep, dead-owner recovery, heartbeats, cook/deliver state machine, the functions behind the API (`createScheduledAction`, `list/update/run/skip/deleteScheduledAction`, not exported from the barrel) and `ScheduledActions`, the class behind `agent.scheduled` that binds them to the agent's home. |
|
|
8
|
+
| `store.ts` | Disk layer under `<home>/scheduled/`: `<id>/task.json`, `<id>/delivery.json`, run files, one short mutex per action; validation of records. |
|
|
9
|
+
| `time.ts` | Pure `DD:HH:MM` and local-clock maths. No timezone, DST or `Intl`; `now` is always passed in. |
|
|
10
|
+
| `tool.ts` | The model-facing `scheduled_actions` tool and `get_datetime`; its description is the `scheduledActionsTool` prompt (`../prompts/scheduled-actions-tool.md`). |
|
|
11
11
|
|
|
12
12
|
## Invariants
|
|
13
13
|
|
|
14
14
|
- The files are the truth: every write re-reads first and goes through the durable writer. The file format is a stable public contract: see `docs/scheduled-action-files.md` and add a changelog row for any change.
|
|
15
15
|
- Write order when a run ends: run file, then `<id>/delivery.json`, then `<id>/task.json`. A crash in between is repaired by the dead-owner recovery (`recoverCooking`, at start and every 15 s): the delivery is rebuilt from the run file, or the run is marked `missed` when no run file exists.
|
|
16
16
|
- `startCook` advances the next times BEFORE the run starts; a run still cooking when the next is due is skipped, never stacked.
|
|
17
|
-
- Runs are fresh stateless workers built by `buildDynamicWorkerConfig`; their spending guardrail is `workerBudget`
|
|
17
|
+
- Runs are fresh stateless workers built by `buildDynamicWorkerConfig`; their spending guardrail is `dynamicSubagents.scheduledActionBudget` only (never `workerBudget`, `budget` or `usageLimit`).
|
|
18
18
|
- The Agent registry holds `WeakRef`s by name: keep every Agent that owns actions referenced, or its runs become `missed`.
|
|
19
19
|
- Several processes may share a home: scheduled actions take no home lock, only the short per-action mutex (`<id>/.mutex`); the one-process lock is on session folders only. Ownership: a process only starts, delivers or recovers actions whose Agent it registered. An action nobody here owns is left alone until its delivery time passed with no run started, then marked `missed` (`no_agent`).
|
|
20
20
|
- Exactly once: a scheduled start re-checks under the action's mutex that it is still `waiting` and due; `markMissed` only changes the state the caller saw; a delivery is emitted only by the process that removed its entry from `delivery.json`.
|
|
@@ -1052,3 +1052,43 @@ export function deleteScheduledAction(
|
|
|
1052
1052
|
): Promise<DeleteResult> {
|
|
1053
1053
|
return schedulerFor(opts).delete(id, { force: opts?.force, purge: opts?.purge });
|
|
1054
1054
|
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* The scheduled-action API of one Agent (`agent.scheduled`): every method works on that agent's home folder, so an Agent
|
|
1058
|
+
* with its own `home` never needs `{ home }` passed. `create` needs `dynamicSubagents.allowScheduledActions` (else a
|
|
1059
|
+
* `ConfigError`). `list`, `update`, `run`, `skip` and `delete` work on every action in the home, as the model's
|
|
1060
|
+
* `scheduled_actions` tool does; `run` needs the action's creating Agent alive in this process.
|
|
1061
|
+
*/
|
|
1062
|
+
export class ScheduledActions {
|
|
1063
|
+
constructor(private readonly agent: Agent) {}
|
|
1064
|
+
|
|
1065
|
+
/** Schedules a new action under this agent. Returns the saved record; throws one `ValidationError` listing every problem. */
|
|
1066
|
+
create(input: ScheduledActionInput): ScheduledActionRecord {
|
|
1067
|
+
return createScheduledAction(this.agent, input);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/** Every action in this agent's home with its full metadata and state, plus a `running` flag. */
|
|
1071
|
+
list(): Array<ScheduledActionRecord & { running: boolean }> {
|
|
1072
|
+
return listScheduledActions({ home: this.agent.home });
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/** Changes instructions, task, tools, timeout, role or any schedule field of action `id` (refused for a schedule change while it cooks). */
|
|
1076
|
+
update(id: string, patch: ScheduledActionPatch): ScheduledActionRecord {
|
|
1077
|
+
return updateScheduledAction(id, patch, { home: this.agent.home });
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** Runs action `id` now, outside its schedule (also resolves a missed run). */
|
|
1081
|
+
run(id: string): ScheduledActionRecord {
|
|
1082
|
+
return runScheduledAction(id, { home: this.agent.home });
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/** Ignores a missed run of `id` and waits for the next scheduled time (a one-shot becomes `done`). */
|
|
1086
|
+
skip(id: string): ScheduledActionRecord {
|
|
1087
|
+
return skipScheduledAction(id, { home: this.agent.home });
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** Deletes action `id` for good. While it is cooking nothing changes unless `force: true`; `purge: true` also deletes its past run files. */
|
|
1091
|
+
delete(id: string, opts?: { force?: boolean; purge?: boolean }): Promise<DeleteResult> {
|
|
1092
|
+
return deleteScheduledAction(id, { home: this.agent.home, ...opts });
|
|
1093
|
+
}
|
|
1094
|
+
}
|