agent-accelerator 0.5.7 → 0.5.9
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 +27 -8
- package/package.json +1 -1
- package/src/agent/agent.ts +84 -45
- package/src/agent/architecture.md +1 -0
- package/src/agent/context.ts +3 -3
- package/src/agent/delegation.ts +12 -12
- package/src/agent/loop.ts +50 -53
- package/src/agent/subagent.ts +4 -4
- package/src/agent/worker.ts +2 -1
- package/src/architecture.md +1 -1
- package/src/index.ts +7 -7
- package/src/models/architecture.md +1 -1
- package/src/models/catalog-cache.ts +7 -7
- package/src/models/catalog.ts +22 -34
- package/src/providers/google.ts +7 -7
- package/src/providers/openai-compat.ts +10 -16
- package/src/providers/openai.ts +8 -7
- package/src/providers/openrouter.ts +4 -4
- package/src/providers/registry.ts +6 -5
- package/src/providers/shared.ts +6 -10
- package/src/scheduler/architecture.md +6 -3
- package/src/scheduler/scheduler.ts +111 -57
- package/src/scheduler/store.ts +273 -59
- package/src/session/architecture.md +1 -1
- package/src/session/store.ts +22 -22
- package/src/streaming/event-stream.ts +11 -11
- package/src/telemetry-file.ts +74 -24
- package/src/telemetry.ts +2 -2
- package/src/tools/executor.ts +1 -1
- package/src/tools/tool.ts +8 -8
- package/src/types/agent.ts +22 -20
- package/src/types/response.ts +8 -8
- package/src/types/scheduler.ts +2 -2
- package/src/types/tool.ts +5 -5
- package/src/update-models.ts +2 -2
- package/src/utils/architecture.md +3 -3
- package/src/utils/cache.ts +4 -4
- package/src/utils/durable.ts +78 -3
- package/src/utils/env.ts +1 -1
- package/src/utils/errors.ts +45 -31
- package/src/utils/headers.ts +4 -4
- package/src/utils/home.ts +1 -1
- package/src/utils/retry.ts +2 -28
- package/src/utils/session.ts +5 -5
package/src/models/catalog.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Model catalog
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Model catalog: the single source of truth (models.dev) for context limits, prices, modalities and reasoning options.
|
|
3
|
+
* The data is cached in `<home>/catalog/models.dev.json` (or `AGENT_CACHE_DIR`) with a 12 hour TTL and refreshed in the
|
|
4
|
+
* background (see `catalog-cache.ts`). `GOOGLE_MODELS`, `OPENAI_MODELS` and `OPENROUTER_MODELS` are views of it,
|
|
5
|
+
* rebuilt in place whenever it changes.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "./catalog-cache.ts";
|
|
20
20
|
import type { ModelSpec, ProviderId, ModelLimit, ModelCost, ModelModalities } from "../types/model.ts";
|
|
21
21
|
import type { ThinkingLevel } from "../types/core.ts";
|
|
22
|
+
import { ConfigError } from "../types/errors.ts";
|
|
22
23
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Provider aliases — maps our internal ids to models.dev keys
|
|
@@ -77,12 +78,12 @@ function normalizeModelIdForLookup(modelId: string): string {
|
|
|
77
78
|
|
|
78
79
|
// ---------------------------------------------------------------------------
|
|
79
80
|
// Internal cache — battle-tested: avoid repeated global scans (7487 models)
|
|
80
|
-
// Bound lookupCache to 1000 entries LRU
|
|
81
|
+
// Bound lookupCache to 1000 entries LRU: evict oldest on overflow.
|
|
81
82
|
// ---------------------------------------------------------------------------
|
|
82
83
|
const lookupCache = new Map<string, ModelSpec | undefined>();
|
|
83
84
|
const providerModelsCache = new Map<string, ModelSpec[]>();
|
|
84
85
|
|
|
85
|
-
/** Bounded LRU set for lookupCache
|
|
86
|
+
/** Bounded LRU set for lookupCache: keeps at most 1000 entries. */
|
|
86
87
|
function setLookupCache(key: string, value: ModelSpec | undefined): void {
|
|
87
88
|
if (lookupCache.has(key)) lookupCache.delete(key);
|
|
88
89
|
else if (lookupCache.size >= 1000) {
|
|
@@ -92,7 +93,7 @@ function setLookupCache(key: string, value: ModelSpec | undefined): void {
|
|
|
92
93
|
lookupCache.set(key, value);
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
// Custom-registered models
|
|
96
|
+
// Custom-registered models: checked first in getModelFromCatalog.
|
|
96
97
|
const customModelRegistry = new Map<string, ModelSpec>();
|
|
97
98
|
|
|
98
99
|
// Case-insensitive key of a registered custom model; in: registerModel and lookups, out: a string.
|
|
@@ -101,7 +102,7 @@ function customRegistryKey(provider: string, modelId: string): string {
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
/**
|
|
104
|
-
* Registers a custom model spec that takes precedence over the catalog
|
|
105
|
+
* Registers a custom model spec that takes precedence over the catalog.
|
|
105
106
|
* Stored in a separate map, checked first in `getModelFromCatalog`.
|
|
106
107
|
*/
|
|
107
108
|
export function registerModel(spec: ModelSpec): void {
|
|
@@ -124,7 +125,7 @@ export function clearCustomModels(): void {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
/**
|
|
127
|
-
* Returns where a model resolves from without touching frozen types
|
|
128
|
+
* Returns where a model resolves from without touching frozen types:
|
|
128
129
|
* `"override"` for custom-registered or MODALITY_OVERRIDES-covered models,
|
|
129
130
|
* `"catalog"` for plain catalog hits, `"unknown"` when unresolvable.
|
|
130
131
|
*/
|
|
@@ -256,7 +257,7 @@ export function getModelFromCatalog(providerInput: string, modelIdInput: string)
|
|
|
256
257
|
const modelId = modelIdInput.trim();
|
|
257
258
|
let result: ModelSpec | undefined;
|
|
258
259
|
|
|
259
|
-
//
|
|
260
|
+
// Custom-registered models win over the catalog.
|
|
260
261
|
const customHit =
|
|
261
262
|
customModelRegistry.get(customRegistryKey(provider, modelId)) ??
|
|
262
263
|
customModelRegistry.get(customRegistryKey(provider, normalizeModelIdForLookup(modelId)));
|
|
@@ -287,7 +288,7 @@ export function getModelFromCatalog(providerInput: string, modelIdInput: string)
|
|
|
287
288
|
return result;
|
|
288
289
|
}
|
|
289
290
|
}
|
|
290
|
-
//
|
|
291
|
+
// Tightened fuzzy match — exact, `endsWith("/"+modelId)` boundary,
|
|
291
292
|
// or normalized equality. The old loose `modelId.endsWith(key)` is removed
|
|
292
293
|
// (e.g. "gpt-4o-mini-extra" must not match "mini").
|
|
293
294
|
for (const [key, raw] of Object.entries(providerData.models as Record<string, any>)) {
|
|
@@ -421,8 +422,8 @@ export function getModelThinkingInfo(provider: string, modelId: string): ModelTh
|
|
|
421
422
|
};
|
|
422
423
|
}
|
|
423
424
|
|
|
424
|
-
/** Thrown when a requested ThinkingLevel is unsupported by the selected model
|
|
425
|
-
export class ThinkingLevelError extends
|
|
425
|
+
/** Thrown when a requested ThinkingLevel is unsupported by the selected model: a `ConfigError` (one line, never retried) with `allowedLevels`. */
|
|
426
|
+
export class ThinkingLevelError extends ConfigError {
|
|
426
427
|
readonly provider: string;
|
|
427
428
|
readonly modelId: string;
|
|
428
429
|
readonly requestedLevel: string;
|
|
@@ -443,31 +444,18 @@ export class ThinkingLevelError extends Error {
|
|
|
443
444
|
reason: string;
|
|
444
445
|
remedy?: string;
|
|
445
446
|
}) {
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const formattedMessage =
|
|
454
|
-
`[Agent Accelerator] ThinkingLevel Mismatch for "${opts.provider}/${opts.modelId}":\n` +
|
|
455
|
-
` Requested Level: "${opts.requestedLevel}"\n` +
|
|
456
|
-
` Issue: ${opts.reason}` +
|
|
457
|
-
allowedLine +
|
|
458
|
-
remedyLines;
|
|
459
|
-
|
|
460
|
-
super(formattedMessage);
|
|
447
|
+
const allowed = opts.allowedLevels.length > 0 ? ` Allowed levels: ${opts.allowedLevels.map((l) => `"${l}"`).join(", ")}.` : "";
|
|
448
|
+
const remedy = opts.remedy ? ` How to fix: ${opts.remedy}` : "";
|
|
449
|
+
super(
|
|
450
|
+
`[Agent Accelerator] Thinking level "${opts.requestedLevel}" does not fit "${opts.provider}/${opts.modelId}": ${opts.reason}${allowed}${remedy}`,
|
|
451
|
+
{ provider: opts.provider, model: opts.modelId }
|
|
452
|
+
);
|
|
461
453
|
this.name = "ThinkingLevelError";
|
|
462
454
|
this.provider = opts.provider;
|
|
463
455
|
this.modelId = opts.modelId;
|
|
464
456
|
this.requestedLevel = opts.requestedLevel;
|
|
465
457
|
this.allowedLevels = opts.allowedLevels;
|
|
466
458
|
this.supportsThinking = opts.supportsThinking;
|
|
467
|
-
|
|
468
|
-
if (Error.captureStackTrace) {
|
|
469
|
-
Error.captureStackTrace(this, ThinkingLevelError);
|
|
470
|
-
}
|
|
471
459
|
}
|
|
472
460
|
}
|
|
473
461
|
|
|
@@ -563,7 +551,7 @@ export function getModelsForProvider(provider: string): ModelSpec[] {
|
|
|
563
551
|
out.push(mapModelSpec(alias as ProviderId, key, raw));
|
|
564
552
|
}
|
|
565
553
|
}
|
|
566
|
-
//
|
|
554
|
+
// Include custom-registered models for this provider/alias set.
|
|
567
555
|
for (const spec of customModelRegistry.values()) {
|
|
568
556
|
if (aliases.includes(String(spec.provider)) || String(spec.provider) === provider) {
|
|
569
557
|
if (!seen.has(spec.id)) {
|
|
@@ -573,7 +561,7 @@ export function getModelsForProvider(provider: string): ModelSpec[] {
|
|
|
573
561
|
}
|
|
574
562
|
}
|
|
575
563
|
// Also include our internal provider id mapping
|
|
576
|
-
//
|
|
564
|
+
// Return copies — cache the canonical array, hand out a fresh copy.
|
|
577
565
|
providerModelsCache.set(provider, out);
|
|
578
566
|
return [...out];
|
|
579
567
|
}
|
package/src/providers/google.ts
CHANGED
|
@@ -383,7 +383,7 @@ function cleanModelId(model: string | ModelSpec): string {
|
|
|
383
383
|
function assertSupportedGeneration(clean: string): void {
|
|
384
384
|
const match = clean.match(/^gemini-(\d+)(?:[.\-]|$)/i);
|
|
385
385
|
if (match && Number(match[1]) < 3) {
|
|
386
|
-
throw new
|
|
386
|
+
throw new ConfigError(
|
|
387
387
|
`[Agent Accelerator] [google/${clean}] Unsupported generation: Gemini ${match[1]}.x and older are strictly rejected by this SDK. ` +
|
|
388
388
|
`Use a Gemini 3 or later model for full support and feature coverage.`
|
|
389
389
|
);
|
|
@@ -609,7 +609,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
609
609
|
const turnStart = steps.length;
|
|
610
610
|
for (const part of m.content) {
|
|
611
611
|
if (part.type === "thinking" && part.thinking) {
|
|
612
|
-
//
|
|
612
|
+
// Preserve namespaced signatures only — foreign-provider
|
|
613
613
|
// signatures never echo here; untagged legacy echoes (compat).
|
|
614
614
|
// Absent signatures are omitted (never sent as empty strings).
|
|
615
615
|
const partProvider = (part as { thoughtSignatureProvider?: unknown }).thoughtSignatureProvider;
|
|
@@ -642,7 +642,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
642
642
|
} else if (m.thoughtSignature && !steps.slice(turnStart).some((s) => s.type === "thought")) {
|
|
643
643
|
// Preserve a signature-only thought so stateless chaining keeps
|
|
644
644
|
// reasoning continuity even when no summary text was retained.
|
|
645
|
-
//
|
|
645
|
+
// Only for google/untagged signatures.
|
|
646
646
|
const msgProvider = (m as { thoughtSignatureProvider?: unknown }).thoughtSignatureProvider;
|
|
647
647
|
if (shouldEchoSignature(msgProvider, "google")) {
|
|
648
648
|
steps.push({ type: "thought", signature: m.thoughtSignature });
|
|
@@ -910,7 +910,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
910
910
|
signal?: AbortSignal
|
|
911
911
|
): RequestInit {
|
|
912
912
|
const headers: Record<string, string> = { "Content-Type": "application/json", ...options?.headers };
|
|
913
|
-
//
|
|
913
|
+
// Key travels as the `x-goog-api-key` header (never `?key=` in the
|
|
914
914
|
// URL, which would leak into logs). Case-insensitive merge so a custom
|
|
915
915
|
// `X-Goog-Api-Key` never duplicates the canonical header. Proxy flows
|
|
916
916
|
// (`apiKey === "unused"`) preserve custom auth instead of overwriting.
|
|
@@ -1022,7 +1022,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1022
1022
|
assertSupportedGeneration(clean);
|
|
1023
1023
|
const apiKey = resolveApiKey(options);
|
|
1024
1024
|
if (!apiKey) {
|
|
1025
|
-
throw new
|
|
1025
|
+
throw new ConfigError(
|
|
1026
1026
|
"[Agent Accelerator] Missing Google API key. Set GEMINI_API_KEY (or GOOGLE_API_KEY) or pass apiKey."
|
|
1027
1027
|
);
|
|
1028
1028
|
}
|
|
@@ -1133,7 +1133,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1133
1133
|
assertSupportedGeneration(clean);
|
|
1134
1134
|
const apiKey = resolveApiKey(options);
|
|
1135
1135
|
if (!apiKey) {
|
|
1136
|
-
throw new
|
|
1136
|
+
throw new ConfigError(
|
|
1137
1137
|
"[Agent Accelerator] Missing Google API key. Set GEMINI_API_KEY (or GOOGLE_API_KEY) or pass apiKey."
|
|
1138
1138
|
);
|
|
1139
1139
|
}
|
|
@@ -1392,7 +1392,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1392
1392
|
noteProviderTurn(sessionId, "google");
|
|
1393
1393
|
checkTurnTokens(sessionId, usage);
|
|
1394
1394
|
|
|
1395
|
-
//
|
|
1395
|
+
// Terminal-state guard — fail `incomplete_stream` (retryable)
|
|
1396
1396
|
// instead of resolving empty success. Skipped for non-SSE payloads
|
|
1397
1397
|
// (JSON mocks) where no SSE frames were ever seen.
|
|
1398
1398
|
const hasTerminal = completedBody !== undefined || sawFinishReason || sawUsage;
|
|
@@ -59,7 +59,7 @@ import { normalizeMediaInput } from "../utils/media.ts";
|
|
|
59
59
|
import { safeStringify } from "../utils/serialization.ts";
|
|
60
60
|
import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
|
|
61
61
|
import { withRetries } from "../utils/retry.ts";
|
|
62
|
-
import { ConfigError } from "../types/errors.ts";
|
|
62
|
+
import { ConfigError, ValidationError } from "../types/errors.ts";
|
|
63
63
|
import {
|
|
64
64
|
parseArguments,
|
|
65
65
|
normalizeThinkingParts,
|
|
@@ -350,13 +350,7 @@ function assertNoVideoOrFileParts(
|
|
|
350
350
|
kind === "video"
|
|
351
351
|
? `[${prefix}/${modelId}] unsupported video input (generic OpenAI-compatible endpoints carry text/image/audio only). Use a video-capable model (e.g. google/gemini-*) or drop the video part.`
|
|
352
352
|
: `[${prefix}/${modelId}] unsupported file input (generic OpenAI-compatible endpoints carry text/image/audio only). Use a file-capable model (e.g. google/gemini-*) or convert the document to text before sending it.`;
|
|
353
|
-
|
|
354
|
-
err.name = "AgentAccelProviderError";
|
|
355
|
-
Object.defineProperties(err, {
|
|
356
|
-
provider: { value: prefix, enumerable: false },
|
|
357
|
-
model: { value: modelId, enumerable: false },
|
|
358
|
-
});
|
|
359
|
-
throw err;
|
|
353
|
+
throw new ValidationError(message, { provider: prefix, model: modelId });
|
|
360
354
|
}
|
|
361
355
|
|
|
362
356
|
// Text, image (link or data URL) and audio parts as Chat content blocks; in: a user message's parts, out: fullHistoryMessages.
|
|
@@ -406,7 +400,7 @@ function resultToOutput(result: unknown): string {
|
|
|
406
400
|
* resent — except echoed Gemini thought signatures, which strict
|
|
407
401
|
* OpenAI-compatible endpoints require on tool calls they generated.
|
|
408
402
|
*
|
|
409
|
-
*
|
|
403
|
+
* Signatures are namespaced — only echo when the stored provider is
|
|
410
404
|
* `google` (or untagged legacy). Signatures minted by other providers never
|
|
411
405
|
* leak onto this transport.
|
|
412
406
|
*/
|
|
@@ -468,7 +462,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
|
|
|
468
462
|
// Echo provider-issued thought signatures verbatim so endpoints
|
|
469
463
|
// that minted them (Gemini behind a compat proxy) keep working.
|
|
470
464
|
// Only present when a previous turn captured one — other endpoints
|
|
471
|
-
// never see this field.
|
|
465
|
+
// never see this field. Echo only google/untagged signatures.
|
|
472
466
|
const storedProvider = (part as { thoughtSignatureProvider?: unknown }).thoughtSignatureProvider;
|
|
473
467
|
if (part.thoughtSignature && shouldEchoSignature(storedProvider, "google")) {
|
|
474
468
|
call.extra_content = { google: { thought_signature: part.thoughtSignature } };
|
|
@@ -540,7 +534,7 @@ function extractSignature(rawTc: {
|
|
|
540
534
|
return typeof sig === "string" && sig ? sig : undefined;
|
|
541
535
|
}
|
|
542
536
|
|
|
543
|
-
/** Tags a captured compat signature with its issuing provider (`google
|
|
537
|
+
/** Tags a captured compat signature with its issuing provider (`google`). */
|
|
544
538
|
function withCompatSignature(sig: string | undefined): { thoughtSignature?: string; thoughtSignatureProvider?: string } {
|
|
545
539
|
if (!sig) return {};
|
|
546
540
|
return { thoughtSignature: sig, thoughtSignatureProvider: "google" };
|
|
@@ -575,7 +569,7 @@ function parseResponse(
|
|
|
575
569
|
|
|
576
570
|
const choice = response.choices?.[0];
|
|
577
571
|
const message = choice?.message;
|
|
578
|
-
//
|
|
572
|
+
// Refusal content surfaces as text with a `refusal` finish reason.
|
|
579
573
|
const refusal = typeof message?.refusal === "string" && message.refusal ? message.refusal : "";
|
|
580
574
|
const text = typeof message?.content === "string" && message.content ? message.content : refusal;
|
|
581
575
|
// Thinking tolerance: plain `reasoning` plus per-block texts (deepseek-style
|
|
@@ -701,7 +695,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
701
695
|
private requireApiKey(baseUrl: string, options?: ProviderRequestOptions): string | undefined {
|
|
702
696
|
const key = this.apiKey(options);
|
|
703
697
|
if (!key && !isLocalEndpoint(baseUrl)) {
|
|
704
|
-
throw new
|
|
698
|
+
throw new ConfigError(
|
|
705
699
|
`[Agent Accelerator] Missing API key for "${this.prefix}". Set ${envPrefixOf(this.prefix)}_API_KEY or pass apiKey. (Local endpoints may omit the key.)`
|
|
706
700
|
);
|
|
707
701
|
}
|
|
@@ -789,7 +783,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
789
783
|
// why: invalid output shapes fail fast at Agent construction/loop validation.
|
|
790
784
|
}
|
|
791
785
|
}
|
|
792
|
-
//
|
|
786
|
+
// Request usage on streams where supported so terminal accounting
|
|
793
787
|
// arrives on the final chunk. Strict servers that reject unknown fields
|
|
794
788
|
// opt out via `COMPAT_NO_STREAM_OPTIONS=1`; an explicit per-call
|
|
795
789
|
// `(options as any).streamOptions === false` also skips it.
|
|
@@ -1068,7 +1062,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1068
1062
|
const choice = (Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
|
|
1069
1063
|
const delta = (choice["delta"] as Record<string, unknown>) ?? {};
|
|
1070
1064
|
// Text delta (content-free accounting frames carry "" — ignored).
|
|
1071
|
-
//
|
|
1065
|
+
// Refusal deltas surface as text with a `refusal` finish.
|
|
1072
1066
|
const deltaRefusal = delta["refusal"];
|
|
1073
1067
|
if (typeof deltaRefusal === "string" && deltaRefusal) {
|
|
1074
1068
|
text += deltaRefusal;
|
|
@@ -1187,7 +1181,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1187
1181
|
sawFinishReason = true;
|
|
1188
1182
|
}
|
|
1189
1183
|
|
|
1190
|
-
//
|
|
1184
|
+
// Terminal-state guard — fail `incomplete_stream` (retryable)
|
|
1191
1185
|
// instead of resolving empty success. Skipped for non-SSE payloads
|
|
1192
1186
|
// (JSON mocks) where no SSE frames were ever seen.
|
|
1193
1187
|
const cleanThinking = thinking.replace(/\n{3,}/g, "\n\n").trim();
|
package/src/providers/openai.ts
CHANGED
|
@@ -51,6 +51,7 @@ import { clampCacheKey } from "../utils/cache.ts";
|
|
|
51
51
|
import { normalizeMediaInput } from "../utils/media.ts";
|
|
52
52
|
import { safeStringify } from "../utils/serialization.ts";
|
|
53
53
|
import { toConciseProviderError, assertModalitiesSupported, assertNoVideoPartsOnResponses } from "../utils/errors.ts";
|
|
54
|
+
import { ConfigError } from "../types/errors.ts";
|
|
54
55
|
import { withRetries } from "../utils/retry.ts";
|
|
55
56
|
import {
|
|
56
57
|
parseArguments,
|
|
@@ -472,7 +473,7 @@ function parseResponse(
|
|
|
472
473
|
}
|
|
473
474
|
}
|
|
474
475
|
|
|
475
|
-
//
|
|
476
|
+
// `incomplete` -> length (content-filter truncations surface as
|
|
476
477
|
// refusal when refusal text is present).
|
|
477
478
|
const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details?.reason;
|
|
478
479
|
const finishReason =
|
|
@@ -716,7 +717,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
716
717
|
const clean = cleanModelId(model);
|
|
717
718
|
const apiKey = resolveApiKey(options);
|
|
718
719
|
if (!apiKey) {
|
|
719
|
-
throw new
|
|
720
|
+
throw new ConfigError(
|
|
720
721
|
"[Agent Accelerator] Missing OpenAI API key. Set OPENAI_API_KEY (or OPENAI_BASE_API_KEY) or pass apiKey."
|
|
721
722
|
);
|
|
722
723
|
}
|
|
@@ -804,7 +805,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
804
805
|
try {
|
|
805
806
|
const apiKey = resolveApiKey(options);
|
|
806
807
|
if (!apiKey) {
|
|
807
|
-
throw new
|
|
808
|
+
throw new ConfigError(
|
|
808
809
|
"[Agent Accelerator] Missing OpenAI API key. Set OPENAI_API_KEY (or OPENAI_BASE_API_KEY) or pass apiKey."
|
|
809
810
|
);
|
|
810
811
|
}
|
|
@@ -828,7 +829,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
828
829
|
body,
|
|
829
830
|
};
|
|
830
831
|
|
|
831
|
-
//
|
|
832
|
+
// Optional per-call timeout races the linked abort signal.
|
|
832
833
|
const streamTimeoutMs = timeoutFor(options as { timeoutMs?: number } | undefined);
|
|
833
834
|
// Transient failures (429/5xx/network) are retried here, before the
|
|
834
835
|
// first byte; once the stream is live it is never retried.
|
|
@@ -895,7 +896,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
895
896
|
type === "response.refusal.delta" ||
|
|
896
897
|
type === "response.output_refusal.delta"
|
|
897
898
|
) {
|
|
898
|
-
//
|
|
899
|
+
// Refusal deltas carry the filter text; surface as text and
|
|
899
900
|
// remember for a `refusal` finish reason.
|
|
900
901
|
if (typeof msg["delta"] === "string" && msg["delta"]) {
|
|
901
902
|
text += msg["delta"] as string;
|
|
@@ -955,7 +956,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
955
956
|
}
|
|
956
957
|
// Non-streaming maps `incomplete` → `length` (max tokens). Streaming
|
|
957
958
|
// must do the same — otherwise truncated turns misreport `stop`.
|
|
958
|
-
//
|
|
959
|
+
// Content-filter incompletes surface as `refusal`.
|
|
959
960
|
const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details?.reason;
|
|
960
961
|
if (response.status === "incomplete") {
|
|
961
962
|
finishReason = incompleteReason === "content_filter" ? "refusal" : "length";
|
|
@@ -1039,7 +1040,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
1039
1040
|
sawFinishReason = true;
|
|
1040
1041
|
}
|
|
1041
1042
|
|
|
1042
|
-
//
|
|
1043
|
+
// Terminal-state guard. Loops ending with no completed body, no
|
|
1043
1044
|
// finish_reason, and no usage while carrying no content must fail as
|
|
1044
1045
|
// `incomplete_stream` (retryable) instead of resolving empty success.
|
|
1045
1046
|
// Skipped for non-SSE payloads (e.g. JSON mocks in delegation tests)
|
|
@@ -46,7 +46,7 @@ import { buildSessionHeaders, setHeaderCaseInsensitive } from "../utils/headers.
|
|
|
46
46
|
import { normalizeMediaInput } from "../utils/media.ts";
|
|
47
47
|
import { safeStringify } from "../utils/serialization.ts";
|
|
48
48
|
import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
|
|
49
|
-
import { isAbortError } from "../types/errors.ts";
|
|
49
|
+
import { ConfigError, isAbortError } from "../types/errors.ts";
|
|
50
50
|
import {
|
|
51
51
|
newToolCallId,
|
|
52
52
|
redactedHeaders,
|
|
@@ -648,7 +648,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
648
648
|
try {
|
|
649
649
|
const apiKey = resolveApiKey(options);
|
|
650
650
|
if (!apiKey) {
|
|
651
|
-
throw new
|
|
651
|
+
throw new ConfigError(
|
|
652
652
|
"[Agent Accelerator] Missing OpenRouter API key. Set OPENROUTER_API_KEY or pass apiKey."
|
|
653
653
|
);
|
|
654
654
|
}
|
|
@@ -741,7 +741,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
741
741
|
const choice = (Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
|
|
742
742
|
const delta = (choice["delta"] as Record<string, unknown>) ?? {};
|
|
743
743
|
// Text delta (content-free accounting frames carry "" — ignored).
|
|
744
|
-
//
|
|
744
|
+
// Refusal deltas surface as text with a `refusal` finish.
|
|
745
745
|
const deltaRefusal = delta["refusal"];
|
|
746
746
|
if (typeof deltaRefusal === "string" && deltaRefusal) {
|
|
747
747
|
text += deltaRefusal;
|
|
@@ -854,7 +854,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
854
854
|
sawFinishReason = true;
|
|
855
855
|
}
|
|
856
856
|
|
|
857
|
-
//
|
|
857
|
+
// Terminal-state guard — fail `incomplete_stream` (retryable)
|
|
858
858
|
// instead of resolving empty success. Skipped for non-SSE payloads
|
|
859
859
|
// (JSON mocks) where no SSE frames were ever seen.
|
|
860
860
|
const cleanThinking = thinking.replace(/\n{3,}/g, "\n\n").trim();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConfigError } from "../types/errors.ts";
|
|
1
2
|
import type { Provider, ProviderId, ModelSpec } from "../types/model.ts";
|
|
2
3
|
import type { ThinkingLevel } from "../types/core.ts";
|
|
3
4
|
import { OpenAICompatibleChatProvider } from "./openai-compat.ts";
|
|
@@ -54,7 +55,7 @@ function removedProviderMessage(id: string): string {
|
|
|
54
55
|
/** Throws for removed provider prefixes. */
|
|
55
56
|
function assertProviderNotRemoved(id: string): void {
|
|
56
57
|
if (REMOVED_PROVIDERS.has(id.trim().toLowerCase())) {
|
|
57
|
-
throw new
|
|
58
|
+
throw new ConfigError(removedProviderMessage(id.trim()));
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -83,7 +84,7 @@ export function ensureCustomProvider(
|
|
|
83
84
|
const id = normalizeProviderPrefix(prefix) as ProviderId;
|
|
84
85
|
if (FIRST_CLASS.has(id.toLowerCase())) {
|
|
85
86
|
if (opts?.baseUrl !== undefined || opts?.apiKey !== undefined) {
|
|
86
|
-
throw new
|
|
87
|
+
throw new ConfigError(
|
|
87
88
|
`[Agent Accelerator] Provider "${id}" is first-class and cannot be replaced with custom baseUrl/apiKey. Pass baseUrl/apiKey per-Agent instead.`
|
|
88
89
|
);
|
|
89
90
|
}
|
|
@@ -93,7 +94,7 @@ export function ensureCustomProvider(
|
|
|
93
94
|
if (existing) {
|
|
94
95
|
const prevBase = (existing as { configuredBaseUrl?: string }).configuredBaseUrl;
|
|
95
96
|
if (opts?.baseUrl !== undefined && prevBase !== undefined && prevBase !== opts.baseUrl) {
|
|
96
|
-
throw new
|
|
97
|
+
throw new ConfigError(
|
|
97
98
|
`[Agent Accelerator] Conflicting re-registration for provider "${id}". Registry is immutable after creation.`
|
|
98
99
|
);
|
|
99
100
|
}
|
|
@@ -120,7 +121,7 @@ export function getProvider(id: ProviderId | string): Provider {
|
|
|
120
121
|
const provider = providerRegistry.get(key);
|
|
121
122
|
if (provider) return provider;
|
|
122
123
|
if (!key) {
|
|
123
|
-
throw new
|
|
124
|
+
throw new ConfigError(
|
|
124
125
|
`Provider '' is not supported or not registered. Available first-support providers: google, openrouter, openai — or any custom prefix via {PREFIX}_API_KEY + {PREFIX}_BASE_URL.`
|
|
125
126
|
);
|
|
126
127
|
}
|
|
@@ -146,7 +147,7 @@ export interface ResolvedModel {
|
|
|
146
147
|
*/
|
|
147
148
|
export function resolveModel(model: string | ModelSpec): ResolvedModel {
|
|
148
149
|
if (!model || (typeof model === "string" && !model.trim())) {
|
|
149
|
-
throw new
|
|
150
|
+
throw new ConfigError(
|
|
150
151
|
"[Agent Accelerator] No model specified. Please specify 'model' in Agent configuration or set the MODEL environment variable."
|
|
151
152
|
);
|
|
152
153
|
}
|
package/src/providers/shared.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared provider helpers
|
|
2
|
+
* Shared provider helpers.
|
|
3
3
|
*
|
|
4
4
|
* Single home for the `redactedHeaders` / `readErrorPayload` /
|
|
5
5
|
* `parseArguments` / `normalizeThinkingParts` helpers previously duplicated
|
|
@@ -78,7 +78,7 @@ export function readErrorPayload(
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/** Retryable incomplete-stream error for truncated SSE
|
|
81
|
+
/** Retryable incomplete-stream error for truncated SSE: a `ProviderError` whose `code` is `incomplete_stream`. */
|
|
82
82
|
export function incompleteStreamError(provider: string, modelId: string): Error {
|
|
83
83
|
const err = toConciseProviderError(
|
|
84
84
|
Object.assign(new Error("Stream ended without terminal event"), {
|
|
@@ -110,15 +110,11 @@ export function isNotFoundChainError(error: unknown): boolean {
|
|
|
110
110
|
NaN
|
|
111
111
|
);
|
|
112
112
|
if (status === 404) return true;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
(err["errorType"] as string | undefined) ??
|
|
116
|
-
""
|
|
117
|
-
).toLowerCase();
|
|
118
|
-
return code === "not_found";
|
|
113
|
+
// `providerCode` on a wrapped ProviderError (its `code` is the SDK kind), `code` on a raw error.
|
|
114
|
+
return [err["providerCode"], err["code"], err["errorType"]].some((c) => String(c ?? "").toLowerCase() === "not_found");
|
|
119
115
|
}
|
|
120
116
|
|
|
121
|
-
/** Per-request timeout ms from provider options
|
|
117
|
+
/** Per-request timeout ms from provider options. */
|
|
122
118
|
export function timeoutFor(options: { timeoutMs?: number } | undefined): number | undefined {
|
|
123
119
|
const t = (options as { timeoutMs?: unknown } | undefined)?.timeoutMs;
|
|
124
120
|
return typeof t === "number" && Number.isFinite(t) && t > 0 ? Math.floor(t) : undefined;
|
|
@@ -149,7 +145,7 @@ export function combinedSignal(signal?: AbortSignal, timeoutMs?: number): { sign
|
|
|
149
145
|
return { signal: ctrl.signal, cleanup };
|
|
150
146
|
}
|
|
151
147
|
|
|
152
|
-
/**
|
|
148
|
+
/** Crypto-backed tool-call id, no Math.random primary. */
|
|
153
149
|
export function newToolCallId(prefix = "call"): string {
|
|
154
150
|
try {
|
|
155
151
|
const c = (globalThis as any)?.crypto;
|
|
@@ -4,16 +4,19 @@ Scheduled actions: a worker that runs later (once or repeating) and saves its an
|
|
|
4
4
|
|
|
5
5
|
| File | What it is |
|
|
6
6
|
| :-- | :-- |
|
|
7
|
-
| `scheduler.ts` | One `Scheduler` per home folder: a 1 s unref'd clock that runs only while something is pending, startup sweep, cook/deliver state machine, and the public functions (`createScheduledAction`, `list/update/run/skip/deleteScheduledAction`). |
|
|
8
|
-
| `store.ts` | Disk layer under `<home>/
|
|
7
|
+
| `scheduler.ts` | One `Scheduler` per home folder: a 1 s unref'd clock that runs only while something is pending, startup sweep, dead-owner recovery, heartbeats, cook/deliver state machine, and the public functions (`createScheduledAction`, `list/update/run/skip/deleteScheduledAction`). |
|
|
8
|
+
| `store.ts` | Disk layer under `<home>/scheduled/`: `<id>/task.json`, `<id>/delivery.json`, run files, one short mutex per action; validation of records. |
|
|
9
9
|
| `time.ts` | Pure `DD:HH:MM` and local-clock maths. No timezone, DST or `Intl`; `now` is always passed in. |
|
|
10
10
|
| `tool.ts` + `scheduled-actions.md` | The model-facing `scheduled_actions` tool and `get_datetime`; the `.md` is the tool description. |
|
|
11
11
|
|
|
12
12
|
## Invariants
|
|
13
13
|
- The files are the truth: every write re-reads first and goes through the durable writer. The file format is a stable public contract: see `docs/scheduled-action-files.md` and add a changelog row for any change.
|
|
14
|
-
- Write order when a run ends: run file, then
|
|
14
|
+
- Write order when a run ends: run file, then `<id>/delivery.json`, then `<id>/task.json`. A crash in between is repaired by the dead-owner recovery (`recoverCooking`, at start and every 15 s): the delivery is rebuilt from the run file, or the run is marked `missed` when no run file exists.
|
|
15
15
|
- `startCook` advances the next times BEFORE the run starts; a run still cooking when the next is due is skipped, never stacked.
|
|
16
16
|
- Runs are fresh stateless workers built by `buildDynamicWorkerConfig`; their spending guardrail is `workerBudget` only.
|
|
17
17
|
- The Agent registry holds `WeakRef`s by name: keep every Agent that owns actions referenced, or its runs become `missed`.
|
|
18
|
+
- Several processes may share a home: scheduled actions take no home lock, only the short per-action mutex (`<id>/.mutex`); the one-process lock is on session folders only. Ownership: a process only starts, delivers or recovers actions whose Agent it registered. An action nobody here owns is left alone until its delivery time passed with no run started, then marked `missed` (`no_agent`).
|
|
19
|
+
- Exactly once: a scheduled start re-checks under the action's mutex that it is still `waiting` and due; `markMissed` only changes the state the caller saw; a delivery is emitted only by the process that removed its entry from `delivery.json`.
|
|
20
|
+
- A run in flight keeps `<id>/.heartbeat` (pid, host, token; mtime refreshed every 5 s, stale after 60 s), written before the record shows `cooking`. A cooking action whose owner is dead (pid gone, stale, or none) is recovered by a live process that owns its Agent, at start and every 15 s. `delete` of a run another live process is cooking is refused, even with `force`.
|
|
18
21
|
- Times come from `Date`, local clock only. Tests inject `scheduler.now` / `tickMs` and call `tick()`.
|
|
19
22
|
- Events: `scheduled_action.{created,updated,started,ready,failed,delivered,missed,skipped,deleted,invalid,store_corrupt,error}`.
|