agent-accelerator 0.5.9 → 0.6.0
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 +3 -1
- 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 +366 -161
- package/src/agent/architecture.md +13 -12
- package/src/agent/config-blocks.ts +39 -12
- package/src/agent/context.ts +8 -3
- package/src/agent/delegation.ts +82 -29
- package/src/agent/loop.ts +444 -365
- package/src/agent/subagent.ts +1 -3
- package/src/agent/subagents-tool.ts +45 -7
- package/src/agent/subagents.md +3 -0
- package/src/agent/turns.ts +3 -1
- package/src/agent/worker.ts +56 -27
- package/src/agent/workers.ts +113 -20
- package/src/architecture.md +16 -15
- package/src/index.ts +34 -10
- package/src/models/architecture.md +4 -3
- package/src/models/catalog-cache.ts +6 -5
- package/src/models/catalog.ts +39 -30
- package/src/providers/architecture.md +10 -8
- package/src/providers/google.ts +147 -59
- package/src/providers/openai-compat.ts +57 -49
- package/src/providers/openai.ts +54 -37
- package/src/providers/openrouter.ts +85 -30
- package/src/providers/registry.ts +6 -21
- package/src/providers/shared.ts +87 -33
- package/src/providers.ts +56 -115
- package/src/scheduler/architecture.md +7 -6
- package/src/scheduler/scheduled-actions.md +4 -0
- package/src/scheduler/scheduler.ts +186 -60
- package/src/scheduler/store.ts +23 -10
- package/src/scheduler/time.ts +17 -3
- package/src/scheduler/tool.ts +33 -7
- package/src/session/architecture.md +7 -6
- package/src/session/journal.ts +38 -12
- package/src/session/store.ts +122 -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 +8 -7
- package/src/tools/executor.ts +50 -48
- package/src/tools/schema.ts +10 -6
- package/src/tools/subagent-result.ts +6 -1
- package/src/tools/tool.ts +9 -11
- package/src/types/agent.ts +2 -21
- package/src/types/architecture.md +11 -10
- package/src/types/core.ts +1 -9
- package/src/types/errors.ts +26 -4
- package/src/types/model.ts +0 -4
- package/src/types/tool.ts +12 -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 +6 -14
- 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
|
@@ -18,12 +18,7 @@ import { TRUNCATED_ARGS_KEY } from "./types/tool.ts";
|
|
|
18
18
|
import { telemetryBus } from "./telemetry.ts";
|
|
19
19
|
|
|
20
20
|
/** Classification of a canonical capability on a given provider. */
|
|
21
|
-
export type ProviderCapabilityStatus =
|
|
22
|
-
| "NATIVE"
|
|
23
|
-
| "TRANSLATED"
|
|
24
|
-
| "AUTOMATIC"
|
|
25
|
-
| "EMULATED"
|
|
26
|
-
| "UNSUPPORTED";
|
|
21
|
+
export type ProviderCapabilityStatus = "NATIVE" | "TRANSLATED" | "AUTOMATIC" | "EMULATED" | "UNSUPPORTED";
|
|
27
22
|
|
|
28
23
|
/**
|
|
29
24
|
* Single warning mechanism for provider capability differences.
|
|
@@ -65,8 +60,12 @@ export function emitProviderWarning(options: {
|
|
|
65
60
|
reason: options.reason,
|
|
66
61
|
fallback: options.fallback,
|
|
67
62
|
});
|
|
68
|
-
try {
|
|
69
|
-
|
|
63
|
+
try {
|
|
64
|
+
(globalThis as any).__agentAccelWarnSink?.(msg, options);
|
|
65
|
+
} catch {}
|
|
66
|
+
try {
|
|
67
|
+
console.warn(msg);
|
|
68
|
+
} catch {}
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
/** Clears deduped-warning state (mainly for tests). */
|
|
@@ -124,9 +123,7 @@ export function clearSessionRouting(sessionId?: string): void {
|
|
|
124
123
|
* Maps a canonical ThinkingLevel onto Google Interactions `thinking_level`.
|
|
125
124
|
* Google values: minimal|low|medium|high (no off switch, no xhigh/max).
|
|
126
125
|
*/
|
|
127
|
-
export function mapThinkingLevelToGoogle(
|
|
128
|
-
level: ThinkingLevel | string | undefined
|
|
129
|
-
): { thinkingLevel?: string } {
|
|
126
|
+
export function mapThinkingLevelToGoogle(level: ThinkingLevel | string | undefined): { thinkingLevel?: string } {
|
|
130
127
|
if (!level) return {};
|
|
131
128
|
const norm = String(level).toLowerCase().trim();
|
|
132
129
|
if (norm === "dynamic") return {}; // server dynamic default (NATIVE)
|
|
@@ -171,9 +168,7 @@ export function mapServiceTierToGoogle(tier: ServiceTier | undefined): "flex" |
|
|
|
171
168
|
* the discontinued Responses skin, which clamped `xhigh` — everything passes
|
|
172
169
|
* through. `max` rides the same verbatim path. `dynamic` omits (server default).
|
|
173
170
|
*/
|
|
174
|
-
export function mapThinkingLevelToOpenRouterChat(
|
|
175
|
-
level: ThinkingLevel | string | undefined
|
|
176
|
-
): { effort?: string } {
|
|
171
|
+
export function mapThinkingLevelToOpenRouterChat(level: ThinkingLevel | string | undefined): { effort?: string } {
|
|
177
172
|
if (!level) return {};
|
|
178
173
|
const norm = String(level).toLowerCase().trim();
|
|
179
174
|
if (norm === "dynamic") return {}; // server default
|
|
@@ -216,9 +211,16 @@ export function resolveCache(
|
|
|
216
211
|
}
|
|
217
212
|
|
|
218
213
|
/** 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(
|
|
214
|
+
export function warnIgnoredMediaResolution(
|
|
215
|
+
provider: string,
|
|
216
|
+
messages: Message[],
|
|
217
|
+
modelRef: string,
|
|
218
|
+
kinds: Array<"image" | "video"> = ["image", "video"]
|
|
219
|
+
): void {
|
|
220
220
|
const used = messages.some(
|
|
221
|
-
(m) =>
|
|
221
|
+
(m) =>
|
|
222
|
+
Array.isArray(m.content) &&
|
|
223
|
+
m.content.some((p) => (p.type === "image" || p.type === "video") && kinds.includes(p.type) && p.resolution)
|
|
222
224
|
);
|
|
223
225
|
if (!used) return;
|
|
224
226
|
emitProviderWarning({
|
|
@@ -231,11 +233,7 @@ export function warnIgnoredMediaResolution(provider: string, messages: Message[]
|
|
|
231
233
|
}
|
|
232
234
|
|
|
233
235
|
/** OpenRouter Chat Completions `tool_choice` wire values. */
|
|
234
|
-
export type OpenRouterChatToolChoice =
|
|
235
|
-
| "auto"
|
|
236
|
-
| "none"
|
|
237
|
-
| "required"
|
|
238
|
-
| { type: "function"; function: { name: string } };
|
|
236
|
+
export type OpenRouterChatToolChoice = "auto" | "none" | "required" | { type: "function"; function: { name: string } };
|
|
239
237
|
|
|
240
238
|
/**
|
|
241
239
|
* Maps canonical toolChoice onto Chat Completions `tool_choice`.
|
|
@@ -260,9 +258,7 @@ export function mapToolChoiceToOpenRouterChat(
|
|
|
260
258
|
* medium). Unlike OpenRouter (which clamps xhigh), OpenAI documents xhigh
|
|
261
259
|
* and max natively so they pass through verbatim. `dynamic` omits (server default).
|
|
262
260
|
*/
|
|
263
|
-
export function mapThinkingLevelToOpenAI(
|
|
264
|
-
level: ThinkingLevel | string | undefined
|
|
265
|
-
): { effort?: string } {
|
|
261
|
+
export function mapThinkingLevelToOpenAI(level: ThinkingLevel | string | undefined): { effort?: string } {
|
|
266
262
|
if (!level) return {};
|
|
267
263
|
const norm = String(level).toLowerCase().trim();
|
|
268
264
|
if (norm === "dynamic") return {}; // server default
|
|
@@ -386,98 +382,36 @@ export function selectRawArguments(startText: string, deltaText: string): string
|
|
|
386
382
|
}
|
|
387
383
|
|
|
388
384
|
// ---------------------------------------------------------------------------
|
|
389
|
-
// Generation controls (maxOutputTokens
|
|
385
|
+
// Generation controls (maxOutputTokens)
|
|
390
386
|
// ---------------------------------------------------------------------------
|
|
391
387
|
|
|
392
388
|
/** Canonical generation controls as carried on `ProviderRequestOptions`. */
|
|
393
389
|
export interface GenerationParams {
|
|
394
390
|
maxOutputTokens?: number;
|
|
395
|
-
stop?: string[];
|
|
396
|
-
seed?: number;
|
|
397
391
|
}
|
|
398
392
|
|
|
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;
|
|
393
|
+
// The output-token cap as a whole number above zero, else undefined; in: the three generation-param mappers, out: their wire field.
|
|
394
|
+
function outputTokenCap(params: GenerationParams | undefined): number | undefined {
|
|
395
|
+
const n = params?.maxOutputTokens;
|
|
396
|
+
return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
|
|
410
397
|
}
|
|
411
398
|
|
|
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;
|
|
399
|
+
/** Chat Completions (OpenRouter + generic compat endpoints): `max_tokens`, only when the caller set it, so strict endpoints never see anything unasked. */
|
|
400
|
+
export function mapGenerationParamsToChat(params: GenerationParams | undefined): { max_tokens?: number } {
|
|
401
|
+
const cap = outputTokenCap(params);
|
|
402
|
+
return cap === undefined ? {} : { max_tokens: cap };
|
|
435
403
|
}
|
|
436
404
|
|
|
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
|
-
}
|
|
405
|
+
/** OpenAI Responses: `max_output_tokens`. */
|
|
406
|
+
export function mapGenerationParamsToOpenAI(params: GenerationParams | undefined): { max_output_tokens?: number } {
|
|
407
|
+
const cap = outputTokenCap(params);
|
|
408
|
+
return cap === undefined ? {} : { max_output_tokens: cap };
|
|
457
409
|
}
|
|
458
410
|
|
|
459
|
-
/**
|
|
460
|
-
export function
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
: {};
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/** Google Interactions `generation_config`: `max_output_tokens` is documented; `stop` and `seed` are not (warn + drop). */
|
|
472
|
-
export function mapGenerationParamsToGoogle(
|
|
473
|
-
params: GenerationParams | undefined,
|
|
474
|
-
modelRef: string
|
|
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
|
-
: {};
|
|
411
|
+
/** Google Interactions `generation_config`: `max_output_tokens`. */
|
|
412
|
+
export function mapGenerationParamsToGoogle(params: GenerationParams | undefined): { max_output_tokens?: number } {
|
|
413
|
+
const cap = outputTokenCap(params);
|
|
414
|
+
return cap === undefined ? {} : { max_output_tokens: cap };
|
|
481
415
|
}
|
|
482
416
|
|
|
483
417
|
// ---------------------------------------------------------------------------
|
|
@@ -488,12 +422,7 @@ export function mapGenerationParamsToGoogle(
|
|
|
488
422
|
function isZodLike(value: unknown): boolean {
|
|
489
423
|
if (!value || typeof value !== "object") return false;
|
|
490
424
|
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
|
-
);
|
|
425
|
+
return typeof v["safeParse"] === "function" || typeof v["parse"] === "function" || "_def" in v || "~standard" in v;
|
|
497
426
|
}
|
|
498
427
|
|
|
499
428
|
// True when a plain object has JSON Schema keywords; in: structured-output normalization, out: a boolean.
|
|
@@ -575,7 +504,9 @@ export function normalizeStructuredOutput(output: unknown): StructuredOutputSpec
|
|
|
575
504
|
* present (bare Zod schema or `{ schema: Zod }`). Used by the loop for
|
|
576
505
|
* post-parse validation; `undefined` means JSON-parse only.
|
|
577
506
|
*/
|
|
578
|
-
export function extractOutputValidator(
|
|
507
|
+
export function extractOutputValidator(
|
|
508
|
+
output: unknown
|
|
509
|
+
): { safeParse: (v: unknown) => { success: boolean; data?: unknown; error?: { message: string } } } | undefined {
|
|
579
510
|
if (!output) return undefined;
|
|
580
511
|
if (isZodLike(output) && typeof (output as { safeParse?: unknown }).safeParse === "function") {
|
|
581
512
|
return output as { safeParse: (v: unknown) => { success: boolean; data?: unknown; error?: { message: string } } };
|
|
@@ -637,9 +568,17 @@ export function parseStructuredOutput(text: string, output: unknown): unknown {
|
|
|
637
568
|
* Maps a normalized spec onto OpenAI Responses `text.format`.
|
|
638
569
|
* `{ type: json_schema, name, schema, strict }` (docs: responses/create).
|
|
639
570
|
*/
|
|
640
|
-
export function mapOutputToOpenAI(
|
|
641
|
-
|
|
642
|
-
|
|
571
|
+
export function mapOutputToOpenAI(spec: StructuredOutputSpec | undefined):
|
|
572
|
+
| {
|
|
573
|
+
format: {
|
|
574
|
+
type: "json_schema";
|
|
575
|
+
name: string;
|
|
576
|
+
schema: Record<string, unknown>;
|
|
577
|
+
strict: boolean;
|
|
578
|
+
description?: string;
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
| undefined {
|
|
643
582
|
if (!spec) return undefined;
|
|
644
583
|
return {
|
|
645
584
|
format: {
|
|
@@ -670,8 +609,10 @@ export function mapOutputToGoogle(
|
|
|
670
609
|
}
|
|
671
610
|
|
|
672
611
|
/** Chat Completions `response_format` wire shape (OpenRouter + compat). */
|
|
673
|
-
export type ChatCompletionsResponseFormat =
|
|
674
|
-
|
|
612
|
+
export type ChatCompletionsResponseFormat = {
|
|
613
|
+
type: "json_schema";
|
|
614
|
+
json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string };
|
|
615
|
+
};
|
|
675
616
|
|
|
676
617
|
/**
|
|
677
618
|
* Maps a normalized spec onto Chat Completions `response_format`.
|
|
@@ -2,14 +2,15 @@
|
|
|
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` + `scheduled-actions.md` | The model-facing `scheduled_actions` tool and `get_datetime`; the `.md` is the tool description.
|
|
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, and the public functions (`createScheduledAction`, `list/update/run/skip/deleteScheduledAction`). |
|
|
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` + `scheduled-actions.md` | The model-facing `scheduled_actions` tool and `get_datetime`; the `.md` is the tool description. |
|
|
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.
|
|
@@ -3,6 +3,7 @@ Schedules a sub-agent (a "scheduled action") to run LATER, once or on repeat, on
|
|
|
3
3
|
Call get_datetime first whenever you create or reschedule, then pick DD from today's date.
|
|
4
4
|
|
|
5
5
|
COMMANDS (one tool, the `command` field)
|
|
6
|
+
|
|
6
7
|
- create: schedule a new action. Needs instructions, task and scheduled_action.{name, id, timeToDeliver, meridiem, startCookingInAdvance}. Optional: role, tools, timeoutMs, scheduled_action.repeatEvery, scheduled_action.retries, scheduled_action.onMissed.
|
|
7
8
|
- list: every action with its metadata and state (waiting, cooking, missed, done).
|
|
8
9
|
- get: one action by `id`, with its latest run result.
|
|
@@ -12,6 +13,7 @@ COMMANDS (one tool, the `command` field)
|
|
|
12
13
|
- skip: ignore a missed run of `id` and wait for the next scheduled time.
|
|
13
14
|
|
|
14
15
|
SCHEDULED_ACTION FIELDS
|
|
16
|
+
|
|
15
17
|
- name: human label.
|
|
16
18
|
- id: lowercase kebab-case, at most 48 characters, e.g. "daily-ai-brief". The SDK appends 6 characters ("daily-ai-brief-d64aj2"); use that full id in every later command.
|
|
17
19
|
- timeToDeliver: "DD:HH:MM". DD = days from today (00 today, 01 tomorrow, 55 = 55 days ahead). HH:MM = clock time on a 12-hour clock, HH 01-12.
|
|
@@ -30,12 +32,14 @@ Weekly report, every Monday at 9:00 AM (use get_datetime to count the days to th
|
|
|
30
32
|
{"command":"create","instructions":"You review a codebase and report its health.","task":"Report test health, open risks and notable changes this week.","scheduled_action":{"name":"Weekly Codebase Health Report","id":"weekly-code-health","timeToDeliver":"03:09:00","meridiem":"AM","startCookingInAdvance":"00:02:00","repeatEvery":"07:00:00"}}
|
|
31
33
|
|
|
32
34
|
TIMING ADVICE (do not guess; talk to the user)
|
|
35
|
+
|
|
33
36
|
- A run must finish before the next one is due, so startCookingInAdvance must be shorter than repeatEvery.
|
|
34
37
|
- Pick the lead time from the job: a quick lookup fits the 10-minute lead ("00:00:10"); research with many tools needs much more.
|
|
35
38
|
- If the job looks too heavy for the gap the user asked for, do not schedule it blindly. Say so and suggest a wider gap, an earlier start, or a lighter task.
|
|
36
39
|
- A run still cooking when the next one is due is skipped, never stacked, so a gap that is too small means lost runs.
|
|
37
40
|
|
|
38
41
|
RULES
|
|
42
|
+
|
|
39
43
|
- Each run starts blank and remembers nothing from earlier runs: put everything the worker needs in instructions and task.
|
|
40
44
|
- Results go to files, not into this chat. Tell the user the action is scheduled and when it will be delivered.
|
|
41
45
|
- Never put secrets in instructions or task: they are stored as plain text.
|