agent-accelerator 0.2.0 → 0.4.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 +94 -14
- package/package.json +1 -1
- package/src/agent/agent.ts +1010 -104
- package/src/agent/context.ts +35 -0
- package/src/agent/delegation.ts +758 -40
- package/src/agent/loop.ts +811 -102
- package/src/agent/subagent.ts +59 -0
- package/src/index.ts +186 -59
- package/src/models/catalog-cache.ts +68 -25
- package/src/models/catalog.ts +101 -21
- package/src/providers/google.ts +99 -39
- package/src/providers/openai-compat.ts +140 -80
- package/src/providers/openai.ts +149 -69
- package/src/providers/openrouter.ts +106 -71
- package/src/providers/registry.ts +16 -12
- package/src/providers/shared.ts +132 -0
- package/src/providers.ts +14 -13
- package/src/session/store.ts +1285 -0
- package/src/streaming/event-stream.ts +156 -41
- package/src/tools/executor.ts +252 -16
- package/src/tools/tool.ts +147 -3
- package/src/types/agent.ts +253 -2
- package/src/types/core.ts +2 -1
- package/src/types/errors.ts +133 -0
- package/src/types/model.ts +1 -1
- package/src/types/response.ts +150 -3
- package/src/types/tool.ts +71 -0
- package/src/update-models.ts +8 -2
- package/src/utils/cache.ts +68 -1
- package/src/utils/env.ts +2 -6
- package/src/utils/headers.ts +153 -19
- package/src/utils/retry.ts +27 -13
- package/src/utils/session.ts +320 -0
- package/src/providers/openrouter-responses.ts +0 -949
- package/src/types/provider-payloads.ts +0 -352
package/src/agent/subagent.ts
CHANGED
|
@@ -7,13 +7,44 @@ import { getSubModel, getModel } from "../utils/env.ts";
|
|
|
7
7
|
/** AgentConfig alias for a delegated worker agent. */
|
|
8
8
|
export interface SubAgentConfig extends AgentConfig {}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Canonical env-based construction (Q-56): builds an `Agent` reading
|
|
12
|
+
* `MODEL`/`SUB_AGENT_MODEL` only for the fields the caller omits, via
|
|
13
|
+
* `getModel()`/`getSubModel()`. Explicit config always wins; constructing an
|
|
14
|
+
* `Agent` with an explicit `model` reads no env for routing. The static
|
|
15
|
+
* `Agent.fromEnv()` (attached below without touching `agent.ts`, owned by
|
|
16
|
+
* another audit lane) delegates here.
|
|
17
|
+
*/
|
|
18
|
+
export function agentFromEnv(
|
|
19
|
+
config?: Omit<AgentConfig, "model"> & { model?: AgentConfig["model"] }
|
|
20
|
+
): Agent {
|
|
21
|
+
return new Agent({
|
|
22
|
+
...config,
|
|
23
|
+
model: config?.model ?? getModel() ?? getSubModel(),
|
|
24
|
+
} as AgentConfig);
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
/**
|
|
11
28
|
* SubAgent is a specialized Agent designed for modular delegation,
|
|
12
29
|
* evaluation, reviewing, or parallel task execution.
|
|
13
30
|
*
|
|
31
|
+
* @deprecated Prefer composing a plain `Agent` and exposing it via
|
|
32
|
+
* `agentToTool()`/`buildAgentTools()` (or `subagents: [...]`) instead of
|
|
33
|
+
* subclassing. `SubAgent` stays for compat and receives no new features.
|
|
34
|
+
*
|
|
14
35
|
* It inherits 100% of the capabilities of Agent, defaults to SUB_AGENT_MODEL,
|
|
15
36
|
* and provides .asTool() and .toTool() for seamless registration into parent agents.
|
|
16
37
|
*
|
|
38
|
+
* Model fallback chain (Q-56, canonical): explicit `config.model` ??
|
|
39
|
+
* `SUB_AGENT_MODEL` (`getSubModel()`) ?? `MODEL` (`getModel()`). Missing
|
|
40
|
+
* everything throws. Constructing any `Agent`/`SubAgent` with an explicit
|
|
41
|
+
* `model` reads no env for routing.
|
|
42
|
+
*
|
|
43
|
+
* Q-47 decomposition boundary: this class stays a thin wrapper. The run/stream
|
|
44
|
+
* loops live in `agent.ts`/`loop.ts`, spawning in `delegation.ts`
|
|
45
|
+
* (`agentToTool`), and model parsing in `utils/session.ts` (`parseModelRef`).
|
|
46
|
+
* No loop, transport, or formatting logic is duplicated here.
|
|
47
|
+
*
|
|
17
48
|
* @example
|
|
18
49
|
* ```ts
|
|
19
50
|
* const researcher = new SubAgent({
|
|
@@ -81,3 +112,31 @@ export class SubAgent extends Agent {
|
|
|
81
112
|
return this.asTool(nameOverride, descriptionOverride);
|
|
82
113
|
}
|
|
83
114
|
}
|
|
115
|
+
|
|
116
|
+
// Q-56: `Agent.fromEnv()` static without touching `agent.ts` (owned by
|
|
117
|
+
// another lane). Wired at module load; typed via the declaration below so
|
|
118
|
+
// `Agent.fromEnv({...})` typechecks wherever this module is imported.
|
|
119
|
+
declare module "./agent.ts" {
|
|
120
|
+
namespace Agent {
|
|
121
|
+
/**
|
|
122
|
+
* Builds an `Agent` from env (`MODEL`/`SUB_AGENT_MODEL`) for omitted
|
|
123
|
+
* fields. Explicit config always wins. See `agentFromEnv`.
|
|
124
|
+
*/
|
|
125
|
+
function fromEnv(
|
|
126
|
+
config?: Omit<import("../types/agent.ts").AgentConfig, "model"> & {
|
|
127
|
+
model?: import("../types/agent.ts").AgentConfig["model"];
|
|
128
|
+
}
|
|
129
|
+
): Agent;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const Ctor = Agent as unknown as {
|
|
135
|
+
fromEnv?: (config?: SubAgentConfig) => Agent;
|
|
136
|
+
};
|
|
137
|
+
if (typeof Ctor.fromEnv !== "function") {
|
|
138
|
+
Ctor.fromEnv = (config?: SubAgentConfig) => agentFromEnv(config);
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// why: static attachment must never break module load (host may freeze Agent).
|
|
142
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,59 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import type { AssistantMessageEventStream as AssistantMessageEventStreamT } from "./streaming/event-stream.ts";
|
|
3
|
+
import type { AgentResponse as AgentResponseT } from "./types/response.ts";
|
|
4
|
+
|
|
5
|
+
// Q-50 public API budget: ~25 core runtime exports + compat/deprecation shims.
|
|
6
|
+
// Core = Agent, tool, SubAgent (deprecated alias), AgentResponse,
|
|
7
|
+
// AssistantMessageEventStream, document conversion, delegation primitives,
|
|
8
|
+
// provider registry, catalog, errors, and option/normalization helpers.
|
|
9
|
+
// Everything else below is a shim kept for compat and marked @deprecated
|
|
10
|
+
// toward a future `/internal` entry. Nothing is deleted (would break tests).
|
|
11
|
+
//
|
|
12
|
+
// Q-27 node split (deferred): this root entry pulls `session/store.ts`, which
|
|
13
|
+
// imports `node:fs` for session persistence. A future `/node` subpath will
|
|
14
|
+
// host the fs-backed persistence/telemetry surface; the root entry stays
|
|
15
|
+
// runtime-agnostic. Until then, browser/bundler consumers should import the
|
|
16
|
+
// documented core shims only.
|
|
17
|
+
//
|
|
18
|
+
// Q-51 one way to run: `run()` is canonical. `ask()` and `run/stream: true`
|
|
19
|
+
// remain for compat but are deprecated — prefer `run()` for single-shot and
|
|
20
|
+
// `stream()` for event iteration. `Run` (below) is the typed streaming handle.
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Typed streaming run handle (Q-51): the `AssistantMessageEventStream`
|
|
24
|
+
* returned by `stream()`/`run(stream:true)`, with its terminal `result()` and
|
|
25
|
+
* `cancel()` surfaced on the type for callers that only need the handle.
|
|
26
|
+
*/
|
|
27
|
+
export type Run = AssistantMessageEventStreamT & {
|
|
28
|
+
result: () => Promise<AgentResponseT>;
|
|
29
|
+
cancel: (reason?: unknown) => void;
|
|
30
|
+
};
|
|
2
31
|
|
|
3
32
|
// Core Agent & Tool Classes
|
|
4
33
|
export { Agent, SubAgentModelError } from "./agent/agent.ts";
|
|
5
|
-
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated Compat alias: prefer composing a plain `Agent` and exposing it
|
|
36
|
+
* via `agentToTool()`/`buildAgentTools()` (or `subagents: [...]`). `SubAgent`
|
|
37
|
+
* stays for compat and receives no new features (Q-56).
|
|
38
|
+
*/
|
|
39
|
+
export { SubAgent, agentFromEnv } from "./agent/subagent.ts";
|
|
6
40
|
export type { SubAgentConfig } from "./agent/subagent.ts";
|
|
7
|
-
export {
|
|
41
|
+
export {
|
|
42
|
+
tool,
|
|
43
|
+
toStandardToolDeclarations,
|
|
44
|
+
assertValidToolName,
|
|
45
|
+
TOOL_NAME_PATTERN,
|
|
46
|
+
normalizeThinkingV2,
|
|
47
|
+
resolveWhenBusy,
|
|
48
|
+
normalizeWorkerTimeout,
|
|
49
|
+
normalizeMaxAttempts,
|
|
50
|
+
} from "./tools/tool.ts";
|
|
8
51
|
export type { CreateToolOptions } from "./tools/tool.ts";
|
|
9
52
|
export { zodToJsonSchema, cleanJsonSchema } from "./tools/schema.ts";
|
|
53
|
+
/**
|
|
54
|
+
* @deprecated Internal executor surface. Prefer `Agent` runs; for direct
|
|
55
|
+
* execution import from the future `/internal` entry (Q-50).
|
|
56
|
+
*/
|
|
10
57
|
export { executeToolCalls } from "./tools/executor.ts";
|
|
11
58
|
|
|
12
59
|
// Client-side document conversion (optional anydoc peer — see utils/documents.ts)
|
|
@@ -31,8 +78,27 @@ export {
|
|
|
31
78
|
buildAgentTools,
|
|
32
79
|
agentToTool,
|
|
33
80
|
createSubagentSpawnTool,
|
|
81
|
+
createChildSessionId,
|
|
82
|
+
createFixedChildSessionId,
|
|
83
|
+
isSessionDescendant,
|
|
84
|
+
getSubAgentTrace,
|
|
85
|
+
getSubAgentTraceScoped,
|
|
86
|
+
listSubAgentTraceIds,
|
|
87
|
+
subscribeToSubAgent,
|
|
88
|
+
appendSubAgentStep,
|
|
89
|
+
pruneTraces,
|
|
90
|
+
trimTraceSteps,
|
|
91
|
+
checkBudget,
|
|
92
|
+
chargeBudgetUsage,
|
|
93
|
+
reserveSpawn,
|
|
94
|
+
isStreamingLoop,
|
|
95
|
+
escapeXml as escapeXmlFromDelegation,
|
|
96
|
+
MAX_SUBAGENT_TRACES,
|
|
97
|
+
SUBAGENT_TRACE_TTL_MS,
|
|
98
|
+
MAX_SUBAGENT_STEPS_PER_TRACE,
|
|
99
|
+
SUBAGENT_SUBSCRIBE_TTL_MS,
|
|
34
100
|
} from "./agent/delegation.ts";
|
|
35
|
-
export type { DynamicSubagentTask, AgentAsToolTarget } from "./agent/delegation.ts";
|
|
101
|
+
export type { DynamicSubagentTask, AgentAsToolTarget, SubAgentTrace, StreamingLoopHandle } from "./agent/delegation.ts";
|
|
36
102
|
|
|
37
103
|
// Providers & Registry — unified multi-provider layer, all native REST
|
|
38
104
|
export {
|
|
@@ -51,6 +117,9 @@ export {
|
|
|
51
117
|
getModelThinkingInfo,
|
|
52
118
|
validateModelThinking,
|
|
53
119
|
getModelsForProvider,
|
|
120
|
+
registerModel,
|
|
121
|
+
clearCustomModels,
|
|
122
|
+
getModelSource,
|
|
54
123
|
ThinkingLevelError,
|
|
55
124
|
refreshModelCatalog,
|
|
56
125
|
ensureModelCatalogFresh,
|
|
@@ -68,34 +137,56 @@ export {
|
|
|
68
137
|
export { resolveEffectiveThinking } from "./agent/agent.ts";
|
|
69
138
|
export { withRetries, isTransientError } from "./utils/retry.ts";
|
|
70
139
|
export { toConciseProviderError, assertModalitiesSupported, assertNoVideoPartsOnResponses } from "./utils/errors.ts";
|
|
71
|
-
// Native provider implementations.
|
|
72
|
-
|
|
140
|
+
// Native provider implementations.
|
|
141
|
+
/** @deprecated The discontinued beta-Responses transport was removed
|
|
142
|
+
* directly for frozen wire-contract evidence only. It is wired nowhere and
|
|
143
|
+
* must not be extended or re-exported here (Q-46). */
|
|
73
144
|
export {
|
|
74
145
|
OpenAICompatibleChatProvider,
|
|
75
|
-
OpenAICompatibleChatProvider as OpenAICompatibleProvider,
|
|
76
146
|
createOpenAICompatibleProvider,
|
|
77
147
|
createCustomProvider,
|
|
78
148
|
CustomProvider,
|
|
79
149
|
} from "./providers/openai-compat.ts";
|
|
150
|
+
/**
|
|
151
|
+
* @deprecated Compat alias of `OpenAICompatibleChatProvider` (canonical).
|
|
152
|
+
* Kept for back-compat only; new code uses the canonical name (Q-50).
|
|
153
|
+
*/
|
|
154
|
+
export { OpenAICompatibleChatProvider as OpenAICompatibleProvider } from "./providers/openai-compat.ts";
|
|
80
155
|
export type { CustomProviderOptions } from "./providers/openai-compat.ts";
|
|
81
156
|
|
|
82
157
|
// Canonical provider contract (provider-agnostic) + native adapters.
|
|
83
158
|
export {
|
|
84
159
|
GoogleInteractionsProvider,
|
|
85
|
-
GoogleInteractionsProvider as GoogleAIStudioProvider,
|
|
86
160
|
clearInteractionChains,
|
|
87
161
|
} from "./providers/google.ts";
|
|
162
|
+
/**
|
|
163
|
+
* @deprecated Compat alias of `GoogleInteractionsProvider` (canonical).
|
|
164
|
+
* Kept for back-compat only; new code uses the canonical name (Q-50).
|
|
165
|
+
*/
|
|
166
|
+
export { GoogleInteractionsProvider as GoogleAIStudioProvider } from "./providers/google.ts";
|
|
88
167
|
export {
|
|
89
168
|
OpenRouterChatCompletionsProvider,
|
|
90
|
-
OpenRouterChatCompletionsProvider as OpenRouterProvider,
|
|
91
169
|
} from "./providers/openrouter.ts";
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
170
|
+
/**
|
|
171
|
+
* @deprecated Compat alias of `OpenRouterChatCompletionsProvider`
|
|
172
|
+
* (canonical). Kept for back-compat only (Q-50).
|
|
173
|
+
*/
|
|
174
|
+
export { OpenRouterChatCompletionsProvider as OpenRouterProvider } from "./providers/openrouter.ts";
|
|
175
|
+
// Discontinued beta-Responses transport, removed from core
|
|
176
|
+
// (was frozen migration evidence, wired nowhere): OpenRouterResponsesProvider.
|
|
95
177
|
export {
|
|
96
178
|
OpenAIResponsesProvider,
|
|
97
|
-
OpenAIResponsesProvider as OpenAIProvider,
|
|
98
179
|
} from "./providers/openai.ts";
|
|
180
|
+
/**
|
|
181
|
+
* @deprecated Compat alias of `OpenAIResponsesProvider` (canonical).
|
|
182
|
+
* Kept for back-compat only; new code uses the canonical name (Q-50).
|
|
183
|
+
*/
|
|
184
|
+
export { OpenAIResponsesProvider as OpenAIProvider } from "./providers/openai.ts";
|
|
185
|
+
/**
|
|
186
|
+
* @deprecated Internal provider mappers/routing helpers. Prefer the
|
|
187
|
+
* provider-agnostic registry (`resolveModel`/`getProvider`); direct mapper
|
|
188
|
+
* use moves to the future `/internal` entry (Q-50).
|
|
189
|
+
*/
|
|
99
190
|
export {
|
|
100
191
|
emitProviderWarning,
|
|
101
192
|
clearEmittedWarnings,
|
|
@@ -155,20 +246,82 @@ export type {
|
|
|
155
246
|
|
|
156
247
|
// Streaming & Events
|
|
157
248
|
export { AssistantMessageEventStream } from "./streaming/event-stream.ts";
|
|
249
|
+
/**
|
|
250
|
+
* @deprecated Internal SSE plumbing. Prefer `AssistantMessageEventStream`;
|
|
251
|
+
* direct parser use moves to the future `/internal` entry (Q-50).
|
|
252
|
+
*/
|
|
158
253
|
export { SSEParser } from "./streaming/sse-parser.ts";
|
|
254
|
+
export type { SteerEntry } from "./agent/loop.ts";
|
|
159
255
|
|
|
160
256
|
// Responses
|
|
161
257
|
export { AgentResponse } from "./types/response.ts";
|
|
162
258
|
|
|
259
|
+
// Core error taxonomy (Q-50 core surface)
|
|
260
|
+
export {
|
|
261
|
+
AgentAccelError,
|
|
262
|
+
ConfigError,
|
|
263
|
+
ProviderError,
|
|
264
|
+
TimeoutError,
|
|
265
|
+
BudgetExceededError,
|
|
266
|
+
ToolError,
|
|
267
|
+
ValidationError,
|
|
268
|
+
SessionLoadError,
|
|
269
|
+
SecurityPolicyError,
|
|
270
|
+
isAbortError,
|
|
271
|
+
formatErrorPlain,
|
|
272
|
+
} from "./types/errors.ts";
|
|
273
|
+
export type { AgentAccelErrorCode, AgentAccelErrorContext } from "./types/errors.ts";
|
|
274
|
+
// Normalized run limits (Q-22; sub-agents inherit unless overridden)
|
|
275
|
+
export type { AgentRunLimits } from "./agent/agent.ts";
|
|
276
|
+
|
|
277
|
+
// Session persistence & telemetry (conversation history + cost rollup)
|
|
278
|
+
export {
|
|
279
|
+
SessionTelemetry,
|
|
280
|
+
loadSessionFile,
|
|
281
|
+
saveSessionFile,
|
|
282
|
+
loadSessionDir,
|
|
283
|
+
saveSessionDir,
|
|
284
|
+
writeFileAtomic,
|
|
285
|
+
findLatestSessionDir,
|
|
286
|
+
sessionDirFor,
|
|
287
|
+
extractMediaToDir,
|
|
288
|
+
resolveMediaPaths,
|
|
289
|
+
buildSessionData,
|
|
290
|
+
serializeSession,
|
|
291
|
+
deserializeSession,
|
|
292
|
+
getSessionContextWindow,
|
|
293
|
+
computeSessionTurnCost,
|
|
294
|
+
formatSessionTokens,
|
|
295
|
+
formatSessionCost,
|
|
296
|
+
formatSessionBanner,
|
|
297
|
+
emptyTotals,
|
|
298
|
+
} from "./session/store.ts";
|
|
299
|
+
export type { PersistedAgentSession, SessionTotals, SessionAgentLike, LoadedSessionDir, PersistedSubAgentTrace } from "./session/store.ts";
|
|
300
|
+
|
|
163
301
|
// Utilities
|
|
164
|
-
|
|
302
|
+
/**
|
|
303
|
+
* @deprecated Session-routing internals. Prefer agent-level `sessionId`;
|
|
304
|
+
* direct use moves to the future `/internal` entry (Q-50).
|
|
305
|
+
*/
|
|
306
|
+
export { createSessionId, hashSessionPart, createTrackingId, nowMs, newRunId, newTurnId, previewPrompt, parseModelRef, resolveAgentConfig } from "./utils/session.ts";
|
|
307
|
+
export type { ParsedModelRef, ResolvedAgentConfig } from "./utils/session.ts";
|
|
308
|
+
/**
|
|
309
|
+
* @deprecated Raw env access. Prefer explicit `Agent` config (which wins over
|
|
310
|
+
* env) and `Agent.fromEnv()`/`agentFromEnv()`; direct use moves to the future
|
|
311
|
+
* `/internal` entry (Q-50).
|
|
312
|
+
*/
|
|
165
313
|
export { getApiKey, getEnv } from "./utils/env.ts";
|
|
166
314
|
export { buildSessionHeaders } from "./utils/headers.ts";
|
|
167
315
|
export { normalizeMediaInput, inferMimeType } from "./utils/media.ts";
|
|
168
316
|
export { base64ToBytes, bytesToBase64 } from "./utils/base64.ts";
|
|
169
317
|
export { toJsonSafe, safeStringify, escapeXml } from "./utils/serialization.ts";
|
|
170
318
|
|
|
171
|
-
// Re-export Zod
|
|
319
|
+
// Re-export Zod (compat only)
|
|
320
|
+
/**
|
|
321
|
+
* @deprecated Compat re-export: prefer adding `zod` as a direct (peer)
|
|
322
|
+
* dependency and importing from `"zod"` (Q-50). Kept so existing
|
|
323
|
+
* `import { z } from "agent-accelerator"` code keeps working.
|
|
324
|
+
*/
|
|
172
325
|
export { z };
|
|
173
326
|
|
|
174
327
|
// TypeScript Type Exports
|
|
@@ -203,6 +356,8 @@ export type {
|
|
|
203
356
|
ToolCallRecord,
|
|
204
357
|
ToolResultRecord,
|
|
205
358
|
StandardToolDeclaration,
|
|
359
|
+
ToolLogger,
|
|
360
|
+
ToolEmitFn,
|
|
206
361
|
} from "./types/tool.ts";
|
|
207
362
|
|
|
208
363
|
export type {
|
|
@@ -218,6 +373,7 @@ export type {
|
|
|
218
373
|
export type {
|
|
219
374
|
AgentResponseJSON,
|
|
220
375
|
SubAgentExecutionMetadata,
|
|
376
|
+
SubAgentStep,
|
|
221
377
|
StreamEvent,
|
|
222
378
|
StreamEventType,
|
|
223
379
|
} from "./types/response.ts";
|
|
@@ -225,51 +381,22 @@ export type {
|
|
|
225
381
|
export type {
|
|
226
382
|
AgentConfig,
|
|
227
383
|
AgentRunOptions,
|
|
384
|
+
AgentTurnEvent,
|
|
228
385
|
DynamicSubagentsConfig,
|
|
386
|
+
MidSessionConfig,
|
|
387
|
+
MidSessionMode,
|
|
388
|
+
WhenBusyMode,
|
|
389
|
+
ModelRef,
|
|
390
|
+
ThinkingOption,
|
|
391
|
+
SamplingConfig,
|
|
392
|
+
RetryPolicyConfig,
|
|
393
|
+
AgentLogger,
|
|
394
|
+
ToolChoiceOption,
|
|
395
|
+
StopOption,
|
|
396
|
+
Budget,
|
|
397
|
+
BudgetState,
|
|
398
|
+
SubAgentRunIds,
|
|
229
399
|
} from "./types/agent.ts";
|
|
230
400
|
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
// NOTE: wire `OpenAIToolChoice` intentionally omitted — it duplicates the
|
|
234
|
-
// canonical `OpenAIToolChoice` exported above (same shape); use that one.
|
|
235
|
-
export type {
|
|
236
|
-
GoogleThinkingLevel,
|
|
237
|
-
GoogleThinkingConfig,
|
|
238
|
-
GoogleFunctionCallingMode,
|
|
239
|
-
GoogleFunctionCallingConfig,
|
|
240
|
-
GoogleToolConfig,
|
|
241
|
-
GoogleFunctionDeclaration,
|
|
242
|
-
GoogleTool,
|
|
243
|
-
GoogleBlob,
|
|
244
|
-
GooglePart,
|
|
245
|
-
GoogleContent,
|
|
246
|
-
GoogleGenerationConfig,
|
|
247
|
-
GoogleCandidate,
|
|
248
|
-
GoogleUsageMetadata,
|
|
249
|
-
GoogleGenerateContentRequest,
|
|
250
|
-
GoogleGenerateContentResponse,
|
|
251
|
-
OpenRouterProviderRouting,
|
|
252
|
-
OpenRouterReasoning,
|
|
253
|
-
OpenRouterParameters,
|
|
254
|
-
OpenRouterUsage,
|
|
255
|
-
OpenRouterChatRequest,
|
|
256
|
-
OpenRouterResponse,
|
|
257
|
-
OpenAIMessageRole,
|
|
258
|
-
OpenAIReasoningEffort,
|
|
259
|
-
OpenAIServiceTier,
|
|
260
|
-
OpenAITextPart,
|
|
261
|
-
OpenAIImageUrlPart,
|
|
262
|
-
OpenAIInputAudioPart,
|
|
263
|
-
OpenAIVideoUrlPart,
|
|
264
|
-
OpenAIContentPart,
|
|
265
|
-
OpenAIToolCall,
|
|
266
|
-
OpenAIMessage,
|
|
267
|
-
OpenAITool,
|
|
268
|
-
OpenAIUsage,
|
|
269
|
-
OpenAIChoice,
|
|
270
|
-
OpenAIDelta,
|
|
271
|
-
OpenAIChunkChoice,
|
|
272
|
-
OpenAIChatCompletionRequest,
|
|
273
|
-
OpenAIChatCompletionResponse,
|
|
274
|
-
OpenAIChatCompletionChunk,
|
|
275
|
-
} from "./types/provider-payloads.ts";
|
|
401
|
+
// Minimalist core: harness modules (RLM/RSI, OTel, testing, CLI doctor, hooks,
|
|
402
|
+
// context-ops, run-tree/record, session-store, legacy wire types) live in userland.
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import { getEnv } from "../utils/env.ts";
|
|
4
5
|
|
|
5
6
|
/** Default TTL: 12 Hours (in milliseconds). */
|
|
6
7
|
export const DEFAULT_CATALOG_TTL_MS = 12 * 60 * 60 * 1000;
|
|
7
8
|
|
|
9
|
+
/** Max accepted models.dev payload size: 15MB (Q-16a). Larger payloads are rejected before parse. */
|
|
10
|
+
export const MAX_CATALOG_PAYLOAD_BYTES = 15 * 1024 * 1024;
|
|
11
|
+
|
|
8
12
|
let globalCatalogTtlMs = DEFAULT_CATALOG_TTL_MS;
|
|
9
13
|
|
|
10
|
-
/** Sets the global model catalog cache TTL in milliseconds.
|
|
14
|
+
/** Sets the global model catalog cache TTL in milliseconds.
|
|
15
|
+
* Note (Q-16a): cache files carry their own `ttlMs`. A global `setCatalogTTL`
|
|
16
|
+
* does not rewrite in-flight file TTLs; it takes effect on the next refresh
|
|
17
|
+
* (fresh download) or when the cached file has no `ttlMs` of its own.
|
|
18
|
+
*/
|
|
11
19
|
export function setCatalogTTL(ttlMs: number): void {
|
|
12
20
|
if (Number.isFinite(ttlMs) && ttlMs > 0) {
|
|
13
21
|
globalCatalogTtlMs = ttlMs;
|
|
@@ -77,9 +85,9 @@ export function getCacheDir(): string {
|
|
|
77
85
|
return path.resolve(process.cwd(), "src/data");
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
/**
|
|
88
|
+
/** Cache file path: `<cacheDir>/models.dev.json`, honoring AGENT_CACHE_DIR (Q-16a/Q-34). */
|
|
81
89
|
export function getCacheFilePath(): string {
|
|
82
|
-
return path.
|
|
90
|
+
return path.join(getCacheDir(), "models.dev.json");
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
function readJsonFileSync(filePath: string): any {
|
|
@@ -97,7 +105,20 @@ function writeJsonFileSync(filePath: string, data: any): void {
|
|
|
97
105
|
if (!fs.existsSync(dir)) {
|
|
98
106
|
fs.mkdirSync(dir, { recursive: true });
|
|
99
107
|
}
|
|
100
|
-
|
|
108
|
+
// Atomic tmp+rename with 0600 so a crash never leaves half-written JSON (Q-16a).
|
|
109
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
110
|
+
const tmp = `${filePath}.tmp-${process.pid}-${rand}`;
|
|
111
|
+
fs.writeFileSync(tmp, JSON.stringify(data), { encoding: "utf-8", mode: 0o600 });
|
|
112
|
+
try { fs.chmodSync(tmp, 0o600); } catch {}
|
|
113
|
+
fs.renameSync(tmp, filePath);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Verifies optional sha256 pin from MODELS_DEV_SHA256 (Q-16a). Returns false on mismatch. */
|
|
117
|
+
export function verifyCatalogSha256(text: string): boolean {
|
|
118
|
+
const pin = getEnv("MODELS_DEV_SHA256")?.trim().toLowerCase();
|
|
119
|
+
if (!pin) return true;
|
|
120
|
+
const hex = createHash("sha256").update(text, "utf8").digest("hex").toLowerCase();
|
|
121
|
+
return hex === pin;
|
|
101
122
|
}
|
|
102
123
|
|
|
103
124
|
// In-memory catalog state
|
|
@@ -225,6 +246,17 @@ export async function refreshModelCatalog(options: RefreshCatalogOptions = {}):
|
|
|
225
246
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
226
247
|
let freshData: Record<string, any>;
|
|
227
248
|
|
|
249
|
+
const fallbackToDiskCache = (): void => {
|
|
250
|
+
const existing = readJsonFileSync(cachePath);
|
|
251
|
+
if (existing && existing.data) {
|
|
252
|
+
activeCatalog = existing.data;
|
|
253
|
+
activeFetchedAt = existing.fetchedAt;
|
|
254
|
+
activeTtlMs = existing.ttlMs || effectiveTtl;
|
|
255
|
+
activeFromCache = true;
|
|
256
|
+
notifyUpdateListeners();
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
228
260
|
try {
|
|
229
261
|
const res = await fetch(apiUrl, {
|
|
230
262
|
headers: {
|
|
@@ -238,33 +270,40 @@ export async function refreshModelCatalog(options: RefreshCatalogOptions = {}):
|
|
|
238
270
|
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
239
271
|
}
|
|
240
272
|
|
|
241
|
-
|
|
273
|
+
// Q-16a: size-cap before parse (check text length, reject >15MB).
|
|
274
|
+
const text = await res.text();
|
|
275
|
+
if (text.length > MAX_CATALOG_PAYLOAD_BYTES) {
|
|
276
|
+
clearTimeout(timer);
|
|
277
|
+
fallbackToDiskCache();
|
|
278
|
+
return getCatalogStatus();
|
|
279
|
+
}
|
|
280
|
+
// Q-16a: optional sha256 pin via MODELS_DEV_SHA256.
|
|
281
|
+
if (!verifyCatalogSha256(text)) {
|
|
282
|
+
clearTimeout(timer);
|
|
283
|
+
fallbackToDiskCache();
|
|
284
|
+
return getCatalogStatus();
|
|
285
|
+
}
|
|
286
|
+
let parsed: unknown;
|
|
287
|
+
try {
|
|
288
|
+
parsed = JSON.parse(text);
|
|
289
|
+
} catch {
|
|
290
|
+
clearTimeout(timer);
|
|
291
|
+
fallbackToDiskCache();
|
|
292
|
+
return getCatalogStatus();
|
|
293
|
+
}
|
|
294
|
+
freshData = parsed as Record<string, any>;
|
|
242
295
|
|
|
243
296
|
// Never let a non-catalog payload (mocked fetch, proxy error page,
|
|
244
297
|
// chat-completion stub) overwrite the good on-disk cache.
|
|
245
298
|
if (!isValidCatalogPayload(freshData)) {
|
|
246
299
|
clearTimeout(timer);
|
|
247
|
-
|
|
248
|
-
if (existing && existing.data) {
|
|
249
|
-
activeCatalog = existing.data;
|
|
250
|
-
activeFetchedAt = existing.fetchedAt;
|
|
251
|
-
activeTtlMs = existing.ttlMs || effectiveTtl;
|
|
252
|
-
activeFromCache = true;
|
|
253
|
-
notifyUpdateListeners();
|
|
254
|
-
}
|
|
300
|
+
fallbackToDiskCache();
|
|
255
301
|
return getCatalogStatus();
|
|
256
302
|
}
|
|
257
303
|
} catch (err: any) {
|
|
258
304
|
clearTimeout(timer);
|
|
259
305
|
// If download fails, retain disk cache if available
|
|
260
|
-
|
|
261
|
-
if (existing && existing.data) {
|
|
262
|
-
activeCatalog = existing.data;
|
|
263
|
-
activeFetchedAt = existing.fetchedAt;
|
|
264
|
-
activeTtlMs = existing.ttlMs || effectiveTtl;
|
|
265
|
-
activeFromCache = true;
|
|
266
|
-
notifyUpdateListeners();
|
|
267
|
-
}
|
|
306
|
+
fallbackToDiskCache();
|
|
268
307
|
return getCatalogStatus();
|
|
269
308
|
} finally {
|
|
270
309
|
clearTimeout(timer);
|
|
@@ -281,19 +320,23 @@ export async function refreshModelCatalog(options: RefreshCatalogOptions = {}):
|
|
|
281
320
|
|
|
282
321
|
// Update in-memory state FIRST so a read-only filesystem still serves the
|
|
283
322
|
// fresh catalog for this process; disk persistence below is best-effort.
|
|
323
|
+
// Q-34: a real download is NOT from cache.
|
|
284
324
|
activeCatalog = freshData;
|
|
285
325
|
activeFetchedAt = fetchedAt;
|
|
286
326
|
activeTtlMs = effectiveTtl;
|
|
287
|
-
activeFromCache =
|
|
327
|
+
activeFromCache = false;
|
|
288
328
|
|
|
289
329
|
// Save to src/data/models.dev.json (or the AGENT_CACHE_DIR override)
|
|
290
330
|
try {
|
|
291
331
|
writeJsonFileSync(cachePath, cachePayload);
|
|
292
332
|
} catch (err) {
|
|
293
333
|
activeFromCache = false;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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 {}
|
|
297
340
|
}
|
|
298
341
|
|
|
299
342
|
notifyUpdateListeners();
|