agent-accelerator 0.5.5 → 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.
Files changed (47) hide show
  1. package/README.md +16 -17
  2. package/package.json +1 -1
  3. package/src/agent/agent.ts +16 -18
  4. package/src/agent/architecture.md +25 -0
  5. package/src/agent/context.ts +4 -0
  6. package/src/agent/delegation.ts +18 -84
  7. package/src/agent/loop.ts +14 -16
  8. package/src/agent/worker.ts +1 -3
  9. package/src/architecture.md +28 -0
  10. package/src/index.ts +6 -73
  11. package/src/models/architecture.md +14 -0
  12. package/src/models/catalog-cache.ts +3 -0
  13. package/src/models/catalog.ts +7 -0
  14. package/src/providers/architecture.md +24 -0
  15. package/src/providers/google.ts +26 -39
  16. package/src/providers/openai-compat.ts +53 -104
  17. package/src/providers/openai.ts +47 -104
  18. package/src/providers/openrouter.ts +114 -175
  19. package/src/providers/registry.ts +1 -0
  20. package/src/providers/shared.ts +134 -5
  21. package/src/providers.ts +30 -59
  22. package/src/scheduler/architecture.md +19 -0
  23. package/src/scheduler/scheduler.ts +21 -0
  24. package/src/scheduler/store.ts +6 -0
  25. package/src/scheduler/time.ts +2 -0
  26. package/src/session/architecture.md +19 -0
  27. package/src/session/store.ts +18 -3
  28. package/src/session/usage.ts +1 -0
  29. package/src/streaming/architecture.md +13 -0
  30. package/src/streaming/event-stream.ts +5 -0
  31. package/src/telemetry.ts +1 -0
  32. package/src/tools/architecture.md +19 -0
  33. package/src/tools/executor.ts +13 -0
  34. package/src/tools/schema.ts +1 -0
  35. package/src/tools/subagent-result.ts +1 -0
  36. package/src/types/architecture.md +20 -0
  37. package/src/types/message.ts +1 -1
  38. package/src/utils/architecture.md +25 -0
  39. package/src/utils/base64.ts +2 -0
  40. package/src/utils/cache.ts +2 -0
  41. package/src/utils/errors.ts +8 -1
  42. package/src/utils/headers.ts +4 -1
  43. package/src/utils/media.ts +11 -1
  44. package/src/utils/raw-capture.ts +3 -0
  45. package/src/utils/retry.ts +1 -0
  46. package/src/utils/safe-io.ts +4 -0
  47. package/src/utils/session.ts +5 -161
@@ -52,7 +52,6 @@ import type { ProviderContext, ContentPart } from "../types/message.ts";
52
52
  import type { StandardToolDeclaration, ToolCallRecord } from "../types/tool.ts";
53
53
  import type { TokenUsage, ThinkingConfig } from "../types/core.ts";
54
54
  import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
55
- import { SSEParser } from "../streaming/sse-parser.ts";
56
55
  import { AgentResponse } from "../types/response.ts";
57
56
  import { getApiKey, getEnv } from "../utils/env.ts";
58
57
  import { buildSessionHeaders, setHeaderCaseInsensitive, shouldEchoSignature } from "../utils/headers.ts";
@@ -68,7 +67,14 @@ import {
68
67
  redactedHeaders,
69
68
  readErrorPayload,
70
69
  incompleteStreamError,
71
- hasTruncatedToolArguments,
70
+ hasUnfinishedToolCall,
71
+ linkStreamAbort,
72
+ withAbortTimer,
73
+ readSseData,
74
+ incompleteStreamCheck,
75
+ failStream,
76
+ toolArgumentsJson,
77
+ toolTurnFinishReason,
72
78
  combinedSignal,
73
79
  timeoutFor,
74
80
  hasAuthHeader,
@@ -186,10 +192,12 @@ interface ChatResponse {
186
192
  // Request building (canonical -> Chat Completions)
187
193
  // ---------------------------------------------------------------------------
188
194
 
195
+ // Env-variable prefix of a custom provider (`my-llm` becomes `MY_LLM`); in: key and URL lookups, out: the prefix.
189
196
  function envPrefixOf(prefix: string): string {
190
197
  return prefix.toUpperCase().replace(/[^A-Z0-9]/g, "_");
191
198
  }
192
199
 
200
+ // Endpoint from the option, the provider's configuration or {PREFIX}_BASE_URL; throws ConfigError when none (never another vendor's URL); in: each request, out: the base URL.
193
201
  function resolveBaseUrl(
194
202
  prefix: string,
195
203
  configuredBaseUrl: string | undefined,
@@ -215,6 +223,7 @@ function resolveBaseUrl(
215
223
  return resolved.replace(/\/+$/, "");
216
224
  }
217
225
 
226
+ // Key from the option, the provider's configuration or {PREFIX}_API_KEY, or `unused` when custom headers carry the auth; in: each request, out: the key or undefined.
218
227
  function resolveApiKey(
219
228
  prefix: string,
220
229
  configuredApiKey: string | undefined,
@@ -276,6 +285,7 @@ function cleanModelId(prefix: string, model: string | ModelSpec): string {
276
285
  return rawId.replace(new RegExp(`^${escaped}/`, "i"), "");
277
286
  }
278
287
 
288
+ // Canonical tool declarations as Chat Completions function tools; in: options.tools, out: request `tools`.
279
289
  function toCompatTools(tools?: StandardToolDeclaration[]): Array<Record<string, unknown>> | undefined {
280
290
  if (!tools || tools.length === 0) return undefined;
281
291
  return tools.map((t) => ({
@@ -303,6 +313,7 @@ function warnThinkingDropped(prefix: string, thinking: ThinkingConfig | undefine
303
313
  });
304
314
  }
305
315
 
316
+ // `wav` or `mp3`, the only formats Chat audio input takes here; in: contentPartsToBlocks, out: input_audio.format.
306
317
  function audioFormatFor(mimeType?: string): string {
307
318
  const mime = (mimeType || "").toLowerCase();
308
319
  if (mime.includes("wav")) return "wav";
@@ -348,6 +359,7 @@ function assertNoVideoOrFileParts(
348
359
  throw err;
349
360
  }
350
361
 
362
+ // Text, image (link or data URL) and audio parts as Chat content blocks; in: a user message's parts, out: fullHistoryMessages.
351
363
  async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPart[]> {
352
364
  const blocks: ChatContentPart[] = [];
353
365
  for (const part of parts) {
@@ -382,6 +394,7 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
382
394
  return blocks;
383
395
  }
384
396
 
397
+ // A tool result as the text of a tool message; in: fullHistoryMessages, out: a string.
385
398
  function resultToOutput(result: unknown): string {
386
399
  if (typeof result === "string") return result;
387
400
  return safeStringify(result);
@@ -450,7 +463,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
450
463
  } = {
451
464
  id: clampToolCallId(part.callId || part.id),
452
465
  type: "function",
453
- function: { name: part.name, arguments: JSON.stringify(part.arguments || {}) },
466
+ function: { name: part.name, arguments: toolArgumentsJson(part.arguments) },
454
467
  };
455
468
  // Echo provider-issued thought signatures verbatim so endpoints
456
469
  // that minted them (Gemini behind a compat proxy) keep working.
@@ -496,6 +509,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
496
509
  // Response mapping (Chat Completions -> canonical)
497
510
  // ---------------------------------------------------------------------------
498
511
 
512
+ // Chat usage as TokenUsage, clamping cache reads to input; in: a reply or the last stream chunk, out: TokenUsage.
499
513
  function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
500
514
  const input = raw?.prompt_tokens ?? 0;
501
515
  const output = raw?.completion_tokens ?? 0;
@@ -511,12 +525,14 @@ function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
511
525
  cacheWriteTokens: raw?.prompt_tokens_details?.cache_write_tokens ?? 0,
512
526
  thinkingTokens: raw?.completion_tokens_details?.reasoning_tokens ?? 0,
513
527
  };
528
+ // `cost` is an OpenRouter extension. A generic endpoint that sends it may use 0 for "not computed", so only a positive value counts as known (OpenRouter's adapter accepts an explicit 0).
514
529
  if (typeof raw?.cost === "number" && raw.cost > 0) {
515
530
  usage.cost = { totalCost: raw.cost };
516
531
  }
517
532
  return usage;
518
533
  }
519
534
 
535
+ // The Gemini thought signature a proxy put on a tool call, if any; in: parseResponse, out: a string or undefined.
520
536
  function extractSignature(rawTc: {
521
537
  extra_content?: { google?: { thought_signature?: string } };
522
538
  }): string | undefined {
@@ -530,6 +546,7 @@ function withCompatSignature(sig: string | undefined): { thoughtSignature?: stri
530
546
  return { thoughtSignature: sig, thoughtSignatureProvider: "google" };
531
547
  }
532
548
 
549
+ // Throws a concise error for an HTTP 200 reply that carries only `error`; in: parseResponse, out: a thrown error.
533
550
  function throwResponseError(
534
551
  response: ChatResponse,
535
552
  prefix: string,
@@ -544,6 +561,7 @@ function throwResponseError(
544
561
  throw toConciseProviderError(failure, prefix, modelId);
545
562
  }
546
563
 
564
+ // Turns a Chat Completions JSON reply into text, thinking, tool calls and usage; in: generate, out: ProviderGenerateResult.
547
565
  function parseResponse(
548
566
  response: ChatResponse,
549
567
  prefix: string,
@@ -658,6 +676,7 @@ export class OpenAICompatibleChatProvider implements Provider {
658
676
  this.configuredApiKey = opts?.apiKey;
659
677
  }
660
678
 
679
+ // The catalog spec of a model, else a permissive generic one; in: the registry, out: a ModelSpec.
661
680
  getModel(modelId: string): ModelSpec | undefined {
662
681
  const clean = cleanModelId(this.prefix, modelId);
663
682
  return (
@@ -668,14 +687,17 @@ export class OpenAICompatibleChatProvider implements Provider {
668
687
  );
669
688
  }
670
689
 
690
+ // This provider's endpoint for one request; in: generate/stream, out: resolveBaseUrl.
671
691
  private baseUrl(options?: ProviderRequestOptions): string {
672
692
  return resolveBaseUrl(this.prefix, this.configuredBaseUrl, options);
673
693
  }
674
694
 
695
+ // This provider's key for one request; in: requireApiKey, out: resolveApiKey.
675
696
  private apiKey(options?: ProviderRequestOptions): string | undefined {
676
697
  return resolveApiKey(this.prefix, this.configuredApiKey, options);
677
698
  }
678
699
 
700
+ // The key, where a missing one only throws for non-local endpoints (loopback and LAN may omit it); in: generate/stream, out: the key or undefined.
679
701
  private requireApiKey(baseUrl: string, options?: ProviderRequestOptions): string | undefined {
680
702
  const key = this.apiKey(options);
681
703
  if (!key && !isLocalEndpoint(baseUrl)) {
@@ -686,6 +708,7 @@ export class OpenAICompatibleChatProvider implements Provider {
686
708
  return key;
687
709
  }
688
710
 
711
+ // Builds the Chat request (messages, tools, tool_choice, generation controls, response_format, stream usage) and warns+drops what a strict endpoint cannot take; in: generate/stream, out: the body.
689
712
  private async buildBody(
690
713
  modelId: string,
691
714
  context: ProviderContext,
@@ -741,7 +764,7 @@ export class OpenAICompatibleChatProvider implements Provider {
741
764
  const compatTools = toCompatTools(tools);
742
765
  if (compatTools) body.tools = compatTools;
743
766
  // Standard Chat Completions generation fields, only when the caller set them.
744
- Object.assign(body, mapGenerationParamsToChat(options));
767
+ Object.assign(body, mapGenerationParamsToChat(options, this.prefix, `${this.prefix}/${modelId}`));
745
768
  const toolChoice = mapToolChoiceToOpenAI(
746
769
  options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
747
770
  );
@@ -782,6 +805,7 @@ export class OpenAICompatibleChatProvider implements Provider {
782
805
  return body;
783
806
  }
784
807
 
808
+ // Fetch options with session-affinity headers (unless opted out), JSON content type and the Bearer key; in: doFetch and stream, out: RequestInit.
785
809
  private requestInit(
786
810
  body: ChatRequestBody,
787
811
  options: ProviderRequestOptions | undefined,
@@ -817,6 +841,7 @@ export class OpenAICompatibleChatProvider implements Provider {
817
841
  return { method: "POST", headers, body: JSON.stringify(body), signal };
818
842
  }
819
843
 
844
+ // One HTTP call under the request timeout, returning status, headers and body text; in: generate, out: the raw reply.
820
845
  private async doFetch(
821
846
  url: string,
822
847
  body: ChatRequestBody,
@@ -839,6 +864,7 @@ export class OpenAICompatibleChatProvider implements Provider {
839
864
  }
840
865
  }
841
866
 
867
+ // 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.
842
868
  private throwIfError(
843
869
  status: number,
844
870
  url: string,
@@ -867,6 +893,7 @@ export class OpenAICompatibleChatProvider implements Provider {
867
893
  throw toConciseProviderError(err, this.prefix, modelId);
868
894
  }
869
895
 
896
+ // Non-streaming turn as one retried JSON request; in: runAgentLoop, out: ProviderGenerateResult.
870
897
  async generate(
871
898
  model: string | ModelSpec,
872
899
  context: ProviderContext,
@@ -939,6 +966,7 @@ export class OpenAICompatibleChatProvider implements Provider {
939
966
  }
940
967
  }
941
968
 
969
+ // Streaming turn over SSE: assembles text, reasoning, tool calls and usage, retrying only before the first byte; in: streamAgentLoop, out: an AssistantMessageEventStream.
942
970
  stream(
943
971
  model: string | ModelSpec,
944
972
  context: ProviderContext,
@@ -949,23 +977,7 @@ export class OpenAICompatibleChatProvider implements Provider {
949
977
  const prefix = this.prefix;
950
978
  const clean = cleanModelId(this.prefix, model);
951
979
 
952
- const linked = new AbortController();
953
- const forwardUserAbort = () => {
954
- try {
955
- linked.abort((options?.signal as { reason?: unknown })?.reason);
956
- } catch {
957
- try {
958
- linked.abort();
959
- } catch {}
960
- }
961
- };
962
- if (options?.signal?.aborted) forwardUserAbort();
963
- else options?.signal?.addEventListener("abort", forwardUserAbort, { once: true });
964
- const removeStreamCancel = eventStream.onCancel(() => {
965
- try {
966
- linked.abort();
967
- } catch {}
968
- });
980
+ const { linked, release } = linkStreamAbort(eventStream, options?.signal);
969
981
 
970
982
  (async () => {
971
983
  try {
@@ -994,21 +1006,7 @@ export class OpenAICompatibleChatProvider implements Provider {
994
1006
  // Transient failures (429/5xx/network) are retried here, before the
995
1007
  // first byte; once the stream is live it is never retried.
996
1008
  const res = await openStreamResponse({
997
- fetchOnce: async () => {
998
- const timer =
999
- streamTimeoutMs && streamTimeoutMs > 0
1000
- ? setTimeout(() => {
1001
- try {
1002
- linked.abort();
1003
- } catch {}
1004
- }, streamTimeoutMs)
1005
- : undefined;
1006
- try {
1007
- return await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal));
1008
- } finally {
1009
- if (timer) clearTimeout(timer);
1010
- }
1011
- },
1009
+ fetchOnce: () => withAbortTimer(linked, streamTimeoutMs, () => (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))),
1012
1010
  onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
1013
1011
  noBodyError: () => toConciseProviderError(new Error("OpenAI-compatible streaming response had no body"), prefix, clean),
1014
1012
  retry: retryOptionsFrom(options),
@@ -1022,9 +1020,6 @@ export class OpenAICompatibleChatProvider implements Provider {
1022
1020
  const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
1023
1021
  eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
1024
1022
 
1025
- const parser = new SSEParser();
1026
- const reader = res.body!.getReader(); // openStreamResponse guarantees a body
1027
- const decoder = new TextDecoder();
1028
1023
  let text = "";
1029
1024
  let thinking = "";
1030
1025
  // Tool calls keyed by choice index: { id, name, startArgs, deltaArgs }.
@@ -1040,18 +1035,6 @@ export class OpenAICompatibleChatProvider implements Provider {
1040
1035
  let framesSeen = 0;
1041
1036
  let responseId: string | undefined;
1042
1037
  let completedBody: unknown = undefined;
1043
- let aborted = false;
1044
- linked.signal.addEventListener(
1045
- "abort",
1046
- () => {
1047
- aborted = true;
1048
- try {
1049
- void reader.cancel();
1050
- } catch {}
1051
- },
1052
- { once: true }
1053
- );
1054
-
1055
1038
  const handleMessage = (data: string): void => {
1056
1039
  if (!data) return;
1057
1040
  if (data === "[DONE]") {
@@ -1176,26 +1159,7 @@ export class OpenAICompatibleChatProvider implements Provider {
1176
1159
  }
1177
1160
  };
1178
1161
 
1179
- while (true) {
1180
- if (linked.signal.aborted || eventStream.isCancelled()) {
1181
- try {
1182
- await reader.cancel();
1183
- } catch {}
1184
- throw Object.assign(new Error("Stream aborted"), { name: "AbortError" });
1185
- }
1186
- const { done, value } = await reader.read();
1187
- if (done) break;
1188
- const chunk = decoder.decode(value, { stream: true });
1189
- for (const m of parser.feed(chunk)) handleMessage(m.data);
1190
- }
1191
- for (const m of parser.flush()) handleMessage(m.data);
1192
- try {
1193
- reader.releaseLock();
1194
- } catch {}
1195
-
1196
- if (linked.signal.aborted || eventStream.isCancelled() || aborted) {
1197
- throw Object.assign(new Error("Stream aborted"), { name: "AbortError" });
1198
- }
1162
+ await readSseData(res.body!, linked, eventStream, handleMessage); // openStreamResponse guarantees a body
1199
1163
 
1200
1164
  const toolCalls: ToolCallRecord[] = [];
1201
1165
  for (const [index, c] of calls) {
@@ -1212,9 +1176,11 @@ export class OpenAICompatibleChatProvider implements Provider {
1212
1176
  eventStream.push({ type: "tool_call_complete", toolCall: record });
1213
1177
  }
1214
1178
 
1179
+ // Did the stream really end? Read before the tool-call and refusal overrides below, which also set sawFinishReason.
1180
+ const hasTerminal = completedBody !== undefined || sawFinishReason || sawUsage;
1215
1181
  noteProviderTurn(sessionId, prefix);
1216
1182
  if (toolCalls.length > 0) {
1217
- finishReason = "tool_calls";
1183
+ finishReason = toolTurnFinishReason(finishReason, toolCalls);
1218
1184
  sawFinishReason = true;
1219
1185
  } else if (refusalSeen) {
1220
1186
  finishReason = "refusal";
@@ -1224,26 +1190,21 @@ export class OpenAICompatibleChatProvider implements Provider {
1224
1190
  // Q-23: terminal-state guard — fail `incomplete_stream` (retryable)
1225
1191
  // instead of resolving empty success. Skipped for non-SSE payloads
1226
1192
  // (JSON mocks) where no SSE frames were ever seen.
1227
- const hasTerminal = completedBody !== undefined || sawFinishReason || sawUsage;
1228
- const cleanThinkingEarly = thinking.replace(/\n{3,}/g, "\n\n").trim();
1229
- const ctEntry = Object.entries(responseHeaders).find(([k]) => k.toLowerCase() === "content-type");
1230
- const isSSEPayload =
1231
- (typeof ctEntry?.[1] === "string" && ((ctEntry[1] as string).includes("event-stream") || (ctEntry[1] as string).startsWith("text/"))) ||
1232
- framesSeen > 0 ||
1233
- badFrames > 0;
1234
- if (!hasTerminal && !text && !cleanThinkingEarly && toolCalls.length === 0 && isSSEPayload) {
1235
- eventStream.fail(incompleteStreamError(prefix, clean));
1236
- return;
1237
- }
1238
- // Refuse unfinished tool calls whose args never parsed (output_item.done
1239
- // never arrived): truncated `{raw}` must not reach the executor (pi
1240
- // api/openai-responses-shared.ts:766 rule). Non-SSE mocks preserve legacy.
1241
- if (!hasTerminal && toolCalls.some((c) => hasTruncatedToolArguments(c.arguments)) && isSSEPayload) {
1242
- eventStream.fail(incompleteStreamError(prefix, clean));
1193
+ const cleanThinking = thinking.replace(/\n{3,}/g, "\n\n").trim();
1194
+ const incomplete = incompleteStreamCheck({
1195
+ provider: prefix,
1196
+ modelId: clean,
1197
+ hasTerminal,
1198
+ empty: !text && !cleanThinking && toolCalls.length === 0,
1199
+ unfinishedCall: hasUnfinishedToolCall(calls.values(), toolCalls),
1200
+ responseHeaders,
1201
+ framesSeen,
1202
+ badFrames,
1203
+ });
1204
+ if (incomplete) {
1205
+ eventStream.fail(incomplete);
1243
1206
  return;
1244
1207
  }
1245
-
1246
- const cleanThinking = cleanThinkingEarly;
1247
1208
  const finalThoughtSignature = toolCalls.map((c) => c.thoughtSignature).find(Boolean);
1248
1209
  const finalResponse = new AgentResponse({
1249
1210
  text,
@@ -1264,21 +1225,9 @@ export class OpenAICompatibleChatProvider implements Provider {
1264
1225
  eventStream.push({ type: "done", delta: "", usage, finishReason, responseId });
1265
1226
  eventStream.end(finalResponse);
1266
1227
  } catch (err: unknown) {
1267
- const raw = err instanceof Error ? err : new Error(String(err));
1268
- const isAbort =
1269
- linked.signal.aborted ||
1270
- eventStream.isCancelled() ||
1271
- (raw as { name?: string }).name === "AbortError";
1272
- eventStream.fail(
1273
- isAbort ? Object.assign(new Error("Stream aborted"), { name: "AbortError" }) : raw
1274
- );
1228
+ failStream(eventStream, linked, err);
1275
1229
  } finally {
1276
- try {
1277
- options?.signal?.removeEventListener("abort", forwardUserAbort);
1278
- } catch {}
1279
- try {
1280
- removeStreamCancel();
1281
- } catch {}
1230
+ release();
1282
1231
  }
1283
1232
  })();
1284
1233