agent-accelerator 0.4.5 → 0.5.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 (45) hide show
  1. package/README.md +149 -57
  2. package/bunfig.toml +1 -0
  3. package/package.json +2 -3
  4. package/src/agent/agent.ts +401 -120
  5. package/src/agent/context.ts +11 -16
  6. package/src/agent/delegation.ts +111 -274
  7. package/src/agent/loop.ts +253 -98
  8. package/src/agent/subagent.ts +4 -12
  9. package/src/agent/worker.ts +435 -0
  10. package/src/index.ts +39 -5
  11. package/src/models/catalog-cache.ts +65 -25
  12. package/src/models/catalog.ts +21 -2
  13. package/src/providers/google.ts +282 -74
  14. package/src/providers/openai-compat.ts +61 -41
  15. package/src/providers/openai.ts +46 -31
  16. package/src/providers/openrouter.ts +42 -31
  17. package/src/providers/shared.ts +110 -16
  18. package/src/providers.ts +119 -6
  19. package/src/scheduler/scheduled-actions.md +41 -0
  20. package/src/scheduler/scheduler.ts +814 -0
  21. package/src/scheduler/store.ts +263 -0
  22. package/src/scheduler/time.ts +148 -0
  23. package/src/scheduler/tool.ts +151 -0
  24. package/src/session/store.ts +211 -104
  25. package/src/streaming/event-stream.ts +6 -4
  26. package/src/telemetry-file.ts +31 -0
  27. package/src/telemetry.ts +65 -0
  28. package/src/tools/executor.ts +163 -38
  29. package/src/tools/subagent-result.ts +60 -0
  30. package/src/types/agent.ts +57 -20
  31. package/src/types/model.ts +18 -0
  32. package/src/types/response.ts +2 -0
  33. package/src/types/scheduler.ts +81 -0
  34. package/src/types/tool.ts +15 -0
  35. package/src/utils/cache.ts +14 -5
  36. package/src/utils/documents.ts +112 -52
  37. package/src/utils/errors.ts +11 -0
  38. package/src/utils/headers.ts +15 -15
  39. package/src/utils/home.ts +30 -0
  40. package/src/utils/media.ts +57 -12
  41. package/src/utils/raw-capture.ts +76 -0
  42. package/src/utils/retry.ts +11 -3
  43. package/src/utils/safe-io.ts +252 -0
  44. package/src/utils/session.ts +7 -8
  45. package/src/data/README.md +0 -84
@@ -6,8 +6,11 @@
6
6
  * across the four REST transports, plus timeout/signal and id helpers.
7
7
  */
8
8
  import { toConciseProviderError } from "../utils/errors.ts";
9
+ import { withRetries } from "../utils/retry.ts";
10
+ import { redactHeadersCentral } from "../utils/cache.ts";
11
+ import { TRUNCATED_ARGS_KEY, isTruncatedArguments } from "../types/tool.ts";
9
12
 
10
- /** Parses a JSON tool-argument string; non-objects degrade to `{ raw }`. */
13
+ /** Parses a JSON tool-argument string; non-objects degrade to the truncated-arguments marker. */
11
14
  export function parseArguments(raw: string | undefined): Record<string, unknown> {
12
15
  if (!raw) return {};
13
16
  try {
@@ -15,9 +18,9 @@ export function parseArguments(raw: string | undefined): Record<string, unknown>
15
18
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
16
19
  return parsed as Record<string, unknown>;
17
20
  }
18
- return { raw };
21
+ return { [TRUNCATED_ARGS_KEY]: raw };
19
22
  } catch {
20
- return { raw };
23
+ return { [TRUNCATED_ARGS_KEY]: raw };
21
24
  }
22
25
  }
23
26
 
@@ -30,17 +33,13 @@ export function normalizeThinkingParts(parts: string[]): string | undefined {
30
33
  }
31
34
 
32
35
  /**
33
- * Redacts secret-bearing headers for the raw audit trail. Superset of the
34
- * four adapter versions: `authorization` (any case) and `x-goog-api-key` are
35
- * redacted; all other headers pass through untouched.
36
+ * Redacts secret-bearing headers for the raw audit trail via the central
37
+ * deny-list: any header whose NAME contains key / token / secret / cookie /
38
+ * authorization (so `x-api-key`, `api-key`, `cf-aig-authorization` …), plus any
39
+ * value that looks like a credential. Everything else passes through.
36
40
  */
37
41
  export function redactedHeaders(headers: Record<string, string>): Record<string, string> {
38
- const out: Record<string, string> = {};
39
- for (const [k, v] of Object.entries(headers)) {
40
- const lower = k.toLowerCase();
41
- out[k] = lower === "authorization" || lower === "x-goog-api-key" ? "[REDACTED]" : v;
42
- }
43
- return out;
42
+ return redactHeadersCentral(headers) as Record<string, string>;
44
43
  }
45
44
 
46
45
  /**
@@ -92,6 +91,30 @@ export function incompleteStreamError(provider: string, modelId: string): Error
92
91
  return err;
93
92
  }
94
93
 
94
+ /**
95
+ * True when an error signals an evicted/invalid chained interaction
96
+ * (Google Interactions `404` / `{code: not_found}` on REST, `{event_type:
97
+ * error, error: {code: not_found}}` on SSE). Structured-data only: HTTP
98
+ * status, `code`, or `errorType` — never message text.
99
+ */
100
+ export function isNotFoundChainError(error: unknown): boolean {
101
+ if (!error || typeof error !== "object") return false;
102
+ const err = error as Record<string, unknown>;
103
+ const status = Number(
104
+ (err["status"] as number | undefined) ??
105
+ (err["statusCode"] as number | undefined) ??
106
+ ((err["response"] as Record<string, unknown> | undefined)?.["status"] as number | undefined) ??
107
+ NaN
108
+ );
109
+ if (status === 404) return true;
110
+ const code = String(
111
+ (err["code"] as string | undefined) ??
112
+ (err["errorType"] as string | undefined) ??
113
+ ""
114
+ ).toLowerCase();
115
+ return code === "not_found";
116
+ }
117
+
95
118
  /** Per-request timeout ms from provider options (Q-22). */
96
119
  export function timeoutFor(options: { timeoutMs?: number } | undefined): number | undefined {
97
120
  const t = (options as { timeoutMs?: unknown } | undefined)?.timeoutMs;
@@ -152,11 +175,9 @@ export function hasAuthHeader(headers: Record<string, string> | undefined): bool
152
175
  return false;
153
176
  }
154
177
 
155
- /** True for truncated streamed tool args (`{ raw }` marker from `parseStreamedToolArguments`). */
178
+ /** True for truncated streamed tool args (the `__truncated_raw` marker from `parseStreamedToolArguments`). */
156
179
  export function hasTruncatedToolArguments(args: unknown): boolean {
157
- if (!args || typeof args !== "object" || Array.isArray(args)) return false;
158
- const keys = Object.keys(args as Record<string, unknown>);
159
- return keys.length === 1 && keys[0] === "raw" && typeof (args as Record<string, unknown>).raw === "string";
180
+ return isTruncatedArguments(args);
160
181
  }
161
182
 
162
183
  /** Short hash for tool-ID clamping (FNV-1a, 8 hex chars, no deps). */
@@ -188,3 +209,76 @@ export function clampToolCallId(id: string): string {
188
209
  const prefix = callId.slice(0, Math.max(1, 40 - hash.length - 1));
189
210
  return `${prefix}_${hash}`;
190
211
  }
212
+
213
+ /** Plain lower-cased header record from a fetch `Headers` object. */
214
+ export function headersToRecord(headers: Headers): Record<string, string> {
215
+ const out: Record<string, string> = {};
216
+ headers.forEach((v, k) => {
217
+ out[k] = v;
218
+ });
219
+ return out;
220
+ }
221
+
222
+ /**
223
+ * Parses a rate-limit hint from response headers into milliseconds:
224
+ * `retry-after-ms`, then `retry-after` (delta-seconds or HTTP-date).
225
+ * Returns undefined when absent or unparseable.
226
+ */
227
+ export function parseRetryAfterMs(headers: Record<string, string> | undefined, now = Date.now()): number | undefined {
228
+ if (!headers) return undefined;
229
+ const get = (name: string): string | undefined => {
230
+ for (const [k, v] of Object.entries(headers)) if (k.toLowerCase() === name) return v;
231
+ return undefined;
232
+ };
233
+ const ms = get("retry-after-ms");
234
+ if (ms !== undefined && ms.trim() !== "" && Number.isFinite(Number(ms)) && Number(ms) >= 0) return Math.round(Number(ms));
235
+ const ra = get("retry-after");
236
+ if (ra === undefined || ra.trim() === "") return undefined;
237
+ const secs = Number(ra);
238
+ if (Number.isFinite(secs)) return secs >= 0 ? Math.round(secs * 1000) : undefined;
239
+ const at = Date.parse(ra);
240
+ return Number.isFinite(at) ? Math.max(0, at - now) : undefined;
241
+ }
242
+
243
+ /**
244
+ * Opens an SSE response, retrying transient failures (429/5xx/network) BEFORE
245
+ * any byte is consumed, so a streaming caller gets the same resilience as
246
+ * `generate()`. Once this resolves the stream is live and never retried.
247
+ * `onHttpError` must throw (usually the adapter's concise `throwIfError`).
248
+ */
249
+ export function openStreamResponse(args: {
250
+ fetchOnce: () => Promise<Response>;
251
+ onHttpError: (res: Response, bodyText: string) => void;
252
+ noBodyError: () => Error;
253
+ retry: ReturnType<typeof retryOptionsFrom>;
254
+ signal: AbortSignal;
255
+ label: { providerId: string; modelId: string };
256
+ }): Promise<Response> {
257
+ return withRetries(
258
+ async () => {
259
+ const res = await args.fetchOnce();
260
+ if (!res.ok) {
261
+ const text = await res.text().catch(() => "");
262
+ args.onHttpError(res, text);
263
+ }
264
+ if (!res.body) throw args.noBodyError();
265
+ return res;
266
+ },
267
+ { ...args.retry, signal: args.signal, label: args.label }
268
+ );
269
+ }
270
+
271
+ /** Retry knobs from provider request options, in the shape `withRetries` expects. */
272
+ export function retryOptionsFrom(options: {
273
+ maxRetries?: number;
274
+ maxRetryDelayMs?: number;
275
+ retryBaseDelayMs?: number;
276
+ retryBudgetMs?: number;
277
+ } | undefined): { maxRetries?: number; maxRetryDelayMs?: number; baseDelayMs?: number; retryBudgetMs?: number } {
278
+ return {
279
+ maxRetries: options?.maxRetries,
280
+ maxRetryDelayMs: options?.maxRetryDelayMs,
281
+ baseDelayMs: options?.retryBaseDelayMs,
282
+ retryBudgetMs: options?.retryBudgetMs,
283
+ };
284
+ }
package/src/providers.ts CHANGED
@@ -13,6 +13,8 @@ import type { CacheConfig, ServiceTier, ThinkingLevel } from "./types/core.ts";
13
13
  import type { StructuredOutputSpec } from "./types/model.ts";
14
14
  import { zodToJsonSchema } from "./tools/schema.ts";
15
15
  import { ValidationError } from "./types/errors.ts";
16
+ import { TRUNCATED_ARGS_KEY } from "./types/tool.ts";
17
+ import { telemetryBus } from "./telemetry.ts";
16
18
 
17
19
  /** Classification of a canonical capability on a given provider. */
18
20
  export type ProviderCapabilityStatus =
@@ -50,11 +52,19 @@ export function emitProviderWarning(options: {
50
52
  if (emittedWarnings.has(key)) return;
51
53
  emittedWarnings.add(key);
52
54
  const requested = options.requested ? ` (requested: "${options.requested}")` : "";
53
- // Leading newline: warnings fire at the start of a turn's request build,
54
- // when the previous turn's streamed text may have left the terminal cursor
55
- // mid-line. Without it the warning glues onto streamed output.
56
- // Q-41 dual: telemetry bus (new) + console.warn (compat for existing captureWarnings tests; /cli logger replaces in 0.5).
57
- { const msg = `[Agent Accelerator] WARNING [${options.provider}] ${options.capability}${requested}: ${options.reason} Using ${options.fallback} instead.`; try { (globalThis as any).__agentAccelTelemetry?.emitWarning?.({ code: "provider_warning", provider: options.provider, capability: options.capability, message: msg }); } catch {} try { (globalThis as any).__agentAccelWarnSink?.(msg, options); } catch {} try { console.warn(msg); } catch {} }
55
+ // Primary channel: the telemetry bus (code `provider_warning`). The legacy
56
+ // global sink and `console.warn` remain for existing capture-based tests and
57
+ // CLIs that predate the bus.
58
+ const msg = `[Agent Accelerator] WARNING [${options.provider}] ${options.capability}${requested}: ${options.reason} Using ${options.fallback} instead.`;
59
+ telemetryBus.emitWarning("provider_warning", msg, {
60
+ provider: options.provider,
61
+ capability: options.capability,
62
+ requested: options.requested,
63
+ reason: options.reason,
64
+ fallback: options.fallback,
65
+ });
66
+ try { (globalThis as any).__agentAccelWarnSink?.(msg, options); } catch {}
67
+ try { console.warn(msg); } catch {}
58
68
  }
59
69
 
60
70
  /** Clears deduped-warning state (mainly for tests). */
@@ -490,7 +500,110 @@ export function parseStreamedToolArguments(startText: string, deltaText: string)
490
500
  // try next candidate
491
501
  }
492
502
  }
493
- return { raw: startText + deltaText };
503
+ return { [TRUNCATED_ARGS_KEY]: startText + deltaText };
504
+ }
505
+
506
+ /**
507
+ * Returns the wire string that actually parsed in
508
+ * {@link parseStreamedToolArguments} (identical candidate trial order), so
509
+ * `rawArguments` audit trails never store concatenation artifacts like
510
+ * `"{}{...}"` (empty start payload + complete delta). Falls back to the raw
511
+ * concatenation when nothing parses, preserving the truncated-call marker
512
+ * path (`{ __truncated_raw }`, never executed).
513
+ *
514
+ * @example `selectRawArguments("{}", '{"location":"Paris"}') // '{"location":"Paris"}'`
515
+ */
516
+ export function selectRawArguments(startText: string, deltaText: string): string {
517
+ const candidates = [startText + deltaText, deltaText, startText].filter((c) => c && c.trim());
518
+ for (const candidate of candidates) {
519
+ try {
520
+ const parsed: unknown = JSON.parse(candidate);
521
+ if (isPlainObject(parsed)) return candidate;
522
+ } catch {
523
+ // try next candidate
524
+ }
525
+ }
526
+ return startText + deltaText;
527
+ }
528
+
529
+ // ---------------------------------------------------------------------------
530
+ // Generation controls (maxOutputTokens / stop / seed)
531
+ // ---------------------------------------------------------------------------
532
+
533
+ /** Canonical generation controls as carried on `ProviderRequestOptions`. */
534
+ export interface GenerationParams {
535
+ maxOutputTokens?: number;
536
+ stop?: string[];
537
+ seed?: number;
538
+ }
539
+
540
+ const finiteNumber = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
541
+
542
+ /** Normalizes the public `stop` option (`string | string[]`) to a clean array, or undefined. */
543
+ export function normalizeStop(stop: string | string[] | undefined): string[] | undefined {
544
+ const list = (Array.isArray(stop) ? stop : stop === undefined ? [] : [stop]).filter(
545
+ (s): s is string => typeof s === "string" && s.length > 0
546
+ );
547
+ return list.length > 0 ? list : undefined;
548
+ }
549
+
550
+ /**
551
+ * Standard Chat Completions fields (OpenRouter + generic compat endpoints):
552
+ * `max_tokens`, `stop`, `seed`. Only set values are
553
+ * emitted, so strict endpoints never see anything the caller did not ask for.
554
+ */
555
+ export function mapGenerationParamsToChat(params: GenerationParams | undefined): Record<string, unknown> {
556
+ const out: Record<string, unknown> = {};
557
+ if (!params) return out;
558
+ if (finiteNumber(params.maxOutputTokens) && params.maxOutputTokens > 0) out["max_tokens"] = Math.floor(params.maxOutputTokens);
559
+ if (params.stop && params.stop.length > 0) out["stop"] = params.stop;
560
+ if (finiteNumber(params.seed)) out["seed"] = Math.floor(params.seed);
561
+ return out;
562
+ }
563
+
564
+ function warnUnsupportedStopSeed(provider: string, modelRef: string, params: GenerationParams, reasonSuffix: string): void {
565
+ if (params.stop && params.stop.length > 0) {
566
+ emitProviderWarning({
567
+ provider,
568
+ capability: "stop sequences",
569
+ requested: `${params.stop.join("|")} (${modelRef})`,
570
+ reason: `the ${reasonSuffix} defines no stop-sequence parameter.`,
571
+ fallback: "no stop sequences (the model decides when to stop)",
572
+ });
573
+ }
574
+ if (finiteNumber(params.seed)) {
575
+ emitProviderWarning({
576
+ provider,
577
+ capability: "seed",
578
+ requested: `${params.seed} (${modelRef})`,
579
+ reason: `the ${reasonSuffix} defines no seed parameter.`,
580
+ fallback: "non-deterministic sampling",
581
+ });
582
+ }
583
+ }
584
+
585
+ /** OpenAI Responses: `max_output_tokens` is native; `stop` and `seed` do not exist on this wire (warn + drop). */
586
+ export function mapGenerationParamsToOpenAI(
587
+ params: GenerationParams | undefined,
588
+ modelRef: string
589
+ ): { max_output_tokens?: number } {
590
+ if (!params) return {};
591
+ warnUnsupportedStopSeed("openai", modelRef, params, "OpenAI Responses API");
592
+ return finiteNumber(params.maxOutputTokens) && params.maxOutputTokens > 0
593
+ ? { max_output_tokens: Math.floor(params.maxOutputTokens) }
594
+ : {};
595
+ }
596
+
597
+ /** Google Interactions `generation_config`: `max_output_tokens` is documented; `stop` and `seed` are not (warn + drop). */
598
+ export function mapGenerationParamsToGoogle(
599
+ params: GenerationParams | undefined,
600
+ modelRef: string
601
+ ): { max_output_tokens?: number } {
602
+ if (!params) return {};
603
+ warnUnsupportedStopSeed("google", modelRef, params, "Gemini Interactions API");
604
+ return finiteNumber(params.maxOutputTokens) && params.maxOutputTokens > 0
605
+ ? { max_output_tokens: Math.floor(params.maxOutputTokens) }
606
+ : {};
494
607
  }
495
608
 
496
609
  // ---------------------------------------------------------------------------
@@ -0,0 +1,41 @@
1
+ Schedules a sub-agent (a "scheduled action") to run LATER, once or on repeat, on its own. Every run starts blank, does its task with the worker tools, and saves its answer to a file. It does NOT reply in this chat. Use it for deliveries such as "a research brief every day at 7:00 AM" or "a codebase health report every Monday at 9:00 AM".
2
+
3
+ Call get_datetime first whenever you create or reschedule, then pick DD from today's date.
4
+
5
+ COMMANDS (one tool, the `command` field)
6
+ - 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.
7
+ - list: every action with its metadata and state (waiting, cooking, missed, done).
8
+ - get: one action by `id`, with its latest run result.
9
+ - update: change instructions, task, tools, timeoutMs, role or any scheduled_action field of the action `id`.
10
+ - kill: delete the action `id` for good. If the answer says it is running, ask the user before retrying with force: true. purge: true also deletes its past run files.
11
+ - run_now: run the action `id` immediately, outside its schedule. Also fixes a missed run.
12
+ - skip: ignore a missed run of `id` and wait for the next scheduled time.
13
+
14
+ SCHEDULED_ACTION FIELDS
15
+ - name: human label.
16
+ - 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
+ - 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.
18
+ - meridiem: "AM" or "PM" for timeToDeliver.
19
+ - startCookingInAdvance: "DD:HH:MM" as a duration (HH 00-23): how long before delivery the run starts. "00:00:10" = 10 minutes, "00:01:00" = 1 hour.
20
+ - repeatEvery (optional): "DD:HH:MM" gap between deliveries. "01:00:00" = every day, "07:00:00" = every week. Omit to run once.
21
+ - retries (optional): extra attempts if a run fails, used only while there is still time before delivery. Default 0.
22
+
23
+ EXAMPLES
24
+ Daily brief, first delivery tomorrow at 7:00 AM, starting 30 minutes ahead:
25
+ {"command":"create","instructions":"You write a short, sourced AI research brief.","task":"Summarize the most important AI news of the last day in 5 bullets.","scheduled_action":{"name":"Daily AI Research Brief","id":"daily-ai-brief","timeToDeliver":"01:07:00","meridiem":"AM","startCookingInAdvance":"00:00:30","repeatEvery":"01:00:00"}}
26
+
27
+ Weekly report, every Monday at 9:00 AM (use get_datetime to count the days to the next Monday), starting 2 hours ahead:
28
+ {"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"}}
29
+
30
+ TIMING ADVICE (do not guess; talk to the user)
31
+ - A run must finish before the next one is due, so startCookingInAdvance must be shorter than repeatEvery.
32
+ - 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.
33
+ - 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.
34
+ - A run still cooking when the next one is due is skipped, never stacked, so a gap that is too small means lost runs.
35
+
36
+ RULES
37
+ - Each run starts blank and remembers nothing from earlier runs: put everything the worker needs in instructions and task.
38
+ - Results go to files, not into this chat. Tell the user the action is scheduled and when it will be delivered.
39
+ - Never put secrets in instructions or task: they are stored as plain text.
40
+ - Actions run only while the host program is running.
41
+ - Worker tools you may grant through `tools`: {{TOOLS}}.