@just-every/ensemble 0.2.212 → 0.2.214

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +152 -91
  2. package/dist/cjs/core/ensemble_request.cjs +734 -333
  3. package/dist/cjs/core/ensemble_request.d.ts.map +1 -1
  4. package/dist/cjs/core/ensemble_request.js.map +1 -1
  5. package/dist/cjs/data/model_data.cjs +28 -1
  6. package/dist/cjs/data/model_data.d.ts.map +1 -1
  7. package/dist/cjs/data/model_data.js.map +1 -1
  8. package/dist/cjs/model_providers/base_provider.d.ts.map +1 -1
  9. package/dist/cjs/model_providers/base_provider.js.map +1 -1
  10. package/dist/cjs/model_providers/claude.cjs +72 -72
  11. package/dist/cjs/model_providers/claude.d.ts.map +1 -1
  12. package/dist/cjs/model_providers/claude.js.map +1 -1
  13. package/dist/cjs/model_providers/gemini.cjs +3 -0
  14. package/dist/cjs/model_providers/gemini.d.ts.map +1 -1
  15. package/dist/cjs/model_providers/gemini.js.map +1 -1
  16. package/dist/cjs/model_providers/openai.cjs +72 -168
  17. package/dist/cjs/model_providers/openai.d.ts.map +1 -1
  18. package/dist/cjs/model_providers/openai.js.map +1 -1
  19. package/dist/cjs/model_providers/openai_chat.cjs +55 -24
  20. package/dist/cjs/model_providers/openai_chat.d.ts.map +1 -1
  21. package/dist/cjs/model_providers/openai_chat.js.map +1 -1
  22. package/dist/cjs/model_providers/openai_image_pricing.cjs +184 -0
  23. package/dist/cjs/model_providers/openai_image_pricing.d.ts +19 -0
  24. package/dist/cjs/model_providers/openai_image_pricing.d.ts.map +1 -0
  25. package/dist/cjs/model_providers/openai_image_pricing.js.map +1 -0
  26. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  27. package/dist/cjs/types/types.d.ts +22 -4
  28. package/dist/cjs/types/types.d.ts.map +1 -1
  29. package/dist/cjs/utils/agent.cjs +4 -6
  30. package/dist/cjs/utils/agent.d.ts.map +1 -1
  31. package/dist/cjs/utils/agent.js.map +1 -1
  32. package/dist/cjs/utils/cost_tracker.cjs +15 -10
  33. package/dist/cjs/utils/cost_tracker.d.ts.map +1 -1
  34. package/dist/cjs/utils/cost_tracker.js.map +1 -1
  35. package/dist/cjs/utils/ensemble_result.cjs +43 -4
  36. package/dist/cjs/utils/ensemble_result.d.ts +10 -1
  37. package/dist/cjs/utils/ensemble_result.d.ts.map +1 -1
  38. package/dist/cjs/utils/ensemble_result.js.map +1 -1
  39. package/dist/cjs/utils/failure_detection.cjs +292 -0
  40. package/dist/cjs/utils/failure_detection.d.ts +51 -0
  41. package/dist/cjs/utils/failure_detection.d.ts.map +1 -0
  42. package/dist/cjs/utils/failure_detection.js.map +1 -0
  43. package/dist/cjs/utils/json_schema.cjs +490 -0
  44. package/dist/cjs/utils/json_schema.d.ts +10 -0
  45. package/dist/cjs/utils/json_schema.d.ts.map +1 -0
  46. package/dist/cjs/utils/json_schema.js.map +1 -0
  47. package/dist/cjs/utils/tool_execution_manager.cjs +28 -4
  48. package/dist/cjs/utils/tool_execution_manager.d.ts +1 -1
  49. package/dist/cjs/utils/tool_execution_manager.d.ts.map +1 -1
  50. package/dist/cjs/utils/tool_execution_manager.js.map +1 -1
  51. package/dist/cjs/utils/verification.cjs +26 -13
  52. package/dist/cjs/utils/verification.d.ts.map +1 -1
  53. package/dist/cjs/utils/verification.js.map +1 -1
  54. package/dist/core/ensemble_request.d.ts.map +1 -1
  55. package/dist/core/ensemble_request.js +734 -333
  56. package/dist/core/ensemble_request.js.map +1 -1
  57. package/dist/data/model_data.d.ts.map +1 -1
  58. package/dist/data/model_data.js +28 -1
  59. package/dist/data/model_data.js.map +1 -1
  60. package/dist/model_providers/base_provider.d.ts.map +1 -1
  61. package/dist/model_providers/base_provider.js.map +1 -1
  62. package/dist/model_providers/claude.d.ts.map +1 -1
  63. package/dist/model_providers/claude.js +72 -72
  64. package/dist/model_providers/claude.js.map +1 -1
  65. package/dist/model_providers/gemini.d.ts.map +1 -1
  66. package/dist/model_providers/gemini.js +3 -0
  67. package/dist/model_providers/gemini.js.map +1 -1
  68. package/dist/model_providers/openai.d.ts.map +1 -1
  69. package/dist/model_providers/openai.js +72 -168
  70. package/dist/model_providers/openai.js.map +1 -1
  71. package/dist/model_providers/openai_chat.d.ts.map +1 -1
  72. package/dist/model_providers/openai_chat.js +55 -24
  73. package/dist/model_providers/openai_chat.js.map +1 -1
  74. package/dist/model_providers/openai_image_pricing.d.ts +19 -0
  75. package/dist/model_providers/openai_image_pricing.d.ts.map +1 -0
  76. package/dist/model_providers/openai_image_pricing.js +176 -0
  77. package/dist/model_providers/openai_image_pricing.js.map +1 -0
  78. package/dist/tsconfig.tsbuildinfo +1 -1
  79. package/dist/types/types.d.ts +22 -4
  80. package/dist/types/types.d.ts.map +1 -1
  81. package/dist/utils/agent.d.ts.map +1 -1
  82. package/dist/utils/agent.js +4 -6
  83. package/dist/utils/agent.js.map +1 -1
  84. package/dist/utils/cost_tracker.d.ts.map +1 -1
  85. package/dist/utils/cost_tracker.js +15 -10
  86. package/dist/utils/cost_tracker.js.map +1 -1
  87. package/dist/utils/ensemble_result.d.ts +10 -1
  88. package/dist/utils/ensemble_result.d.ts.map +1 -1
  89. package/dist/utils/ensemble_result.js +43 -4
  90. package/dist/utils/ensemble_result.js.map +1 -1
  91. package/dist/utils/failure_detection.d.ts +51 -0
  92. package/dist/utils/failure_detection.d.ts.map +1 -0
  93. package/dist/utils/failure_detection.js +280 -0
  94. package/dist/utils/failure_detection.js.map +1 -0
  95. package/dist/utils/json_schema.d.ts +10 -0
  96. package/dist/utils/json_schema.d.ts.map +1 -0
  97. package/dist/utils/json_schema.js +486 -0
  98. package/dist/utils/json_schema.js.map +1 -0
  99. package/dist/utils/tool_execution_manager.d.ts +1 -1
  100. package/dist/utils/tool_execution_manager.d.ts.map +1 -1
  101. package/dist/utils/tool_execution_manager.js +28 -4
  102. package/dist/utils/tool_execution_manager.js.map +1 -1
  103. package/dist/utils/verification.d.ts.map +1 -1
  104. package/dist/utils/verification.js +26 -13
  105. package/dist/utils/verification.js.map +1 -1
  106. 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 { truncateLargeValues } from '../utils/truncate_utils.js';
13
- const MAX_ERROR_ATTEMPTS = 5;
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
- const maxToolCalls = agent?.maxToolCalls ?? 200;
65
- const maxRounds = agent?.maxToolCallRoundsPerTurn ?? Infinity;
66
- let hasToolCalls = false;
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
- do {
75
- hasToolCalls = false;
76
- hasError = false;
77
- let terminalToolSucceededThisRound = false;
78
- let currentRoundRequestId;
79
- const currentRoundMessages = [];
80
- const currentRoundErrors = [];
81
- let currentRoundToolCalls = 0;
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
- const stream = executeRound(model, agent, history, totalToolCalls, maxToolCalls, trace);
89
- try {
90
- for await (const event of stream) {
91
- yield event;
92
- switch (event.type) {
93
- case 'agent_start': {
94
- currentRoundRequestId = event.request_id;
95
- break;
96
- }
97
- case 'message_complete': {
98
- const messageEvent = event;
99
- if (messageEvent.content) {
100
- lastMessageContent = messageEvent.content;
101
- currentRoundMessages.push(messageEvent.content);
102
- }
103
- break;
104
- }
105
- case 'tool_start': {
106
- const toolEvent = event;
107
- if (toolEvent.tool_call) {
108
- const toolName = toolEvent.tool_call.function.name;
109
- currentRoundToolCalls += 1;
110
- await trace.emitToolStart(event.request_id, toolEvent.tool_call.id, {
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
- catch (roundError) {
157
- hasError = true;
158
- const errorMessage = roundError instanceof Error ? roundError.message : String(roundError);
159
- currentRoundErrors.push(errorMessage);
160
- yield {
161
- type: 'error',
162
- request_id: currentRoundRequestId,
163
- error: errorMessage,
164
- recoverable: true,
165
- timestamp: new Date().toISOString(),
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 (terminalToolSucceededThisRound) {
169
- hasToolCalls = false;
170
- hasError = false;
259
+ if (round.terminalToolSucceeded) {
260
+ finalRound = { round, model };
261
+ break;
171
262
  }
172
- if (hasToolCalls) {
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 (hasError) {
179
- ++errorRounds;
180
- }
181
- const willRetryForError = hasError && errorRounds < MAX_ERROR_ATTEMPTS;
182
- const willContinueForTools = hasToolCalls && toolCallRounds < maxRounds && totalToolCalls < maxToolCalls;
183
- const willContinue = willRetryForError || willContinueForTools;
184
- let requestStatus = 'completed';
185
- if (hasError) {
186
- requestStatus = willContinue ? 'error_retrying' : 'error';
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
- } while ((hasError && errorRounds < MAX_ERROR_ATTEMPTS) ||
204
- (hasToolCalls && toolCallRounds < maxRounds && totalToolCalls < maxToolCalls));
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
- else if (hasToolCalls && totalToolCalls >= maxToolCalls) {
210
- console.log('[ensembleRequest] Total tool calls limit reached');
211
- turnEndReason = 'max_tool_calls_reached';
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
- else if (hasError && errorRounds >= MAX_ERROR_ATTEMPTS) {
302
+ if (terminalFailure) {
214
303
  turnStatus = 'error';
215
- turnEndReason = 'max_error_attempts_reached';
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
- if (agent?.verifier && lastMessageContent) {
218
- const verificationResult = await performVerification(agent, lastMessageContent, await history.getMessages());
219
- if (verificationResult) {
220
- for await (const event of verificationResult) {
221
- yield event;
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
- const error = err;
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 = error.message || 'Unknown error';
231
- yield {
232
- type: 'error',
233
- error: error.message || 'Unknown error',
234
- code: error.code,
235
- details: error.details,
236
- recoverable: error.recoverable,
237
- timestamp: new Date().toISOString(),
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(model, agent, history, currentToolCalls, maxToolCalls, trace) {
254
- const requestId = randomUUID();
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: requestId,
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
- if (agent.onRequest) {
278
- [agent, messages] = await agent.onRequest(agent, messages);
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
- await waitWhilePaused(100, agent.abortSignal);
281
- const provider = getModelProvider(model);
282
- await trace.emitRequestStart(requestId, {
283
- agent_id: agent.agent_id,
284
- provider: provider.provider_id,
285
- model,
286
- payload: {
287
- messages,
288
- model_settings: agent.modelSettings,
289
- tool_names: agent.tools?.map(tool => tool.definition.function.name) || [],
290
- },
291
- });
292
- const stream = 'createResponseStreamWithRetry' in provider
293
- ? provider.createResponseStreamWithRetry(messages, model, agent, requestId)
294
- : provider.createResponseStream(messages, model, agent, requestId);
295
- const toolPromises = [];
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
- agent.onToolEvent = async (event) => {
489
+ let sawTerminalProviderOutcome = false;
490
+ roundAgent.onToolEvent = async (event) => {
300
491
  toolEventBuffer.push(event);
301
492
  };
302
- for await (let event of stream) {
303
- event = { ...event, request_id: requestId };
304
- if (event.type === 'tool_start') {
305
- const toolEvent = event;
306
- if (toolEvent.tool_call) {
307
- const toolCall = toolEvent.tool_call;
308
- let argumentsFormatted;
309
- try {
310
- const tool = agent.tools?.find(t => t.definition.function.name === toolCall.function.name);
311
- if (tool && 'definition' in tool && tool.definition.function.parameters.properties) {
312
- const parsedArgs = JSON.parse(toolCall.function.arguments || '{}');
313
- if (typeof parsedArgs === 'object' && parsedArgs !== null && !Array.isArray(parsedArgs)) {
314
- const paramNames = Object.keys(tool.definition.function.parameters.properties);
315
- const orderedArgs = {};
316
- for (const param of paramNames) {
317
- if (param in parsedArgs) {
318
- orderedArgs[param] = parsedArgs[param];
319
- }
320
- }
321
- argumentsFormatted = JSON.stringify(orderedArgs, null, 2);
322
- }
323
- }
324
- }
325
- catch (error) {
326
- console.debug('Failed to format tool arguments:', error);
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
- if (argumentsFormatted) {
329
- toolCallFormattedArgs.set(toolCall.id, argumentsFormatted);
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
- const modifiedEvent = {
332
- ...event,
333
- tool_call: {
334
- ...toolCall,
335
- function: {
336
- ...toolCall.function,
337
- arguments_formatted: argumentsFormatted,
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
- yield event;
345
- await emitEvent(event, agent, model);
346
- switch (event.type) {
347
- case 'cost_update': {
348
- const costEvent = event;
349
- if (costEvent.usage?.cost) {
350
- totalCost += costEvent.usage.cost;
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
- case 'message_complete': {
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 (sawToolCallThisRound) {
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
- if (messageEvent.thinking_content ||
360
- (!messageEvent.content && messageEvent.message_id)) {
361
- const thinkingMessage = convertToThinkingMessage(messageEvent, model);
362
- if (agent.onThinking) {
363
- await agent.onThinking(thinkingMessage);
701
+ case 'message_complete': {
702
+ const messageEvent = event;
703
+ if (sawToolCallThisRound) {
704
+ break;
364
705
  }
365
- history.add(thinkingMessage);
366
- yield {
367
- type: 'response_output',
368
- message: thinkingMessage,
369
- request_id: requestId,
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
- if (messageEvent.content) {
373
- const contentMessage = convertToOutputMessage(messageEvent, model, 'completed');
374
- if (agent.onResponse) {
375
- await agent.onResponse(contentMessage);
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
- history.add(contentMessage);
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: contentMessage,
381
- request_id: requestId,
768
+ message: functionCall,
769
+ request_id: roundRequestId,
382
770
  };
383
- }
384
- break;
385
- }
386
- case 'tool_start': {
387
- const toolEvent = event;
388
- if (!toolEvent.tool_call) {
389
- break;
390
- }
391
- sawToolCallThisRound = true;
392
- const remainingCalls = maxToolCalls - currentToolCalls;
393
- if (remainingCalls <= 0) {
394
- console.warn(`Tool call limit reached (${maxToolCalls}). Skipping tool calls.`);
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
- const request_duration = Date.now() - startTime;
415
- const toolResults = await Promise.all(toolPromises);
416
- const terminalToolNames = getTerminalToolNames(agent);
417
- for (const toolResult of toolResults) {
418
- const toolName = toolResult.toolCall.function.name;
419
- const isTerminalTool = terminalToolNames.has(toolName);
420
- const formattedArgs = toolCallFormattedArgs.get(toolResult.toolCall.id);
421
- const toolCallWithFormattedArgs = formattedArgs
422
- ? {
423
- ...toolResult.toolCall,
424
- function: {
425
- ...toolResult.toolCall.function,
426
- arguments_formatted: formattedArgs,
427
- },
428
- }
429
- : toolResult.toolCall;
430
- const toolDoneEvent = {
431
- type: 'tool_done',
432
- request_id: requestId,
433
- tool_call: toolCallWithFormattedArgs,
434
- result: {
435
- call_id: toolResult.call_id || toolResult.id,
436
- output: toolResult.output,
437
- error: toolResult.error,
438
- },
439
- };
440
- yield toolDoneEvent;
441
- await emitEvent(toolDoneEvent, agent, model);
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
- const duration_with_tools = Date.now() - startTime;
453
- const agentDoneEvent = {
831
+ roundSummary.durationWithTools = Date.now() - startTime;
832
+ roundSummary.requestCost = totalCost > 0 ? totalCost : undefined;
833
+ roundSummary.agentDoneEvent = {
454
834
  type: 'agent_done',
455
- request_id: requestId,
456
- request_cost: totalCost > 0 ? totalCost : undefined,
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
- yield agentDoneEvent;
462
- await emitEvent(agentDoneEvent, agent, model);
463
- for (const bufferedEvent of toolEventBuffer) {
464
- yield { ...bufferedEvent, request_id: requestId };
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
- else {
516
- yield {
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 errorOutput = error instanceof Error
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
- await agent.onToolError(toolCallResult);
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);