agent-accelerator 0.5.9 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -16
- package/examples/prompts/SYSTEM_PROMPT.md +2 -0
- package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
- package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
- package/package.json +4 -1
- package/src/agent/agent.ts +447 -172
- package/src/agent/architecture.md +16 -12
- package/src/agent/config-blocks.ts +39 -12
- package/src/agent/context.ts +9 -3
- package/src/agent/defaults.ts +88 -0
- package/src/agent/delegation.ts +86 -28
- package/src/agent/loop.ts +474 -374
- package/src/agent/subagent.ts +1 -3
- package/src/agent/subagents-tool.ts +50 -19
- package/src/agent/turns.ts +3 -1
- package/src/agent/usage-limit.ts +92 -0
- package/src/agent/worker.ts +73 -30
- package/src/agent/workers.ts +126 -21
- package/src/architecture.md +17 -15
- package/src/index.ts +43 -19
- package/src/models/architecture.md +4 -3
- package/src/models/catalog-cache.ts +6 -5
- package/src/models/catalog.ts +39 -30
- 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/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +4 -0
- package/src/prompts/structured-output.md +2 -0
- package/src/{agent/subagents.md → prompts/subagents-tool.md} +3 -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 +10 -8
- package/src/providers/google.ts +228 -69
- package/src/providers/openai-compat.ts +74 -56
- package/src/providers/openai.ts +64 -41
- package/src/providers/openrouter.ts +270 -48
- package/src/providers/registry.ts +6 -21
- package/src/providers/shared.ts +87 -33
- package/src/providers.ts +115 -124
- package/src/scheduler/architecture.md +8 -7
- package/src/scheduler/scheduler.ts +226 -60
- package/src/scheduler/store.ts +23 -10
- package/src/scheduler/time.ts +17 -3
- package/src/scheduler/tool.ts +38 -16
- package/src/session/architecture.md +7 -6
- package/src/session/journal.ts +39 -12
- package/src/session/store.ts +126 -61
- package/src/session/usage.ts +58 -9
- package/src/streaming/architecture.md +4 -3
- package/src/streaming/event-stream.ts +26 -12
- package/src/telemetry.ts +6 -2
- package/src/tools/architecture.md +9 -7
- package/src/tools/executor.ts +109 -62
- package/src/tools/schema.ts +10 -6
- package/src/tools/subagent-result.ts +6 -1
- package/src/tools/tool-content.ts +32 -0
- package/src/tools/tool.ts +9 -11
- package/src/types/agent.ts +37 -22
- package/src/types/architecture.md +11 -10
- package/src/types/core.ts +5 -9
- package/src/types/errors.ts +26 -4
- package/src/types/message.ts +7 -0
- package/src/types/model.ts +4 -5
- package/src/types/scheduler.ts +4 -4
- package/src/types/tool.ts +14 -2
- package/src/update-models.ts +5 -5
- package/src/utils/architecture.md +16 -15
- package/src/utils/cache.ts +1 -3
- package/src/utils/durable.ts +17 -5
- package/src/utils/env.ts +9 -7
- package/src/utils/errors.ts +9 -17
- package/src/utils/headers.ts +3 -10
- package/src/utils/media.ts +23 -8
- package/src/utils/raw-capture.ts +9 -2
- package/src/utils/retry.ts +15 -3
- package/src/utils/safe-io.ts +29 -7
- package/src/utils/serialization.ts +1 -4
- package/src/utils/session.ts +1 -2
package/src/providers/shared.ts
CHANGED
|
@@ -28,9 +28,7 @@ export function parseArguments(raw: string | undefined): Record<string, unknown>
|
|
|
28
28
|
|
|
29
29
|
/** Trims/collapses assembled thinking parts (no edge-tripling). */
|
|
30
30
|
export function normalizeThinkingParts(parts: string[]): string | undefined {
|
|
31
|
-
const cleaned = parts
|
|
32
|
-
.map((p) => p.replace(/\n{3,}/g, "\n\n").trim())
|
|
33
|
-
.filter((p) => p.length > 0);
|
|
31
|
+
const cleaned = parts.map((p) => p.replace(/\n{3,}/g, "\n\n").trim()).filter((p) => p.length > 0);
|
|
34
32
|
return cleaned.length > 0 ? cleaned.join("\n") : undefined;
|
|
35
33
|
}
|
|
36
34
|
|
|
@@ -49,21 +47,35 @@ export function redactedHeaders(headers: Record<string, string>): Record<string,
|
|
|
49
47
|
* OpenAI `type`, OpenRouter `metadata.error_type` + `metadata.raw` + `metadata.limit_source`, and Google
|
|
50
48
|
* `status` code shapes; unknown bodies degrade to a 300-char slice.
|
|
51
49
|
*/
|
|
52
|
-
export function readErrorPayload(
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
export function readErrorPayload(bodyText: string): {
|
|
51
|
+
message: string;
|
|
52
|
+
code?: string | number;
|
|
53
|
+
errorType?: string;
|
|
54
|
+
raw?: string;
|
|
55
|
+
limitSource?: string;
|
|
56
|
+
} {
|
|
55
57
|
try {
|
|
56
58
|
const parsed: unknown = JSON.parse(bodyText);
|
|
57
59
|
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
58
|
-
const err = (
|
|
60
|
+
const err = (
|
|
61
|
+
first as {
|
|
62
|
+
error?: {
|
|
63
|
+
message?: string;
|
|
64
|
+
code?: string | number;
|
|
65
|
+
type?: string;
|
|
66
|
+
status?: string | number;
|
|
67
|
+
metadata?: { error_type?: string; raw?: unknown; limit_source?: unknown };
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
)?.error;
|
|
59
71
|
if (err && typeof err.message === "string") {
|
|
60
72
|
const code = err.code ?? err.status;
|
|
61
|
-
const errorType =
|
|
62
|
-
err.type ?? err.metadata?.error_type ?? (first as { error_type?: string })?.error_type;
|
|
73
|
+
const errorType = err.type ?? err.metadata?.error_type ?? (first as { error_type?: string })?.error_type;
|
|
63
74
|
const rawCandidate =
|
|
64
75
|
(err as { metadata?: { raw?: unknown } }).metadata?.raw ??
|
|
65
76
|
(first as { metadata?: { raw?: unknown } })?.metadata?.raw;
|
|
66
|
-
const raw =
|
|
77
|
+
const raw =
|
|
78
|
+
typeof rawCandidate === "string" && rawCandidate.trim() ? rawCandidate.trim().slice(0, 500) : undefined;
|
|
67
79
|
return {
|
|
68
80
|
message: err.message,
|
|
69
81
|
...(code !== undefined ? { code } : {}),
|
|
@@ -111,7 +123,9 @@ export function isNotFoundChainError(error: unknown): boolean {
|
|
|
111
123
|
);
|
|
112
124
|
if (status === 404) return true;
|
|
113
125
|
// `providerCode` on a wrapped ProviderError (its `code` is the SDK kind), `code` on a raw error.
|
|
114
|
-
return [err["providerCode"], err["code"], err["errorType"]].some(
|
|
126
|
+
return [err["providerCode"], err["code"], err["errorType"]].some(
|
|
127
|
+
(c) => String(c ?? "").toLowerCase() === "not_found"
|
|
128
|
+
);
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
/** Per-request timeout ms from provider options. */
|
|
@@ -121,25 +135,40 @@ export function timeoutFor(options: { timeoutMs?: number } | undefined): number
|
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
/** Merges a parent signal with a timeout into one effective signal. Caller must call cleanup when the request settles so the timer never holds the loop open. */
|
|
124
|
-
export function combinedSignal(
|
|
138
|
+
export function combinedSignal(
|
|
139
|
+
signal?: AbortSignal,
|
|
140
|
+
timeoutMs?: number
|
|
141
|
+
): { signal: AbortSignal | undefined; cleanup: () => void } {
|
|
125
142
|
if (timeoutMs === undefined) return { signal, cleanup: () => {} };
|
|
126
143
|
const ctrl = new AbortController();
|
|
127
144
|
const timer = setTimeout(() => {
|
|
128
|
-
try {
|
|
145
|
+
try {
|
|
146
|
+
ctrl.abort();
|
|
147
|
+
} catch {}
|
|
129
148
|
}, timeoutMs);
|
|
130
|
-
try {
|
|
149
|
+
try {
|
|
150
|
+
(timer as unknown as { unref?: () => void }).unref?.();
|
|
151
|
+
} catch {}
|
|
131
152
|
const cleanup = () => {
|
|
132
|
-
try {
|
|
153
|
+
try {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
} catch {}
|
|
133
156
|
};
|
|
134
157
|
if (signal) {
|
|
135
158
|
if (signal.aborted) {
|
|
136
159
|
cleanup();
|
|
137
160
|
ctrl.abort(signal.reason); // the caller's reason survives (`interrupt(new Error("..."))`)
|
|
138
161
|
} else {
|
|
139
|
-
signal.addEventListener(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
162
|
+
signal.addEventListener(
|
|
163
|
+
"abort",
|
|
164
|
+
() => {
|
|
165
|
+
cleanup();
|
|
166
|
+
try {
|
|
167
|
+
ctrl.abort(signal.reason);
|
|
168
|
+
} catch {}
|
|
169
|
+
},
|
|
170
|
+
{ once: true }
|
|
171
|
+
);
|
|
143
172
|
}
|
|
144
173
|
}
|
|
145
174
|
return { signal: ctrl.signal, cleanup };
|
|
@@ -167,7 +196,11 @@ export function hasAuthHeader(headers: Record<string, string> | undefined): bool
|
|
|
167
196
|
if (!headers) return false;
|
|
168
197
|
for (const [k, v] of Object.entries(headers)) {
|
|
169
198
|
const lower = k.toLowerCase();
|
|
170
|
-
if (
|
|
199
|
+
if (
|
|
200
|
+
(lower === "authorization" || lower === "cf-aig-authorization") &&
|
|
201
|
+
typeof v === "string" &&
|
|
202
|
+
v.trim().length > 0
|
|
203
|
+
) {
|
|
171
204
|
return true;
|
|
172
205
|
}
|
|
173
206
|
}
|
|
@@ -180,7 +213,10 @@ export function hasTruncatedToolArguments(args: unknown): boolean {
|
|
|
180
213
|
}
|
|
181
214
|
|
|
182
215
|
// True when a stream with no terminal event left a tool call whose arguments never fully arrived (none at all, or cut mid-JSON); in: the adapter's assembled calls and their records, out: its incomplete_stream guard.
|
|
183
|
-
export function hasUnfinishedToolCall(
|
|
216
|
+
export function hasUnfinishedToolCall(
|
|
217
|
+
calls: Iterable<{ startArgs: string; deltaArgs: string }>,
|
|
218
|
+
toolCalls: Array<{ arguments: unknown }>
|
|
219
|
+
): boolean {
|
|
184
220
|
for (const c of calls) if (!c.startArgs.trim() && !c.deltaArgs.trim()) return true;
|
|
185
221
|
return toolCalls.some((c) => isTruncatedArguments(c.arguments));
|
|
186
222
|
}
|
|
@@ -220,7 +256,9 @@ export function clampToolCallId(id: string): string {
|
|
|
220
256
|
|
|
221
257
|
// The finish reason of a turn that produced tool calls: `tool_calls`, except that a turn cut off by the output limit (`length`) keeps saying so when one of its calls was cut mid-JSON; in: the wire finish reason + the assembled calls, out: each streaming adapter.
|
|
222
258
|
export function toolTurnFinishReason(finishReason: string, toolCalls: Array<{ arguments: unknown }>): string {
|
|
223
|
-
return finishReason === "length" && toolCalls.some((c) => isTruncatedArguments(c.arguments))
|
|
259
|
+
return finishReason === "length" && toolCalls.some((c) => isTruncatedArguments(c.arguments))
|
|
260
|
+
? "length"
|
|
261
|
+
: "tool_calls";
|
|
224
262
|
}
|
|
225
263
|
|
|
226
264
|
// The JSON text of a tool call's arguments for a history message: `{}` for a call whose streamed arguments were cut off (the internal `__truncated_raw` marker was never the model's own output and is never resent); in: a stored tool call, out: each adapter's history builder.
|
|
@@ -249,7 +287,8 @@ export function parseRetryAfterMs(headers: Record<string, string> | undefined, n
|
|
|
249
287
|
return undefined;
|
|
250
288
|
};
|
|
251
289
|
const ms = get("retry-after-ms");
|
|
252
|
-
if (ms !== undefined && ms.trim() !== "" && Number.isFinite(Number(ms)) && Number(ms) >= 0)
|
|
290
|
+
if (ms !== undefined && ms.trim() !== "" && Number.isFinite(Number(ms)) && Number(ms) >= 0)
|
|
291
|
+
return Math.round(Number(ms));
|
|
253
292
|
const ra = get("retry-after");
|
|
254
293
|
if (ra === undefined || ra.trim() === "") return undefined;
|
|
255
294
|
const secs = Number(ra);
|
|
@@ -287,12 +326,16 @@ export function openStreamResponse(args: {
|
|
|
287
326
|
}
|
|
288
327
|
|
|
289
328
|
/** Retry knobs from provider request options, in the shape `withRetries` expects. */
|
|
290
|
-
export function retryOptionsFrom(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
329
|
+
export function retryOptionsFrom(
|
|
330
|
+
options:
|
|
331
|
+
| {
|
|
332
|
+
maxRetries?: number;
|
|
333
|
+
maxRetryDelayMs?: number;
|
|
334
|
+
retryBaseDelayMs?: number;
|
|
335
|
+
retryBudgetMs?: number;
|
|
336
|
+
}
|
|
337
|
+
| undefined
|
|
338
|
+
): { maxRetries?: number; maxRetryDelayMs?: number; baseDelayMs?: number; retryBudgetMs?: number } {
|
|
296
339
|
return {
|
|
297
340
|
maxRetries: options?.maxRetries,
|
|
298
341
|
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
@@ -306,7 +349,10 @@ export function retryOptionsFrom(options: {
|
|
|
306
349
|
// ---------------------------------------------------------------------------
|
|
307
350
|
|
|
308
351
|
// Ties a provider stream to the caller's signal and to the stream's own cancel: one controller that aborts when either does; in: an adapter's stream(), out: the controller its request uses and the `release` its finally block calls.
|
|
309
|
-
export function linkStreamAbort(
|
|
352
|
+
export function linkStreamAbort(
|
|
353
|
+
eventStream: AssistantMessageEventStream,
|
|
354
|
+
signal: AbortSignal | undefined
|
|
355
|
+
): { linked: AbortController; release: () => void } {
|
|
310
356
|
const linked = new AbortController();
|
|
311
357
|
const forwardUserAbort = (): void => {
|
|
312
358
|
try {
|
|
@@ -338,7 +384,11 @@ export function linkStreamAbort(eventStream: AssistantMessageEventStream, signal
|
|
|
338
384
|
}
|
|
339
385
|
|
|
340
386
|
// Runs one request attempt under a timer that aborts `controller` after `ms` (none when unset); in: an adapter's fetchOnce, out: its Response.
|
|
341
|
-
export async function withAbortTimer<T>(
|
|
387
|
+
export async function withAbortTimer<T>(
|
|
388
|
+
controller: AbortController,
|
|
389
|
+
ms: number | undefined,
|
|
390
|
+
run: () => Promise<T>
|
|
391
|
+
): Promise<T> {
|
|
342
392
|
const timer =
|
|
343
393
|
ms && ms > 0
|
|
344
394
|
? setTimeout(() => {
|
|
@@ -397,13 +447,17 @@ export function incompleteStreamCheck(a: {
|
|
|
397
447
|
}): Error | undefined {
|
|
398
448
|
if (a.hasTerminal || !(a.empty || a.unfinishedCall)) return undefined;
|
|
399
449
|
const contentType = Object.entries(a.responseHeaders).find(([k]) => k.toLowerCase() === "content-type")?.[1];
|
|
400
|
-
const isSSEPayload =
|
|
450
|
+
const isSSEPayload =
|
|
451
|
+
(typeof contentType === "string" && (contentType.includes("event-stream") || contentType.startsWith("text/"))) ||
|
|
452
|
+
a.framesSeen > 0 ||
|
|
453
|
+
a.badFrames > 0;
|
|
401
454
|
return isSSEPayload ? incompleteStreamError(a.provider, a.modelId) : undefined;
|
|
402
455
|
}
|
|
403
456
|
|
|
404
457
|
// Fails a stream with the right error: an abort (the signal, the stream's cancel, or an AbortError) becomes the plain `AbortError`, anything else passes through; in: an adapter's catch block, out: the stream's result() rejection.
|
|
405
458
|
export function failStream(eventStream: AssistantMessageEventStream, linked: AbortController, err: unknown): void {
|
|
406
459
|
const raw = err instanceof Error ? err : new Error(String(err));
|
|
407
|
-
const isAbort =
|
|
460
|
+
const isAbort =
|
|
461
|
+
linked.signal.aborted || eventStream.isCancelled() || (raw as { name?: string }).name === "AbortError";
|
|
408
462
|
eventStream.fail(isAbort ? Object.assign(new Error("Stream aborted"), { name: "AbortError" }) : raw);
|
|
409
463
|
}
|
package/src/providers.ts
CHANGED
|
@@ -16,14 +16,10 @@ 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
|
-
export type ProviderCapabilityStatus =
|
|
22
|
-
| "NATIVE"
|
|
23
|
-
| "TRANSLATED"
|
|
24
|
-
| "AUTOMATIC"
|
|
25
|
-
| "EMULATED"
|
|
26
|
-
| "UNSUPPORTED";
|
|
22
|
+
export type ProviderCapabilityStatus = "NATIVE" | "TRANSLATED" | "AUTOMATIC" | "EMULATED" | "UNSUPPORTED";
|
|
27
23
|
|
|
28
24
|
/**
|
|
29
25
|
* Single warning mechanism for provider capability differences.
|
|
@@ -65,8 +61,12 @@ export function emitProviderWarning(options: {
|
|
|
65
61
|
reason: options.reason,
|
|
66
62
|
fallback: options.fallback,
|
|
67
63
|
});
|
|
68
|
-
try {
|
|
69
|
-
|
|
64
|
+
try {
|
|
65
|
+
(globalThis as any).__agentAccelWarnSink?.(msg, options);
|
|
66
|
+
} catch {}
|
|
67
|
+
try {
|
|
68
|
+
console.warn(msg);
|
|
69
|
+
} catch {}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/** Clears deduped-warning state (mainly for tests). */
|
|
@@ -124,9 +124,7 @@ export function clearSessionRouting(sessionId?: string): void {
|
|
|
124
124
|
* Maps a canonical ThinkingLevel onto Google Interactions `thinking_level`.
|
|
125
125
|
* Google values: minimal|low|medium|high (no off switch, no xhigh/max).
|
|
126
126
|
*/
|
|
127
|
-
export function mapThinkingLevelToGoogle(
|
|
128
|
-
level: ThinkingLevel | string | undefined
|
|
129
|
-
): { thinkingLevel?: string } {
|
|
127
|
+
export function mapThinkingLevelToGoogle(level: ThinkingLevel | string | undefined): { thinkingLevel?: string } {
|
|
130
128
|
if (!level) return {};
|
|
131
129
|
const norm = String(level).toLowerCase().trim();
|
|
132
130
|
if (norm === "dynamic") return {}; // server dynamic default (NATIVE)
|
|
@@ -171,9 +169,7 @@ export function mapServiceTierToGoogle(tier: ServiceTier | undefined): "flex" |
|
|
|
171
169
|
* the discontinued Responses skin, which clamped `xhigh` — everything passes
|
|
172
170
|
* through. `max` rides the same verbatim path. `dynamic` omits (server default).
|
|
173
171
|
*/
|
|
174
|
-
export function mapThinkingLevelToOpenRouterChat(
|
|
175
|
-
level: ThinkingLevel | string | undefined
|
|
176
|
-
): { effort?: string } {
|
|
172
|
+
export function mapThinkingLevelToOpenRouterChat(level: ThinkingLevel | string | undefined): { effort?: string } {
|
|
177
173
|
if (!level) return {};
|
|
178
174
|
const norm = String(level).toLowerCase().trim();
|
|
179
175
|
if (norm === "dynamic") return {}; // server default
|
|
@@ -216,9 +212,16 @@ export function resolveCache(
|
|
|
216
212
|
}
|
|
217
213
|
|
|
218
214
|
/** Warns once per provider/model that per-media `resolution` is ignored (no such control on that wire for the given kinds of media); in: an adapter's request messages, out: telemetry bus only. */
|
|
219
|
-
export function warnIgnoredMediaResolution(
|
|
215
|
+
export function warnIgnoredMediaResolution(
|
|
216
|
+
provider: string,
|
|
217
|
+
messages: Message[],
|
|
218
|
+
modelRef: string,
|
|
219
|
+
kinds: Array<"image" | "video"> = ["image", "video"]
|
|
220
|
+
): void {
|
|
220
221
|
const used = messages.some(
|
|
221
|
-
(m) =>
|
|
222
|
+
(m) =>
|
|
223
|
+
Array.isArray(m.content) &&
|
|
224
|
+
m.content.some((p) => (p.type === "image" || p.type === "video") && kinds.includes(p.type) && p.resolution)
|
|
222
225
|
);
|
|
223
226
|
if (!used) return;
|
|
224
227
|
emitProviderWarning({
|
|
@@ -230,12 +233,49 @@ export function warnIgnoredMediaResolution(provider: string, messages: Message[]
|
|
|
230
233
|
});
|
|
231
234
|
}
|
|
232
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
|
+
|
|
233
277
|
/** OpenRouter Chat Completions `tool_choice` wire values. */
|
|
234
|
-
export type OpenRouterChatToolChoice =
|
|
235
|
-
| "auto"
|
|
236
|
-
| "none"
|
|
237
|
-
| "required"
|
|
238
|
-
| { type: "function"; function: { name: string } };
|
|
278
|
+
export type OpenRouterChatToolChoice = "auto" | "none" | "required" | { type: "function"; function: { name: string } };
|
|
239
279
|
|
|
240
280
|
/**
|
|
241
281
|
* Maps canonical toolChoice onto Chat Completions `tool_choice`.
|
|
@@ -245,9 +285,10 @@ export type OpenRouterChatToolChoice =
|
|
|
245
285
|
* (`{type:function,name}`): completions nests the name under `function`.
|
|
246
286
|
*/
|
|
247
287
|
export function mapToolChoiceToOpenRouterChat(
|
|
248
|
-
choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
288
|
+
choice: "auto" | "none" | "required" | "validated" | { type: "function"; function: { name: string } } | undefined
|
|
249
289
|
): OpenRouterChatToolChoice | undefined {
|
|
250
290
|
if (!choice || choice === "auto") return undefined;
|
|
291
|
+
if (choice === "validated") return (warnValidatedToolChoice("openrouter"), undefined);
|
|
251
292
|
if (choice === "none" || choice === "required") return choice;
|
|
252
293
|
const name = choice.function?.name;
|
|
253
294
|
if (name) return { type: "function", function: { name } };
|
|
@@ -260,9 +301,7 @@ export function mapToolChoiceToOpenRouterChat(
|
|
|
260
301
|
* medium). Unlike OpenRouter (which clamps xhigh), OpenAI documents xhigh
|
|
261
302
|
* and max natively so they pass through verbatim. `dynamic` omits (server default).
|
|
262
303
|
*/
|
|
263
|
-
export function mapThinkingLevelToOpenAI(
|
|
264
|
-
level: ThinkingLevel | string | undefined
|
|
265
|
-
): { effort?: string } {
|
|
304
|
+
export function mapThinkingLevelToOpenAI(level: ThinkingLevel | string | undefined): { effort?: string } {
|
|
266
305
|
if (!level) return {};
|
|
267
306
|
const norm = String(level).toLowerCase().trim();
|
|
268
307
|
if (norm === "dynamic") return {}; // server default
|
|
@@ -296,9 +335,11 @@ export type OpenAIToolChoice = "auto" | "none" | "required" | { type: "function"
|
|
|
296
335
|
* variants are out of scope and never emitted.
|
|
297
336
|
*/
|
|
298
337
|
export function mapToolChoiceToOpenAI(
|
|
299
|
-
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"
|
|
300
340
|
): OpenAIToolChoice | undefined {
|
|
301
341
|
if (!choice || choice === "auto") return undefined;
|
|
342
|
+
if (choice === "validated") return (warnValidatedToolChoice(provider), undefined);
|
|
302
343
|
if (choice === "none" || choice === "required") return choice;
|
|
303
344
|
const name = choice.function?.name;
|
|
304
345
|
if (name) return { type: "function", name };
|
|
@@ -306,7 +347,7 @@ export function mapToolChoiceToOpenAI(
|
|
|
306
347
|
}
|
|
307
348
|
|
|
308
349
|
/** Normalized tool-choice mode shared by provider adapters. */
|
|
309
|
-
export type CanonicalToolChoiceMode = "auto" | "any" | "none";
|
|
350
|
+
export type CanonicalToolChoiceMode = "auto" | "any" | "none" | "validated";
|
|
310
351
|
|
|
311
352
|
/**
|
|
312
353
|
* Normalizes canonical toolChoice into a provider-neutral mode + optional
|
|
@@ -314,13 +355,14 @@ export type CanonicalToolChoiceMode = "auto" | "any" | "none";
|
|
|
314
355
|
* `{ allowed_tools: { mode, tools? } }` in its own adapter.
|
|
315
356
|
*/
|
|
316
357
|
export function normalizeToolChoice(
|
|
317
|
-
choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined,
|
|
358
|
+
choice: "auto" | "none" | "required" | "validated" | { type: "function"; function: { name: string } } | undefined,
|
|
318
359
|
availableToolNames: string[]
|
|
319
360
|
): { mode: CanonicalToolChoiceMode; tools?: string[] } | undefined {
|
|
320
361
|
if (!choice) return undefined;
|
|
321
362
|
if (typeof choice === "string") {
|
|
322
363
|
if (choice === "auto") return undefined; // server default
|
|
323
364
|
if (choice === "none") return { mode: "none" };
|
|
365
|
+
if (choice === "validated") return { mode: "validated" };
|
|
324
366
|
if (choice === "required") {
|
|
325
367
|
return availableToolNames.length > 0 ? { mode: "any", tools: availableToolNames } : { mode: "any" };
|
|
326
368
|
}
|
|
@@ -386,98 +428,36 @@ export function selectRawArguments(startText: string, deltaText: string): string
|
|
|
386
428
|
}
|
|
387
429
|
|
|
388
430
|
// ---------------------------------------------------------------------------
|
|
389
|
-
// Generation controls (maxOutputTokens
|
|
431
|
+
// Generation controls (maxOutputTokens)
|
|
390
432
|
// ---------------------------------------------------------------------------
|
|
391
433
|
|
|
392
434
|
/** Canonical generation controls as carried on `ProviderRequestOptions`. */
|
|
393
435
|
export interface GenerationParams {
|
|
394
436
|
maxOutputTokens?: number;
|
|
395
|
-
stop?: string[];
|
|
396
|
-
seed?: number;
|
|
397
437
|
}
|
|
398
438
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
/** Normalizes the public `stop` option (`string | string[]`) to a clean array, or undefined. */
|
|
405
|
-
export function normalizeStop(stop: string | string[] | undefined): string[] | undefined {
|
|
406
|
-
const list = (Array.isArray(stop) ? stop : stop === undefined ? [] : [stop]).filter(
|
|
407
|
-
(s): s is string => typeof s === "string" && s.length > 0
|
|
408
|
-
);
|
|
409
|
-
return list.length > 0 ? list : undefined;
|
|
439
|
+
// The output-token cap as a whole number above zero, else undefined; in: the three generation-param mappers, out: their wire field.
|
|
440
|
+
function outputTokenCap(params: GenerationParams | undefined): number | undefined {
|
|
441
|
+
const n = params?.maxOutputTokens;
|
|
442
|
+
return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
|
|
410
443
|
}
|
|
411
444
|
|
|
412
|
-
/**
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
*/
|
|
417
|
-
export function mapGenerationParamsToChat(params: GenerationParams | undefined, provider = "chat", modelRef = ""): Record<string, unknown> {
|
|
418
|
-
const out: Record<string, unknown> = {};
|
|
419
|
-
if (!params) return out;
|
|
420
|
-
if (finiteNumber(params.maxOutputTokens) && params.maxOutputTokens > 0) out["max_tokens"] = Math.floor(params.maxOutputTokens);
|
|
421
|
-
if (params.stop && params.stop.length > 0) {
|
|
422
|
-
out["stop"] = params.stop.slice(0, MAX_CHAT_STOP);
|
|
423
|
-
if (params.stop.length > MAX_CHAT_STOP) {
|
|
424
|
-
emitProviderWarning({
|
|
425
|
-
provider,
|
|
426
|
-
capability: "stop sequences",
|
|
427
|
-
requested: `${params.stop.length} sequences (${modelRef})`,
|
|
428
|
-
reason: `Chat Completions allows at most ${MAX_CHAT_STOP} stop sequences.`,
|
|
429
|
-
fallback: `the first ${MAX_CHAT_STOP}`,
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
if (finiteNumber(params.seed)) out["seed"] = Math.floor(params.seed);
|
|
434
|
-
return out;
|
|
445
|
+
/** Chat Completions (OpenRouter + generic compat endpoints): `max_tokens`, only when the caller set it, so strict endpoints never see anything unasked. */
|
|
446
|
+
export function mapGenerationParamsToChat(params: GenerationParams | undefined): { max_tokens?: number } {
|
|
447
|
+
const cap = outputTokenCap(params);
|
|
448
|
+
return cap === undefined ? {} : { max_tokens: cap };
|
|
435
449
|
}
|
|
436
450
|
|
|
437
|
-
|
|
438
|
-
function
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
provider,
|
|
442
|
-
capability: "stop sequences",
|
|
443
|
-
requested: `${params.stop.join("|")} (${modelRef})`,
|
|
444
|
-
reason: `the ${reasonSuffix} defines no stop-sequence parameter.`,
|
|
445
|
-
fallback: "no stop sequences (the model decides when to stop)",
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
if (finiteNumber(params.seed)) {
|
|
449
|
-
emitProviderWarning({
|
|
450
|
-
provider,
|
|
451
|
-
capability: "seed",
|
|
452
|
-
requested: `${params.seed} (${modelRef})`,
|
|
453
|
-
reason: `the ${reasonSuffix} defines no seed parameter.`,
|
|
454
|
-
fallback: "non-deterministic sampling",
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/** OpenAI Responses: `max_output_tokens` is native; `stop` and `seed` do not exist on this wire (warn + drop). */
|
|
460
|
-
export function mapGenerationParamsToOpenAI(
|
|
461
|
-
params: GenerationParams | undefined,
|
|
462
|
-
modelRef: string
|
|
463
|
-
): { max_output_tokens?: number } {
|
|
464
|
-
if (!params) return {};
|
|
465
|
-
warnUnsupportedStopSeed("openai", modelRef, params, "OpenAI Responses API");
|
|
466
|
-
return finiteNumber(params.maxOutputTokens) && params.maxOutputTokens > 0
|
|
467
|
-
? { max_output_tokens: Math.floor(params.maxOutputTokens) }
|
|
468
|
-
: {};
|
|
451
|
+
/** OpenAI Responses: `max_output_tokens`. */
|
|
452
|
+
export function mapGenerationParamsToOpenAI(params: GenerationParams | undefined): { max_output_tokens?: number } {
|
|
453
|
+
const cap = outputTokenCap(params);
|
|
454
|
+
return cap === undefined ? {} : { max_output_tokens: cap };
|
|
469
455
|
}
|
|
470
456
|
|
|
471
|
-
/** Google Interactions `generation_config`: `max_output_tokens
|
|
472
|
-
export function mapGenerationParamsToGoogle(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
): { max_output_tokens?: number } {
|
|
476
|
-
if (!params) return {};
|
|
477
|
-
warnUnsupportedStopSeed("google", modelRef, params, "Gemini Interactions API");
|
|
478
|
-
return finiteNumber(params.maxOutputTokens) && params.maxOutputTokens > 0
|
|
479
|
-
? { max_output_tokens: Math.floor(params.maxOutputTokens) }
|
|
480
|
-
: {};
|
|
457
|
+
/** Google Interactions `generation_config`: `max_output_tokens`. */
|
|
458
|
+
export function mapGenerationParamsToGoogle(params: GenerationParams | undefined): { max_output_tokens?: number } {
|
|
459
|
+
const cap = outputTokenCap(params);
|
|
460
|
+
return cap === undefined ? {} : { max_output_tokens: cap };
|
|
481
461
|
}
|
|
482
462
|
|
|
483
463
|
// ---------------------------------------------------------------------------
|
|
@@ -488,12 +468,7 @@ export function mapGenerationParamsToGoogle(
|
|
|
488
468
|
function isZodLike(value: unknown): boolean {
|
|
489
469
|
if (!value || typeof value !== "object") return false;
|
|
490
470
|
const v = value as Record<string, unknown>;
|
|
491
|
-
return
|
|
492
|
-
typeof v["safeParse"] === "function" ||
|
|
493
|
-
typeof v["parse"] === "function" ||
|
|
494
|
-
"_def" in v ||
|
|
495
|
-
"~standard" in v
|
|
496
|
-
);
|
|
471
|
+
return typeof v["safeParse"] === "function" || typeof v["parse"] === "function" || "_def" in v || "~standard" in v;
|
|
497
472
|
}
|
|
498
473
|
|
|
499
474
|
// True when a plain object has JSON Schema keywords; in: structured-output normalization, out: a boolean.
|
|
@@ -575,7 +550,9 @@ export function normalizeStructuredOutput(output: unknown): StructuredOutputSpec
|
|
|
575
550
|
* present (bare Zod schema or `{ schema: Zod }`). Used by the loop for
|
|
576
551
|
* post-parse validation; `undefined` means JSON-parse only.
|
|
577
552
|
*/
|
|
578
|
-
export function extractOutputValidator(
|
|
553
|
+
export function extractOutputValidator(
|
|
554
|
+
output: unknown
|
|
555
|
+
): { safeParse: (v: unknown) => { success: boolean; data?: unknown; error?: { message: string } } } | undefined {
|
|
579
556
|
if (!output) return undefined;
|
|
580
557
|
if (isZodLike(output) && typeof (output as { safeParse?: unknown }).safeParse === "function") {
|
|
581
558
|
return output as { safeParse: (v: unknown) => { success: boolean; data?: unknown; error?: { message: string } } };
|
|
@@ -637,9 +614,17 @@ export function parseStructuredOutput(text: string, output: unknown): unknown {
|
|
|
637
614
|
* Maps a normalized spec onto OpenAI Responses `text.format`.
|
|
638
615
|
* `{ type: json_schema, name, schema, strict }` (docs: responses/create).
|
|
639
616
|
*/
|
|
640
|
-
export function mapOutputToOpenAI(
|
|
641
|
-
|
|
642
|
-
|
|
617
|
+
export function mapOutputToOpenAI(spec: StructuredOutputSpec | undefined):
|
|
618
|
+
| {
|
|
619
|
+
format: {
|
|
620
|
+
type: "json_schema";
|
|
621
|
+
name: string;
|
|
622
|
+
schema: Record<string, unknown>;
|
|
623
|
+
strict: boolean;
|
|
624
|
+
description?: string;
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
| undefined {
|
|
643
628
|
if (!spec) return undefined;
|
|
644
629
|
return {
|
|
645
630
|
format: {
|
|
@@ -670,8 +655,10 @@ export function mapOutputToGoogle(
|
|
|
670
655
|
}
|
|
671
656
|
|
|
672
657
|
/** Chat Completions `response_format` wire shape (OpenRouter + compat). */
|
|
673
|
-
export type ChatCompletionsResponseFormat =
|
|
674
|
-
|
|
658
|
+
export type ChatCompletionsResponseFormat = {
|
|
659
|
+
type: "json_schema";
|
|
660
|
+
json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string };
|
|
661
|
+
};
|
|
675
662
|
|
|
676
663
|
/**
|
|
677
664
|
* Maps a normalized spec onto Chat Completions `response_format`.
|
|
@@ -713,8 +700,11 @@ export function mapOutputToCompatChat(
|
|
|
713
700
|
*
|
|
714
701
|
* @example `augmentSystemPromptWithStructuredOutput(sys, spec)`
|
|
715
702
|
*/
|
|
716
|
-
export function buildStructuredOutputInstruction(
|
|
717
|
-
|
|
703
|
+
export function buildStructuredOutputInstruction(
|
|
704
|
+
spec: StructuredOutputSpec,
|
|
705
|
+
template = DEFAULT_PROMPTS.structuredOutput
|
|
706
|
+
): string {
|
|
707
|
+
return fillPrompt(template, { schema: JSON.stringify(spec.schema) });
|
|
718
708
|
}
|
|
719
709
|
|
|
720
710
|
/**
|
|
@@ -724,11 +714,12 @@ export function buildStructuredOutputInstruction(spec: StructuredOutputSpec): st
|
|
|
724
714
|
*/
|
|
725
715
|
export function augmentSystemPromptWithStructuredOutput(
|
|
726
716
|
systemPrompt: string | undefined,
|
|
727
|
-
spec: StructuredOutputSpec | undefined
|
|
717
|
+
spec: StructuredOutputSpec | undefined,
|
|
718
|
+
template?: string
|
|
728
719
|
): string | undefined {
|
|
729
720
|
if (!spec) return systemPrompt;
|
|
730
|
-
const instruction = `[Structured Output]\n${buildStructuredOutputInstruction(spec)}`;
|
|
721
|
+
const instruction = `[Structured Output]\n${buildStructuredOutputInstruction(spec, template)}`;
|
|
731
722
|
if (!systemPrompt) return instruction;
|
|
732
|
-
if (systemPrompt.includes(
|
|
723
|
+
if (systemPrompt.includes(instruction)) return systemPrompt;
|
|
733
724
|
return `${systemPrompt}\n\n${instruction}`;
|
|
734
725
|
}
|
|
@@ -2,18 +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` | 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,
|
|
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
|
- 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.
|
|
14
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.
|
|
15
16
|
- `startCook` advances the next times BEFORE the run starts; a run still cooking when the next is due is skipped, never stacked.
|
|
16
|
-
- 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`).
|
|
17
18
|
- The Agent registry holds `WeakRef`s by name: keep every Agent that owns actions referenced, or its runs become `missed`.
|
|
18
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`).
|
|
19
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`.
|