@just-every/ensemble 0.2.211 → 0.2.213
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 +152 -91
- package/dist/cjs/core/ensemble_request.cjs +734 -333
- package/dist/cjs/core/ensemble_request.d.ts.map +1 -1
- package/dist/cjs/core/ensemble_request.js.map +1 -1
- package/dist/cjs/data/model_data.cjs +22 -0
- package/dist/cjs/data/model_data.d.ts.map +1 -1
- package/dist/cjs/data/model_data.js.map +1 -1
- package/dist/cjs/model_providers/base_provider.d.ts.map +1 -1
- package/dist/cjs/model_providers/base_provider.js.map +1 -1
- package/dist/cjs/model_providers/claude.cjs +72 -72
- package/dist/cjs/model_providers/claude.d.ts.map +1 -1
- package/dist/cjs/model_providers/claude.js.map +1 -1
- package/dist/cjs/model_providers/gemini.cjs +3 -0
- package/dist/cjs/model_providers/gemini.d.ts.map +1 -1
- package/dist/cjs/model_providers/gemini.js.map +1 -1
- package/dist/cjs/model_providers/model_provider.cjs +1 -0
- package/dist/cjs/model_providers/model_provider.d.ts.map +1 -1
- package/dist/cjs/model_providers/model_provider.js.map +1 -1
- package/dist/cjs/model_providers/openai.cjs +42 -113
- package/dist/cjs/model_providers/openai.d.ts.map +1 -1
- package/dist/cjs/model_providers/openai.js.map +1 -1
- package/dist/cjs/model_providers/openai_chat.cjs +55 -24
- package/dist/cjs/model_providers/openai_chat.d.ts.map +1 -1
- package/dist/cjs/model_providers/openai_chat.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/cjs/types/types.d.ts +20 -2
- package/dist/cjs/types/types.d.ts.map +1 -1
- package/dist/cjs/utils/agent.cjs +4 -6
- package/dist/cjs/utils/agent.d.ts.map +1 -1
- package/dist/cjs/utils/agent.js.map +1 -1
- package/dist/cjs/utils/ensemble_result.cjs +43 -4
- package/dist/cjs/utils/ensemble_result.d.ts +10 -1
- package/dist/cjs/utils/ensemble_result.d.ts.map +1 -1
- package/dist/cjs/utils/ensemble_result.js.map +1 -1
- package/dist/cjs/utils/failure_detection.cjs +292 -0
- package/dist/cjs/utils/failure_detection.d.ts +51 -0
- package/dist/cjs/utils/failure_detection.d.ts.map +1 -0
- package/dist/cjs/utils/failure_detection.js.map +1 -0
- package/dist/cjs/utils/json_schema.cjs +490 -0
- package/dist/cjs/utils/json_schema.d.ts +10 -0
- package/dist/cjs/utils/json_schema.d.ts.map +1 -0
- package/dist/cjs/utils/json_schema.js.map +1 -0
- package/dist/cjs/utils/tool_execution_manager.cjs +28 -4
- package/dist/cjs/utils/tool_execution_manager.d.ts +1 -1
- package/dist/cjs/utils/tool_execution_manager.d.ts.map +1 -1
- package/dist/cjs/utils/tool_execution_manager.js.map +1 -1
- package/dist/cjs/utils/verification.cjs +26 -13
- package/dist/cjs/utils/verification.d.ts.map +1 -1
- package/dist/cjs/utils/verification.js.map +1 -1
- package/dist/core/ensemble_request.d.ts.map +1 -1
- package/dist/core/ensemble_request.js +734 -333
- package/dist/core/ensemble_request.js.map +1 -1
- package/dist/data/model_data.d.ts.map +1 -1
- package/dist/data/model_data.js +22 -0
- package/dist/data/model_data.js.map +1 -1
- package/dist/model_providers/base_provider.d.ts.map +1 -1
- package/dist/model_providers/base_provider.js.map +1 -1
- package/dist/model_providers/claude.d.ts.map +1 -1
- package/dist/model_providers/claude.js +72 -72
- package/dist/model_providers/claude.js.map +1 -1
- package/dist/model_providers/gemini.d.ts.map +1 -1
- package/dist/model_providers/gemini.js +3 -0
- package/dist/model_providers/gemini.js.map +1 -1
- package/dist/model_providers/model_provider.d.ts.map +1 -1
- package/dist/model_providers/model_provider.js +1 -0
- package/dist/model_providers/model_provider.js.map +1 -1
- package/dist/model_providers/openai.d.ts.map +1 -1
- package/dist/model_providers/openai.js +42 -113
- package/dist/model_providers/openai.js.map +1 -1
- package/dist/model_providers/openai_chat.d.ts.map +1 -1
- package/dist/model_providers/openai_chat.js +55 -24
- package/dist/model_providers/openai_chat.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/types.d.ts +20 -2
- package/dist/types/types.d.ts.map +1 -1
- package/dist/utils/agent.d.ts.map +1 -1
- package/dist/utils/agent.js +4 -6
- package/dist/utils/agent.js.map +1 -1
- package/dist/utils/ensemble_result.d.ts +10 -1
- package/dist/utils/ensemble_result.d.ts.map +1 -1
- package/dist/utils/ensemble_result.js +43 -4
- package/dist/utils/ensemble_result.js.map +1 -1
- package/dist/utils/failure_detection.d.ts +51 -0
- package/dist/utils/failure_detection.d.ts.map +1 -0
- package/dist/utils/failure_detection.js +280 -0
- package/dist/utils/failure_detection.js.map +1 -0
- package/dist/utils/json_schema.d.ts +10 -0
- package/dist/utils/json_schema.d.ts.map +1 -0
- package/dist/utils/json_schema.js +486 -0
- package/dist/utils/json_schema.js.map +1 -0
- package/dist/utils/tool_execution_manager.d.ts +1 -1
- package/dist/utils/tool_execution_manager.d.ts.map +1 -1
- package/dist/utils/tool_execution_manager.js +28 -4
- package/dist/utils/tool_execution_manager.js.map +1 -1
- package/dist/utils/verification.d.ts.map +1 -1
- package/dist/utils/verification.js +26 -13
- package/dist/utils/verification.js.map +1 -1
- package/package.json +1 -1
|
@@ -9,9 +9,13 @@ import { waitWhilePaused } from '../utils/pause_controller.js';
|
|
|
9
9
|
import { emitEvent } from '../utils/event_controller.js';
|
|
10
10
|
import { createTraceContext } from '../utils/trace_context.js';
|
|
11
11
|
import { convertToThinkingMessage, convertToOutputMessage, convertToFunctionCall, convertToFunctionCallOutput, } from '../utils/message_converter.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import { createOperationGuard, normalizeFailure, RequestLifecycleController, selectMoreSevereFailure, streamWithAbortAndTimeout, toErrorEvent, } from '../utils/failure_detection.js';
|
|
13
|
+
import { validateJsonResponseContent } from '../utils/json_schema.js';
|
|
14
|
+
import { runningToolTracker } from '../utils/running_tool_tracker.js';
|
|
15
|
+
import { calculateDelay } from '../utils/retry_handler.js';
|
|
16
|
+
const DEFAULT_MAX_ERROR_RETRIES = 4;
|
|
14
17
|
const DEFAULT_TERMINAL_TOOL_NAMES = new Set(['task_complete', 'task_fatal_error']);
|
|
18
|
+
const TOOL_FAILURE_FINALIZATION_TIMEOUT_MS = 50;
|
|
15
19
|
const getTerminalToolNames = (agent) => {
|
|
16
20
|
const toolNames = new Set(DEFAULT_TERMINAL_TOOL_NAMES);
|
|
17
21
|
for (const name of agent.terminalToolNames ?? []) {
|
|
@@ -21,9 +25,81 @@ const getTerminalToolNames = (agent) => {
|
|
|
21
25
|
}
|
|
22
26
|
return toolNames;
|
|
23
27
|
};
|
|
28
|
+
const hasTerminalTextContent = (content, expectsStructuredOutput) => {
|
|
29
|
+
if (typeof content !== 'string') {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return expectsStructuredOutput ? content.trim().length > 0 : content.length > 0;
|
|
33
|
+
};
|
|
34
|
+
const getMaxErrorRetries = (agent) => {
|
|
35
|
+
const configuredMaxRetries = agent.retryOptions?.maxRetries;
|
|
36
|
+
if (typeof configuredMaxRetries !== 'number' || Number.isNaN(configuredMaxRetries)) {
|
|
37
|
+
return DEFAULT_MAX_ERROR_RETRIES;
|
|
38
|
+
}
|
|
39
|
+
return Math.max(0, Math.floor(configuredMaxRetries));
|
|
40
|
+
};
|
|
41
|
+
const waitForRetryDelay = async (delayMs, abortSignal) => {
|
|
42
|
+
if (delayMs <= 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
await new Promise((resolve, reject) => {
|
|
46
|
+
if (abortSignal?.aborted) {
|
|
47
|
+
reject(abortSignal.reason ?? new Error('Retry wait aborted'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const timeoutId = setTimeout(() => {
|
|
51
|
+
if (abortSignal && abortListener) {
|
|
52
|
+
abortSignal.removeEventListener('abort', abortListener);
|
|
53
|
+
}
|
|
54
|
+
resolve();
|
|
55
|
+
}, delayMs);
|
|
56
|
+
const abortListener = abortSignal
|
|
57
|
+
? () => {
|
|
58
|
+
clearTimeout(timeoutId);
|
|
59
|
+
abortSignal.removeEventListener('abort', abortListener);
|
|
60
|
+
reject(abortSignal.reason ?? new Error('Retry wait aborted'));
|
|
61
|
+
}
|
|
62
|
+
: undefined;
|
|
63
|
+
if (abortSignal && abortListener) {
|
|
64
|
+
abortSignal.addEventListener('abort', abortListener, { once: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
const getOuterRequestTimeoutMs = (agent) => {
|
|
69
|
+
const timeoutMs = agent.modelSettings?.timeout_ms;
|
|
70
|
+
if (typeof timeoutMs !== 'number' || Number.isNaN(timeoutMs) || timeoutMs <= 0) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
return Math.floor(timeoutMs);
|
|
74
|
+
};
|
|
75
|
+
const getRemainingRequestTimeoutMs = (requestTimeoutMs, requestStartedAt) => {
|
|
76
|
+
if (requestTimeoutMs === undefined || requestStartedAt === undefined) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return Math.max(0, requestTimeoutMs - (Date.now() - requestStartedAt));
|
|
80
|
+
};
|
|
81
|
+
const createRequestTimeoutError = (model, timeoutMs) => {
|
|
82
|
+
const error = new Error(`Request generation for ${model} timed out after ${timeoutMs}ms`);
|
|
83
|
+
error.code = 'ETIMEDOUT';
|
|
84
|
+
error.recoverable = false;
|
|
85
|
+
return error;
|
|
86
|
+
};
|
|
87
|
+
const getFailureRetryOverrides = (agent) => ({
|
|
88
|
+
retryableErrors: agent.retryOptions?.additionalRetryableErrors,
|
|
89
|
+
retryableStatusCodes: agent.retryOptions?.additionalRetryableStatusCodes,
|
|
90
|
+
});
|
|
24
91
|
setEnsembleRequestFunction(ensembleRequest);
|
|
25
92
|
setImageToTextFunction(ensembleRequest);
|
|
26
93
|
export async function* ensembleRequest(messages, agent = {}) {
|
|
94
|
+
if (agent.jsonSchema && !agent.modelSettings?.json_schema) {
|
|
95
|
+
agent = {
|
|
96
|
+
...agent,
|
|
97
|
+
modelSettings: {
|
|
98
|
+
...agent.modelSettings,
|
|
99
|
+
json_schema: agent.jsonSchema,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
27
103
|
const conversationHistory = agent?.historyThread || messages;
|
|
28
104
|
if (agent.instructions) {
|
|
29
105
|
const alreadyHasInstructions = conversationHistory.some(msg => {
|
|
@@ -55,187 +131,228 @@ export async function* ensembleRequest(messages, agent = {}) {
|
|
|
55
131
|
compactionThreshold: 0.7,
|
|
56
132
|
});
|
|
57
133
|
const trace = createTraceContext(agent, 'chat');
|
|
134
|
+
const lifecycle = new RequestLifecycleController();
|
|
135
|
+
const maxToolCalls = agent?.maxToolCalls ?? 200;
|
|
136
|
+
const maxRounds = agent?.maxToolCallRoundsPerTurn ?? Infinity;
|
|
137
|
+
const maxErrorRetries = getMaxErrorRetries(agent);
|
|
138
|
+
const maxErrorAttempts = maxErrorRetries + 1;
|
|
139
|
+
const outerRequestTimeoutMs = getOuterRequestTimeoutMs(agent);
|
|
140
|
+
const outerRequestStartedAt = outerRequestTimeoutMs !== undefined ? Date.now() : undefined;
|
|
141
|
+
const modelHistory = [];
|
|
142
|
+
let lastModelUsed;
|
|
58
143
|
let totalToolCalls = 0;
|
|
59
144
|
let toolCallRounds = 0;
|
|
60
145
|
let errorRounds = 0;
|
|
146
|
+
let lastMessageContent = '';
|
|
61
147
|
let turnStatus = 'completed';
|
|
62
148
|
let turnEndReason = 'completed';
|
|
63
149
|
let turnError;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
let
|
|
67
|
-
let hasError = false;
|
|
68
|
-
let lastMessageContent = '';
|
|
69
|
-
const modelHistory = [];
|
|
150
|
+
let terminalFailure;
|
|
151
|
+
let terminalFailureEventEmitted = false;
|
|
152
|
+
let finalRound;
|
|
70
153
|
await trace.emitTurnStart({
|
|
71
154
|
input_messages: conversationHistory,
|
|
72
155
|
});
|
|
73
156
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let currentRoundRequestDuration;
|
|
83
|
-
let currentRoundDurationWithTools;
|
|
84
|
-
let currentRoundRequestCost;
|
|
85
|
-
const terminalToolNames = getTerminalToolNames(agent);
|
|
157
|
+
const emitRoundAgentDone = async function* (round, model) {
|
|
158
|
+
if (!round.agentDoneEvent) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
yield round.agentDoneEvent;
|
|
162
|
+
await emitEvent(round.agentDoneEvent, round.agentDoneAgent ?? agent, model);
|
|
163
|
+
};
|
|
164
|
+
while (!terminalFailure) {
|
|
86
165
|
const model = await getModelFromAgent(agent, 'reasoning_mini', modelHistory);
|
|
166
|
+
const roundRequestId = randomUUID();
|
|
167
|
+
const startedStatusEvent = lifecycle.begin(roundRequestId);
|
|
87
168
|
modelHistory.push(model);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
tool_name: toolName,
|
|
112
|
-
arguments: toolEvent.tool_call.function.arguments,
|
|
113
|
-
arguments_formatted: toolEvent.tool_call.function.arguments_formatted,
|
|
114
|
-
});
|
|
115
|
-
if (!terminalToolNames.has(toolName)) {
|
|
116
|
-
hasToolCalls = true;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
++totalToolCalls;
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
case 'tool_done': {
|
|
123
|
-
const toolEvent = event;
|
|
124
|
-
if (toolEvent.tool_call) {
|
|
125
|
-
const toolName = toolEvent.tool_call.function.name;
|
|
126
|
-
if (terminalToolNames.has(toolName) && !toolEvent.result?.error) {
|
|
127
|
-
terminalToolSucceededThisRound = true;
|
|
128
|
-
}
|
|
129
|
-
await trace.emitToolDone(event.request_id, toolEvent.tool_call.id, {
|
|
130
|
-
tool_name: toolName,
|
|
131
|
-
call_id: toolEvent.result?.call_id,
|
|
132
|
-
output: toolEvent.result?.output,
|
|
133
|
-
error: toolEvent.result?.error,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
case 'agent_done': {
|
|
139
|
-
const agentDoneEvent = event;
|
|
140
|
-
currentRoundRequestDuration = agentDoneEvent.request_duration;
|
|
141
|
-
currentRoundDurationWithTools = agentDoneEvent.duration_with_tools;
|
|
142
|
-
currentRoundRequestCost = agentDoneEvent.request_cost;
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
case 'error': {
|
|
146
|
-
hasError = true;
|
|
147
|
-
const errorEvent = event;
|
|
148
|
-
if (errorEvent.error) {
|
|
149
|
-
currentRoundErrors.push(String(errorEvent.error));
|
|
150
|
-
}
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
169
|
+
lastModelUsed = model;
|
|
170
|
+
const round = yield* executeRound({
|
|
171
|
+
roundRequestId,
|
|
172
|
+
model,
|
|
173
|
+
agent,
|
|
174
|
+
history,
|
|
175
|
+
currentToolCalls: totalToolCalls,
|
|
176
|
+
maxToolCalls,
|
|
177
|
+
trace,
|
|
178
|
+
startedStatusEvent,
|
|
179
|
+
requestTimeoutMs: outerRequestTimeoutMs,
|
|
180
|
+
requestStartedAt: outerRequestStartedAt,
|
|
181
|
+
});
|
|
182
|
+
totalToolCalls += round.toolCallsStarted;
|
|
183
|
+
if (round.messages.length > 0) {
|
|
184
|
+
lastMessageContent = round.messages.at(-1) || lastMessageContent;
|
|
185
|
+
}
|
|
186
|
+
if (round.hasFollowupToolCalls) {
|
|
187
|
+
++toolCallRounds;
|
|
188
|
+
}
|
|
189
|
+
const willRetryForError = (() => {
|
|
190
|
+
if (!round.failure) {
|
|
191
|
+
return false;
|
|
154
192
|
}
|
|
193
|
+
++errorRounds;
|
|
194
|
+
return !round.emittedTerminalOutput && round.failure.recoverable && errorRounds <= maxErrorRetries;
|
|
195
|
+
})();
|
|
196
|
+
const willContinueForTools = !round.failure &&
|
|
197
|
+
!round.terminalToolSucceeded &&
|
|
198
|
+
round.hasFollowupToolCalls &&
|
|
199
|
+
toolCallRounds < maxRounds &&
|
|
200
|
+
totalToolCalls < maxToolCalls;
|
|
201
|
+
let requestStatus = 'completed';
|
|
202
|
+
if (round.failure) {
|
|
203
|
+
requestStatus = willRetryForError ? 'error_retrying' : 'error';
|
|
155
204
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
205
|
+
else if (round.hasFollowupToolCalls && !round.terminalToolSucceeded) {
|
|
206
|
+
requestStatus = willContinueForTools ? 'waiting_for_followup_request' : 'tool_limit_reached';
|
|
207
|
+
}
|
|
208
|
+
await trace.emitRequestEnd(round.requestId, {
|
|
209
|
+
status: requestStatus,
|
|
210
|
+
will_continue: willRetryForError || willContinueForTools,
|
|
211
|
+
tool_calls: round.toolCallsStarted,
|
|
212
|
+
final_response: round.messages.length > 0 ? round.messages.join('\n') : undefined,
|
|
213
|
+
errors: round.errors.length > 0 ? round.errors : undefined,
|
|
214
|
+
request_duration_ms: round.requestDuration,
|
|
215
|
+
duration_with_tools_ms: round.durationWithTools,
|
|
216
|
+
request_cost: round.requestCost,
|
|
217
|
+
});
|
|
218
|
+
if (round.failure) {
|
|
219
|
+
const terminalRoundFailure = willRetryForError
|
|
220
|
+
? round.failure
|
|
221
|
+
: {
|
|
222
|
+
...round.failure,
|
|
223
|
+
recoverable: false,
|
|
224
|
+
terminal: true,
|
|
225
|
+
};
|
|
226
|
+
const errorEvent = toErrorEvent(terminalRoundFailure, {
|
|
227
|
+
request_id: round.requestId,
|
|
228
|
+
});
|
|
229
|
+
yield errorEvent;
|
|
230
|
+
await emitEvent(errorEvent, agent, model);
|
|
231
|
+
if (willRetryForError) {
|
|
232
|
+
agent.retryOptions?.onRetry?.({
|
|
233
|
+
message: round.failure.error,
|
|
234
|
+
code: round.failure.code,
|
|
235
|
+
details: round.failure.details,
|
|
236
|
+
recoverable: round.failure.recoverable,
|
|
237
|
+
}, errorRounds);
|
|
238
|
+
const retryingEvent = lifecycle.retrying(round.failure, errorRounds, maxErrorAttempts);
|
|
239
|
+
if (retryingEvent) {
|
|
240
|
+
yield retryingEvent;
|
|
241
|
+
await emitEvent(retryingEvent, agent, model);
|
|
242
|
+
}
|
|
243
|
+
const retryDelayMs = calculateDelay(errorRounds, agent.retryOptions);
|
|
244
|
+
const remainingTimeoutMs = getRemainingRequestTimeoutMs(outerRequestTimeoutMs, outerRequestStartedAt);
|
|
245
|
+
const boundedRetryDelayMs = remainingTimeoutMs === undefined
|
|
246
|
+
? retryDelayMs
|
|
247
|
+
: remainingTimeoutMs < retryDelayMs
|
|
248
|
+
? 0
|
|
249
|
+
: retryDelayMs;
|
|
250
|
+
yield* emitRoundAgentDone(round, model);
|
|
251
|
+
await waitForRetryDelay(boundedRetryDelayMs, agent.abortSignal);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
terminalFailure = terminalRoundFailure;
|
|
255
|
+
terminalFailureEventEmitted = true;
|
|
256
|
+
finalRound = { round, model };
|
|
257
|
+
break;
|
|
167
258
|
}
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
|
|
259
|
+
if (round.terminalToolSucceeded) {
|
|
260
|
+
finalRound = { round, model };
|
|
261
|
+
break;
|
|
171
262
|
}
|
|
172
|
-
if (
|
|
173
|
-
++toolCallRounds;
|
|
263
|
+
if (willContinueForTools) {
|
|
174
264
|
if (agent.modelSettings?.tool_choice) {
|
|
265
|
+
agent = {
|
|
266
|
+
...agent,
|
|
267
|
+
modelSettings: {
|
|
268
|
+
...agent.modelSettings,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
175
271
|
delete agent.modelSettings.tool_choice;
|
|
176
272
|
}
|
|
273
|
+
yield* emitRoundAgentDone(round, model);
|
|
274
|
+
continue;
|
|
177
275
|
}
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
else if (hasToolCalls) {
|
|
189
|
-
requestStatus = willContinue ? 'waiting_for_followup_request' : 'tool_limit_reached';
|
|
190
|
-
}
|
|
191
|
-
if (currentRoundRequestId) {
|
|
192
|
-
await trace.emitRequestEnd(currentRoundRequestId, {
|
|
193
|
-
status: requestStatus,
|
|
194
|
-
will_continue: willContinue,
|
|
195
|
-
tool_calls: currentRoundToolCalls,
|
|
196
|
-
final_response: currentRoundMessages.length > 0 ? currentRoundMessages.join('\n') : undefined,
|
|
197
|
-
errors: currentRoundErrors.length > 0 ? currentRoundErrors : undefined,
|
|
198
|
-
request_duration_ms: currentRoundRequestDuration,
|
|
199
|
-
duration_with_tools_ms: currentRoundDurationWithTools,
|
|
200
|
-
request_cost: currentRoundRequestCost,
|
|
276
|
+
if (round.hasFollowupToolCalls && !round.terminalToolSucceeded) {
|
|
277
|
+
terminalFailure = normalizeFailure(new Error(toolCallRounds >= maxRounds
|
|
278
|
+
? `Tool call rounds limit reached (${maxRounds}).`
|
|
279
|
+
: `Tool call limit reached (${maxToolCalls}).`), {
|
|
280
|
+
recoverable: false,
|
|
281
|
+
reason: toolCallRounds >= maxRounds
|
|
282
|
+
? 'max_tool_call_rounds_reached'
|
|
283
|
+
: 'max_tool_calls_reached',
|
|
284
|
+
...getFailureRetryOverrides(agent),
|
|
201
285
|
});
|
|
286
|
+
finalRound = { round, model };
|
|
287
|
+
break;
|
|
202
288
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (hasToolCalls && toolCallRounds >= maxRounds) {
|
|
206
|
-
console.log('[ensembleRequest] Tool call rounds limit reached');
|
|
207
|
-
turnEndReason = 'max_tool_call_rounds_reached';
|
|
289
|
+
finalRound = { round, model };
|
|
290
|
+
break;
|
|
208
291
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
292
|
+
if (!terminalFailure && agent.verifier && lastMessageContent) {
|
|
293
|
+
const verification = yield* performVerification(agent, lastMessageContent, await history.getMessages());
|
|
294
|
+
if (!verification.passed) {
|
|
295
|
+
terminalFailure = normalizeFailure(new Error(verification.error || 'Verification failed'), {
|
|
296
|
+
recoverable: false,
|
|
297
|
+
reason: 'verification_failed',
|
|
298
|
+
...getFailureRetryOverrides(agent),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
212
301
|
}
|
|
213
|
-
|
|
302
|
+
if (terminalFailure) {
|
|
214
303
|
turnStatus = 'error';
|
|
215
|
-
turnEndReason = '
|
|
304
|
+
turnEndReason = terminalFailure.reason || 'terminal_failure';
|
|
305
|
+
turnError = terminalFailure.error;
|
|
306
|
+
if (!terminalFailureEventEmitted) {
|
|
307
|
+
const errorEvent = toErrorEvent(terminalFailure, {
|
|
308
|
+
request_id: lifecycle.getRequestId(),
|
|
309
|
+
});
|
|
310
|
+
yield errorEvent;
|
|
311
|
+
await emitEvent(errorEvent, agent, lastModelUsed);
|
|
312
|
+
}
|
|
313
|
+
const failedEvent = lifecycle.fail(terminalFailure, errorRounds || 1, maxErrorAttempts);
|
|
314
|
+
if (failedEvent) {
|
|
315
|
+
yield failedEvent;
|
|
316
|
+
await emitEvent(failedEvent, agent, lastModelUsed);
|
|
317
|
+
}
|
|
216
318
|
}
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
319
|
+
else {
|
|
320
|
+
const completedEvent = lifecycle.complete();
|
|
321
|
+
if (completedEvent) {
|
|
322
|
+
yield completedEvent;
|
|
323
|
+
await emitEvent(completedEvent, agent, lastModelUsed);
|
|
223
324
|
}
|
|
224
325
|
}
|
|
326
|
+
if (finalRound) {
|
|
327
|
+
yield* emitRoundAgentDone(finalRound.round, finalRound.model);
|
|
328
|
+
}
|
|
225
329
|
}
|
|
226
330
|
catch (err) {
|
|
227
|
-
|
|
331
|
+
if (!lifecycle.getRequestId()) {
|
|
332
|
+
const startedEvent = lifecycle.begin(randomUUID());
|
|
333
|
+
if (startedEvent) {
|
|
334
|
+
yield startedEvent;
|
|
335
|
+
await emitEvent(startedEvent, agent, lastModelUsed);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const failure = normalizeFailure(err, {
|
|
339
|
+
recoverable: false,
|
|
340
|
+
reason: 'exception',
|
|
341
|
+
...getFailureRetryOverrides(agent),
|
|
342
|
+
});
|
|
228
343
|
turnStatus = 'error';
|
|
229
344
|
turnEndReason = 'exception';
|
|
230
|
-
turnError =
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
345
|
+
turnError = failure.error;
|
|
346
|
+
const errorEvent = toErrorEvent(failure, {
|
|
347
|
+
request_id: lifecycle.getRequestId(),
|
|
348
|
+
});
|
|
349
|
+
yield errorEvent;
|
|
350
|
+
await emitEvent(errorEvent, agent, lastModelUsed);
|
|
351
|
+
const failedEvent = lifecycle.fail(failure, errorRounds || 1, maxErrorAttempts);
|
|
352
|
+
if (failedEvent) {
|
|
353
|
+
yield failedEvent;
|
|
354
|
+
await emitEvent(failedEvent, agent, lastModelUsed);
|
|
355
|
+
}
|
|
239
356
|
}
|
|
240
357
|
finally {
|
|
241
358
|
await trace.emitTurnEnd(turnStatus, turnEndReason, {
|
|
@@ -250,14 +367,29 @@ export async function* ensembleRequest(messages, agent = {}) {
|
|
|
250
367
|
};
|
|
251
368
|
}
|
|
252
369
|
}
|
|
253
|
-
async function* executeRound(
|
|
254
|
-
const
|
|
370
|
+
async function* executeRound(options) {
|
|
371
|
+
const { roundRequestId, model, agent, history, currentToolCalls, maxToolCalls, trace, startedStatusEvent } = options;
|
|
255
372
|
const startTime = Date.now();
|
|
256
373
|
let totalCost = 0;
|
|
257
374
|
let messages = await history.getMessages(model);
|
|
375
|
+
let roundAgentDefinition = agent;
|
|
376
|
+
let requestGuard;
|
|
377
|
+
let toolExecutionGuard;
|
|
378
|
+
let roundAgent = agent;
|
|
379
|
+
let provider;
|
|
380
|
+
let stream;
|
|
381
|
+
const roundSummary = {
|
|
382
|
+
requestId: roundRequestId,
|
|
383
|
+
messages: [],
|
|
384
|
+
errors: [],
|
|
385
|
+
toolCallsStarted: 0,
|
|
386
|
+
hasFollowupToolCalls: false,
|
|
387
|
+
emittedTerminalOutput: false,
|
|
388
|
+
terminalToolSucceeded: false,
|
|
389
|
+
};
|
|
258
390
|
const agentStartEvent = {
|
|
259
391
|
type: 'agent_start',
|
|
260
|
-
request_id:
|
|
392
|
+
request_id: roundRequestId,
|
|
261
393
|
input: 'content' in messages[0] && typeof messages[0].content === 'string' ? messages[0].content : undefined,
|
|
262
394
|
timestamp: new Date().toISOString(),
|
|
263
395
|
agent: {
|
|
@@ -274,199 +406,447 @@ async function* executeRound(model, agent, history, currentToolCalls, maxToolCal
|
|
|
274
406
|
};
|
|
275
407
|
yield agentStartEvent;
|
|
276
408
|
await emitEvent(agentStartEvent, agent, model);
|
|
277
|
-
|
|
278
|
-
|
|
409
|
+
try {
|
|
410
|
+
if (roundAgentDefinition.onRequest) {
|
|
411
|
+
const [nextAgent, nextMessages] = await roundAgentDefinition.onRequest(roundAgentDefinition, messages);
|
|
412
|
+
roundAgentDefinition = nextAgent;
|
|
413
|
+
messages = nextMessages;
|
|
414
|
+
}
|
|
415
|
+
const remainingTimeoutMs = getRemainingRequestTimeoutMs(options.requestTimeoutMs, options.requestStartedAt);
|
|
416
|
+
const needsRequestGuard = Boolean(roundAgentDefinition.abortSignal || remainingTimeoutMs !== undefined);
|
|
417
|
+
if (needsRequestGuard) {
|
|
418
|
+
if (options.requestTimeoutMs !== undefined && remainingTimeoutMs !== undefined && remainingTimeoutMs <= 0) {
|
|
419
|
+
throw createRequestTimeoutError(model, options.requestTimeoutMs);
|
|
420
|
+
}
|
|
421
|
+
requestGuard = createOperationGuard({
|
|
422
|
+
operationName: `Request generation for ${model}`,
|
|
423
|
+
abortSignal: roundAgentDefinition.abortSignal,
|
|
424
|
+
timeoutMs: remainingTimeoutMs,
|
|
425
|
+
});
|
|
426
|
+
roundAgent = {
|
|
427
|
+
...roundAgentDefinition,
|
|
428
|
+
abortSignal: requestGuard.signal,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
roundAgent = roundAgentDefinition;
|
|
433
|
+
}
|
|
434
|
+
await waitWhilePaused(100, roundAgent.abortSignal);
|
|
435
|
+
toolExecutionGuard = createOperationGuard({
|
|
436
|
+
operationName: `Tool execution for ${model}`,
|
|
437
|
+
abortSignal: roundAgent.abortSignal,
|
|
438
|
+
});
|
|
439
|
+
if (startedStatusEvent) {
|
|
440
|
+
yield startedStatusEvent;
|
|
441
|
+
await emitEvent(startedStatusEvent, roundAgent, model);
|
|
442
|
+
}
|
|
443
|
+
provider = getModelProvider(model);
|
|
444
|
+
await trace.emitRequestStart(roundRequestId, {
|
|
445
|
+
agent_id: roundAgent.agent_id,
|
|
446
|
+
provider: provider.provider_id,
|
|
447
|
+
model,
|
|
448
|
+
payload: {
|
|
449
|
+
messages,
|
|
450
|
+
model_settings: roundAgent.modelSettings,
|
|
451
|
+
tool_names: roundAgent.tools?.map(tool => tool.definition.function.name) || [],
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
const rawStream = provider.createResponseStream(messages, model, roundAgent, roundRequestId);
|
|
455
|
+
stream = streamWithAbortAndTimeout(rawStream, {
|
|
456
|
+
abortSignal: requestGuard?.signal,
|
|
457
|
+
});
|
|
279
458
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
459
|
+
catch (error) {
|
|
460
|
+
requestGuard?.cleanup();
|
|
461
|
+
toolExecutionGuard?.cleanup();
|
|
462
|
+
const failure = normalizeFailure(error, {
|
|
463
|
+
reason: 'request_setup_failed',
|
|
464
|
+
...getFailureRetryOverrides(agent),
|
|
465
|
+
});
|
|
466
|
+
roundSummary.failure = failure;
|
|
467
|
+
roundSummary.errors.push(failure.error);
|
|
468
|
+
roundSummary.requestDuration = Date.now() - startTime;
|
|
469
|
+
roundSummary.durationWithTools = roundSummary.requestDuration;
|
|
470
|
+
roundSummary.agentDoneEvent = {
|
|
471
|
+
type: 'agent_done',
|
|
472
|
+
request_id: roundRequestId,
|
|
473
|
+
request_duration: roundSummary.requestDuration,
|
|
474
|
+
duration_with_tools: roundSummary.durationWithTools,
|
|
475
|
+
timestamp: new Date().toISOString(),
|
|
476
|
+
};
|
|
477
|
+
roundSummary.agentDoneAgent = roundAgentDefinition;
|
|
478
|
+
return roundSummary;
|
|
479
|
+
}
|
|
480
|
+
const terminalToolNames = getTerminalToolNames(roundAgent);
|
|
481
|
+
const expectsStructuredOutput = Boolean(roundAgent.modelSettings?.json_schema?.schema);
|
|
482
|
+
const structuredOutputSchema = roundAgent.modelSettings?.json_schema?.strict === true
|
|
483
|
+
? roundAgent.modelSettings.json_schema.schema
|
|
484
|
+
: undefined;
|
|
485
|
+
const toolExecutions = [];
|
|
296
486
|
const toolCallFormattedArgs = new Map();
|
|
297
487
|
const toolEventBuffer = [];
|
|
298
488
|
let sawToolCallThisRound = false;
|
|
299
|
-
|
|
489
|
+
let sawTerminalProviderOutcome = false;
|
|
490
|
+
roundAgent.onToolEvent = async (event) => {
|
|
300
491
|
toolEventBuffer.push(event);
|
|
301
492
|
};
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
493
|
+
const finalizeToolResults = async function* (mode) {
|
|
494
|
+
const waitForPendingExecutions = async (executions, timeoutMs) => {
|
|
495
|
+
if (executions.length === 0) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const completionPromise = Promise.all(executions.map(execution => execution.promise.then(() => undefined)));
|
|
499
|
+
if (timeoutMs === undefined) {
|
|
500
|
+
await completionPromise;
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
await Promise.race([
|
|
504
|
+
completionPromise,
|
|
505
|
+
new Promise(resolve => setTimeout(resolve, timeoutMs)),
|
|
506
|
+
]);
|
|
507
|
+
};
|
|
508
|
+
const waitForAllExecutions = async (executions, abortSignal) => {
|
|
509
|
+
if (executions.length === 0) {
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
const completionPromise = Promise.all(executions.map(execution => execution.promise.then(() => undefined))).then(() => true);
|
|
513
|
+
if (!abortSignal) {
|
|
514
|
+
return completionPromise;
|
|
515
|
+
}
|
|
516
|
+
if (abortSignal.aborted) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
return new Promise(resolve => {
|
|
520
|
+
const abortListener = () => {
|
|
521
|
+
abortSignal.removeEventListener('abort', abortListener);
|
|
522
|
+
resolve(false);
|
|
523
|
+
};
|
|
524
|
+
completionPromise.then(completed => {
|
|
525
|
+
abortSignal.removeEventListener('abort', abortListener);
|
|
526
|
+
resolve(completed);
|
|
527
|
+
});
|
|
528
|
+
abortSignal.addEventListener('abort', abortListener, { once: true });
|
|
529
|
+
});
|
|
530
|
+
};
|
|
531
|
+
let finalizationMode = mode;
|
|
532
|
+
if (finalizationMode === 'wait_all') {
|
|
533
|
+
const completedAllExecutions = await waitForAllExecutions(toolExecutions.filter(execution => !execution.settled), requestGuard?.signal);
|
|
534
|
+
if (!completedAllExecutions) {
|
|
535
|
+
finalizationMode = 'bounded_failure';
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (finalizationMode === 'bounded_failure') {
|
|
539
|
+
toolExecutionGuard?.abort(roundSummary.failure?.error
|
|
540
|
+
? new Error(roundSummary.failure.error)
|
|
541
|
+
: new Error('Request finalized after terminal provider failure.'));
|
|
542
|
+
await waitForPendingExecutions(toolExecutions.filter(execution => !execution.settled), TOOL_FAILURE_FINALIZATION_TIMEOUT_MS);
|
|
543
|
+
for (const execution of toolExecutions) {
|
|
544
|
+
if (!execution.settled) {
|
|
545
|
+
runningToolTracker.abortRunningTool(execution.toolCall.id || execution.toolCall.call_id || '');
|
|
327
546
|
}
|
|
328
|
-
|
|
329
|
-
|
|
547
|
+
}
|
|
548
|
+
await waitForPendingExecutions(toolExecutions.filter(execution => !execution.settled), TOOL_FAILURE_FINALIZATION_TIMEOUT_MS);
|
|
549
|
+
for (const execution of toolExecutions) {
|
|
550
|
+
const runningToolId = execution.toolCall.id || execution.toolCall.call_id || '';
|
|
551
|
+
if (execution.settled) {
|
|
552
|
+
const leakedRunningTool = runningToolId
|
|
553
|
+
? runningToolTracker.getRunningTool(runningToolId)
|
|
554
|
+
: undefined;
|
|
555
|
+
if (leakedRunningTool) {
|
|
556
|
+
const failureResult = execution.result ?? createToolFinalizationFailureResult(execution.toolCall);
|
|
557
|
+
await runningToolTracker.failRunningTool(runningToolId, failureResult.error || 'Tool execution failed during bounded finalization.');
|
|
558
|
+
}
|
|
330
559
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const toolResults = finalizationMode === 'wait_all'
|
|
563
|
+
? await Promise.all(toolExecutions.map(execution => execution.promise))
|
|
564
|
+
: toolExecutions.flatMap(execution => (execution.settled && execution.result ? [execution.result] : []));
|
|
565
|
+
for (const toolResult of toolResults) {
|
|
566
|
+
const toolName = toolResult.toolCall.function.name;
|
|
567
|
+
const isTerminalTool = terminalToolNames.has(toolName);
|
|
568
|
+
const formattedArgs = toolCallFormattedArgs.get(toolResult.toolCall.id);
|
|
569
|
+
const toolCallWithFormattedArgs = formattedArgs
|
|
570
|
+
? {
|
|
571
|
+
...toolResult.toolCall,
|
|
572
|
+
function: {
|
|
573
|
+
...toolResult.toolCall.function,
|
|
574
|
+
arguments_formatted: formattedArgs,
|
|
339
575
|
},
|
|
576
|
+
}
|
|
577
|
+
: toolResult.toolCall;
|
|
578
|
+
const toolDoneEvent = {
|
|
579
|
+
type: 'tool_done',
|
|
580
|
+
request_id: roundRequestId,
|
|
581
|
+
tool_call: toolCallWithFormattedArgs,
|
|
582
|
+
result: {
|
|
583
|
+
call_id: toolResult.call_id || toolResult.id,
|
|
584
|
+
output: toolResult.output,
|
|
585
|
+
error: toolResult.error,
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
if (isTerminalTool && !toolResult.error) {
|
|
589
|
+
roundSummary.terminalToolSucceeded = true;
|
|
590
|
+
}
|
|
591
|
+
yield toolDoneEvent;
|
|
592
|
+
await emitEvent(toolDoneEvent, roundAgent, model);
|
|
593
|
+
await trace.emitToolDone(roundRequestId, toolResult.toolCall.id, {
|
|
594
|
+
tool_name: toolName,
|
|
595
|
+
call_id: toolResult.call_id,
|
|
596
|
+
output: toolResult.output,
|
|
597
|
+
error: toolResult.error,
|
|
598
|
+
});
|
|
599
|
+
if (!isTerminalTool) {
|
|
600
|
+
const functionOutput = convertToFunctionCallOutput(toolResult, model, 'completed');
|
|
601
|
+
history.add(functionOutput);
|
|
602
|
+
yield {
|
|
603
|
+
type: 'response_output',
|
|
604
|
+
message: functionOutput,
|
|
605
|
+
request_id: roundRequestId,
|
|
340
606
|
};
|
|
341
|
-
event = modifiedEvent;
|
|
342
607
|
}
|
|
343
608
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
609
|
+
for (const bufferedEvent of toolEventBuffer) {
|
|
610
|
+
yield { ...bufferedEvent, request_id: roundRequestId };
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
try {
|
|
614
|
+
for await (let event of stream) {
|
|
615
|
+
event = { ...event, request_id: roundRequestId };
|
|
616
|
+
if (event.type === 'error') {
|
|
617
|
+
const failure = normalizeFailure(event, {
|
|
618
|
+
error: event.error,
|
|
619
|
+
recoverable: event.recoverable,
|
|
620
|
+
code: event.code,
|
|
621
|
+
details: event.details,
|
|
622
|
+
...getFailureRetryOverrides(agent),
|
|
623
|
+
});
|
|
624
|
+
roundSummary.failure = selectMoreSevereFailure(roundSummary.failure, failure);
|
|
625
|
+
roundSummary.errors.push(failure.error);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (event.type === 'message_complete' && structuredOutputSchema) {
|
|
629
|
+
const messageEvent = event;
|
|
630
|
+
if (hasTerminalTextContent(messageEvent.content, true)) {
|
|
631
|
+
const validationResult = validateJsonResponseContent(messageEvent.content, structuredOutputSchema);
|
|
632
|
+
if (!validationResult.ok && 'error' in validationResult) {
|
|
633
|
+
const failure = normalizeFailure(new Error(validationResult.error), {
|
|
634
|
+
recoverable: false,
|
|
635
|
+
reason: 'structured_output_validation_failed',
|
|
636
|
+
});
|
|
637
|
+
roundSummary.failure = selectMoreSevereFailure(roundSummary.failure, failure);
|
|
638
|
+
roundSummary.errors.push(failure.error);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
351
641
|
}
|
|
352
|
-
break;
|
|
353
642
|
}
|
|
354
|
-
|
|
643
|
+
if (event.type === 'tool_start') {
|
|
644
|
+
const toolEvent = event;
|
|
645
|
+
if (toolEvent.tool_call) {
|
|
646
|
+
const toolCall = toolEvent.tool_call;
|
|
647
|
+
let argumentsFormatted;
|
|
648
|
+
try {
|
|
649
|
+
const tool = roundAgent.tools?.find(t => t.definition.function.name === toolCall.function.name);
|
|
650
|
+
if (tool?.definition.function.parameters.properties) {
|
|
651
|
+
const parsedArgs = JSON.parse(toolCall.function.arguments || '{}');
|
|
652
|
+
if (typeof parsedArgs === 'object' && parsedArgs !== null && !Array.isArray(parsedArgs)) {
|
|
653
|
+
const paramNames = Object.keys(tool.definition.function.parameters.properties);
|
|
654
|
+
const orderedArgs = {};
|
|
655
|
+
for (const param of paramNames) {
|
|
656
|
+
if (param in parsedArgs) {
|
|
657
|
+
orderedArgs[param] = parsedArgs[param];
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
argumentsFormatted = JSON.stringify(orderedArgs, null, 2);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
console.debug('Failed to format tool arguments:', error);
|
|
666
|
+
}
|
|
667
|
+
if (argumentsFormatted) {
|
|
668
|
+
toolCallFormattedArgs.set(toolCall.id, argumentsFormatted);
|
|
669
|
+
}
|
|
670
|
+
event = {
|
|
671
|
+
...event,
|
|
672
|
+
tool_call: {
|
|
673
|
+
...toolCall,
|
|
674
|
+
function: {
|
|
675
|
+
...toolCall.function,
|
|
676
|
+
arguments_formatted: argumentsFormatted,
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (event.type === 'message_complete') {
|
|
355
683
|
const messageEvent = event;
|
|
356
|
-
if (
|
|
684
|
+
if (hasTerminalTextContent(messageEvent.content, expectsStructuredOutput)) {
|
|
685
|
+
sawTerminalProviderOutcome = true;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
else if (event.type === 'tool_start' || event.type === 'file_complete') {
|
|
689
|
+
sawTerminalProviderOutcome = true;
|
|
690
|
+
}
|
|
691
|
+
yield event;
|
|
692
|
+
await emitEvent(event, roundAgent, model);
|
|
693
|
+
switch (event.type) {
|
|
694
|
+
case 'cost_update': {
|
|
695
|
+
const costEvent = event;
|
|
696
|
+
if (costEvent.usage?.cost) {
|
|
697
|
+
totalCost += costEvent.usage.cost;
|
|
698
|
+
}
|
|
357
699
|
break;
|
|
358
700
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
await agent.onThinking(thinkingMessage);
|
|
701
|
+
case 'message_complete': {
|
|
702
|
+
const messageEvent = event;
|
|
703
|
+
if (sawToolCallThisRound) {
|
|
704
|
+
break;
|
|
364
705
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
706
|
+
if (messageEvent.thinking_content ||
|
|
707
|
+
(!messageEvent.content && messageEvent.message_id)) {
|
|
708
|
+
const thinkingMessage = convertToThinkingMessage(messageEvent, model);
|
|
709
|
+
if (roundAgent.onThinking) {
|
|
710
|
+
await roundAgent.onThinking(thinkingMessage);
|
|
711
|
+
}
|
|
712
|
+
history.add(thinkingMessage);
|
|
713
|
+
yield {
|
|
714
|
+
type: 'response_output',
|
|
715
|
+
message: thinkingMessage,
|
|
716
|
+
request_id: roundRequestId,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
if (hasTerminalTextContent(messageEvent.content, expectsStructuredOutput)) {
|
|
720
|
+
roundSummary.emittedTerminalOutput = true;
|
|
721
|
+
roundSummary.messages.push(messageEvent.content);
|
|
722
|
+
const contentMessage = convertToOutputMessage(messageEvent, model, 'completed');
|
|
723
|
+
if (roundAgent.onResponse) {
|
|
724
|
+
await roundAgent.onResponse(contentMessage);
|
|
725
|
+
}
|
|
726
|
+
history.add(contentMessage);
|
|
727
|
+
yield {
|
|
728
|
+
type: 'response_output',
|
|
729
|
+
message: contentMessage,
|
|
730
|
+
request_id: roundRequestId,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
case 'file_complete': {
|
|
736
|
+
roundSummary.emittedTerminalOutput = true;
|
|
737
|
+
break;
|
|
371
738
|
}
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
if (
|
|
375
|
-
|
|
739
|
+
case 'tool_start': {
|
|
740
|
+
const toolEvent = event;
|
|
741
|
+
if (!toolEvent.tool_call) {
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
if (!sawToolCallThisRound) {
|
|
745
|
+
roundSummary.emittedTerminalOutput = false;
|
|
746
|
+
roundSummary.messages = [];
|
|
376
747
|
}
|
|
377
|
-
|
|
748
|
+
sawToolCallThisRound = true;
|
|
749
|
+
const remainingCalls = maxToolCalls - currentToolCalls - roundSummary.toolCallsStarted;
|
|
750
|
+
if (remainingCalls <= 0) {
|
|
751
|
+
console.warn(`Tool call limit reached (${maxToolCalls}). Skipping tool calls.`);
|
|
752
|
+
const failure = normalizeFailure(new Error(`Tool call limit reached (${maxToolCalls}). Cannot execute tool ${toolEvent.tool_call.function.name}.`), {
|
|
753
|
+
recoverable: false,
|
|
754
|
+
reason: 'max_tool_calls_reached',
|
|
755
|
+
...getFailureRetryOverrides(agent),
|
|
756
|
+
});
|
|
757
|
+
roundSummary.failure = selectMoreSevereFailure(roundSummary.failure, failure);
|
|
758
|
+
if (!roundSummary.errors.includes(failure.error)) {
|
|
759
|
+
roundSummary.errors.push(failure.error);
|
|
760
|
+
}
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
const toolCall = toolEvent.tool_call;
|
|
764
|
+
const functionCall = convertToFunctionCall(toolCall, model, 'completed');
|
|
765
|
+
history.add(functionCall);
|
|
378
766
|
yield {
|
|
379
767
|
type: 'response_output',
|
|
380
|
-
message:
|
|
381
|
-
request_id:
|
|
768
|
+
message: functionCall,
|
|
769
|
+
request_id: roundRequestId,
|
|
382
770
|
};
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
771
|
+
++roundSummary.toolCallsStarted;
|
|
772
|
+
if (!terminalToolNames.has(toolCall.function.name)) {
|
|
773
|
+
roundSummary.hasFollowupToolCalls = true;
|
|
774
|
+
}
|
|
775
|
+
await trace.emitToolStart(roundRequestId, toolCall.id, {
|
|
776
|
+
tool_name: toolCall.function.name,
|
|
777
|
+
arguments: toolCall.function.arguments,
|
|
778
|
+
arguments_formatted: toolCall.function.arguments_formatted,
|
|
779
|
+
});
|
|
780
|
+
const trackedExecution = {
|
|
781
|
+
toolCall,
|
|
782
|
+
promise: processToolCall(toolCall, {
|
|
783
|
+
...roundAgent,
|
|
784
|
+
abortSignal: toolExecutionGuard?.signal ?? roundAgent.abortSignal,
|
|
785
|
+
}),
|
|
786
|
+
settled: false,
|
|
787
|
+
};
|
|
788
|
+
trackedExecution.promise = trackedExecution.promise.then(result => {
|
|
789
|
+
if (!trackedExecution.settled) {
|
|
790
|
+
trackedExecution.settled = true;
|
|
791
|
+
trackedExecution.result = result;
|
|
792
|
+
}
|
|
793
|
+
return trackedExecution.result ?? result;
|
|
794
|
+
});
|
|
795
|
+
toolExecutions.push(trackedExecution);
|
|
395
796
|
break;
|
|
396
797
|
}
|
|
397
|
-
const toolCall = toolEvent.tool_call;
|
|
398
|
-
const functionCall = convertToFunctionCall(toolCall, model, 'completed');
|
|
399
|
-
toolPromises.push(processToolCall(toolCall, agent));
|
|
400
|
-
history.add(functionCall);
|
|
401
|
-
yield {
|
|
402
|
-
type: 'response_output',
|
|
403
|
-
message: functionCall,
|
|
404
|
-
request_id: requestId,
|
|
405
|
-
};
|
|
406
|
-
break;
|
|
407
|
-
}
|
|
408
|
-
case 'error': {
|
|
409
|
-
console.error('[executeRound] Error event:', truncateLargeValues(event.error));
|
|
410
|
-
break;
|
|
411
798
|
}
|
|
412
799
|
}
|
|
413
800
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (!isTerminalTool) {
|
|
443
|
-
const functionOutput = convertToFunctionCallOutput(toolResult, model, 'completed');
|
|
444
|
-
history.add(functionOutput);
|
|
445
|
-
yield {
|
|
446
|
-
type: 'response_output',
|
|
447
|
-
message: functionOutput,
|
|
448
|
-
request_id: requestId,
|
|
449
|
-
};
|
|
801
|
+
catch (error) {
|
|
802
|
+
const streamFailure = normalizeFailure(error, {
|
|
803
|
+
reason: 'request_stream_failed',
|
|
804
|
+
...getFailureRetryOverrides(agent),
|
|
805
|
+
});
|
|
806
|
+
roundSummary.failure = selectMoreSevereFailure(roundSummary.failure, streamFailure);
|
|
807
|
+
roundSummary.errors.push(streamFailure.error);
|
|
808
|
+
}
|
|
809
|
+
if (!sawTerminalProviderOutcome && !roundSummary.failure) {
|
|
810
|
+
const emptyResponseFailure = normalizeFailure(new Error(`Provider ${provider.provider_id} ended the stream without any terminal content, tool calls, files, or errors.`), {
|
|
811
|
+
recoverable: false,
|
|
812
|
+
reason: 'empty_provider_response',
|
|
813
|
+
...getFailureRetryOverrides(agent),
|
|
814
|
+
});
|
|
815
|
+
roundSummary.failure = emptyResponseFailure;
|
|
816
|
+
roundSummary.errors.push(emptyResponseFailure.error);
|
|
817
|
+
}
|
|
818
|
+
roundSummary.requestDuration = Date.now() - startTime;
|
|
819
|
+
const shouldUseBoundedFailureFinalization = Boolean(roundSummary.failure?.terminal);
|
|
820
|
+
yield* finalizeToolResults(shouldUseBoundedFailureFinalization ? 'bounded_failure' : 'wait_all');
|
|
821
|
+
if (requestGuard?.signal.aborted) {
|
|
822
|
+
const abortFailure = normalizeFailure(requestGuard.signal.reason, {
|
|
823
|
+
reason: 'request_stream_failed',
|
|
824
|
+
...getFailureRetryOverrides(agent),
|
|
825
|
+
});
|
|
826
|
+
roundSummary.failure = selectMoreSevereFailure(roundSummary.failure, abortFailure);
|
|
827
|
+
if (!roundSummary.errors.includes(abortFailure.error)) {
|
|
828
|
+
roundSummary.errors.push(abortFailure.error);
|
|
450
829
|
}
|
|
451
830
|
}
|
|
452
|
-
|
|
453
|
-
|
|
831
|
+
roundSummary.durationWithTools = Date.now() - startTime;
|
|
832
|
+
roundSummary.requestCost = totalCost > 0 ? totalCost : undefined;
|
|
833
|
+
roundSummary.agentDoneEvent = {
|
|
454
834
|
type: 'agent_done',
|
|
455
|
-
request_id:
|
|
456
|
-
request_cost:
|
|
457
|
-
request_duration,
|
|
458
|
-
duration_with_tools,
|
|
835
|
+
request_id: roundRequestId,
|
|
836
|
+
request_cost: roundSummary.requestCost,
|
|
837
|
+
request_duration: roundSummary.requestDuration,
|
|
838
|
+
duration_with_tools: roundSummary.durationWithTools,
|
|
459
839
|
timestamp: new Date().toISOString(),
|
|
460
840
|
};
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
841
|
+
roundSummary.agentDoneAgent = roundAgent;
|
|
842
|
+
requestGuard?.cleanup();
|
|
843
|
+
toolExecutionGuard?.cleanup();
|
|
844
|
+
return roundSummary;
|
|
466
845
|
}
|
|
467
846
|
async function* performVerification(agent, output, messages, attempt = 0) {
|
|
468
|
-
if (!agent.verifier)
|
|
469
|
-
return;
|
|
847
|
+
if (!agent.verifier) {
|
|
848
|
+
return { passed: true };
|
|
849
|
+
}
|
|
470
850
|
const maxAttempts = agent.maxVerificationAttempts || 2;
|
|
471
851
|
const verification = await verifyOutput(agent.verifier, output, messages);
|
|
472
852
|
if (verification.status === 'pass') {
|
|
@@ -474,7 +854,7 @@ async function* performVerification(agent, output, messages, attempt = 0) {
|
|
|
474
854
|
type: 'message_delta',
|
|
475
855
|
content: '\n\n✓ Output verified',
|
|
476
856
|
};
|
|
477
|
-
return;
|
|
857
|
+
return { passed: true };
|
|
478
858
|
}
|
|
479
859
|
if (attempt < maxAttempts - 1) {
|
|
480
860
|
yield {
|
|
@@ -503,27 +883,37 @@ async function* performVerification(agent, output, messages, attempt = 0) {
|
|
|
503
883
|
const retryStream = ensembleRequest(retryMessages, retryAgent);
|
|
504
884
|
let retryOutput = '';
|
|
505
885
|
for await (const event of retryStream) {
|
|
886
|
+
if (event.type === 'operation_status' || event.type === 'error') {
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
506
889
|
yield event;
|
|
507
890
|
if (event.type === 'message_complete' && 'content' in event) {
|
|
508
891
|
retryOutput = event.content;
|
|
509
892
|
}
|
|
510
893
|
}
|
|
511
894
|
if (retryOutput) {
|
|
512
|
-
yield* performVerification(agent, retryOutput, messages, attempt + 1);
|
|
895
|
+
return yield* performVerification(agent, retryOutput, messages, attempt + 1);
|
|
513
896
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
type: 'message_delta',
|
|
518
|
-
content: `\n\n❌ Verification failed after ${maxAttempts} attempts: ${verification.reason}`,
|
|
897
|
+
return {
|
|
898
|
+
passed: false,
|
|
899
|
+
error: 'Verification retry did not produce a final response.',
|
|
519
900
|
};
|
|
520
901
|
}
|
|
902
|
+
const failureMessage = `Verification failed after ${maxAttempts} attempts: ${verification.reason}`;
|
|
903
|
+
yield {
|
|
904
|
+
type: 'message_delta',
|
|
905
|
+
content: `\n\n❌ ${failureMessage}`,
|
|
906
|
+
};
|
|
907
|
+
return {
|
|
908
|
+
passed: false,
|
|
909
|
+
error: failureMessage,
|
|
910
|
+
};
|
|
521
911
|
}
|
|
522
912
|
async function processToolCall(toolCall, agent) {
|
|
523
|
-
if (agent.onToolCall) {
|
|
524
|
-
await agent.onToolCall(toolCall);
|
|
525
|
-
}
|
|
526
913
|
try {
|
|
914
|
+
if (agent.onToolCall) {
|
|
915
|
+
await agent.onToolCall(toolCall);
|
|
916
|
+
}
|
|
527
917
|
if (!agent.tools) {
|
|
528
918
|
throw new Error('No tools available for agent');
|
|
529
919
|
}
|
|
@@ -531,7 +921,7 @@ async function processToolCall(toolCall, agent) {
|
|
|
531
921
|
if (!tool || !('function' in tool)) {
|
|
532
922
|
throw new Error(`Tool ${toolCall.function.name} not found`);
|
|
533
923
|
}
|
|
534
|
-
const rawResult = await handleToolCall(toolCall, tool, agent);
|
|
924
|
+
const rawResult = await handleToolCall(toolCall, tool, agent, agent.abortSignal);
|
|
535
925
|
const processedResult = await processToolResult(toolCall, rawResult, agent, tool.allowSummary);
|
|
536
926
|
const toolCallResult = {
|
|
537
927
|
toolCall,
|
|
@@ -545,21 +935,32 @@ async function processToolCall(toolCall, agent) {
|
|
|
545
935
|
return toolCallResult;
|
|
546
936
|
}
|
|
547
937
|
catch (error) {
|
|
548
|
-
const
|
|
549
|
-
? `Tool execution failed: ${error.message}`
|
|
550
|
-
: `Tool execution failed: ${String(error)}`;
|
|
551
|
-
const toolCallResult = {
|
|
552
|
-
toolCall,
|
|
553
|
-
id: toolCall.id,
|
|
554
|
-
call_id: toolCall.call_id || toolCall.id,
|
|
555
|
-
error: errorOutput,
|
|
556
|
-
};
|
|
938
|
+
const toolCallResult = createToolFailureResult(toolCall, error);
|
|
557
939
|
if (agent.onToolError) {
|
|
558
|
-
|
|
940
|
+
try {
|
|
941
|
+
await agent.onToolError(toolCallResult);
|
|
942
|
+
}
|
|
943
|
+
catch (hookError) {
|
|
944
|
+
console.error('[processToolCall] onToolError hook failed:', hookError);
|
|
945
|
+
}
|
|
559
946
|
}
|
|
560
947
|
return toolCallResult;
|
|
561
948
|
}
|
|
562
949
|
}
|
|
950
|
+
function createToolFailureResult(toolCall, error) {
|
|
951
|
+
const errorOutput = error instanceof Error
|
|
952
|
+
? `Tool execution failed: ${error.message}`
|
|
953
|
+
: `Tool execution failed: ${String(error)}`;
|
|
954
|
+
return {
|
|
955
|
+
toolCall,
|
|
956
|
+
id: toolCall.id,
|
|
957
|
+
call_id: toolCall.call_id || toolCall.id,
|
|
958
|
+
error: errorOutput,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function createToolFinalizationFailureResult(toolCall) {
|
|
962
|
+
return createToolFailureResult(toolCall, 'Tool did not finish before request finalization after a terminal provider failure.');
|
|
963
|
+
}
|
|
563
964
|
export function mergeHistoryThread(mainHistory, thread, startIndex) {
|
|
564
965
|
const newMessages = thread.slice(startIndex);
|
|
565
966
|
mainHistory.push(...newMessages);
|