agent-accelerator 0.5.4 → 0.5.6
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 +34 -17
- package/package.json +1 -1
- package/src/agent/agent.ts +111 -35
- package/src/agent/config-blocks.ts +80 -0
- package/src/agent/loop.ts +6 -4
- package/src/agent/subagents-tool.ts +2 -2
- package/src/agent/subagents.md +2 -2
- package/src/agent/worker.ts +7 -5
- package/src/agent/workers.ts +2 -2
- package/src/index.ts +3 -3
- package/src/providers/google.ts +6 -39
- package/src/providers/openai-compat.ts +31 -104
- package/src/providers/openai.ts +30 -103
- package/src/providers/openrouter.ts +151 -400
- package/src/providers/shared.ts +136 -7
- package/src/providers.ts +21 -6
- package/src/scheduler/scheduled-actions.md +4 -2
- package/src/scheduler/scheduler.ts +87 -50
- package/src/scheduler/store.ts +23 -1
- package/src/scheduler/tool.ts +9 -8
- package/src/session/store.ts +3 -3
- package/src/streaming/event-stream.ts +7 -2
- package/src/tools/executor.ts +17 -2
- package/src/tools/tool.ts +0 -16
- package/src/types/agent.ts +32 -17
- package/src/types/message.ts +1 -1
- package/src/types/scheduler.ts +9 -2
- package/src/utils/errors.ts +4 -1
- package/src/utils/headers.ts +1 -1
- package/src/utils/media.ts +10 -1
package/src/agent/worker.ts
CHANGED
|
@@ -171,9 +171,11 @@ export function normalizeToolMap(
|
|
|
171
171
|
// Shared dynamic-worker helpers (pure, no store access)
|
|
172
172
|
// ---------------------------------------------------------------------------
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
export function
|
|
176
|
-
|
|
174
|
+
// The time limit of one scheduled run in ms, 0 = none: the action's own `timeoutMs`, never above the developer's `workerTimeoutMs` (Infinity = none), which is also the limit when the action sets none; in: Agent.dynamicSubagents.workerTimeoutMs + the saved action, out: Scheduler.attempt.
|
|
175
|
+
export function scheduledRunTimeoutMs(cap: number, taskTimeoutMs: number | undefined): number {
|
|
176
|
+
const own = typeof taskTimeoutMs === "number" && taskTimeoutMs > 0 ? Math.floor(taskTimeoutMs) : Infinity;
|
|
177
|
+
const ms = Math.min(own, cap);
|
|
178
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
/**
|
|
@@ -236,8 +238,8 @@ export function resolveWorkerCredentials(
|
|
|
236
238
|
return { apiKeyToUse: undefined, baseUrlToUse: undefined, attemptedProvider };
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
/** How many levels of workers may exist below an agent unless `dynamicSubagents.maxDepth` says otherwise. */
|
|
240
|
-
export const DEFAULT_MAX_DEPTH =
|
|
241
|
+
/** How many levels of workers may exist below an agent unless `dynamicSubagents.maxDepth` says otherwise: unlimited. */
|
|
242
|
+
export const DEFAULT_MAX_DEPTH = Infinity;
|
|
241
243
|
|
|
242
244
|
/**
|
|
243
245
|
* Config of a dynamic worker (scheduled actions, the worker registry): the developer's fixed worker
|
package/src/agent/workers.ts
CHANGED
|
@@ -271,7 +271,7 @@ export class WorkerRegistry {
|
|
|
271
271
|
"[Agent Accelerator] Cannot spawn workers: no sub-agent model is configured. Set dynamicSubagents.model in Agent config or set the SUB_AGENT_MODEL environment variable."
|
|
272
272
|
);
|
|
273
273
|
}
|
|
274
|
-
const maxOpen = parent.dynamicSubagents?.maxSpawn ??
|
|
274
|
+
const maxOpen = parent.dynamicSubagents?.maxSpawn ?? Infinity;
|
|
275
275
|
if (this.list().length >= maxOpen) {
|
|
276
276
|
throw new ValidationError(`Too many open workers (${maxOpen} is the limit). Close one first; open workers: ${this.list().map((w) => w.id).join(", ")}.`);
|
|
277
277
|
}
|
|
@@ -361,7 +361,7 @@ export class WorkerRegistry {
|
|
|
361
361
|
else delete meta.last;
|
|
362
362
|
this.save(entry);
|
|
363
363
|
}
|
|
364
|
-
const onAbort = (): void => agent.interrupt(opts.signal?.reason);
|
|
364
|
+
const onAbort = (): void => agent.interrupt(opts.signal?.reason instanceof Error ? opts.signal.reason : undefined);
|
|
365
365
|
try {
|
|
366
366
|
if (opts.signal?.aborted) throw new Error("aborted before the worker started");
|
|
367
367
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ export { Agent, SubAgentModelError } from "./agent/agent.ts";
|
|
|
37
37
|
*/
|
|
38
38
|
export { SubAgent, agentFromEnv } from "./agent/subagent.ts";
|
|
39
39
|
export type { SubAgentConfig } from "./agent/subagent.ts";
|
|
40
|
+
export type { EditableAgentConfig } from "./types/agent.ts";
|
|
40
41
|
export {
|
|
41
42
|
tool,
|
|
42
43
|
toStandardToolDeclarations,
|
|
@@ -44,7 +45,6 @@ export {
|
|
|
44
45
|
TOOL_NAME_PATTERN,
|
|
45
46
|
normalizeThinkingV2,
|
|
46
47
|
resolveWhenBusy,
|
|
47
|
-
normalizeWorkerTimeout,
|
|
48
48
|
normalizeMaxAttempts,
|
|
49
49
|
} from "./tools/tool.ts";
|
|
50
50
|
export type { CreateToolOptions } from "./tools/tool.ts";
|
|
@@ -94,7 +94,7 @@ export {
|
|
|
94
94
|
updateScheduledAction,
|
|
95
95
|
runScheduledAction,
|
|
96
96
|
skipScheduledAction,
|
|
97
|
-
|
|
97
|
+
deleteScheduledAction,
|
|
98
98
|
} from "./scheduler/scheduler.ts";
|
|
99
99
|
export { getDatetimeTool } from "./scheduler/tool.ts";
|
|
100
100
|
export { setHome, resolveHome } from "./utils/home.ts";
|
|
@@ -106,7 +106,7 @@ export type {
|
|
|
106
106
|
ScheduledState,
|
|
107
107
|
ScheduledRunStatus,
|
|
108
108
|
ScheduleSpec,
|
|
109
|
-
|
|
109
|
+
DeleteResult,
|
|
110
110
|
} from "./types/scheduler.ts";
|
|
111
111
|
|
|
112
112
|
// Providers & Registry — unified multi-provider layer, all native REST
|
package/src/providers/google.ts
CHANGED
|
@@ -58,6 +58,8 @@ import {
|
|
|
58
58
|
retryOptionsFrom,
|
|
59
59
|
headersToRecord,
|
|
60
60
|
parseRetryAfterMs,
|
|
61
|
+
linkStreamAbort,
|
|
62
|
+
withAbortTimer,
|
|
61
63
|
} from "./shared.ts";
|
|
62
64
|
import { telemetryBus } from "../telemetry.ts";
|
|
63
65
|
import { createGenericModelSpec } from "../models/catalog.ts";
|
|
@@ -1097,23 +1099,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1097
1099
|
const startTime = Date.now();
|
|
1098
1100
|
const clean = cleanModelId(model);
|
|
1099
1101
|
|
|
1100
|
-
const linked =
|
|
1101
|
-
const forwardUserAbort = () => {
|
|
1102
|
-
try {
|
|
1103
|
-
linked.abort((options?.signal as { reason?: unknown })?.reason);
|
|
1104
|
-
} catch {
|
|
1105
|
-
try {
|
|
1106
|
-
linked.abort();
|
|
1107
|
-
} catch {}
|
|
1108
|
-
}
|
|
1109
|
-
};
|
|
1110
|
-
if (options?.signal?.aborted) forwardUserAbort();
|
|
1111
|
-
else options?.signal?.addEventListener("abort", forwardUserAbort, { once: true });
|
|
1112
|
-
const removeStreamCancel = eventStream.onCancel(() => {
|
|
1113
|
-
try {
|
|
1114
|
-
linked.abort();
|
|
1115
|
-
} catch {}
|
|
1116
|
-
});
|
|
1102
|
+
const { linked, release } = linkStreamAbort(eventStream, options?.signal);
|
|
1117
1103
|
|
|
1118
1104
|
(async () => {
|
|
1119
1105
|
// Hoisted for the catch-block self-heal (consts inside try are not
|
|
@@ -1159,21 +1145,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1159
1145
|
// (429/5xx/network) are retried before the first byte; a live stream is never retried.
|
|
1160
1146
|
const open = (target: string, method: "POST" | "GET"): Promise<Response> =>
|
|
1161
1147
|
openStreamResponse({
|
|
1162
|
-
fetchOnce:
|
|
1163
|
-
const timer =
|
|
1164
|
-
streamTimeoutMs && streamTimeoutMs > 0
|
|
1165
|
-
? setTimeout(() => {
|
|
1166
|
-
try {
|
|
1167
|
-
linked.abort();
|
|
1168
|
-
} catch {}
|
|
1169
|
-
}, streamTimeoutMs)
|
|
1170
|
-
: undefined;
|
|
1171
|
-
try {
|
|
1172
|
-
return await (options?.fetch ?? fetch)(target, this.requestInit(method, method === "POST" ? body : undefined, options, apiKey, linked.signal));
|
|
1173
|
-
} finally {
|
|
1174
|
-
if (timer) clearTimeout(timer);
|
|
1175
|
-
}
|
|
1176
|
-
},
|
|
1148
|
+
fetchOnce: () => withAbortTimer(linked, streamTimeoutMs, () => (options?.fetch ?? fetch)(target, this.requestInit(method, method === "POST" ? body : undefined, options, apiKey, linked.signal))),
|
|
1177
1149
|
onHttpError: (r, text) => this.throwIfError(r.status, r.statusText, target, text, clean, headersToRecord(r.headers)),
|
|
1178
1150
|
noBodyError: () => toConciseProviderError(new Error("Google streaming response had no body"), "google", clean),
|
|
1179
1151
|
retry: retryOptionsFrom(options),
|
|
@@ -1210,7 +1182,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1210
1182
|
() => {
|
|
1211
1183
|
aborted = true;
|
|
1212
1184
|
try {
|
|
1213
|
-
void reader?.cancel();
|
|
1185
|
+
void reader?.cancel().catch(() => {}); // an aborted body is already errored: its cancel() rejects, and nobody else listens
|
|
1214
1186
|
} catch {}
|
|
1215
1187
|
},
|
|
1216
1188
|
{ once: true }
|
|
@@ -1465,12 +1437,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1465
1437
|
eventStream.fail(raw);
|
|
1466
1438
|
}
|
|
1467
1439
|
} finally {
|
|
1468
|
-
|
|
1469
|
-
options?.signal?.removeEventListener("abort", forwardUserAbort);
|
|
1470
|
-
} catch {}
|
|
1471
|
-
try {
|
|
1472
|
-
removeStreamCancel();
|
|
1473
|
-
} catch {}
|
|
1440
|
+
release();
|
|
1474
1441
|
}
|
|
1475
1442
|
})();
|
|
1476
1443
|
|
|
@@ -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
|
-
|
|
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,
|
|
@@ -450,7 +456,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
|
|
|
450
456
|
} = {
|
|
451
457
|
id: clampToolCallId(part.callId || part.id),
|
|
452
458
|
type: "function",
|
|
453
|
-
function: { name: part.name, arguments:
|
|
459
|
+
function: { name: part.name, arguments: toolArgumentsJson(part.arguments) },
|
|
454
460
|
};
|
|
455
461
|
// Echo provider-issued thought signatures verbatim so endpoints
|
|
456
462
|
// that minted them (Gemini behind a compat proxy) keep working.
|
|
@@ -741,7 +747,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
741
747
|
const compatTools = toCompatTools(tools);
|
|
742
748
|
if (compatTools) body.tools = compatTools;
|
|
743
749
|
// Standard Chat Completions generation fields, only when the caller set them.
|
|
744
|
-
Object.assign(body, mapGenerationParamsToChat(options));
|
|
750
|
+
Object.assign(body, mapGenerationParamsToChat(options, this.prefix, `${this.prefix}/${modelId}`));
|
|
745
751
|
const toolChoice = mapToolChoiceToOpenAI(
|
|
746
752
|
options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
747
753
|
);
|
|
@@ -949,23 +955,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
949
955
|
const prefix = this.prefix;
|
|
950
956
|
const clean = cleanModelId(this.prefix, model);
|
|
951
957
|
|
|
952
|
-
const linked =
|
|
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
|
-
});
|
|
958
|
+
const { linked, release } = linkStreamAbort(eventStream, options?.signal);
|
|
969
959
|
|
|
970
960
|
(async () => {
|
|
971
961
|
try {
|
|
@@ -994,21 +984,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
994
984
|
// Transient failures (429/5xx/network) are retried here, before the
|
|
995
985
|
// first byte; once the stream is live it is never retried.
|
|
996
986
|
const res = await openStreamResponse({
|
|
997
|
-
fetchOnce:
|
|
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
|
-
},
|
|
987
|
+
fetchOnce: () => withAbortTimer(linked, streamTimeoutMs, () => (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))),
|
|
1012
988
|
onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
|
|
1013
989
|
noBodyError: () => toConciseProviderError(new Error("OpenAI-compatible streaming response had no body"), prefix, clean),
|
|
1014
990
|
retry: retryOptionsFrom(options),
|
|
@@ -1022,9 +998,6 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1022
998
|
const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
1023
999
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
1024
1000
|
|
|
1025
|
-
const parser = new SSEParser();
|
|
1026
|
-
const reader = res.body!.getReader(); // openStreamResponse guarantees a body
|
|
1027
|
-
const decoder = new TextDecoder();
|
|
1028
1001
|
let text = "";
|
|
1029
1002
|
let thinking = "";
|
|
1030
1003
|
// Tool calls keyed by choice index: { id, name, startArgs, deltaArgs }.
|
|
@@ -1040,18 +1013,6 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1040
1013
|
let framesSeen = 0;
|
|
1041
1014
|
let responseId: string | undefined;
|
|
1042
1015
|
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
1016
|
const handleMessage = (data: string): void => {
|
|
1056
1017
|
if (!data) return;
|
|
1057
1018
|
if (data === "[DONE]") {
|
|
@@ -1176,26 +1137,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1176
1137
|
}
|
|
1177
1138
|
};
|
|
1178
1139
|
|
|
1179
|
-
|
|
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
|
-
}
|
|
1140
|
+
await readSseData(res.body!, linked, eventStream, handleMessage); // openStreamResponse guarantees a body
|
|
1199
1141
|
|
|
1200
1142
|
const toolCalls: ToolCallRecord[] = [];
|
|
1201
1143
|
for (const [index, c] of calls) {
|
|
@@ -1212,9 +1154,11 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1212
1154
|
eventStream.push({ type: "tool_call_complete", toolCall: record });
|
|
1213
1155
|
}
|
|
1214
1156
|
|
|
1157
|
+
// Did the stream really end? Read before the tool-call and refusal overrides below, which also set sawFinishReason.
|
|
1158
|
+
const hasTerminal = completedBody !== undefined || sawFinishReason || sawUsage;
|
|
1215
1159
|
noteProviderTurn(sessionId, prefix);
|
|
1216
1160
|
if (toolCalls.length > 0) {
|
|
1217
|
-
finishReason =
|
|
1161
|
+
finishReason = toolTurnFinishReason(finishReason, toolCalls);
|
|
1218
1162
|
sawFinishReason = true;
|
|
1219
1163
|
} else if (refusalSeen) {
|
|
1220
1164
|
finishReason = "refusal";
|
|
@@ -1224,26 +1168,21 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1224
1168
|
// Q-23: terminal-state guard — fail `incomplete_stream` (retryable)
|
|
1225
1169
|
// instead of resolving empty success. Skipped for non-SSE payloads
|
|
1226
1170
|
// (JSON mocks) where no SSE frames were ever seen.
|
|
1227
|
-
const
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
|
|
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));
|
|
1171
|
+
const cleanThinking = thinking.replace(/\n{3,}/g, "\n\n").trim();
|
|
1172
|
+
const incomplete = incompleteStreamCheck({
|
|
1173
|
+
provider: prefix,
|
|
1174
|
+
modelId: clean,
|
|
1175
|
+
hasTerminal,
|
|
1176
|
+
empty: !text && !cleanThinking && toolCalls.length === 0,
|
|
1177
|
+
unfinishedCall: hasUnfinishedToolCall(calls.values(), toolCalls),
|
|
1178
|
+
responseHeaders,
|
|
1179
|
+
framesSeen,
|
|
1180
|
+
badFrames,
|
|
1181
|
+
});
|
|
1182
|
+
if (incomplete) {
|
|
1183
|
+
eventStream.fail(incomplete);
|
|
1243
1184
|
return;
|
|
1244
1185
|
}
|
|
1245
|
-
|
|
1246
|
-
const cleanThinking = cleanThinkingEarly;
|
|
1247
1186
|
const finalThoughtSignature = toolCalls.map((c) => c.thoughtSignature).find(Boolean);
|
|
1248
1187
|
const finalResponse = new AgentResponse({
|
|
1249
1188
|
text,
|
|
@@ -1264,21 +1203,9 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1264
1203
|
eventStream.push({ type: "done", delta: "", usage, finishReason, responseId });
|
|
1265
1204
|
eventStream.end(finalResponse);
|
|
1266
1205
|
} catch (err: unknown) {
|
|
1267
|
-
|
|
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
|
-
);
|
|
1206
|
+
failStream(eventStream, linked, err);
|
|
1275
1207
|
} finally {
|
|
1276
|
-
|
|
1277
|
-
options?.signal?.removeEventListener("abort", forwardUserAbort);
|
|
1278
|
-
} catch {}
|
|
1279
|
-
try {
|
|
1280
|
-
removeStreamCancel();
|
|
1281
|
-
} catch {}
|
|
1208
|
+
release();
|
|
1282
1209
|
}
|
|
1283
1210
|
})();
|
|
1284
1211
|
|
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,
|
|
@@ -340,7 +346,7 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
|
|
|
340
346
|
id: normalizeResponsesItemId(itemId),
|
|
341
347
|
call_id: normalizeResponsesIdPart(callId),
|
|
342
348
|
name: part.name,
|
|
343
|
-
arguments:
|
|
349
|
+
arguments: toolArgumentsJson(part.arguments),
|
|
344
350
|
});
|
|
345
351
|
} else if (part.type === "text" && part.text) {
|
|
346
352
|
texts.push(part.text);
|
|
@@ -776,23 +782,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
776
782
|
const startTime = Date.now();
|
|
777
783
|
const clean = cleanModelId(model);
|
|
778
784
|
|
|
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
|
-
});
|
|
785
|
+
const { linked, release } = linkStreamAbort(eventStream, options?.signal);
|
|
796
786
|
|
|
797
787
|
(async () => {
|
|
798
788
|
try {
|
|
@@ -827,21 +817,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
827
817
|
// Transient failures (429/5xx/network) are retried here, before the
|
|
828
818
|
// first byte; once the stream is live it is never retried.
|
|
829
819
|
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
|
-
},
|
|
820
|
+
fetchOnce: () => withAbortTimer(linked, streamTimeoutMs, () => (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))),
|
|
845
821
|
onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
|
|
846
822
|
noBodyError: () => toConciseProviderError(new Error("OpenAI streaming response had no body"), "openai", clean),
|
|
847
823
|
retry: retryOptionsFrom(options),
|
|
@@ -855,9 +831,6 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
855
831
|
const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
856
832
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
857
833
|
|
|
858
|
-
const parser = new SSEParser();
|
|
859
|
-
const reader = res.body!.getReader(); // openStreamResponse guarantees a body
|
|
860
|
-
const decoder = new TextDecoder();
|
|
861
834
|
let text = "";
|
|
862
835
|
let thinking = "";
|
|
863
836
|
// Tool calls keyed by output_index: { itemId, callId, name, startArgs, deltaArgs }.
|
|
@@ -871,18 +844,6 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
871
844
|
let framesSeen = 0;
|
|
872
845
|
let responseId: string | undefined;
|
|
873
846
|
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
847
|
const handleMessage = (data: string): void => {
|
|
887
848
|
if (!data || data === "[DONE]") {
|
|
888
849
|
if (data === "[DONE]") sawFinishReason = sawFinishReason || completedBody !== undefined || sawUsage;
|
|
@@ -1035,26 +996,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
1035
996
|
}
|
|
1036
997
|
};
|
|
1037
998
|
|
|
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
|
-
}
|
|
999
|
+
await readSseData(res.body!, linked, eventStream, handleMessage); // openStreamResponse guarantees a body
|
|
1058
1000
|
|
|
1059
1001
|
const toolCalls: ToolCallRecord[] = [];
|
|
1060
1002
|
for (const c of calls.values()) {
|
|
@@ -1070,9 +1012,11 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
1070
1012
|
eventStream.push({ type: "tool_call_complete", toolCall: record });
|
|
1071
1013
|
}
|
|
1072
1014
|
|
|
1015
|
+
// Did the stream really end? Read before the tool-call and refusal overrides below, which also set sawFinishReason.
|
|
1016
|
+
const hasTerminal = completedBody !== undefined || sawFinishReason || sawUsage;
|
|
1073
1017
|
noteProviderTurn(sessionId, "openai");
|
|
1074
1018
|
if (toolCalls.length > 0) {
|
|
1075
|
-
finishReason =
|
|
1019
|
+
finishReason = toolTurnFinishReason(finishReason, toolCalls);
|
|
1076
1020
|
sawFinishReason = true;
|
|
1077
1021
|
} else if (refusalSeen && finishReason !== "length") {
|
|
1078
1022
|
finishReason = "refusal";
|
|
@@ -1085,26 +1029,21 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
1085
1029
|
// Skipped for non-SSE payloads (e.g. JSON mocks in delegation tests)
|
|
1086
1030
|
// where no SSE frames were ever seen — those preserve legacy empty
|
|
1087
1031
|
// 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));
|
|
1032
|
+
const cleanThinking = thinking.replace(/\n{3,}/g, "\n\n").trim();
|
|
1033
|
+
const incomplete = incompleteStreamCheck({
|
|
1034
|
+
provider: "openai",
|
|
1035
|
+
modelId: clean,
|
|
1036
|
+
hasTerminal,
|
|
1037
|
+
empty: !text && !cleanThinking && toolCalls.length === 0,
|
|
1038
|
+
unfinishedCall: hasUnfinishedToolCall(calls.values(), toolCalls),
|
|
1039
|
+
responseHeaders,
|
|
1040
|
+
framesSeen,
|
|
1041
|
+
badFrames,
|
|
1042
|
+
});
|
|
1043
|
+
if (incomplete) {
|
|
1044
|
+
eventStream.fail(incomplete);
|
|
1104
1045
|
return;
|
|
1105
1046
|
}
|
|
1106
|
-
|
|
1107
|
-
const cleanThinking = cleanThinkingEarly;
|
|
1108
1047
|
const finalResponse = new AgentResponse({
|
|
1109
1048
|
text,
|
|
1110
1049
|
thinking: cleanThinking || undefined,
|
|
@@ -1124,21 +1063,9 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
1124
1063
|
eventStream.push({ type: "done", delta: "", usage, finishReason, responseId });
|
|
1125
1064
|
eventStream.end(finalResponse);
|
|
1126
1065
|
} 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
|
-
);
|
|
1066
|
+
failStream(eventStream, linked, err);
|
|
1135
1067
|
} finally {
|
|
1136
|
-
|
|
1137
|
-
options?.signal?.removeEventListener("abort", forwardUserAbort);
|
|
1138
|
-
} catch {}
|
|
1139
|
-
try {
|
|
1140
|
-
removeStreamCancel();
|
|
1141
|
-
} catch {}
|
|
1068
|
+
release();
|
|
1142
1069
|
}
|
|
1143
1070
|
})();
|
|
1144
1071
|
|