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
@@ -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("/");
@@ -280,7 +293,11 @@ export function getModelFromCatalog(providerInput: string, modelIdInput: string)
280
293
  }
281
294
  }
282
295
 
283
- // 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
+ }
284
301
  const index = getGlobalIndex();
285
302
  const lowerModelId = modelId.toLowerCase();
286
303
  const strippedLower = normalizeModelIdForLookup(modelId).toLowerCase();
@@ -310,7 +327,9 @@ export interface ModelThinkingInfo {
310
327
  * @example `const info = getModelThinkingInfo("google", "gemini-3.5-flash-lite");`
311
328
  */
312
329
  export function getModelThinkingInfo(provider: string, modelId: string): ModelThinkingInfo {
313
- const spec = getModelFromCatalog(provider, modelId) || getModelFromCatalog(modelId, modelId);
330
+ const spec =
331
+ getModelFromCatalog(provider, modelId) ||
332
+ (allowsGlobalFallback(provider, modelId) ? getModelFromCatalog(modelId, modelId) : undefined);
314
333
  if (!spec) {
315
334
  return {
316
335
  supportsThinking: true,