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.
Files changed (67) hide show
  1. package/README.md +3 -1
  2. package/examples/prompts/SYSTEM_PROMPT.md +2 -0
  3. package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
  4. package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
  5. package/package.json +4 -1
  6. package/src/agent/agent.ts +366 -161
  7. package/src/agent/architecture.md +13 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +8 -3
  10. package/src/agent/delegation.ts +82 -29
  11. package/src/agent/loop.ts +444 -365
  12. package/src/agent/subagent.ts +1 -3
  13. package/src/agent/subagents-tool.ts +45 -7
  14. package/src/agent/subagents.md +3 -0
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/worker.ts +56 -27
  17. package/src/agent/workers.ts +113 -20
  18. package/src/architecture.md +16 -15
  19. package/src/index.ts +34 -10
  20. package/src/models/architecture.md +4 -3
  21. package/src/models/catalog-cache.ts +6 -5
  22. package/src/models/catalog.ts +39 -30
  23. package/src/providers/architecture.md +10 -8
  24. package/src/providers/google.ts +147 -59
  25. package/src/providers/openai-compat.ts +57 -49
  26. package/src/providers/openai.ts +54 -37
  27. package/src/providers/openrouter.ts +85 -30
  28. package/src/providers/registry.ts +6 -21
  29. package/src/providers/shared.ts +87 -33
  30. package/src/providers.ts +56 -115
  31. package/src/scheduler/architecture.md +7 -6
  32. package/src/scheduler/scheduled-actions.md +4 -0
  33. package/src/scheduler/scheduler.ts +186 -60
  34. package/src/scheduler/store.ts +23 -10
  35. package/src/scheduler/time.ts +17 -3
  36. package/src/scheduler/tool.ts +33 -7
  37. package/src/session/architecture.md +7 -6
  38. package/src/session/journal.ts +38 -12
  39. package/src/session/store.ts +122 -61
  40. package/src/session/usage.ts +58 -9
  41. package/src/streaming/architecture.md +4 -3
  42. package/src/streaming/event-stream.ts +26 -12
  43. package/src/telemetry.ts +6 -2
  44. package/src/tools/architecture.md +8 -7
  45. package/src/tools/executor.ts +50 -48
  46. package/src/tools/schema.ts +10 -6
  47. package/src/tools/subagent-result.ts +6 -1
  48. package/src/tools/tool.ts +9 -11
  49. package/src/types/agent.ts +2 -21
  50. package/src/types/architecture.md +11 -10
  51. package/src/types/core.ts +1 -9
  52. package/src/types/errors.ts +26 -4
  53. package/src/types/model.ts +0 -4
  54. package/src/types/tool.ts +12 -2
  55. package/src/update-models.ts +5 -5
  56. package/src/utils/architecture.md +16 -15
  57. package/src/utils/cache.ts +1 -3
  58. package/src/utils/durable.ts +17 -5
  59. package/src/utils/env.ts +9 -7
  60. package/src/utils/errors.ts +6 -14
  61. package/src/utils/headers.ts +3 -10
  62. package/src/utils/media.ts +23 -8
  63. package/src/utils/raw-capture.ts +9 -2
  64. package/src/utils/retry.ts +15 -3
  65. package/src/utils/safe-io.ts +29 -7
  66. package/src/utils/serialization.ts +1 -4
  67. package/src/utils/session.ts +1 -2
@@ -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
- bodyText: string
54
- ): { message: string; code?: string | number; errorType?: string; raw?: string; limitSource?: string } {
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 = (first as { error?: { message?: string; code?: string | number; type?: string; status?: string | number; metadata?: { error_type?: string; raw?: unknown; limit_source?: unknown } } })?.error;
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 = typeof rawCandidate === "string" && rawCandidate.trim() ? rawCandidate.trim().slice(0, 500) : undefined;
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((c) => String(c ?? "").toLowerCase() === "not_found");
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(signal?: AbortSignal, timeoutMs?: number): { signal: AbortSignal | undefined; cleanup: () => void } {
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 { ctrl.abort(); } catch {}
145
+ try {
146
+ ctrl.abort();
147
+ } catch {}
129
148
  }, timeoutMs);
130
- try { (timer as unknown as { unref?: () => void }).unref?.(); } catch {}
149
+ try {
150
+ (timer as unknown as { unref?: () => void }).unref?.();
151
+ } catch {}
131
152
  const cleanup = () => {
132
- try { clearTimeout(timer); } catch {}
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("abort", () => {
140
- cleanup();
141
- try { ctrl.abort(signal.reason); } catch {}
142
- }, { once: true });
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 ((lower === "authorization" || lower === "cf-aig-authorization") && typeof v === "string" && v.trim().length > 0) {
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(calls: Iterable<{ startArgs: string; deltaArgs: string }>, toolCalls: Array<{ arguments: unknown }>): boolean {
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)) ? "length" : "tool_calls";
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) return Math.round(Number(ms));
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(options: {
291
- maxRetries?: number;
292
- maxRetryDelayMs?: number;
293
- retryBaseDelayMs?: number;
294
- retryBudgetMs?: number;
295
- } | undefined): { maxRetries?: number; maxRetryDelayMs?: number; baseDelayMs?: number; retryBudgetMs?: number } {
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(eventStream: AssistantMessageEventStream, signal: AbortSignal | undefined): { linked: AbortController; release: () => void } {
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>(controller: AbortController, ms: number | undefined, run: () => Promise<T>): Promise<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 = (typeof contentType === "string" && (contentType.includes("event-stream") || contentType.startsWith("text/"))) || a.framesSeen > 0 || a.badFrames > 0;
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 = linked.signal.aborted || eventStream.isCancelled() || (raw as { name?: string }).name === "AbortError";
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 { (globalThis as any).__agentAccelWarnSink?.(msg, options); } catch {}
69
- try { console.warn(msg); } catch {}
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(provider: string, messages: Message[], modelRef: string, kinds: Array<"image" | "video"> = ["image", "video"]): void {
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) => Array.isArray(m.content) && m.content.some((p) => (p.type === "image" || p.type === "video") && kinds.includes(p.type) && p.resolution)
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 / stop / seed)
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
- /** Most stop sequences a Chat Completions request may carry (OpenRouter chat-completion api-ref `maxItems`, OpenAI the same). */
400
- const MAX_CHAT_STOP = 4;
401
- // True for a finite number; in: the generation-param mappers, out: a type guard.
402
- const finiteNumber = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
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
- * Standard Chat Completions fields (OpenRouter + generic compat endpoints):
414
- * `max_tokens`, `stop`, `seed`. Only set values are
415
- * emitted, so strict endpoints never see anything the caller did not ask for.
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
- // Warns that `stop`/`seed` have no parameter on this wire and are dropped; in: the OpenAI and Google mappers, out: provider_warning events.
438
- function warnUnsupportedStopSeed(provider: string, modelRef: string, params: GenerationParams, reasonSuffix: string): void {
439
- if (params.stop && params.stop.length > 0) {
440
- emitProviderWarning({
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
- /** 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
- : {};
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(output: unknown): { safeParse: (v: unknown) => { success: boolean; data?: unknown; error?: { message: string } } } | undefined {
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
- spec: StructuredOutputSpec | undefined
642
- ): { format: { type: "json_schema"; name: string; schema: Record<string, unknown>; strict: boolean; description?: string } } | undefined {
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
- | { type: "json_schema"; json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string } };
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 | 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. |
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.