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.
- package/README.md +16 -17
- 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 +14 -16
- 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 +26 -39
- package/src/providers/openai-compat.ts +53 -104
- package/src/providers/openai.ts +47 -104
- package/src/providers/openrouter.ts +114 -175
- package/src/providers/registry.ts +1 -0
- package/src/providers/shared.ts +134 -5
- package/src/providers.ts +30 -59
- 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 +18 -3
- 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/types/message.ts +1 -1
- 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 +8 -1
- package/src/utils/headers.ts +4 -1
- package/src/utils/media.ts +11 -1
- 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
|
@@ -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
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1137
|
-
options?.signal?.removeEventListener("abort", forwardUserAbort);
|
|
1138
|
-
} catch {}
|
|
1139
|
-
try {
|
|
1140
|
-
removeStreamCancel();
|
|
1141
|
-
} catch {}
|
|
1084
|
+
release();
|
|
1142
1085
|
}
|
|
1143
1086
|
})();
|
|
1144
1087
|
|