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.
- package/README.md +149 -57
- package/bunfig.toml +1 -0
- package/package.json +2 -3
- package/src/agent/agent.ts +414 -118
- package/src/agent/context.ts +11 -16
- package/src/agent/delegation.ts +111 -274
- package/src/agent/loop.ts +327 -99
- package/src/agent/subagent.ts +4 -12
- package/src/agent/worker.ts +435 -0
- package/src/index.ts +58 -5
- package/src/models/catalog-cache.ts +65 -25
- package/src/models/catalog.ts +47 -2
- package/src/providers/google.ts +338 -81
- package/src/providers/openai-compat.ts +171 -61
- package/src/providers/openai.ts +174 -44
- package/src/providers/openrouter.ts +129 -44
- package/src/providers/shared.ts +169 -17
- package/src/providers.ts +375 -11
- package/src/scheduler/scheduled-actions.md +41 -0
- package/src/scheduler/scheduler.ts +814 -0
- package/src/scheduler/store.ts +263 -0
- package/src/scheduler/time.ts +148 -0
- package/src/scheduler/tool.ts +151 -0
- package/src/session/store.ts +229 -113
- package/src/streaming/event-stream.ts +6 -4
- package/src/telemetry-file.ts +31 -0
- package/src/telemetry.ts +65 -0
- package/src/tools/executor.ts +163 -38
- package/src/tools/subagent-result.ts +60 -0
- package/src/types/agent.ts +74 -22
- package/src/types/model.ts +39 -0
- package/src/types/response.ts +9 -0
- package/src/types/scheduler.ts +81 -0
- package/src/types/tool.ts +15 -0
- package/src/utils/cache.ts +14 -5
- package/src/utils/documents.ts +112 -52
- package/src/utils/env.ts +21 -7
- package/src/utils/errors.ts +26 -1
- package/src/utils/headers.ts +15 -15
- package/src/utils/home.ts +30 -0
- package/src/utils/media.ts +57 -12
- package/src/utils/raw-capture.ts +76 -0
- package/src/utils/retry.ts +11 -3
- package/src/utils/safe-io.ts +252 -0
- package/src/utils/session.ts +7 -8
- 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
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
402
|
+
if (current.loaded && !options.force) {
|
|
403
|
+
start().catch(() => {}); // background revalidation; failures are non-fatal
|
|
404
|
+
return current;
|
|
405
|
+
}
|
|
406
|
+
return start();
|
|
367
407
|
}
|
package/src/models/catalog.ts
CHANGED
|
@@ -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 =
|
|
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: {},
|