agent-accelerator 0.5.6 → 0.5.7
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 +6 -13
- package/package.json +1 -1
- package/src/agent/agent.ts +16 -18
- package/src/agent/architecture.md +25 -0
- package/src/agent/context.ts +4 -0
- package/src/agent/delegation.ts +18 -84
- package/src/agent/loop.ts +13 -15
- package/src/agent/worker.ts +1 -3
- package/src/architecture.md +28 -0
- package/src/index.ts +6 -73
- package/src/models/architecture.md +14 -0
- package/src/models/catalog-cache.ts +3 -0
- package/src/models/catalog.ts +7 -0
- package/src/providers/architecture.md +24 -0
- package/src/providers/google.ts +20 -0
- package/src/providers/openai-compat.ts +22 -0
- package/src/providers/openai.ts +17 -1
- package/src/providers/openrouter.ts +12 -0
- package/src/providers/registry.ts +1 -0
- package/src/providers.ts +9 -53
- package/src/scheduler/architecture.md +19 -0
- package/src/scheduler/scheduler.ts +21 -0
- package/src/scheduler/store.ts +6 -0
- package/src/scheduler/time.ts +2 -0
- package/src/session/architecture.md +19 -0
- package/src/session/store.ts +15 -0
- package/src/session/usage.ts +1 -0
- package/src/streaming/architecture.md +13 -0
- package/src/streaming/event-stream.ts +5 -0
- package/src/telemetry.ts +1 -0
- package/src/tools/architecture.md +19 -0
- package/src/tools/executor.ts +13 -0
- package/src/tools/schema.ts +1 -0
- package/src/tools/subagent-result.ts +1 -0
- package/src/types/architecture.md +20 -0
- package/src/utils/architecture.md +25 -0
- package/src/utils/base64.ts +2 -0
- package/src/utils/cache.ts +2 -0
- package/src/utils/errors.ts +4 -0
- package/src/utils/headers.ts +4 -1
- package/src/utils/media.ts +1 -0
- package/src/utils/raw-capture.ts +3 -0
- package/src/utils/retry.ts +1 -0
- package/src/utils/safe-io.ts +4 -0
- package/src/utils/session.ts +5 -161
package/src/providers/openai.ts
CHANGED
|
@@ -167,6 +167,7 @@ const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
|
167
167
|
// Request building (canonical -> Responses)
|
|
168
168
|
// ---------------------------------------------------------------------------
|
|
169
169
|
|
|
170
|
+
// Endpoint from the option, OPENAI_BASE_URL/OPENAI_API_BASE, else api.openai.com; in: each request, out: the base URL without a trailing slash.
|
|
170
171
|
function resolveBaseUrl(options?: ProviderRequestOptions): string {
|
|
171
172
|
return (
|
|
172
173
|
options?.baseUrl ||
|
|
@@ -178,6 +179,7 @@ function resolveBaseUrl(options?: ProviderRequestOptions): string {
|
|
|
178
179
|
).replace(/\/+$/, "");
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
// Key from the option or env, or `unused` when custom headers carry the auth; in: each request, out: the key or undefined.
|
|
181
183
|
function resolveApiKey(options?: ProviderRequestOptions): string | undefined {
|
|
182
184
|
const key = getApiKey("openai", options?.apiKey, options?.env);
|
|
183
185
|
if (key) return key;
|
|
@@ -191,6 +193,7 @@ function cleanModelId(model: string | ModelSpec): string {
|
|
|
191
193
|
return rawId.replace(/^openai\//i, "");
|
|
192
194
|
}
|
|
193
195
|
|
|
196
|
+
// Canonical tool declarations as Responses function tools; in: options.tools, out: request `tools`.
|
|
194
197
|
function toOpenAITools(tools?: StandardToolDeclaration[]): Array<Record<string, unknown>> | undefined {
|
|
195
198
|
if (!tools || tools.length === 0) return undefined;
|
|
196
199
|
return tools.map((t) => ({
|
|
@@ -202,6 +205,7 @@ function toOpenAITools(tools?: StandardToolDeclaration[]): Array<Record<string,
|
|
|
202
205
|
}));
|
|
203
206
|
}
|
|
204
207
|
|
|
208
|
+
// Text and media parts as input_text, input_image and input_file blocks; web links pass through, the rest is inlined as data URLs; in: a user message's parts, out: fullHistoryInput.
|
|
205
209
|
async function contentPartsToBlocks(parts: ContentPart[]): Promise<ResponseContentPart[]> {
|
|
206
210
|
const blocks: ResponseContentPart[] = [];
|
|
207
211
|
for (const part of parts) {
|
|
@@ -258,17 +262,20 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ResponseConte
|
|
|
258
262
|
return blocks;
|
|
259
263
|
}
|
|
260
264
|
|
|
265
|
+
// A tool result as the text of a function_call_output item; in: fullHistoryInput, out: a string.
|
|
261
266
|
function resultToOutput(result: unknown): string {
|
|
262
267
|
if (typeof result === "string") return result;
|
|
263
268
|
return safeStringify(result);
|
|
264
269
|
}
|
|
265
270
|
|
|
271
|
+
// Makes an id safe for the Responses wire (allowed characters, at most 64, no trailing underscores); in: fullHistoryInput, out: the id.
|
|
266
272
|
function normalizeResponsesIdPart(part: string): string {
|
|
267
273
|
const sanitized = String(part ?? "").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
268
274
|
const truncated = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
|
|
269
275
|
return truncated.replace(/_+$/, "") || "fc_unknown";
|
|
270
276
|
}
|
|
271
277
|
|
|
278
|
+
// Gives a function-call item id the required `fc_` prefix; in: fullHistoryInput, out: the id.
|
|
272
279
|
function normalizeResponsesItemId(itemId: string): string {
|
|
273
280
|
let norm = normalizeResponsesIdPart(itemId);
|
|
274
281
|
if (!norm.startsWith("fc_")) {
|
|
@@ -390,6 +397,7 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
|
|
|
390
397
|
// Response mapping (Responses -> canonical)
|
|
391
398
|
// ---------------------------------------------------------------------------
|
|
392
399
|
|
|
400
|
+
// Responses usage as TokenUsage, clamping cache reads to input; in: a response or the completed event, out: TokenUsage.
|
|
393
401
|
function mapUsage(raw?: ResponsesObject["usage"]): TokenUsage {
|
|
394
402
|
const input = raw?.input_tokens ?? 0;
|
|
395
403
|
const output = raw?.output_tokens ?? 0;
|
|
@@ -406,6 +414,7 @@ function mapUsage(raw?: ResponsesObject["usage"]): TokenUsage {
|
|
|
406
414
|
};
|
|
407
415
|
}
|
|
408
416
|
|
|
417
|
+
// Turns a Responses object into text, reasoning, tool calls and usage, mapping refusals and incomplete to finish reasons and throwing on failed; in: generate, out: ProviderGenerateResult.
|
|
409
418
|
function parseResponse(
|
|
410
419
|
response: ResponsesObject,
|
|
411
420
|
modelId: string,
|
|
@@ -509,6 +518,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
509
518
|
return getModelsForProvider("openai");
|
|
510
519
|
}
|
|
511
520
|
|
|
521
|
+
// The catalog spec of a model, else a permissive generic one; in: the registry, out: a ModelSpec.
|
|
512
522
|
getModel(modelId: string): ModelSpec | undefined {
|
|
513
523
|
const clean = cleanModelId(modelId);
|
|
514
524
|
return (
|
|
@@ -519,6 +529,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
519
529
|
);
|
|
520
530
|
}
|
|
521
531
|
|
|
532
|
+
// Builds the stateless Responses request (full history, store:false, tools, reasoning, tier, text.format, cache key and retention); in: generate/stream, out: the body.
|
|
522
533
|
private async buildBody(
|
|
523
534
|
modelId: string,
|
|
524
535
|
context: ProviderContext,
|
|
@@ -620,6 +631,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
620
631
|
return body;
|
|
621
632
|
}
|
|
622
633
|
|
|
634
|
+
// Fetch options with session-affinity headers, JSON content type and the Bearer key; in: doFetch and stream, out: RequestInit.
|
|
623
635
|
private requestInit(
|
|
624
636
|
body: ResponsesRequestBody,
|
|
625
637
|
options: ProviderRequestOptions | undefined,
|
|
@@ -640,6 +652,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
640
652
|
return { method: "POST", headers, body: JSON.stringify(body), signal };
|
|
641
653
|
}
|
|
642
654
|
|
|
655
|
+
// One HTTP call under the request timeout, returning status, headers and body text; in: generate, out: the raw reply.
|
|
643
656
|
private async doFetch(
|
|
644
657
|
url: string,
|
|
645
658
|
body: ResponsesRequestBody,
|
|
@@ -662,6 +675,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
662
675
|
}
|
|
663
676
|
}
|
|
664
677
|
|
|
678
|
+
// Throws a concise provider error (status, code, Retry-After, key hint) for a non-2xx reply; in: generate and stream, out: nothing or a thrown error.
|
|
665
679
|
private throwIfError(
|
|
666
680
|
status: number,
|
|
667
681
|
url: string,
|
|
@@ -692,6 +706,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
692
706
|
throw toConciseProviderError(err, "openai", modelId);
|
|
693
707
|
}
|
|
694
708
|
|
|
709
|
+
// Non-streaming turn as one retried JSON request; in: runAgentLoop, out: ProviderGenerateResult.
|
|
695
710
|
async generate(
|
|
696
711
|
model: string | ModelSpec,
|
|
697
712
|
context: ProviderContext,
|
|
@@ -773,6 +788,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
773
788
|
}
|
|
774
789
|
}
|
|
775
790
|
|
|
791
|
+
// Streaming turn over SSE: assembles text, reasoning, tool calls and usage, retrying only before the first byte; in: streamAgentLoop, out: an AssistantMessageEventStream.
|
|
776
792
|
stream(
|
|
777
793
|
model: string | ModelSpec,
|
|
778
794
|
context: ProviderContext,
|
|
@@ -793,7 +809,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
793
809
|
);
|
|
794
810
|
}
|
|
795
811
|
assertModalitiesSupported(context, "openai", clean);
|
|
796
|
-
|
|
812
|
+
assertNoVideoPartsOnResponses(context, "openai", clean);
|
|
797
813
|
const baseUrl = resolveBaseUrl(options);
|
|
798
814
|
const url = `${baseUrl}/responses`;
|
|
799
815
|
const sessionId = options?.sessionId;
|
|
@@ -145,6 +145,7 @@ const DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
|
|
|
145
145
|
// Request building (canonical -> Chat Completions)
|
|
146
146
|
// ---------------------------------------------------------------------------
|
|
147
147
|
|
|
148
|
+
// Endpoint from the option, OPENROUTER_BASE_URL, else openrouter.ai; in: each request, out: the base URL without a trailing slash.
|
|
148
149
|
function resolveBaseUrl(options?: ProviderRequestOptions): string {
|
|
149
150
|
return (
|
|
150
151
|
options?.baseUrl ||
|
|
@@ -154,6 +155,7 @@ function resolveBaseUrl(options?: ProviderRequestOptions): string {
|
|
|
154
155
|
).replace(/\/+$/, "");
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
// Key from the option or env, or `unused` when custom headers carry the auth; in: each request, out: the key or undefined.
|
|
157
159
|
function resolveApiKey(options?: ProviderRequestOptions): string | undefined {
|
|
158
160
|
const key = getApiKey("openrouter", options?.apiKey, options?.env);
|
|
159
161
|
if (key) return key;
|
|
@@ -172,11 +174,13 @@ function cleanModelId(model: string | ModelSpec): string {
|
|
|
172
174
|
|
|
173
175
|
// The HTTP status of an error chunk inside a stream (the HTTP status itself is 200): its numeric `code`, else the status of its retryable `error_type` (errors-and-debugging.md, "Typed Error Codes"); in: the chunk's error, out: streamInto, where it makes the error retryable.
|
|
174
176
|
const RETRYABLE_ERROR_TYPE_STATUS: Record<string, number> = { rate_limit_exceeded: 429, provider_overloaded: 503, provider_unavailable: 502, timeout: 504, server: 500 };
|
|
177
|
+
// HTTP-like status of an error chunk inside a stream, or undefined when unknown; in: streamInto's error branch, out: the status that makes the error retryable.
|
|
175
178
|
function midStreamStatus(code: unknown, errorType: unknown): number | undefined {
|
|
176
179
|
if (typeof code === "number" && Number.isInteger(code) && code >= 100 && code <= 599) return code;
|
|
177
180
|
return typeof errorType === "string" ? RETRYABLE_ERROR_TYPE_STATUS[errorType] : undefined;
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
// Canonical tool declarations as Chat Completions function tools; in: options.tools, out: request `tools`.
|
|
180
184
|
function toOpenRouterTools(tools?: StandardToolDeclaration[]): Array<Record<string, unknown>> | undefined {
|
|
181
185
|
if (!tools || tools.length === 0) return undefined;
|
|
182
186
|
return tools.map((t) => ({
|
|
@@ -192,6 +196,7 @@ function toOpenRouterTools(tools?: StandardToolDeclaration[]): Array<Record<stri
|
|
|
192
196
|
|
|
193
197
|
// The `input_audio.format` for a MIME type: Chat Completions takes any format string the provider supports (audio.md: wav, mp3, aiff, aac, ogg, flac, m4a, pcm16 ...), so the real format is sent, never a guess; in: an audio part's MIME type, out: contentPartsToBlocks.
|
|
194
198
|
const AUDIO_FORMAT: Record<string, string> = { wav: "wav", wave: "wav", mpeg: "mp3", mp3: "mp3", mp4: "m4a", m4a: "m4a", aac: "aac", ogg: "ogg", flac: "flac", aiff: "aiff" };
|
|
199
|
+
// The `input_audio.format` for an audio MIME type (see AUDIO_FORMAT above); in: contentPartsToBlocks, out: the format string.
|
|
195
200
|
function audioFormatFor(mimeType?: string): string {
|
|
196
201
|
const subtype = (mimeType || "").toLowerCase().split(";")[0]!.split("/")[1]?.replace(/^x-/, "") ?? "";
|
|
197
202
|
return AUDIO_FORMAT[subtype] ?? (subtype || "mp3");
|
|
@@ -278,6 +283,7 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
|
|
|
278
283
|
return blocks;
|
|
279
284
|
}
|
|
280
285
|
|
|
286
|
+
// A tool result as the text of a tool message; in: fullHistoryMessages, out: a string.
|
|
281
287
|
function resultToOutput(result: unknown): string {
|
|
282
288
|
if (typeof result === "string") return result;
|
|
283
289
|
return safeStringify(result);
|
|
@@ -392,6 +398,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
|
|
|
392
398
|
// Response mapping (Chat Completions -> canonical)
|
|
393
399
|
// ---------------------------------------------------------------------------
|
|
394
400
|
|
|
401
|
+
// Chat usage as TokenUsage with cache reads clamped to input, cache writes, reasoning tokens and OpenRouter's reported cost (0 counts); in: the last stream chunk, out: TokenUsage.
|
|
395
402
|
function mapUsage(raw?: ChatUsage): TokenUsage {
|
|
396
403
|
const input = raw?.prompt_tokens ?? 0;
|
|
397
404
|
const output = raw?.completion_tokens ?? 0;
|
|
@@ -430,6 +437,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
430
437
|
return getModelsForProvider("openrouter");
|
|
431
438
|
}
|
|
432
439
|
|
|
440
|
+
// The catalog spec of a model, else a permissive generic one; in: the registry, out: a ModelSpec.
|
|
433
441
|
getModel(modelId: string): ModelSpec | undefined {
|
|
434
442
|
const clean = cleanModelId(modelId);
|
|
435
443
|
return (
|
|
@@ -440,6 +448,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
440
448
|
);
|
|
441
449
|
}
|
|
442
450
|
|
|
451
|
+
// Builds the Chat request (messages, tools, tool_choice, reasoning, tier, cache_control, generation controls, response_format, session_id); in: streamInto, out: the body.
|
|
443
452
|
private async buildBody(
|
|
444
453
|
modelId: string,
|
|
445
454
|
context: ProviderContext,
|
|
@@ -518,6 +527,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
518
527
|
return body;
|
|
519
528
|
}
|
|
520
529
|
|
|
530
|
+
// Fetch options with session and opt-in attribution headers, JSON content type and the Bearer key; in: streamInto, out: RequestInit.
|
|
521
531
|
private requestInit(
|
|
522
532
|
body: ChatRequestBody,
|
|
523
533
|
options: ProviderRequestOptions | undefined,
|
|
@@ -537,6 +547,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
537
547
|
return { method: "POST", headers, body: JSON.stringify(body), signal };
|
|
538
548
|
}
|
|
539
549
|
|
|
550
|
+
// Throws a concise provider error (status, code, Retry-After, key and media-link hints) for a non-2xx reply, marking the in-flight-budget 402 retryable; in: streamInto, out: nothing or a thrown error.
|
|
540
551
|
private throwIfError(
|
|
541
552
|
status: number,
|
|
542
553
|
url: string,
|
|
@@ -612,6 +623,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
612
623
|
}
|
|
613
624
|
}
|
|
614
625
|
|
|
626
|
+
// Streaming turn: a normal event stream from streamInto; in: streamAgentLoop, out: an AssistantMessageEventStream.
|
|
615
627
|
stream(
|
|
616
628
|
model: string | ModelSpec,
|
|
617
629
|
context: ProviderContext,
|
|
@@ -42,6 +42,7 @@ const FIRST_CLASS = new Set([
|
|
|
42
42
|
// resolve anywhere.
|
|
43
43
|
const REMOVED_PROVIDERS = new Set(["opencode", "opencode-go"]);
|
|
44
44
|
|
|
45
|
+
// The error text for a removed provider prefix; in: assertProviderNotRemoved, out: a message.
|
|
45
46
|
function removedProviderMessage(id: string): string {
|
|
46
47
|
return (
|
|
47
48
|
`[Agent Accelerator] Provider "${id}" has been removed: OpenCode free models are ` +
|
package/src/providers.ts
CHANGED
|
@@ -42,6 +42,7 @@ export type ProviderCapabilityStatus =
|
|
|
42
42
|
*/
|
|
43
43
|
const emittedWarnings = new Set<string>();
|
|
44
44
|
|
|
45
|
+
/** Warns once per provider, capability and request about something that cannot be applied as asked: a `provider_warning` bus event, the legacy global sink and console; in: the mappers and adapters, out: those channels. */
|
|
45
46
|
export function emitProviderWarning(options: {
|
|
46
47
|
provider: string;
|
|
47
48
|
capability: string;
|
|
@@ -190,40 +191,6 @@ export function mapThinkingLevelToOpenRouterChat(
|
|
|
190
191
|
return {};
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
/**
|
|
194
|
-
* Maps a canonical ThinkingLevel onto OpenRouter Responses `reasoning.effort`.
|
|
195
|
-
* Documented efforts: minimal|low|medium|high (server default medium).
|
|
196
|
-
* Wire-validated extras: `none` disables reasoning output; `xhigh`/`max` are
|
|
197
|
-
* echoed but are NOT documented levels, so they clamp to `high` with a warning.
|
|
198
|
-
*
|
|
199
|
-
* @deprecated The Responses skin is discontinued for OpenRouter
|
|
200
|
-
* (removed from core). Use
|
|
201
|
-
* {@link mapThinkingLevelToOpenRouterChat} with the stable Chat Completions
|
|
202
|
-
* transport instead.
|
|
203
|
-
*/
|
|
204
|
-
export function mapThinkingLevelToOpenRouter(
|
|
205
|
-
level: ThinkingLevel | string | undefined
|
|
206
|
-
): { effort?: string } {
|
|
207
|
-
if (!level) return {};
|
|
208
|
-
const norm = String(level).toLowerCase().trim();
|
|
209
|
-
if (norm === "dynamic") return {}; // server default (medium)
|
|
210
|
-
if (norm === "none") return { effort: "none" };
|
|
211
|
-
if (norm === "xhigh" || norm === "max") {
|
|
212
|
-
emitProviderWarning({
|
|
213
|
-
provider: "openrouter",
|
|
214
|
-
capability: "thinking level",
|
|
215
|
-
requested: String(level),
|
|
216
|
-
reason: "OpenRouter documents reasoning efforts up to high.",
|
|
217
|
-
fallback: "reasoning effort high",
|
|
218
|
-
});
|
|
219
|
-
return { effort: "high" };
|
|
220
|
-
}
|
|
221
|
-
if (norm === "minimal" || norm === "low" || norm === "medium" || norm === "high") {
|
|
222
|
-
return { effort: norm };
|
|
223
|
-
}
|
|
224
|
-
return {};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
194
|
/** Maps canonical ServiceTier onto OpenRouter `service_tier` (omit = auto). */
|
|
228
195
|
export function mapServiceTierToOpenRouter(tier: ServiceTier | undefined): "flex" | "priority" | undefined {
|
|
229
196
|
if (tier === "flex" || tier === "priority") return tier;
|
|
@@ -287,25 +254,6 @@ export function mapToolChoiceToOpenRouterChat(
|
|
|
287
254
|
return undefined;
|
|
288
255
|
}
|
|
289
256
|
|
|
290
|
-
/** OpenRouter Responses `tool_choice` wire values.
|
|
291
|
-
* @deprecated Discontinued skin; see {@link OpenRouterChatToolChoice}. */
|
|
292
|
-
export type OpenRouterToolChoice = "auto" | "none" | "required" | { type: "function"; name: string };
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Maps canonical toolChoice onto OpenRouter Responses `tool_choice`.
|
|
296
|
-
* `auto` is the server default (omitted); `required` is natively supported
|
|
297
|
-
* (validated); a function pin passes through verbatim.
|
|
298
|
-
*/
|
|
299
|
-
export function mapToolChoiceToOpenRouter(
|
|
300
|
-
choice: "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
301
|
-
): OpenRouterToolChoice | undefined {
|
|
302
|
-
if (!choice || choice === "auto") return undefined;
|
|
303
|
-
if (choice === "none" || choice === "required") return choice;
|
|
304
|
-
const name = choice.function?.name;
|
|
305
|
-
if (name) return { type: "function", name };
|
|
306
|
-
return undefined;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
257
|
/**
|
|
310
258
|
* Maps a canonical ThinkingLevel onto OpenAI Responses `reasoning.effort`.
|
|
311
259
|
* Documented efforts: none|minimal|low|medium|high|xhigh|max (server default
|
|
@@ -383,6 +331,7 @@ export function normalizeToolChoice(
|
|
|
383
331
|
return { mode: "any" };
|
|
384
332
|
}
|
|
385
333
|
|
|
334
|
+
// True for a non-array object; in: the argument and schema helpers, out: a type guard.
|
|
386
335
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
387
336
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
388
337
|
}
|
|
@@ -449,6 +398,7 @@ export interface GenerationParams {
|
|
|
449
398
|
|
|
450
399
|
/** Most stop sequences a Chat Completions request may carry (OpenRouter chat-completion api-ref `maxItems`, OpenAI the same). */
|
|
451
400
|
const MAX_CHAT_STOP = 4;
|
|
401
|
+
// True for a finite number; in: the generation-param mappers, out: a type guard.
|
|
452
402
|
const finiteNumber = (v: unknown): v is number => typeof v === "number" && Number.isFinite(v);
|
|
453
403
|
|
|
454
404
|
/** Normalizes the public `stop` option (`string | string[]`) to a clean array, or undefined. */
|
|
@@ -484,6 +434,7 @@ export function mapGenerationParamsToChat(params: GenerationParams | undefined,
|
|
|
484
434
|
return out;
|
|
485
435
|
}
|
|
486
436
|
|
|
437
|
+
// Warns that `stop`/`seed` have no parameter on this wire and are dropped; in: the OpenAI and Google mappers, out: provider_warning events.
|
|
487
438
|
function warnUnsupportedStopSeed(provider: string, modelRef: string, params: GenerationParams, reasonSuffix: string): void {
|
|
488
439
|
if (params.stop && params.stop.length > 0) {
|
|
489
440
|
emitProviderWarning({
|
|
@@ -533,6 +484,7 @@ export function mapGenerationParamsToGoogle(
|
|
|
533
484
|
// Structured outputs (canonical, pure, no I/O)
|
|
534
485
|
// ---------------------------------------------------------------------------
|
|
535
486
|
|
|
487
|
+
// True when a value looks like a Zod schema (parse/safeParse/_def/~standard); in: structured-output normalization, out: a boolean.
|
|
536
488
|
function isZodLike(value: unknown): boolean {
|
|
537
489
|
if (!value || typeof value !== "object") return false;
|
|
538
490
|
const v = value as Record<string, unknown>;
|
|
@@ -544,6 +496,7 @@ function isZodLike(value: unknown): boolean {
|
|
|
544
496
|
);
|
|
545
497
|
}
|
|
546
498
|
|
|
499
|
+
// True when a plain object has JSON Schema keywords; in: structured-output normalization, out: a boolean.
|
|
547
500
|
function looksLikeJsonSchema(value: unknown): boolean {
|
|
548
501
|
if (!isPlainObject(value)) return false;
|
|
549
502
|
const v = value as Record<string, unknown>;
|
|
@@ -559,12 +512,14 @@ function looksLikeJsonSchema(value: unknown): boolean {
|
|
|
559
512
|
);
|
|
560
513
|
}
|
|
561
514
|
|
|
515
|
+
// A structured-output name limited to letters, digits, `_` and `-` (64 chars), default `structured_output`; in: normalizeStructuredOutput, out: the name.
|
|
562
516
|
function sanitizeOutputName(raw: unknown): string {
|
|
563
517
|
const base = typeof raw === "string" && raw.trim() ? raw.trim() : "structured_output";
|
|
564
518
|
const cleaned = base.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) || "structured_output";
|
|
565
519
|
return cleaned;
|
|
566
520
|
}
|
|
567
521
|
|
|
522
|
+
// A Zod schema or JSON Schema object as a JSON Schema object, else a ValidationError; in: normalizeStructuredOutput, out: the schema.
|
|
568
523
|
function schemaToJsonSchema(schema: unknown, what: string): Record<string, unknown> {
|
|
569
524
|
if (isZodLike(schema)) {
|
|
570
525
|
try {
|
|
@@ -634,6 +589,7 @@ export function extractOutputValidator(output: unknown): { safeParse: (v: unknow
|
|
|
634
589
|
return undefined;
|
|
635
590
|
}
|
|
636
591
|
|
|
592
|
+
// Removes a surrounding ```json fence from a model answer; in: parseStructuredOutput, out: the bare text.
|
|
637
593
|
function stripCodeFences(text: string): string {
|
|
638
594
|
const trimmed = text.trim();
|
|
639
595
|
const fenced = trimmed.match(/^```(?:json)?\s*\n([\s\S]*?)\n```\s*$/i);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# src/scheduler/
|
|
2
|
+
|
|
3
|
+
Scheduled actions: a worker that runs later (once or repeating) and saves its answer to a file. Off unless `dynamicSubagents.allowScheduledActions` is set.
|
|
4
|
+
|
|
5
|
+
| File | What it is |
|
|
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>/scheduled_actions/`: `tasks.json`, `deliveries.json`, run files; validation of records. |
|
|
9
|
+
| `time.ts` | Pure `DD:HH:MM` and local-clock maths. No timezone, DST or `Intl`; `now` is always passed in. |
|
|
10
|
+
| `tool.ts` + `scheduled-actions.md` | The model-facing `scheduled_actions` tool and `get_datetime`; the `.md` is the tool description. |
|
|
11
|
+
|
|
12
|
+
## Invariants
|
|
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 `deliveries.json`, then `tasks.json`. The startup sweep repairs a crash in between.
|
|
15
|
+
- `startCook` advances the next times BEFORE the run starts; a run still cooking when the next is due is skipped, never stacked.
|
|
16
|
+
- Runs are fresh stateless workers built by `buildDynamicWorkerConfig`; their spending guardrail is `workerBudget` only.
|
|
17
|
+
- The Agent registry holds `WeakRef`s by name: keep every Agent that owns actions referenced, or its runs become `missed`.
|
|
18
|
+
- Times come from `Date`, local clock only. Tests inject `scheduler.now` / `tickMs` and call `tick()`.
|
|
19
|
+
- Events: `scheduled_action.{created,updated,started,ready,failed,delivered,missed,skipped,deleted,invalid,store_corrupt,error}`.
|
|
@@ -81,6 +81,7 @@ interface RunningEntry {
|
|
|
81
81
|
missed: boolean;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// The message of any thrown value; in: catch blocks here, out: event and error text.
|
|
84
85
|
const errText = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
85
86
|
|
|
86
87
|
// A run's exact spending as run-header fields, the numbers `usageReport()` keeps for workers (tokens split, cache hit rate, cost and where the price came from); in: the one-shot worker's totals over all attempts, out: finish's RunHeader.
|
|
@@ -106,10 +107,12 @@ function usageFields(totals: UsageTotals | undefined): Partial<RunHeader> {
|
|
|
106
107
|
// Whole seconds since the epoch: stored times have no milliseconds, so lateness is compared at this precision; in: a clock reading or stored time, out: finish and deliver.
|
|
107
108
|
const secondOf = (d: Date): number => Math.floor(d.getTime() / 1000);
|
|
108
109
|
|
|
110
|
+
// Emits one `scheduled_action.<code>` event on the telemetry bus; in: the scheduler and public functions, out: telemetryBus.
|
|
109
111
|
function emit(code: string, message: string, data: Record<string, unknown>, level: "info" | "warn" = "info"): void {
|
|
110
112
|
telemetryBus.emit({ code: `scheduled_action.${code}`, message: `[Agent Accelerator] ${message}`, level, data });
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
// The ValidationError for an unknown action id; in: lookups by id, out: a thrown error.
|
|
113
116
|
function notFound(id: string): ValidationError {
|
|
114
117
|
return new ValidationError(`No scheduled action with id "${id}".`);
|
|
115
118
|
}
|
|
@@ -118,6 +121,7 @@ function notFound(id: string): ValidationError {
|
|
|
118
121
|
// Validation (collects EVERY problem so the model can fix a call in one go)
|
|
119
122
|
// ---------------------------------------------------------------------------
|
|
120
123
|
|
|
124
|
+
// Runs one validation step and records its message instead of throwing, so one call can report every problem; in: validateInput/checkSchedule, out: the problems list.
|
|
121
125
|
function collect(problems: string[], fn: () => void): void {
|
|
122
126
|
try {
|
|
123
127
|
fn();
|
|
@@ -126,10 +130,12 @@ function collect(problems: string[], fn: () => void): void {
|
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
|
|
133
|
+
// Throws unless the value is a non-empty string; in: field validation, out: collect.
|
|
129
134
|
function requireText(field: string, value: unknown): void {
|
|
130
135
|
if (typeof value !== "string" || !value.trim()) throw new ValidationError(`${field}: required, a non-empty string.`);
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
// Throws unless every requested worker tool name exists in the developer's pool; in: create/update validation, out: collect.
|
|
133
139
|
function checkTools(value: unknown, pool: PoolTools): void {
|
|
134
140
|
if (!Array.isArray(value) || value.some((n) => typeof n !== "string")) {
|
|
135
141
|
throw new ValidationError("tools: must be an array of worker tool names, e.g. [\"recent_news\"].");
|
|
@@ -174,6 +180,7 @@ function checkSchedule(
|
|
|
174
180
|
}
|
|
175
181
|
}
|
|
176
182
|
|
|
183
|
+
// Validates role, tools and timeoutMs of a create or update; in: validateInput/updateScheduledAction, out: the problems list.
|
|
177
184
|
function checkOptionalFields(input: Pick<ScheduledActionPatch, "role" | "tools" | "timeoutMs">, pool: PoolTools | undefined, problems: string[]): void {
|
|
178
185
|
if (input.role !== undefined && typeof input.role !== "string") problems.push("role: must be a string.");
|
|
179
186
|
if (input.tools !== undefined && pool) collect(problems, () => checkTools(input.tools, pool));
|
|
@@ -208,9 +215,11 @@ export function validateInput(
|
|
|
208
215
|
return problems;
|
|
209
216
|
}
|
|
210
217
|
|
|
218
|
+
// Joins validation problems into one numbered ValidationError message; in: create/update failures, out: the error text.
|
|
211
219
|
const joinProblems = (what: string, problems: string[]): string =>
|
|
212
220
|
`${what} (${problems.length} problem${problems.length === 1 ? "" : "s"}): ${problems.map((p, i) => `(${i + 1}) ${p}`).join(" ")}`;
|
|
213
221
|
|
|
222
|
+
// Six random lowercase/digit characters appended to an action id; in: createScheduledAction, out: the unique id suffix.
|
|
214
223
|
function randomSuffix(): string {
|
|
215
224
|
const bytes = new Uint8Array(6);
|
|
216
225
|
globalThis.crypto.getRandomValues(bytes);
|
|
@@ -244,6 +253,7 @@ export class Scheduler {
|
|
|
244
253
|
(this.timer as { unref?: () => void }).unref?.();
|
|
245
254
|
}
|
|
246
255
|
|
|
256
|
+
// Stops the clock's interval; in: tick when nothing is pending and resetSchedulers, out: no more ticks.
|
|
247
257
|
stop(): void {
|
|
248
258
|
if (this.timer) clearInterval(this.timer);
|
|
249
259
|
this.timer = undefined;
|
|
@@ -266,6 +276,7 @@ export class Scheduler {
|
|
|
266
276
|
}
|
|
267
277
|
}
|
|
268
278
|
|
|
279
|
+
// Reports an invalid record or delivery once per file/id/problem and leaves the file untouched; in: snapshot/startCook, out: a scheduled_action.invalid warning.
|
|
269
280
|
private warnInvalid(id: string, problem: string, file = "tasks.json"): void {
|
|
270
281
|
const key = `${file}:${id}:${problem}`;
|
|
271
282
|
if (this.warned.has(key)) return;
|
|
@@ -292,6 +303,7 @@ export class Scheduler {
|
|
|
292
303
|
return { live, pending };
|
|
293
304
|
}
|
|
294
305
|
|
|
306
|
+
// True while anything keeps the clock useful (a run in flight, a saved delivery, a waiting or cooking action); in: ensureRunning and tick, out: start/stop of the interval.
|
|
295
307
|
private hasPending(): boolean {
|
|
296
308
|
const { live, pending } = this.snapshot();
|
|
297
309
|
return this.running.size > 0 || pending.length > 0 || live.some((t) => t.state === "waiting" || t.state === "cooking");
|
|
@@ -359,6 +371,7 @@ export class Scheduler {
|
|
|
359
371
|
|
|
360
372
|
// ----- delivery ----------------------------------------------------------
|
|
361
373
|
|
|
374
|
+
// Delivers every saved run whose delivery time has come; in: tick, out: deliver calls.
|
|
362
375
|
private deliverDue(now: Date): void {
|
|
363
376
|
for (const entry of this.snapshot().pending) {
|
|
364
377
|
if (parseLocalIso(entry.deliverAt).getTime() <= now.getTime()) this.deliver(entry, now);
|
|
@@ -394,6 +407,7 @@ export class Scheduler {
|
|
|
394
407
|
|
|
395
408
|
// ----- starting runs -----------------------------------------------------
|
|
396
409
|
|
|
410
|
+
// Starts waiting actions whose cook time has come and skips the overlap of those still cooking; in: tick, out: startCook/skipOverlap calls.
|
|
397
411
|
private startDue(now: Date): void {
|
|
398
412
|
for (const t of this.snapshot().live) {
|
|
399
413
|
if (!t.nextCookAt || parseLocalIso(t.nextCookAt).getTime() > now.getTime()) continue;
|
|
@@ -468,6 +482,7 @@ export class Scheduler {
|
|
|
468
482
|
return started;
|
|
469
483
|
}
|
|
470
484
|
|
|
485
|
+
// The display name of an action from tasks.json, else its id; in: startCook's no-agent message, out: event text.
|
|
471
486
|
private nameOf(id: string): string {
|
|
472
487
|
return readTasks(this.home).find((t) => t.id === id)?.name ?? id;
|
|
473
488
|
}
|
|
@@ -511,6 +526,7 @@ export class Scheduler {
|
|
|
511
526
|
|
|
512
527
|
// ----- the run -----------------------------------------------------------
|
|
513
528
|
|
|
529
|
+
// Runs one action to its outcome and writes the result unless the action was deleted meanwhile; in: launch, out: finish.
|
|
514
530
|
private async runWorker(task: ScheduledActionRecord, agent: Agent, entry: RunningEntry): Promise<void> {
|
|
515
531
|
const deliverAt = parseLocalIso(task.current!.deliverAt);
|
|
516
532
|
const outcome = await this.execute(task, agent, entry, deliverAt);
|
|
@@ -556,6 +572,7 @@ export class Scheduler {
|
|
|
556
572
|
}
|
|
557
573
|
}
|
|
558
574
|
|
|
575
|
+
// One worker run with a per-attempt timeout and the delete signal, raced so a hung provider cannot hold it; in: execute, out: the worker's AgentResponse.
|
|
559
576
|
private async attempt(worker: Agent, task: ScheduledActionRecord, agent: Agent, entry: RunningEntry): Promise<AgentResponse> {
|
|
560
577
|
const ms = scheduledRunTimeoutMs(agent.dynamicSubagents?.workerTimeoutMs ?? Infinity, task.timeoutMs);
|
|
561
578
|
// One controller per attempt (the timeout is per attempt), fed by the entry's delete controller.
|
|
@@ -613,6 +630,7 @@ export class Scheduler {
|
|
|
613
630
|
|
|
614
631
|
// ----- commands ----------------------------------------------------------
|
|
615
632
|
|
|
633
|
+
// Starts an action immediately outside its schedule (also resolves a missed one); in: runScheduledAction, out: the cooking record.
|
|
616
634
|
runNow(id: string): ScheduledActionRecord {
|
|
617
635
|
const rec = readTasks(this.home).find((t) => t.id === id);
|
|
618
636
|
if (!rec) throw notFound(id);
|
|
@@ -629,6 +647,7 @@ export class Scheduler {
|
|
|
629
647
|
return started;
|
|
630
648
|
}
|
|
631
649
|
|
|
650
|
+
// Ignores a missed occurrence and schedules the next one (a one-shot becomes done); in: skipScheduledAction and onMissed: skip, out: the updated record.
|
|
632
651
|
skip(id: string): ScheduledActionRecord {
|
|
633
652
|
const now = this.now();
|
|
634
653
|
const rec = updateTasks(this.home, (tasks) => {
|
|
@@ -647,6 +666,7 @@ export class Scheduler {
|
|
|
647
666
|
return rec;
|
|
648
667
|
}
|
|
649
668
|
|
|
669
|
+
// Deletes an action (aborting a cooking run only with force) and optionally purges its run files; in: deleteScheduledAction, out: a DeleteResult.
|
|
650
670
|
async delete(id: string, opts: { force?: boolean; purge?: boolean }): Promise<DeleteResult> {
|
|
651
671
|
const rec = readTasks(this.home).find((t) => t.id === id);
|
|
652
672
|
if (!rec) return { ok: false, reason: "not_found" };
|
|
@@ -699,6 +719,7 @@ export function resetSchedulers(): void {
|
|
|
699
719
|
schedulers.clear();
|
|
700
720
|
}
|
|
701
721
|
|
|
722
|
+
// The scheduler of a resolved home (option, setHome, env or default); in: the public functions, out: a Scheduler.
|
|
702
723
|
const schedulerFor = (opts?: HomeOptions): Scheduler => getScheduler(resolveHome(opts?.home));
|
|
703
724
|
|
|
704
725
|
/** Makes `agent` the live runner for actions created under its name, and starts the clock if work is pending. */
|
package/src/scheduler/store.ts
CHANGED
|
@@ -72,8 +72,11 @@ export interface RunHeader {
|
|
|
72
72
|
error?: string;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// The `scheduled_actions` folder of a home; in: the path helpers, out: its path.
|
|
75
76
|
const baseDir = (home: string): string => path.join(home, "scheduled_actions");
|
|
77
|
+
// Path of tasks.json; in: readTasks/updateTasks, out: its path.
|
|
76
78
|
const tasksFile = (home: string): string => path.join(baseDir(home), "tasks.json");
|
|
79
|
+
// Path of deliveries.json; in: readDeliveries/updateDeliveries, out: its path.
|
|
77
80
|
const deliveriesFile = (home: string): string => path.join(baseDir(home), "deliveries.json");
|
|
78
81
|
|
|
79
82
|
/** Absolute path of a run file given its `scheduled_actions/`-relative path. */
|
|
@@ -134,6 +137,7 @@ function readList(file: string, key: "tasks" | "pending"): Raw[] {
|
|
|
134
137
|
return list ?? [];
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
// Writes `{ version, <key>: [...] }` durably (with a .bak); in: updateTasks/updateDeliveries, out: the file.
|
|
137
141
|
function writeList(file: string, key: "tasks" | "pending", list: Raw[]): void {
|
|
138
142
|
writeFileDurable(file, JSON.stringify({ version: 1, [key]: list }, null, 2) + "\n");
|
|
139
143
|
}
|
|
@@ -151,6 +155,7 @@ export function updateTasks<T>(home: string, fn: (tasks: ScheduledActionRecord[]
|
|
|
151
155
|
return result;
|
|
152
156
|
}
|
|
153
157
|
|
|
158
|
+
/** Every saved delivery as stored (entries may be invalid: check with validateDelivery); in: the scheduler, out: PendingDelivery[]. */
|
|
154
159
|
export function readDeliveries(home: string): PendingDelivery[] {
|
|
155
160
|
return readList(deliveriesFile(home), "pending") as unknown as PendingDelivery[];
|
|
156
161
|
}
|
|
@@ -229,6 +234,7 @@ export function validateDelivery(raw: unknown): string | undefined {
|
|
|
229
234
|
// Run files
|
|
230
235
|
// ---------------------------------------------------------------------------
|
|
231
236
|
|
|
237
|
+
// Renders a run file: the `---` header lines, a blank line, the result text; in: writeRunFile, out: the file text.
|
|
232
238
|
function renderRun(header: RunHeader, body: string): string {
|
|
233
239
|
const lines = ["---"];
|
|
234
240
|
for (const [key, value] of Object.entries(header)) {
|
package/src/scheduler/time.ts
CHANGED
|
@@ -15,7 +15,9 @@ const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
|
|
|
15
15
|
const DDHHMM = /^(\d{2,}):(\d{2}):(\d{2})$/;
|
|
16
16
|
const LOCAL_ISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})$/;
|
|
17
17
|
|
|
18
|
+
// Two-digit zero padding; in: date formatting here, out: a string.
|
|
18
19
|
const pad2 = (n: number): string => String(n).padStart(2, "0");
|
|
20
|
+
// JSON text of a value for error messages; in: the validators, out: a string.
|
|
19
21
|
const show = (v: unknown): string => JSON.stringify(v) ?? String(v);
|
|
20
22
|
|
|
21
23
|
/** Parses `DD:HH:MM` (DD >= 2 digits, MM < 60). HH is range-checked by the caller. */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# src/session/
|
|
2
|
+
|
|
3
|
+
Saving a conversation, its spending and its tool logs.
|
|
4
|
+
|
|
5
|
+
| File | What it is |
|
|
6
|
+
| :-- | :-- |
|
|
7
|
+
| `store.ts` | Snapshot format (`session.json`, one line per field and per turn), load validation, `SessionTelemetry`, media extraction, journal replay (`replayJournal`), and `priceUsage` (the one cost model, integer micro-USD). |
|
|
8
|
+
| `journal.ts` | `SessionJournal`: after each step appends one fsynced line to `journal.ndjson`; writes a fresh snapshot (new `epoch`) when history changed any other way or the journal passed 500 lines / 4 MB. |
|
|
9
|
+
| `usage.ts` | Pure spending accounting: `UsageTotals`, `usageLine`, `buildUsageReport`. No Agent import. |
|
|
10
|
+
| `tool-log.ts` | `ToolLogs`: one fsynced NDJSON file per tool call under `<session>/tool-logs/`. |
|
|
11
|
+
|
|
12
|
+
## Invariants
|
|
13
|
+
- Layout: `<home>/sessions/<id>/{session.json, journal.ndjson, media/, tool-logs/, workers/}`.
|
|
14
|
+
- A journal whose header epoch differs from the snapshot's is ignored on load.
|
|
15
|
+
- Journal record types: `base`, `msg`, `meta`, `run`, `end`, `tool`, `pending`, `usage`. Replay ignores unknown types, drops a torn last line, and stops at earlier damage with `session.journal_corrupt`.
|
|
16
|
+
- Media is content-addressed (`media/<kind>-h<hash>.<ext>`), written once, never deleted.
|
|
17
|
+
- Valid sessions over the load limits throw `SessionLoadError` and are never quarantined; only corrupt files are renamed `*.corrupt-<ts>`.
|
|
18
|
+
- Pricing: a provider-reported cost wins and is never rescaled; unknown pricing stays unknown, never $0; cache reads and writes are subsets of input.
|
|
19
|
+
- Never call `saveSessionDir` on a folder an Agent persists to.
|