agent-accelerator 0.4.0 → 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 (46) 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 +414 -118
  5. package/src/agent/context.ts +11 -16
  6. package/src/agent/delegation.ts +111 -274
  7. package/src/agent/loop.ts +327 -99
  8. package/src/agent/subagent.ts +4 -12
  9. package/src/agent/worker.ts +435 -0
  10. package/src/index.ts +58 -5
  11. package/src/models/catalog-cache.ts +65 -25
  12. package/src/models/catalog.ts +47 -2
  13. package/src/providers/google.ts +338 -81
  14. package/src/providers/openai-compat.ts +171 -61
  15. package/src/providers/openai.ts +174 -44
  16. package/src/providers/openrouter.ts +129 -44
  17. package/src/providers/shared.ts +169 -17
  18. package/src/providers.ts +375 -11
  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 +229 -113
  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 +74 -22
  31. package/src/types/model.ts +39 -0
  32. package/src/types/response.ts +9 -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/env.ts +21 -7
  38. package/src/utils/errors.ts +26 -1
  39. package/src/utils/headers.ts +15 -15
  40. package/src/utils/home.ts +30 -0
  41. package/src/utils/media.ts +57 -12
  42. package/src/utils/raw-capture.ts +76 -0
  43. package/src/utils/retry.ts +11 -3
  44. package/src/utils/safe-io.ts +252 -0
  45. package/src/utils/session.ts +7 -8
  46. package/src/data/README.md +0 -84
@@ -2,6 +2,8 @@ import * as path from "node:path";
2
2
  import * as fs from "node:fs";
3
3
  import { createHash } from "node:crypto";
4
4
  import { getEnv } from "../utils/env.ts";
5
+ import { telemetryBus } from "../telemetry.ts";
6
+ import { onHomeChange, resolveHome } from "../utils/home.ts";
5
7
 
6
8
  /** Default TTL: 12 Hours (in milliseconds). */
7
9
  export const DEFAULT_CATALOG_TTL_MS = 12 * 60 * 60 * 1000;
@@ -75,14 +77,14 @@ function notifyUpdateListeners(): void {
75
77
  }
76
78
 
77
79
  /**
78
- * Catalog cache directory. Defaults to `src/data` (repo convention); override
79
- * with `AGENT_CACHE_DIR` when embedding as a package or running on a
80
- * read-only filesystem (serverless/containers).
80
+ * Catalog cache directory: `<home>/catalog` (the single Agent Accelerator home,
81
+ * see `resolveHome`), or `AGENT_CACHE_DIR` when set (read-only or serverless
82
+ * filesystems).
81
83
  */
82
84
  export function getCacheDir(): string {
83
85
  const override = getEnv("AGENT_CACHE_DIR");
84
86
  if (override && override.trim()) return path.resolve(override.trim());
85
- return path.resolve(process.cwd(), "src/data");
87
+ return path.join(resolveHome(), "catalog");
86
88
  }
87
89
 
88
90
  /** Cache file path: `<cacheDir>/models.dev.json`, honoring AGENT_CACHE_DIR (Q-16a/Q-34). */
@@ -121,13 +123,22 @@ export function verifyCatalogSha256(text: string): boolean {
121
123
  return hex === pin;
122
124
  }
123
125
 
126
+ /** Wall-clock of the last failed network refresh (0 = none); drives the retry backoff. */
127
+ let lastRefreshFailureAt = 0;
128
+ /** After a failed refresh, `ensureModelCatalogFresh` stays quiet this long (10 min). */
129
+ export const REFRESH_FAILURE_BACKOFF_MS = 10 * 60 * 1000;
130
+ /** Network timeout for a background refresh when a usable catalog is already loaded. */
131
+ const BACKGROUND_REFRESH_TIMEOUT_MS = 8_000;
132
+
133
+ let inFlightRefresh: Promise<CatalogStatus> | null = null;
134
+
124
135
  // In-memory catalog state
125
136
  let activeCatalog: Record<string, any> = {};
126
137
  let activeFetchedAt: number | undefined = undefined;
127
138
  let activeTtlMs: number = globalCatalogTtlMs;
128
139
  let activeFromCache = false;
129
140
 
130
- // Synchronous bootstrap: load from src/data/models.dev.json if present
141
+ // Synchronous bootstrap: load <home>/catalog/models.dev.json if present
131
142
  function initializeCatalogSync(): void {
132
143
  const cachePath = getCacheFilePath();
133
144
  const cached = readJsonFileSync(cachePath);
@@ -155,6 +166,11 @@ function initializeCatalogSync(): void {
155
166
  }
156
167
 
157
168
  initializeCatalogSync();
169
+ // `setHome()` moves the cache folder: reload from the new place.
170
+ onHomeChange(() => {
171
+ initializeCatalogSync();
172
+ notifyUpdateListeners();
173
+ });
158
174
 
159
175
  /** Returns the active in-memory catalog data. */
160
176
  export function getActiveCatalog(): Record<string, any> {
@@ -216,7 +232,7 @@ export function isValidCatalogPayload(data: unknown): data is Record<string, any
216
232
 
217
233
  /**
218
234
  * Refreshes the model catalog by downloading from models.dev
219
- * directly into src/data/models.dev.json with a 12-hour TTL.
235
+ * into `<home>/catalog/models.dev.json` with a 12-hour TTL.
220
236
  */
221
237
  export async function refreshModelCatalog(options: RefreshCatalogOptions = {}): Promise<CatalogStatus> {
222
238
  const effectiveTtl = options.ttlMs ?? globalCatalogTtlMs;
@@ -247,6 +263,7 @@ export async function refreshModelCatalog(options: RefreshCatalogOptions = {}):
247
263
  let freshData: Record<string, any>;
248
264
 
249
265
  const fallbackToDiskCache = (): void => {
266
+ lastRefreshFailureAt = Date.now();
250
267
  const existing = readJsonFileSync(cachePath);
251
268
  if (existing && existing.data) {
252
269
  activeCatalog = existing.data;
@@ -309,6 +326,7 @@ export async function refreshModelCatalog(options: RefreshCatalogOptions = {}):
309
326
  clearTimeout(timer);
310
327
  }
311
328
 
329
+ lastRefreshFailureAt = 0;
312
330
  const fetchedAt = Date.now();
313
331
  const cachePayload: CatalogCacheFile = {
314
332
  fetchedAt,
@@ -326,42 +344,64 @@ export async function refreshModelCatalog(options: RefreshCatalogOptions = {}):
326
344
  activeTtlMs = effectiveTtl;
327
345
  activeFromCache = false;
328
346
 
329
- // Save to src/data/models.dev.json (or the AGENT_CACHE_DIR override)
347
+ // Save to <home>/catalog/models.dev.json (or the AGENT_CACHE_DIR override)
330
348
  try {
331
349
  writeJsonFileSync(cachePath, cachePayload);
332
350
  } catch (err) {
333
351
  activeFromCache = false;
334
- try {
335
- (globalThis as any).__agentAccelTelemetry?.emitWarning?.({
336
- code: "catalog_cache_write_failed",
337
- message: `[Agent Accelerator] Could not write model catalog cache (${err instanceof Error ? err.message : String(err)}). Continuing with the in-memory catalog.`,
338
- });
339
- } catch {}
352
+ telemetryBus.emitWarning(
353
+ "catalog_cache_write_failed",
354
+ `[Agent Accelerator] Could not write model catalog cache (${err instanceof Error ? err.message : String(err)}). Continuing with the in-memory catalog.`,
355
+ { path: cachePath }
356
+ );
340
357
  }
341
358
 
342
359
  notifyUpdateListeners();
343
360
  return getCatalogStatus();
344
361
  }
345
362
 
346
- let inFlightRefresh: Promise<CatalogStatus> | null = null;
363
+
364
+ /** True when `AGENT_CATALOG_REFRESH` is `off`/`0`/`false`: never touch the network automatically. */
365
+ function automaticRefreshDisabled(): boolean {
366
+ const v = getEnv("AGENT_CATALOG_REFRESH")?.trim().toLowerCase();
367
+ return v === "off" || v === "0" || v === "false";
368
+ }
347
369
 
348
370
  /**
349
- * Ensures that the model catalog is loaded and fresh.
350
- * Called automatically before Agent runs. If the cache is expired, triggers a refresh.
371
+ * Ensures that the model catalog is loaded and fresh. Called automatically
372
+ * before Agent runs.
373
+ *
374
+ * - Fresh and loaded: returns immediately.
375
+ * - Expired but loaded (stale-while-revalidate): returns the stale catalog at
376
+ * once and refreshes in the background with a short timeout, so a run is
377
+ * never delayed by models.dev being slow or unreachable.
378
+ * - Nothing loaded yet (cold start): waits for the download.
379
+ * - A failed refresh is not retried automatically for 10 minutes.
380
+ * - `AGENT_CATALOG_REFRESH=off` disables automatic refreshes entirely
381
+ * (air-gapped hosts, hermetic tests); `force` and `refreshModelCatalog`
382
+ * still work.
351
383
  */
352
384
  export async function ensureModelCatalogFresh(options: RefreshCatalogOptions = {}): Promise<CatalogStatus> {
353
385
  const current = getCatalogStatus();
354
- if (!current.isExpired && current.loaded && !options.force) {
355
- return current;
386
+ if (!options.force) {
387
+ if (!current.isExpired && current.loaded) return current;
388
+ if (automaticRefreshDisabled()) return current;
389
+ if (Date.now() - lastRefreshFailureAt < REFRESH_FAILURE_BACKOFF_MS) return current;
356
390
  }
357
391
 
358
- if (inFlightRefresh) {
392
+ const start = (): Promise<CatalogStatus> => {
393
+ if (!inFlightRefresh) {
394
+ const timeoutMs = options.timeoutMs ?? (current.loaded ? BACKGROUND_REFRESH_TIMEOUT_MS : undefined);
395
+ inFlightRefresh = refreshModelCatalog({ ...options, ...(timeoutMs !== undefined ? { timeoutMs } : {}) }).finally(() => {
396
+ inFlightRefresh = null;
397
+ });
398
+ }
359
399
  return inFlightRefresh;
360
- }
361
-
362
- inFlightRefresh = refreshModelCatalog(options).finally(() => {
363
- inFlightRefresh = null;
364
- });
400
+ };
365
401
 
366
- return inFlightRefresh;
402
+ if (current.loaded && !options.force) {
403
+ start().catch(() => {}); // background revalidation; failures are non-fatal
404
+ return current;
405
+ }
406
+ return start();
367
407
  }
@@ -52,6 +52,19 @@ function applyModalityOverrides(provider: string, modelId: string, base: ModelMo
52
52
  };
53
53
  }
54
54
 
55
+ /**
56
+ * Whether a lookup may fall back to the GLOBAL model index (any provider's
57
+ * entry with that id). Allowed for first-class providers and for explicit
58
+ * bare-id searches (`getModelFromCatalog(id, id)`). Denied for custom
59
+ * prefixes (`ollama/gpt-oss-20b`): a self-hosted model must not inherit the
60
+ * price, modalities or reasoning flags of an unrelated hosted model that
61
+ * happens to share its id: unknown stays unknown.
62
+ */
63
+ function allowsGlobalFallback(providerInput: string, modelIdInput: string): boolean {
64
+ const p = providerInput.toLowerCase();
65
+ return p in PROVIDER_ALIASES || p === "gemini" || p === modelIdInput.trim().toLowerCase();
66
+ }
67
+
55
68
  function normalizeModelIdForLookup(modelId: string): string {
56
69
  if (modelId.includes("/")) {
57
70
  const parts = modelId.split("/");
@@ -138,6 +151,7 @@ function mapCapabilities(raw: any, cost: ModelCost, reasoning: boolean, toolCall
138
151
  supportsExplicitCaching: cost.cache_read !== undefined,
139
152
  supportsParallelToolCalls: toolCall,
140
153
  supportsStreaming: true,
154
+ supportsStructuredOutput: raw.structured_output === undefined ? true : !!raw.structured_output,
141
155
  supportsReasoningToggle: Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "toggle"),
142
156
  supportsReasoningEffort: Array.isArray(raw.reasoning_options) && raw.reasoning_options.some((o: any) => o.type === "effort"),
143
157
  modalities: [...(modalities.input || []), ...(modalities.output || [])].filter((v: any, i: any, a: any) => a.indexOf(v) === i),
@@ -279,7 +293,11 @@ export function getModelFromCatalog(providerInput: string, modelIdInput: string)
279
293
  }
280
294
  }
281
295
 
282
- // Global O(1) fallback via indexed catalog
296
+ // Global O(1) fallback via indexed catalog (first-class providers / bare ids only)
297
+ if (!allowsGlobalFallback(providerInput, modelId)) {
298
+ setLookupCache(cacheKey, undefined);
299
+ return undefined;
300
+ }
283
301
  const index = getGlobalIndex();
284
302
  const lowerModelId = modelId.toLowerCase();
285
303
  const strippedLower = normalizeModelIdForLookup(modelId).toLowerCase();
@@ -309,7 +327,9 @@ export interface ModelThinkingInfo {
309
327
  * @example `const info = getModelThinkingInfo("google", "gemini-3.5-flash-lite");`
310
328
  */
311
329
  export function getModelThinkingInfo(provider: string, modelId: string): ModelThinkingInfo {
312
- const spec = getModelFromCatalog(provider, modelId) || getModelFromCatalog(modelId, modelId);
330
+ const spec =
331
+ getModelFromCatalog(provider, modelId) ||
332
+ (allowsGlobalFallback(provider, modelId) ? getModelFromCatalog(modelId, modelId) : undefined);
313
333
  if (!spec) {
314
334
  return {
315
335
  supportsThinking: true,
@@ -588,6 +608,30 @@ export {
588
608
  type RefreshCatalogOptions,
589
609
  } from "./catalog-cache.ts";
590
610
 
611
+ /**
612
+ * True when the model can enforce `response_format`/`text.format` natively.
613
+ * Unknown models (custom endpoints, dynamic routers) count as capable —
614
+ * the provider verdict stands, matching `assertModalitiesSupported`.
615
+ * Models with catalog `structured_output: false` return false so callers can
616
+ * emulate via prompt instruction + client validation instead of 400ing.
617
+ *
618
+ * @example `modelSupportsStructuredOutput("openrouter", "inclusionai/ling-3.0-flash-sante:free")`
619
+ */
620
+ export function modelSupportsStructuredOutput(provider: string, modelId: string): boolean {
621
+ try {
622
+ const spec = getModelFromCatalog(provider, modelId);
623
+ if (!spec) return true;
624
+ if (typeof spec.capabilities?.supportsStructuredOutput === "boolean") {
625
+ return spec.capabilities.supportsStructuredOutput;
626
+ }
627
+ const raw = (spec as { raw?: { structured_output?: unknown } }).raw;
628
+ if (raw && typeof raw.structured_output === "boolean") return raw.structured_output;
629
+ return true;
630
+ } catch {
631
+ return true;
632
+ }
633
+ }
634
+
591
635
  /**
592
636
  * Permissive placeholder so custom endpoints never fail preflight:
593
637
  * allows every ThinkingLevel (none -> max + dynamic).
@@ -622,6 +666,7 @@ export function createGenericModelSpec(provider: string, modelId: string): Model
622
666
  supportsLongCacheRetention: false,
623
667
  supportsParallelToolCalls: true,
624
668
  supportsStreaming: true,
669
+ supportsStructuredOutput: true,
625
670
  modalities: ["text"],
626
671
  },
627
672
  pricing: {},