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
@@ -44,7 +44,6 @@ import type { ProviderContext, ContentPart } from "../types/message.ts";
44
44
  import type { StandardToolDeclaration, ToolCallRecord } from "../types/tool.ts";
45
45
  import type { TokenUsage } from "../types/core.ts";
46
46
  import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
47
- import { SSEParser } from "../streaming/sse-parser.ts";
48
47
  import { AgentResponse } from "../types/response.ts";
49
48
  import { getApiKey, getEnv } from "../utils/env.ts";
50
49
  import { buildSessionHeaders, setHeaderCaseInsensitive } from "../utils/headers.ts";
@@ -60,7 +59,14 @@ import {
60
59
  redactedHeaders,
61
60
  readErrorPayload,
62
61
  incompleteStreamError,
63
- hasTruncatedToolArguments,
62
+ hasUnfinishedToolCall,
63
+ linkStreamAbort,
64
+ withAbortTimer,
65
+ readSseData,
66
+ incompleteStreamCheck,
67
+ failStream,
68
+ toolArgumentsJson,
69
+ toolTurnFinishReason,
64
70
  combinedSignal,
65
71
  timeoutFor,
66
72
  hasAuthHeader,
@@ -161,6 +167,7 @@ const DEFAULT_BASE_URL = "https://api.openai.com/v1";
161
167
  // Request building (canonical -> Responses)
162
168
  // ---------------------------------------------------------------------------
163
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.
164
171
  function resolveBaseUrl(options?: ProviderRequestOptions): string {
165
172
  return (
166
173
  options?.baseUrl ||
@@ -172,6 +179,7 @@ function resolveBaseUrl(options?: ProviderRequestOptions): string {
172
179
  ).replace(/\/+$/, "");
173
180
  }
174
181
 
182
+ // Key from the option or env, or `unused` when custom headers carry the auth; in: each request, out: the key or undefined.
175
183
  function resolveApiKey(options?: ProviderRequestOptions): string | undefined {
176
184
  const key = getApiKey("openai", options?.apiKey, options?.env);
177
185
  if (key) return key;
@@ -185,6 +193,7 @@ function cleanModelId(model: string | ModelSpec): string {
185
193
  return rawId.replace(/^openai\//i, "");
186
194
  }
187
195
 
196
+ // Canonical tool declarations as Responses function tools; in: options.tools, out: request `tools`.
188
197
  function toOpenAITools(tools?: StandardToolDeclaration[]): Array<Record<string, unknown>> | undefined {
189
198
  if (!tools || tools.length === 0) return undefined;
190
199
  return tools.map((t) => ({
@@ -196,6 +205,7 @@ function toOpenAITools(tools?: StandardToolDeclaration[]): Array<Record<string,
196
205
  }));
197
206
  }
198
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.
199
209
  async function contentPartsToBlocks(parts: ContentPart[]): Promise<ResponseContentPart[]> {
200
210
  const blocks: ResponseContentPart[] = [];
201
211
  for (const part of parts) {
@@ -252,17 +262,20 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ResponseConte
252
262
  return blocks;
253
263
  }
254
264
 
265
+ // A tool result as the text of a function_call_output item; in: fullHistoryInput, out: a string.
255
266
  function resultToOutput(result: unknown): string {
256
267
  if (typeof result === "string") return result;
257
268
  return safeStringify(result);
258
269
  }
259
270
 
271
+ // Makes an id safe for the Responses wire (allowed characters, at most 64, no trailing underscores); in: fullHistoryInput, out: the id.
260
272
  function normalizeResponsesIdPart(part: string): string {
261
273
  const sanitized = String(part ?? "").replace(/[^a-zA-Z0-9_-]/g, "_");
262
274
  const truncated = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
263
275
  return truncated.replace(/_+$/, "") || "fc_unknown";
264
276
  }
265
277
 
278
+ // Gives a function-call item id the required `fc_` prefix; in: fullHistoryInput, out: the id.
266
279
  function normalizeResponsesItemId(itemId: string): string {
267
280
  let norm = normalizeResponsesIdPart(itemId);
268
281
  if (!norm.startsWith("fc_")) {
@@ -340,7 +353,7 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
340
353
  id: normalizeResponsesItemId(itemId),
341
354
  call_id: normalizeResponsesIdPart(callId),
342
355
  name: part.name,
343
- arguments: JSON.stringify(part.arguments || {}),
356
+ arguments: toolArgumentsJson(part.arguments),
344
357
  });
345
358
  } else if (part.type === "text" && part.text) {
346
359
  texts.push(part.text);
@@ -384,6 +397,7 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
384
397
  // Response mapping (Responses -> canonical)
385
398
  // ---------------------------------------------------------------------------
386
399
 
400
+ // Responses usage as TokenUsage, clamping cache reads to input; in: a response or the completed event, out: TokenUsage.
387
401
  function mapUsage(raw?: ResponsesObject["usage"]): TokenUsage {
388
402
  const input = raw?.input_tokens ?? 0;
389
403
  const output = raw?.output_tokens ?? 0;
@@ -400,6 +414,7 @@ function mapUsage(raw?: ResponsesObject["usage"]): TokenUsage {
400
414
  };
401
415
  }
402
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.
403
418
  function parseResponse(
404
419
  response: ResponsesObject,
405
420
  modelId: string,
@@ -503,6 +518,7 @@ export class OpenAIResponsesProvider implements Provider {
503
518
  return getModelsForProvider("openai");
504
519
  }
505
520
 
521
+ // The catalog spec of a model, else a permissive generic one; in: the registry, out: a ModelSpec.
506
522
  getModel(modelId: string): ModelSpec | undefined {
507
523
  const clean = cleanModelId(modelId);
508
524
  return (
@@ -513,6 +529,7 @@ export class OpenAIResponsesProvider implements Provider {
513
529
  );
514
530
  }
515
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.
516
533
  private async buildBody(
517
534
  modelId: string,
518
535
  context: ProviderContext,
@@ -614,6 +631,7 @@ export class OpenAIResponsesProvider implements Provider {
614
631
  return body;
615
632
  }
616
633
 
634
+ // Fetch options with session-affinity headers, JSON content type and the Bearer key; in: doFetch and stream, out: RequestInit.
617
635
  private requestInit(
618
636
  body: ResponsesRequestBody,
619
637
  options: ProviderRequestOptions | undefined,
@@ -634,6 +652,7 @@ export class OpenAIResponsesProvider implements Provider {
634
652
  return { method: "POST", headers, body: JSON.stringify(body), signal };
635
653
  }
636
654
 
655
+ // One HTTP call under the request timeout, returning status, headers and body text; in: generate, out: the raw reply.
637
656
  private async doFetch(
638
657
  url: string,
639
658
  body: ResponsesRequestBody,
@@ -656,6 +675,7 @@ export class OpenAIResponsesProvider implements Provider {
656
675
  }
657
676
  }
658
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.
659
679
  private throwIfError(
660
680
  status: number,
661
681
  url: string,
@@ -686,6 +706,7 @@ export class OpenAIResponsesProvider implements Provider {
686
706
  throw toConciseProviderError(err, "openai", modelId);
687
707
  }
688
708
 
709
+ // Non-streaming turn as one retried JSON request; in: runAgentLoop, out: ProviderGenerateResult.
689
710
  async generate(
690
711
  model: string | ModelSpec,
691
712
  context: ProviderContext,
@@ -767,6 +788,7 @@ export class OpenAIResponsesProvider implements Provider {
767
788
  }
768
789
  }
769
790
 
791
+ // Streaming turn over SSE: assembles text, reasoning, tool calls and usage, retrying only before the first byte; in: streamAgentLoop, out: an AssistantMessageEventStream.
770
792
  stream(
771
793
  model: string | ModelSpec,
772
794
  context: ProviderContext,
@@ -776,23 +798,7 @@ export class OpenAIResponsesProvider implements Provider {
776
798
  const startTime = Date.now();
777
799
  const clean = cleanModelId(model);
778
800
 
779
- const linked = new AbortController();
780
- const forwardUserAbort = () => {
781
- try {
782
- linked.abort((options?.signal as { reason?: unknown })?.reason);
783
- } catch {
784
- try {
785
- linked.abort();
786
- } catch {}
787
- }
788
- };
789
- if (options?.signal?.aborted) forwardUserAbort();
790
- else options?.signal?.addEventListener("abort", forwardUserAbort, { once: true });
791
- const removeStreamCancel = eventStream.onCancel(() => {
792
- try {
793
- linked.abort();
794
- } catch {}
795
- });
801
+ const { linked, release } = linkStreamAbort(eventStream, options?.signal);
796
802
 
797
803
  (async () => {
798
804
  try {
@@ -803,7 +809,7 @@ export class OpenAIResponsesProvider implements Provider {
803
809
  );
804
810
  }
805
811
  assertModalitiesSupported(context, "openai", clean);
806
- assertNoVideoPartsOnResponses(context, "openai", clean);
812
+ assertNoVideoPartsOnResponses(context, "openai", clean);
807
813
  const baseUrl = resolveBaseUrl(options);
808
814
  const url = `${baseUrl}/responses`;
809
815
  const sessionId = options?.sessionId;
@@ -827,21 +833,7 @@ export class OpenAIResponsesProvider implements Provider {
827
833
  // Transient failures (429/5xx/network) are retried here, before the
828
834
  // first byte; once the stream is live it is never retried.
829
835
  const res = await openStreamResponse({
830
- fetchOnce: async () => {
831
- const timer =
832
- streamTimeoutMs && streamTimeoutMs > 0
833
- ? setTimeout(() => {
834
- try {
835
- linked.abort();
836
- } catch {}
837
- }, streamTimeoutMs)
838
- : undefined;
839
- try {
840
- return await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal));
841
- } finally {
842
- if (timer) clearTimeout(timer);
843
- }
844
- },
836
+ fetchOnce: () => withAbortTimer(linked, streamTimeoutMs, () => (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))),
845
837
  onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
846
838
  noBodyError: () => toConciseProviderError(new Error("OpenAI streaming response had no body"), "openai", clean),
847
839
  retry: retryOptionsFrom(options),
@@ -855,9 +847,6 @@ export class OpenAIResponsesProvider implements Provider {
855
847
  const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
856
848
  eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
857
849
 
858
- const parser = new SSEParser();
859
- const reader = res.body!.getReader(); // openStreamResponse guarantees a body
860
- const decoder = new TextDecoder();
861
850
  let text = "";
862
851
  let thinking = "";
863
852
  // Tool calls keyed by output_index: { itemId, callId, name, startArgs, deltaArgs }.
@@ -871,18 +860,6 @@ export class OpenAIResponsesProvider implements Provider {
871
860
  let framesSeen = 0;
872
861
  let responseId: string | undefined;
873
862
  let completedBody: unknown = undefined;
874
- let aborted = false;
875
- linked.signal.addEventListener(
876
- "abort",
877
- () => {
878
- aborted = true;
879
- try {
880
- void reader.cancel();
881
- } catch {}
882
- },
883
- { once: true }
884
- );
885
-
886
863
  const handleMessage = (data: string): void => {
887
864
  if (!data || data === "[DONE]") {
888
865
  if (data === "[DONE]") sawFinishReason = sawFinishReason || completedBody !== undefined || sawUsage;
@@ -1035,26 +1012,7 @@ export class OpenAIResponsesProvider implements Provider {
1035
1012
  }
1036
1013
  };
1037
1014
 
1038
- while (true) {
1039
- if (linked.signal.aborted || eventStream.isCancelled()) {
1040
- try {
1041
- await reader.cancel();
1042
- } catch {}
1043
- throw Object.assign(new Error("Stream aborted"), { name: "AbortError" });
1044
- }
1045
- const { done, value } = await reader.read();
1046
- if (done) break;
1047
- const chunk = decoder.decode(value, { stream: true });
1048
- for (const m of parser.feed(chunk)) handleMessage(m.data);
1049
- }
1050
- for (const m of parser.flush()) handleMessage(m.data);
1051
- try {
1052
- reader.releaseLock();
1053
- } catch {}
1054
-
1055
- if (linked.signal.aborted || eventStream.isCancelled() || aborted) {
1056
- throw Object.assign(new Error("Stream aborted"), { name: "AbortError" });
1057
- }
1015
+ await readSseData(res.body!, linked, eventStream, handleMessage); // openStreamResponse guarantees a body
1058
1016
 
1059
1017
  const toolCalls: ToolCallRecord[] = [];
1060
1018
  for (const c of calls.values()) {
@@ -1070,9 +1028,11 @@ export class OpenAIResponsesProvider implements Provider {
1070
1028
  eventStream.push({ type: "tool_call_complete", toolCall: record });
1071
1029
  }
1072
1030
 
1031
+ // Did the stream really end? Read before the tool-call and refusal overrides below, which also set sawFinishReason.
1032
+ const hasTerminal = completedBody !== undefined || sawFinishReason || sawUsage;
1073
1033
  noteProviderTurn(sessionId, "openai");
1074
1034
  if (toolCalls.length > 0) {
1075
- finishReason = "tool_calls";
1035
+ finishReason = toolTurnFinishReason(finishReason, toolCalls);
1076
1036
  sawFinishReason = true;
1077
1037
  } else if (refusalSeen && finishReason !== "length") {
1078
1038
  finishReason = "refusal";
@@ -1085,26 +1045,21 @@ export class OpenAIResponsesProvider implements Provider {
1085
1045
  // Skipped for non-SSE payloads (e.g. JSON mocks in delegation tests)
1086
1046
  // where no SSE frames were ever seen — those preserve legacy empty
1087
1047
  // success rather than failing the caller on a mock artifact.
1088
- const hasTerminal = completedBody !== undefined || sawFinishReason || sawUsage;
1089
- const cleanThinkingEarly = thinking.replace(/\n{3,}/g, "\n\n").trim();
1090
- const ctEntry = Object.entries(responseHeaders).find(([k]) => k.toLowerCase() === "content-type");
1091
- const isSSEPayload =
1092
- (typeof ctEntry?.[1] === "string" && ((ctEntry[1] as string).includes("event-stream") || (ctEntry[1] as string).startsWith("text/"))) ||
1093
- framesSeen > 0 ||
1094
- badFrames > 0;
1095
- if (!hasTerminal && !text && !cleanThinkingEarly && toolCalls.length === 0 && isSSEPayload) {
1096
- eventStream.fail(incompleteStreamError("openai", clean));
1097
- return;
1098
- }
1099
- // Refuse unfinished tool calls whose args never parsed (output_item.done
1100
- // never arrived): truncated `{raw}` must not reach the executor (pi
1101
- // api/openai-responses-shared.ts:766 rule). Non-SSE mocks preserve legacy.
1102
- if (!hasTerminal && toolCalls.some((c) => hasTruncatedToolArguments(c.arguments)) && isSSEPayload) {
1103
- eventStream.fail(incompleteStreamError("openai", clean));
1048
+ const cleanThinking = thinking.replace(/\n{3,}/g, "\n\n").trim();
1049
+ const incomplete = incompleteStreamCheck({
1050
+ provider: "openai",
1051
+ modelId: clean,
1052
+ hasTerminal,
1053
+ empty: !text && !cleanThinking && toolCalls.length === 0,
1054
+ unfinishedCall: hasUnfinishedToolCall(calls.values(), toolCalls),
1055
+ responseHeaders,
1056
+ framesSeen,
1057
+ badFrames,
1058
+ });
1059
+ if (incomplete) {
1060
+ eventStream.fail(incomplete);
1104
1061
  return;
1105
1062
  }
1106
-
1107
- const cleanThinking = cleanThinkingEarly;
1108
1063
  const finalResponse = new AgentResponse({
1109
1064
  text,
1110
1065
  thinking: cleanThinking || undefined,
@@ -1124,21 +1079,9 @@ export class OpenAIResponsesProvider implements Provider {
1124
1079
  eventStream.push({ type: "done", delta: "", usage, finishReason, responseId });
1125
1080
  eventStream.end(finalResponse);
1126
1081
  } catch (err: unknown) {
1127
- const raw = err instanceof Error ? err : new Error(String(err));
1128
- const isAbort =
1129
- linked.signal.aborted ||
1130
- eventStream.isCancelled() ||
1131
- (raw as { name?: string }).name === "AbortError";
1132
- eventStream.fail(
1133
- isAbort ? Object.assign(new Error("Stream aborted"), { name: "AbortError" }) : raw
1134
- );
1082
+ failStream(eventStream, linked, err);
1135
1083
  } finally {
1136
- try {
1137
- options?.signal?.removeEventListener("abort", forwardUserAbort);
1138
- } catch {}
1139
- try {
1140
- removeStreamCancel();
1141
- } catch {}
1084
+ release();
1142
1085
  }
1143
1086
  })();
1144
1087