@openrouter/agent 0.1.0

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 (98) hide show
  1. package/README.md +367 -0
  2. package/esm/api-shape-helpers/claude-message.d.ts +218 -0
  3. package/esm/api-shape-helpers/claude-message.d.ts.map +1 -0
  4. package/esm/api-shape-helpers/claude-message.js +6 -0
  5. package/esm/api-shape-helpers/claude-message.js.map +1 -0
  6. package/esm/index.d.ts +22 -0
  7. package/esm/index.d.ts.map +1 -0
  8. package/esm/index.js +27 -0
  9. package/esm/index.js.map +1 -0
  10. package/esm/inner-loop/call-model.d.ts +67 -0
  11. package/esm/inner-loop/call-model.d.ts.map +1 -0
  12. package/esm/inner-loop/call-model.js +116 -0
  13. package/esm/inner-loop/call-model.js.map +1 -0
  14. package/esm/lib/anthropic-compat.d.ts +51 -0
  15. package/esm/lib/anthropic-compat.d.ts.map +1 -0
  16. package/esm/lib/anthropic-compat.js +216 -0
  17. package/esm/lib/anthropic-compat.js.map +1 -0
  18. package/esm/lib/anthropic-compat.test.d.ts +2 -0
  19. package/esm/lib/anthropic-compat.test.d.ts.map +1 -0
  20. package/esm/lib/anthropic-compat.test.js +668 -0
  21. package/esm/lib/anthropic-compat.test.js.map +1 -0
  22. package/esm/lib/async-params.d.ts +107 -0
  23. package/esm/lib/async-params.d.ts.map +1 -0
  24. package/esm/lib/async-params.js +94 -0
  25. package/esm/lib/async-params.js.map +1 -0
  26. package/esm/lib/chat-compat.d.ts +46 -0
  27. package/esm/lib/chat-compat.d.ts.map +1 -0
  28. package/esm/lib/chat-compat.js +111 -0
  29. package/esm/lib/chat-compat.js.map +1 -0
  30. package/esm/lib/chat-compat.test.d.ts +2 -0
  31. package/esm/lib/chat-compat.test.d.ts.map +1 -0
  32. package/esm/lib/chat-compat.test.js +405 -0
  33. package/esm/lib/chat-compat.test.js.map +1 -0
  34. package/esm/lib/claude-constants.d.ts +22 -0
  35. package/esm/lib/claude-constants.d.ts.map +1 -0
  36. package/esm/lib/claude-constants.js +20 -0
  37. package/esm/lib/claude-constants.js.map +1 -0
  38. package/esm/lib/claude-type-guards.d.ts +10 -0
  39. package/esm/lib/claude-type-guards.d.ts.map +1 -0
  40. package/esm/lib/claude-type-guards.js +68 -0
  41. package/esm/lib/claude-type-guards.js.map +1 -0
  42. package/esm/lib/conversation-state.d.ts +61 -0
  43. package/esm/lib/conversation-state.d.ts.map +1 -0
  44. package/esm/lib/conversation-state.js +230 -0
  45. package/esm/lib/conversation-state.js.map +1 -0
  46. package/esm/lib/model-result.d.ts +370 -0
  47. package/esm/lib/model-result.d.ts.map +1 -0
  48. package/esm/lib/model-result.js +1483 -0
  49. package/esm/lib/model-result.js.map +1 -0
  50. package/esm/lib/next-turn-params.d.ts +30 -0
  51. package/esm/lib/next-turn-params.d.ts.map +1 -0
  52. package/esm/lib/next-turn-params.js +129 -0
  53. package/esm/lib/next-turn-params.js.map +1 -0
  54. package/esm/lib/reusable-stream.d.ts +39 -0
  55. package/esm/lib/reusable-stream.d.ts.map +1 -0
  56. package/esm/lib/reusable-stream.js +192 -0
  57. package/esm/lib/reusable-stream.js.map +1 -0
  58. package/esm/lib/stop-conditions.d.ts +80 -0
  59. package/esm/lib/stop-conditions.d.ts.map +1 -0
  60. package/esm/lib/stop-conditions.js +104 -0
  61. package/esm/lib/stop-conditions.js.map +1 -0
  62. package/esm/lib/stream-transformers.d.ts +109 -0
  63. package/esm/lib/stream-transformers.d.ts.map +1 -0
  64. package/esm/lib/stream-transformers.js +856 -0
  65. package/esm/lib/stream-transformers.js.map +1 -0
  66. package/esm/lib/stream-type-guards.d.ts +29 -0
  67. package/esm/lib/stream-type-guards.d.ts.map +1 -0
  68. package/esm/lib/stream-type-guards.js +85 -0
  69. package/esm/lib/stream-type-guards.js.map +1 -0
  70. package/esm/lib/tool-context.d.ts +68 -0
  71. package/esm/lib/tool-context.d.ts.map +1 -0
  72. package/esm/lib/tool-context.js +188 -0
  73. package/esm/lib/tool-context.js.map +1 -0
  74. package/esm/lib/tool-event-broadcaster.d.ts +44 -0
  75. package/esm/lib/tool-event-broadcaster.d.ts.map +1 -0
  76. package/esm/lib/tool-event-broadcaster.js +162 -0
  77. package/esm/lib/tool-event-broadcaster.js.map +1 -0
  78. package/esm/lib/tool-executor.d.ts +73 -0
  79. package/esm/lib/tool-executor.d.ts.map +1 -0
  80. package/esm/lib/tool-executor.js +267 -0
  81. package/esm/lib/tool-executor.js.map +1 -0
  82. package/esm/lib/tool-orchestrator.d.ts +50 -0
  83. package/esm/lib/tool-orchestrator.d.ts.map +1 -0
  84. package/esm/lib/tool-orchestrator.js +180 -0
  85. package/esm/lib/tool-orchestrator.js.map +1 -0
  86. package/esm/lib/tool-types.d.ts +572 -0
  87. package/esm/lib/tool-types.d.ts.map +1 -0
  88. package/esm/lib/tool-types.js +80 -0
  89. package/esm/lib/tool-types.js.map +1 -0
  90. package/esm/lib/tool.d.ts +108 -0
  91. package/esm/lib/tool.d.ts.map +1 -0
  92. package/esm/lib/tool.js +84 -0
  93. package/esm/lib/tool.js.map +1 -0
  94. package/esm/lib/turn-context.d.ts +50 -0
  95. package/esm/lib/turn-context.d.ts.map +1 -0
  96. package/esm/lib/turn-context.js +61 -0
  97. package/esm/lib/turn-context.js.map +1 -0
  98. package/package.json +125 -0
@@ -0,0 +1,1483 @@
1
+ import { betaResponsesSend } from '@openrouter/sdk/funcs/betaResponsesSend';
2
+ import { hasAsyncFunctions, resolveAsyncFunctions, } from './async-params.js';
3
+ import { appendToMessages, createInitialState, createRejectedResult, createUnsentResult, extractTextFromResponse as extractTextFromResponseState, partitionToolCalls, unsentResultsToAPIFormat, updateState, } from './conversation-state.js';
4
+ import { applyNextTurnParamsToRequest, executeNextTurnParamsFunctions, } from './next-turn-params.js';
5
+ import { ReusableReadableStream } from './reusable-stream.js';
6
+ import { isStopConditionMet, stepCountIs } from './stop-conditions.js';
7
+ import { buildItemsStream, buildResponsesMessageStream, buildToolCallStream, consumeStreamForCompletion, extractReasoningDeltas, extractResponsesMessageFromResponse, extractTextDeltas, extractTextFromResponse, extractToolCallsFromResponse, extractToolDeltas, itemsStreamHandlers, streamTerminationEvents, } from './stream-transformers.js';
8
+ import { hasTypeProperty, isFunctionCallItem, isOutputTextDeltaEvent, isReasoningDeltaEvent, isResponseCompletedEvent, isResponseFailedEvent, isResponseIncompleteEvent, } from './stream-type-guards.js';
9
+ import { resolveContext, ToolContextStore } from './tool-context.js';
10
+ import { ToolEventBroadcaster } from './tool-event-broadcaster.js';
11
+ import { executeTool } from './tool-executor.js';
12
+ import { hasExecuteFunction, isToolCallOutputEvent } from './tool-types.js';
13
+ /**
14
+ * Default maximum number of tool execution steps if no stopWhen is specified.
15
+ * This prevents infinite loops in tool execution.
16
+ */
17
+ const DEFAULT_MAX_STEPS = 5;
18
+ /**
19
+ * Type guard for stream event with toReadableStream method
20
+ * Checks constructor name, prototype, and method availability
21
+ */
22
+ function isEventStream(value) {
23
+ if (value === null || typeof value !== 'object') {
24
+ return false;
25
+ }
26
+ // Check constructor name for EventStream
27
+ const constructorName = Object.getPrototypeOf(value)?.constructor?.name;
28
+ if (constructorName === 'EventStream') {
29
+ return true;
30
+ }
31
+ // Fallback: check for toReadableStream method (may be on prototype)
32
+ const maybeStream = value;
33
+ return typeof maybeStream.toReadableStream === 'function';
34
+ }
35
+ /**
36
+ * A wrapper around a streaming response that provides multiple consumption patterns.
37
+ *
38
+ * Allows consuming the response in multiple ways:
39
+ * - `await result.getText()` - Get just the text
40
+ * - `await result.getResponse()` - Get the full response object
41
+ * - `for await (const delta of result.getTextStream())` - Stream text deltas
42
+ * - `for await (const msg of result.getNewMessagesStream())` - Stream cumulative message snapshots
43
+ * - `for await (const event of result.getFullResponsesStream())` - Stream all response events
44
+ *
45
+ * For message format conversion, use the helper functions:
46
+ * - `toChatMessage(response)` for OpenAI chat format
47
+ * - `toClaudeMessage(response)` for Anthropic Claude format
48
+ *
49
+ * All consumption patterns can be used concurrently thanks to the underlying
50
+ * ReusableReadableStream implementation.
51
+ *
52
+ * @template TTools - The tools array type to enable typed tool calls and results
53
+ * @template TShared - The shape of the shared context (inferred from sharedContextSchema)
54
+ */
55
+ export class ModelResult {
56
+ constructor(options) {
57
+ this.reusableStream = null;
58
+ this.textPromise = null;
59
+ this.initPromise = null;
60
+ this.toolExecutionPromise = null;
61
+ this.finalResponse = null;
62
+ this.toolEventBroadcaster = null;
63
+ this.allToolExecutionRounds = [];
64
+ // Track resolved request after async function resolution
65
+ this.resolvedRequest = null;
66
+ // State management for multi-turn conversations
67
+ this.stateAccessor = null;
68
+ this.currentState = null;
69
+ this.requireApprovalFn = null;
70
+ this.approvedToolCalls = [];
71
+ this.rejectedToolCalls = [];
72
+ this.isResumingFromApproval = false;
73
+ // Unified turn broadcaster for multi-turn streaming
74
+ this.turnBroadcaster = null;
75
+ this.initialStreamPipeStarted = false;
76
+ this.initialPipePromise = null;
77
+ // Context store for typed tool context (persists across turns)
78
+ this.contextStore = null;
79
+ this.options = options;
80
+ // Runtime validation: approval decisions require state
81
+ const hasApprovalDecisions = (options.approveToolCalls && options.approveToolCalls.length > 0) ||
82
+ (options.rejectToolCalls && options.rejectToolCalls.length > 0);
83
+ if (hasApprovalDecisions && !options.state) {
84
+ throw new Error('approveToolCalls and rejectToolCalls require a state accessor. ' +
85
+ 'Provide a StateAccessor via the "state" parameter to persist approval decisions.');
86
+ }
87
+ // Initialize state management
88
+ this.stateAccessor = options.state ?? null;
89
+ this.requireApprovalFn = options.requireApproval ?? null;
90
+ this.approvedToolCalls = options.approveToolCalls ?? [];
91
+ this.rejectedToolCalls = options.rejectToolCalls ?? [];
92
+ }
93
+ /**
94
+ * Get or create the unified turn broadcaster (lazy initialization).
95
+ * Broadcasts all API stream events, tool events, and turn delimiters across turns.
96
+ */
97
+ ensureTurnBroadcaster() {
98
+ if (!this.turnBroadcaster) {
99
+ this.turnBroadcaster = new ToolEventBroadcaster();
100
+ }
101
+ return this.turnBroadcaster;
102
+ }
103
+ /**
104
+ * Start piping the initial stream into the turn broadcaster.
105
+ * Idempotent — only starts once even if called multiple times.
106
+ * Wraps the initial stream events with turn.start(0) / turn.end(0) delimiters.
107
+ */
108
+ startInitialStreamPipe() {
109
+ if (this.initialStreamPipeStarted)
110
+ return;
111
+ this.initialStreamPipeStarted = true;
112
+ const broadcaster = this.ensureTurnBroadcaster();
113
+ if (!this.reusableStream) {
114
+ return;
115
+ }
116
+ const stream = this.reusableStream;
117
+ this.initialPipePromise = (async () => {
118
+ broadcaster.push({
119
+ type: 'turn.start',
120
+ turnNumber: 0,
121
+ timestamp: Date.now(),
122
+ });
123
+ const consumer = stream.createConsumer();
124
+ for await (const event of consumer) {
125
+ broadcaster.push(event);
126
+ }
127
+ broadcaster.push({
128
+ type: 'turn.end',
129
+ turnNumber: 0,
130
+ timestamp: Date.now(),
131
+ });
132
+ })().catch((error) => {
133
+ broadcaster.complete(error instanceof Error ? error : new Error(String(error)));
134
+ });
135
+ }
136
+ /**
137
+ * Pipe a follow-up stream into the turn broadcaster and capture the completed response.
138
+ * Emits turn.start / turn.end delimiters around the stream events.
139
+ */
140
+ async pipeAndConsumeStream(stream, turnNumber) {
141
+ const broadcaster = this.turnBroadcaster;
142
+ broadcaster.push({
143
+ type: 'turn.start',
144
+ turnNumber,
145
+ timestamp: Date.now(),
146
+ });
147
+ const consumer = stream.createConsumer();
148
+ let completedResponse = null;
149
+ for await (const event of consumer) {
150
+ broadcaster.push(event);
151
+ if (isResponseCompletedEvent(event)) {
152
+ completedResponse = event.response;
153
+ }
154
+ if (isResponseFailedEvent(event)) {
155
+ const errorMsg = 'message' in event ? String(event.message) : 'Response failed';
156
+ throw new Error(errorMsg);
157
+ }
158
+ if (isResponseIncompleteEvent(event)) {
159
+ completedResponse = event.response;
160
+ }
161
+ }
162
+ broadcaster.push({
163
+ type: 'turn.end',
164
+ turnNumber,
165
+ timestamp: Date.now(),
166
+ });
167
+ if (!completedResponse) {
168
+ throw new Error('Follow-up stream ended without a completed response');
169
+ }
170
+ return completedResponse;
171
+ }
172
+ /**
173
+ * Push a tool result event to both the legacy tool event broadcaster
174
+ * and the unified turn broadcaster.
175
+ */
176
+ broadcastToolResult(toolCallId, result, preliminaryResults) {
177
+ this.toolEventBroadcaster?.push({
178
+ type: 'tool_result',
179
+ toolCallId,
180
+ result,
181
+ ...(preliminaryResults?.length && {
182
+ preliminaryResults,
183
+ }),
184
+ });
185
+ this.turnBroadcaster?.push({
186
+ type: 'tool.result',
187
+ toolCallId,
188
+ result,
189
+ timestamp: Date.now(),
190
+ ...(preliminaryResults?.length && {
191
+ preliminaryResults,
192
+ }),
193
+ });
194
+ }
195
+ /**
196
+ * Push a preliminary result event to both the legacy tool event broadcaster
197
+ * and the unified turn broadcaster.
198
+ */
199
+ broadcastPreliminaryResult(toolCallId, result) {
200
+ this.toolEventBroadcaster?.push({
201
+ type: 'preliminary_result',
202
+ toolCallId,
203
+ result,
204
+ });
205
+ this.turnBroadcaster?.push({
206
+ type: 'tool.preliminary_result',
207
+ toolCallId,
208
+ result,
209
+ timestamp: Date.now(),
210
+ });
211
+ }
212
+ /**
213
+ * Set up the turn broadcaster with tool execution and return the consumer.
214
+ * Used by stream methods that need to iterate over all turns.
215
+ */
216
+ startTurnBroadcasterExecution() {
217
+ const broadcaster = this.ensureTurnBroadcaster();
218
+ this.startInitialStreamPipe();
219
+ const consumer = broadcaster.createConsumer();
220
+ const executionPromise = this.executeToolsIfNeeded().finally(async () => {
221
+ // Wait for the initial stream pipe to finish pushing all events
222
+ // (including turn.end) before marking the broadcaster as complete.
223
+ // Without this, turn.end can be silently dropped if the pipe hasn't
224
+ // finished when executeToolsIfNeeded completes.
225
+ if (this.initialPipePromise) {
226
+ await this.initialPipePromise;
227
+ }
228
+ broadcaster.complete();
229
+ });
230
+ return {
231
+ consumer,
232
+ executionPromise,
233
+ };
234
+ }
235
+ /**
236
+ * Type guard to check if a value is a non-streaming response
237
+ * Only requires 'output' field and absence of 'toReadableStream' method
238
+ */
239
+ isNonStreamingResponse(value) {
240
+ return (value !== null &&
241
+ typeof value === 'object' &&
242
+ 'output' in value &&
243
+ !('toReadableStream' in value));
244
+ }
245
+ // =========================================================================
246
+ // Extracted Helper Methods for executeToolsIfNeeded
247
+ // =========================================================================
248
+ /**
249
+ * Get initial response from stream or cached final response.
250
+ * Consumes the stream to completion if needed to extract the response.
251
+ *
252
+ * @returns The complete non-streaming response
253
+ * @throws Error if neither stream nor response has been initialized
254
+ */
255
+ async getInitialResponse() {
256
+ if (this.finalResponse) {
257
+ return this.finalResponse;
258
+ }
259
+ if (this.reusableStream) {
260
+ return consumeStreamForCompletion(this.reusableStream);
261
+ }
262
+ throw new Error('Neither stream nor response initialized');
263
+ }
264
+ /**
265
+ * Save response output to state.
266
+ * Appends the response output to the message history and records the response ID.
267
+ *
268
+ * @param response - The API response to save
269
+ */
270
+ async saveResponseToState(response) {
271
+ if (!this.stateAccessor || !this.currentState)
272
+ return;
273
+ const outputItems = Array.isArray(response.output)
274
+ ? response.output
275
+ : [
276
+ response.output,
277
+ ];
278
+ await this.saveStateSafely({
279
+ messages: appendToMessages(this.currentState.messages, outputItems),
280
+ previousResponseId: response.id,
281
+ });
282
+ }
283
+ /**
284
+ * Mark state as complete.
285
+ * Sets the conversation status to 'complete' indicating no further tool execution is needed.
286
+ */
287
+ async markStateComplete() {
288
+ await this.saveStateSafely({
289
+ status: 'complete',
290
+ });
291
+ }
292
+ /**
293
+ * Save tool results to state.
294
+ * Appends tool execution results to the message history for multi-turn context.
295
+ *
296
+ * @param toolResults - The tool execution results to save
297
+ */
298
+ async saveToolResultsToState(toolResults) {
299
+ if (!this.currentState)
300
+ return;
301
+ await this.saveStateSafely({
302
+ messages: appendToMessages(this.currentState.messages, toolResults),
303
+ });
304
+ }
305
+ /**
306
+ * Check if execution should be interrupted by external signal.
307
+ * Polls the state accessor for interruption flags set by external processes.
308
+ *
309
+ * @param currentResponse - The current response to save as partial state
310
+ * @returns True if interrupted and caller should exit, false to continue
311
+ */
312
+ async checkForInterruption(currentResponse) {
313
+ if (!this.stateAccessor)
314
+ return false;
315
+ const freshState = await this.stateAccessor.load();
316
+ if (!freshState?.interruptedBy)
317
+ return false;
318
+ // Save partial state
319
+ if (this.currentState) {
320
+ const currentToolCalls = extractToolCallsFromResponse(currentResponse);
321
+ await this.saveStateSafely({
322
+ status: 'interrupted',
323
+ partialResponse: {
324
+ text: extractTextFromResponseState(currentResponse),
325
+ toolCalls: currentToolCalls,
326
+ },
327
+ });
328
+ }
329
+ this.finalResponse = currentResponse;
330
+ return true;
331
+ }
332
+ /**
333
+ * Check if stop conditions are met.
334
+ * Returns true if execution should stop.
335
+ *
336
+ * @remarks
337
+ * Default: stepCountIs(DEFAULT_MAX_STEPS) if no stopWhen is specified.
338
+ * This evaluates stop conditions against the complete step history.
339
+ */
340
+ async shouldStopExecution() {
341
+ const stopWhen = this.options.stopWhen ?? stepCountIs(DEFAULT_MAX_STEPS);
342
+ const stopConditions = Array.isArray(stopWhen)
343
+ ? stopWhen
344
+ : [
345
+ stopWhen,
346
+ ];
347
+ return isStopConditionMet({
348
+ stopConditions,
349
+ steps: this.allToolExecutionRounds.map((round) => ({
350
+ stepType: 'continue',
351
+ text: extractTextFromResponse(round.response),
352
+ toolCalls: round.toolCalls,
353
+ toolResults: round.toolResults.map((tr) => ({
354
+ toolCallId: tr.callId,
355
+ toolName: round.toolCalls.find((tc) => tc.id === tr.callId)?.name ?? '',
356
+ result: typeof tr.output === 'string' ? JSON.parse(tr.output) : tr.output,
357
+ })),
358
+ response: round.response,
359
+ usage: round.response.usage,
360
+ finishReason: undefined,
361
+ })),
362
+ });
363
+ }
364
+ /**
365
+ * Check if any tool calls have execute functions.
366
+ * Used to determine if automatic tool execution should be attempted.
367
+ *
368
+ * @param toolCalls - The tool calls to check
369
+ * @returns True if at least one tool call has an executable function
370
+ */
371
+ hasExecutableToolCalls(toolCalls) {
372
+ return toolCalls.some((toolCall) => {
373
+ const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
374
+ return tool && hasExecuteFunction(tool);
375
+ });
376
+ }
377
+ /**
378
+ * Execute tools that can auto-execute (don't require approval) in parallel.
379
+ *
380
+ * @param toolCalls - The tool calls to execute
381
+ * @param turnContext - The current turn context
382
+ * @returns Array of unsent tool results for later submission
383
+ */
384
+ async executeAutoApproveTools(toolCalls, turnContext) {
385
+ const toolCallPromises = toolCalls.map(async (tc) => {
386
+ const tool = this.options.tools?.find((t) => t.function.name === tc.name);
387
+ if (!tool || !hasExecuteFunction(tool)) {
388
+ return null;
389
+ }
390
+ const result = await executeTool(tool, tc, turnContext, undefined, this.contextStore ?? undefined, this.options.sharedContextSchema);
391
+ if (result.error) {
392
+ return createRejectedResult(tc.id, String(tc.name), result.error.message);
393
+ }
394
+ return createUnsentResult(tc.id, String(tc.name), result.result);
395
+ });
396
+ const settledResults = await Promise.allSettled(toolCallPromises);
397
+ const results = [];
398
+ for (let i = 0; i < settledResults.length; i++) {
399
+ const settled = settledResults[i];
400
+ const tc = toolCalls[i];
401
+ if (!settled || !tc)
402
+ continue;
403
+ if (settled.status === 'rejected') {
404
+ const errorMessage = settled.reason instanceof Error ? settled.reason.message : String(settled.reason);
405
+ results.push(createRejectedResult(tc.id, String(tc.name), errorMessage));
406
+ continue;
407
+ }
408
+ if (settled.value) {
409
+ results.push(settled.value);
410
+ }
411
+ }
412
+ return results;
413
+ }
414
+ /**
415
+ * Check for tools requiring approval and handle accordingly.
416
+ * Partitions tool calls into those needing approval and those that can auto-execute.
417
+ *
418
+ * @param toolCalls - The tool calls to check
419
+ * @param currentRound - The current execution round (1-indexed)
420
+ * @param currentResponse - The current response to save if pausing
421
+ * @returns True if execution should pause for approval, false to continue
422
+ * @throws Error if approval is required but no state accessor is configured
423
+ */
424
+ async handleApprovalCheck(toolCalls, currentRound, currentResponse) {
425
+ if (!this.options.tools)
426
+ return false;
427
+ const turnContext = {
428
+ numberOfTurns: currentRound,
429
+ // context is handled via contextStore, not on TurnContext
430
+ };
431
+ const { requiresApproval: needsApproval, autoExecute } = await partitionToolCalls(toolCalls, this.options.tools, turnContext, this.requireApprovalFn ?? undefined);
432
+ if (needsApproval.length === 0)
433
+ return false;
434
+ // Validate: approval requires state accessor
435
+ if (!this.stateAccessor) {
436
+ const toolNames = needsApproval.map((tc) => tc.name).join(', ');
437
+ throw new Error(`Tool(s) require approval but no state accessor is configured: ${toolNames}. ` +
438
+ 'Provide a StateAccessor via the "state" parameter to enable approval workflows.');
439
+ }
440
+ // Execute auto-approve tools
441
+ const unsentResults = await this.executeAutoApproveTools(autoExecute, turnContext);
442
+ // Save state with pending approvals
443
+ const stateUpdates = {
444
+ pendingToolCalls: needsApproval,
445
+ status: 'awaiting_approval',
446
+ };
447
+ if (unsentResults.length > 0) {
448
+ stateUpdates.unsentToolResults = unsentResults;
449
+ }
450
+ await this.saveStateSafely(stateUpdates);
451
+ this.finalResponse = currentResponse;
452
+ return true; // Pause for approval
453
+ }
454
+ /**
455
+ * Execute all tools in a single round in parallel.
456
+ * Emits tool.result events after tool execution completes.
457
+ *
458
+ * @param toolCalls - The tool calls to execute
459
+ * @param turnContext - The current turn context
460
+ * @returns Array of function call outputs formatted for the API
461
+ */
462
+ async executeToolRound(toolCalls, turnContext) {
463
+ const toolCallPromises = toolCalls.map(async (toolCall) => {
464
+ const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
465
+ if (!tool || !hasExecuteFunction(tool)) {
466
+ return null;
467
+ }
468
+ // Check if arguments failed to parse (remained as string instead of object)
469
+ const args = toolCall.arguments;
470
+ if (typeof args === 'string') {
471
+ const rawArgs = args;
472
+ const errorMessage = `Failed to parse tool call arguments for "${toolCall.name}": The model provided invalid JSON. ` +
473
+ `Raw arguments received: "${rawArgs}". ` +
474
+ `Please provide valid JSON arguments for this tool call.`;
475
+ this.broadcastToolResult(toolCall.id, {
476
+ error: errorMessage,
477
+ });
478
+ return {
479
+ type: 'parse_error',
480
+ toolCall,
481
+ output: {
482
+ type: 'function_call_output',
483
+ id: `output_${toolCall.id}`,
484
+ callId: toolCall.id,
485
+ output: JSON.stringify({
486
+ error: errorMessage,
487
+ }),
488
+ },
489
+ };
490
+ }
491
+ const preliminaryResultsForCall = [];
492
+ const hasBroadcaster = this.toolEventBroadcaster || this.turnBroadcaster;
493
+ const onPreliminaryResult = hasBroadcaster
494
+ ? (callId, resultValue) => {
495
+ const typedResult = resultValue;
496
+ preliminaryResultsForCall.push(typedResult);
497
+ this.broadcastPreliminaryResult(callId, typedResult);
498
+ }
499
+ : undefined;
500
+ const result = await executeTool(tool, toolCall, turnContext, onPreliminaryResult, this.contextStore ?? undefined, this.options.sharedContextSchema);
501
+ return {
502
+ type: 'execution',
503
+ toolCall,
504
+ tool,
505
+ result,
506
+ preliminaryResultsForCall,
507
+ };
508
+ });
509
+ const settledResults = await Promise.allSettled(toolCallPromises);
510
+ const toolResults = [];
511
+ for (let i = 0; i < settledResults.length; i++) {
512
+ const settled = settledResults[i];
513
+ const originalToolCall = toolCalls[i];
514
+ if (!settled || !originalToolCall)
515
+ continue;
516
+ if (settled.status === 'rejected') {
517
+ const errorMessage = settled.reason instanceof Error ? settled.reason.message : String(settled.reason);
518
+ this.broadcastToolResult(originalToolCall.id, {
519
+ error: errorMessage,
520
+ });
521
+ const rejectedOutput = {
522
+ type: 'function_call_output',
523
+ id: `output_${originalToolCall.id}`,
524
+ callId: originalToolCall.id,
525
+ output: JSON.stringify({
526
+ error: errorMessage,
527
+ }),
528
+ };
529
+ toolResults.push(rejectedOutput);
530
+ this.turnBroadcaster?.push({
531
+ type: 'tool.call_output',
532
+ output: rejectedOutput,
533
+ timestamp: Date.now(),
534
+ });
535
+ continue;
536
+ }
537
+ const value = settled.value;
538
+ if (!value)
539
+ continue;
540
+ if (value.type === 'parse_error') {
541
+ toolResults.push(value.output);
542
+ this.turnBroadcaster?.push({
543
+ type: 'tool.call_output',
544
+ output: value.output,
545
+ timestamp: Date.now(),
546
+ });
547
+ continue;
548
+ }
549
+ const toolResult = (value.result.error
550
+ ? {
551
+ error: value.result.error.message,
552
+ }
553
+ : value.result.result);
554
+ this.broadcastToolResult(value.toolCall.id, toolResult, value.preliminaryResultsForCall.length > 0 ? value.preliminaryResultsForCall : undefined);
555
+ const executedOutput = {
556
+ type: 'function_call_output',
557
+ id: `output_${value.toolCall.id}`,
558
+ callId: value.toolCall.id,
559
+ output: value.result.error
560
+ ? JSON.stringify({
561
+ error: value.result.error.message,
562
+ })
563
+ : JSON.stringify(value.result.result),
564
+ };
565
+ toolResults.push(executedOutput);
566
+ this.turnBroadcaster?.push({
567
+ type: 'tool.call_output',
568
+ output: executedOutput,
569
+ timestamp: Date.now(),
570
+ });
571
+ }
572
+ return toolResults;
573
+ }
574
+ /**
575
+ * Resolve async functions for the current turn.
576
+ * Updates the resolved request with turn-specific parameter values.
577
+ *
578
+ * @param turnContext - The turn context for parameter resolution
579
+ */
580
+ async resolveAsyncFunctionsForTurn(turnContext) {
581
+ if (hasAsyncFunctions(this.options.request)) {
582
+ const resolved = await resolveAsyncFunctions(this.options.request, turnContext);
583
+ // Preserve accumulated input from previous turns
584
+ const preservedInput = this.resolvedRequest?.input;
585
+ const preservedStream = this.resolvedRequest?.stream;
586
+ this.resolvedRequest = {
587
+ ...resolved,
588
+ stream: preservedStream ?? true,
589
+ ...(preservedInput !== undefined && {
590
+ input: preservedInput,
591
+ }),
592
+ };
593
+ }
594
+ }
595
+ /**
596
+ * Apply nextTurnParams from executed tools.
597
+ * Allows tools to modify request parameters for subsequent turns.
598
+ *
599
+ * @param toolCalls - The tool calls that were just executed
600
+ */
601
+ async applyNextTurnParams(toolCalls) {
602
+ if (!this.options.tools || toolCalls.length === 0 || !this.resolvedRequest) {
603
+ return;
604
+ }
605
+ const computedParams = await executeNextTurnParamsFunctions(toolCalls, this.options.tools, this.resolvedRequest);
606
+ if (Object.keys(computedParams).length > 0) {
607
+ this.resolvedRequest = applyNextTurnParamsToRequest(this.resolvedRequest, computedParams);
608
+ }
609
+ }
610
+ /**
611
+ * Make a follow-up API request with tool results.
612
+ * Uses streaming and pipes events through the turn broadcaster when available.
613
+ */
614
+ async makeFollowupRequest(currentResponse, toolResults, turnNumber) {
615
+ const originalInput = this.resolvedRequest?.input;
616
+ const normalizedOriginalInput = Array.isArray(originalInput)
617
+ ? originalInput
618
+ : originalInput
619
+ ? [
620
+ {
621
+ role: 'user',
622
+ content: originalInput,
623
+ },
624
+ ]
625
+ : [];
626
+ const newInput = [
627
+ ...normalizedOriginalInput,
628
+ ...(Array.isArray(currentResponse.output)
629
+ ? currentResponse.output
630
+ : [
631
+ currentResponse.output,
632
+ ]),
633
+ ...toolResults,
634
+ ];
635
+ if (!this.resolvedRequest) {
636
+ throw new Error('Request not initialized');
637
+ }
638
+ // Update resolvedRequest.input with accumulated conversation for next turn
639
+ this.resolvedRequest = {
640
+ ...this.resolvedRequest,
641
+ input: newInput,
642
+ };
643
+ const newRequest = {
644
+ ...this.resolvedRequest,
645
+ stream: true,
646
+ };
647
+ const newResult = await betaResponsesSend(this.options.client, {
648
+ responsesRequest: newRequest,
649
+ }, this.options.options);
650
+ if (!newResult.ok) {
651
+ throw newResult.error;
652
+ }
653
+ // Handle streaming or non-streaming response
654
+ const value = newResult.value;
655
+ if (isEventStream(value)) {
656
+ const followUpStream = new ReusableReadableStream(value);
657
+ if (this.turnBroadcaster) {
658
+ return this.pipeAndConsumeStream(followUpStream, turnNumber);
659
+ }
660
+ return consumeStreamForCompletion(followUpStream);
661
+ }
662
+ else if (this.isNonStreamingResponse(value)) {
663
+ return value;
664
+ }
665
+ else {
666
+ throw new Error('Unexpected response type from API');
667
+ }
668
+ }
669
+ /**
670
+ * Validate the final response has required fields.
671
+ *
672
+ * @param response - The response to validate
673
+ * @throws Error if response is missing required fields or has invalid output
674
+ */
675
+ validateFinalResponse(response) {
676
+ if (!response?.id || !response?.output) {
677
+ throw new Error('Invalid final response: missing required fields');
678
+ }
679
+ if (!Array.isArray(response.output) || response.output.length === 0) {
680
+ throw new Error('Invalid final response: empty or invalid output');
681
+ }
682
+ }
683
+ /**
684
+ * Resolve async functions in the request for a given turn context.
685
+ * Extracts non-function fields and resolves any async parameter functions.
686
+ *
687
+ * @param context - The turn context for parameter resolution
688
+ * @returns The resolved request without async functions
689
+ */
690
+ async resolveRequestForContext(context) {
691
+ if (hasAsyncFunctions(this.options.request)) {
692
+ return resolveAsyncFunctions(this.options.request, context);
693
+ }
694
+ // Already resolved, extract non-function fields
695
+ // Filter out stopWhen and state-related fields that aren't part of the API request
696
+ const { stopWhen: _, state: _s, requireApproval: _r, approveToolCalls: _a, rejectToolCalls: _rj, context: _c, ...rest } = this.options.request;
697
+ return rest;
698
+ }
699
+ /**
700
+ * Safely persist state with error handling.
701
+ * Wraps state save operations to ensure failures are properly reported.
702
+ *
703
+ * @param updates - Optional partial state updates to apply before saving
704
+ * @throws Error if state persistence fails
705
+ */
706
+ async saveStateSafely(updates) {
707
+ if (!this.stateAccessor || !this.currentState)
708
+ return;
709
+ if (updates) {
710
+ this.currentState = updateState(this.currentState, updates);
711
+ }
712
+ try {
713
+ await this.stateAccessor.save(this.currentState);
714
+ }
715
+ catch (error) {
716
+ const message = error instanceof Error ? error.message : String(error);
717
+ throw new Error(`Failed to persist conversation state: ${message}`);
718
+ }
719
+ }
720
+ /**
721
+ * Remove optional properties from state when they should be cleared.
722
+ * Uses delete to properly remove optional properties rather than setting undefined.
723
+ *
724
+ * @param props - Array of property names to remove from current state
725
+ */
726
+ clearOptionalStateProperties(props) {
727
+ if (!this.currentState)
728
+ return;
729
+ for (const prop of props) {
730
+ delete this.currentState[prop];
731
+ }
732
+ }
733
+ // =========================================================================
734
+ // Core Methods
735
+ // =========================================================================
736
+ /**
737
+ * Initialize the stream if not already started
738
+ * This is idempotent - multiple calls will return the same promise
739
+ */
740
+ initStream() {
741
+ if (this.initPromise) {
742
+ return this.initPromise;
743
+ }
744
+ this.initPromise = (async () => {
745
+ // Load or create state if accessor provided
746
+ if (this.stateAccessor) {
747
+ const loadedState = await this.stateAccessor.load();
748
+ if (loadedState) {
749
+ this.currentState = loadedState;
750
+ // Check if we're resuming from awaiting_approval with decisions
751
+ if (loadedState.status === 'awaiting_approval' &&
752
+ (this.approvedToolCalls.length > 0 || this.rejectedToolCalls.length > 0)) {
753
+ // Initialize context store before resuming so tools have access
754
+ if (this.options.context !== undefined) {
755
+ const approvalContext = {
756
+ numberOfTurns: 0,
757
+ };
758
+ const resolvedCtx = await resolveContext(this.options.context, approvalContext);
759
+ this.contextStore = new ToolContextStore(resolvedCtx);
760
+ }
761
+ this.isResumingFromApproval = true;
762
+ await this.processApprovalDecisions();
763
+ return; // Skip normal initialization, we're resuming
764
+ }
765
+ // Check for interruption flag and handle
766
+ if (loadedState.interruptedBy) {
767
+ // Clear interruption flag and continue from saved state
768
+ this.currentState = updateState(loadedState, {
769
+ status: 'in_progress',
770
+ });
771
+ this.clearOptionalStateProperties([
772
+ 'interruptedBy',
773
+ ]);
774
+ await this.saveStateSafely();
775
+ }
776
+ }
777
+ else {
778
+ this.currentState = createInitialState();
779
+ }
780
+ // Update status to in_progress
781
+ await this.saveStateSafely({
782
+ status: 'in_progress',
783
+ });
784
+ }
785
+ // Resolve async functions before initial request
786
+ // Build initial turn context (turn 0 for initial request)
787
+ const initialContext = {
788
+ numberOfTurns: 0,
789
+ };
790
+ // Initialize context store from the context option
791
+ if (this.options.context !== undefined) {
792
+ const resolvedCtx = await resolveContext(this.options.context, initialContext);
793
+ this.contextStore = new ToolContextStore(resolvedCtx);
794
+ }
795
+ // Resolve any async functions first
796
+ let baseRequest = await this.resolveRequestForContext(initialContext);
797
+ // If we have state with existing messages, use those as input
798
+ if (this.currentState &&
799
+ this.currentState.messages &&
800
+ Array.isArray(this.currentState.messages) &&
801
+ this.currentState.messages.length > 0) {
802
+ // Append new input to existing messages
803
+ const newInput = baseRequest.input;
804
+ if (newInput) {
805
+ const inputArray = Array.isArray(newInput)
806
+ ? newInput
807
+ : [
808
+ newInput,
809
+ ];
810
+ baseRequest = {
811
+ ...baseRequest,
812
+ input: appendToMessages(this.currentState.messages, inputArray),
813
+ };
814
+ }
815
+ else {
816
+ baseRequest = {
817
+ ...baseRequest,
818
+ input: this.currentState.messages,
819
+ };
820
+ }
821
+ }
822
+ // Store resolved request with stream mode
823
+ this.resolvedRequest = {
824
+ ...baseRequest,
825
+ stream: true,
826
+ };
827
+ // Force stream mode for initial request
828
+ const request = this.resolvedRequest;
829
+ // Make the API request
830
+ const apiResult = await betaResponsesSend(this.options.client, {
831
+ responsesRequest: request,
832
+ }, this.options.options);
833
+ if (!apiResult.ok) {
834
+ throw apiResult.error;
835
+ }
836
+ // Handle both streaming and non-streaming responses
837
+ // The API may return a non-streaming response even when stream: true is requested
838
+ if (isEventStream(apiResult.value)) {
839
+ this.reusableStream = new ReusableReadableStream(apiResult.value);
840
+ }
841
+ else if (this.isNonStreamingResponse(apiResult.value)) {
842
+ // API returned a complete response directly - use it as the final response
843
+ this.finalResponse = apiResult.value;
844
+ }
845
+ else {
846
+ throw new Error('Unexpected response type from API');
847
+ }
848
+ })();
849
+ return this.initPromise;
850
+ }
851
+ /**
852
+ * Process approval/rejection decisions and resume execution
853
+ */
854
+ async processApprovalDecisions() {
855
+ if (!this.currentState || !this.stateAccessor) {
856
+ throw new Error('Cannot process approval decisions without state');
857
+ }
858
+ const pendingCalls = this.currentState.pendingToolCalls ?? [];
859
+ const unsentResults = [
860
+ ...(this.currentState.unsentToolResults ?? []),
861
+ ];
862
+ // Build turn context - numberOfTurns represents the current turn (1-indexed after initial)
863
+ const turnContext = {
864
+ numberOfTurns: this.allToolExecutionRounds.length + 1,
865
+ // context is handled via contextStore, not on TurnContext
866
+ };
867
+ // Process approvals - execute the approved tools
868
+ for (const callId of this.approvedToolCalls) {
869
+ const toolCall = pendingCalls.find((tc) => tc.id === callId);
870
+ if (!toolCall)
871
+ continue;
872
+ const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
873
+ if (!tool || !hasExecuteFunction(tool)) {
874
+ // Can't execute, create error result
875
+ unsentResults.push(createRejectedResult(callId, String(toolCall.name), 'Tool not found or not executable'));
876
+ continue;
877
+ }
878
+ const result = await executeTool(tool, toolCall, turnContext, undefined, this.contextStore ?? undefined, this.options.sharedContextSchema);
879
+ if (result.error) {
880
+ unsentResults.push(createRejectedResult(callId, String(toolCall.name), result.error.message));
881
+ }
882
+ else {
883
+ unsentResults.push(createUnsentResult(callId, String(toolCall.name), result.result));
884
+ }
885
+ }
886
+ // Process rejections
887
+ for (const callId of this.rejectedToolCalls) {
888
+ const toolCall = pendingCalls.find((tc) => tc.id === callId);
889
+ if (!toolCall)
890
+ continue;
891
+ unsentResults.push(createRejectedResult(callId, String(toolCall.name), 'Rejected by user'));
892
+ }
893
+ // Remove processed calls from pending
894
+ const processedIds = new Set([
895
+ ...this.approvedToolCalls,
896
+ ...this.rejectedToolCalls,
897
+ ]);
898
+ const remainingPending = pendingCalls.filter((tc) => !processedIds.has(tc.id));
899
+ // Update state - conditionally include optional properties only if they have values
900
+ const stateUpdates = {
901
+ status: remainingPending.length > 0 ? 'awaiting_approval' : 'in_progress',
902
+ };
903
+ if (remainingPending.length > 0) {
904
+ stateUpdates.pendingToolCalls = remainingPending;
905
+ }
906
+ if (unsentResults.length > 0) {
907
+ stateUpdates.unsentToolResults = unsentResults;
908
+ }
909
+ await this.saveStateSafely(stateUpdates);
910
+ // Clear optional properties if they should be empty
911
+ const propsToClear = [];
912
+ if (remainingPending.length === 0)
913
+ propsToClear.push('pendingToolCalls');
914
+ if (unsentResults.length === 0)
915
+ propsToClear.push('unsentToolResults');
916
+ if (propsToClear.length > 0) {
917
+ this.clearOptionalStateProperties(propsToClear);
918
+ await this.saveStateSafely();
919
+ }
920
+ // If we still have pending approvals, stop here
921
+ if (remainingPending.length > 0) {
922
+ return;
923
+ }
924
+ // Otherwise, continue with tool execution using unsent results
925
+ await this.continueWithUnsentResults();
926
+ }
927
+ /**
928
+ * Continue execution with unsent tool results
929
+ */
930
+ async continueWithUnsentResults() {
931
+ if (!this.currentState || !this.stateAccessor)
932
+ return;
933
+ const unsentResults = this.currentState.unsentToolResults ?? [];
934
+ if (unsentResults.length === 0)
935
+ return;
936
+ // Convert to API format
937
+ const toolOutputs = unsentResultsToAPIFormat(unsentResults);
938
+ // Build new input with tool results
939
+ const currentMessages = this.currentState.messages;
940
+ const newInput = appendToMessages(currentMessages, toolOutputs);
941
+ // Clear unsent results from state
942
+ this.currentState = updateState(this.currentState, {
943
+ messages: newInput,
944
+ });
945
+ this.clearOptionalStateProperties([
946
+ 'unsentToolResults',
947
+ ]);
948
+ await this.saveStateSafely();
949
+ // Build request with the updated input
950
+ // numberOfTurns represents the current turn number (1-indexed after initial)
951
+ const turnContext = {
952
+ numberOfTurns: this.allToolExecutionRounds.length + 1,
953
+ };
954
+ const baseRequest = await this.resolveRequestForContext(turnContext);
955
+ // Create request with the accumulated messages
956
+ const request = {
957
+ ...baseRequest,
958
+ input: newInput,
959
+ stream: true,
960
+ };
961
+ this.resolvedRequest = request;
962
+ // Make the API request
963
+ const apiResult = await betaResponsesSend(this.options.client, {
964
+ responsesRequest: request,
965
+ }, this.options.options);
966
+ if (!apiResult.ok) {
967
+ throw apiResult.error;
968
+ }
969
+ // Handle both streaming and non-streaming responses
970
+ if (isEventStream(apiResult.value)) {
971
+ this.reusableStream = new ReusableReadableStream(apiResult.value);
972
+ }
973
+ else if (this.isNonStreamingResponse(apiResult.value)) {
974
+ this.finalResponse = apiResult.value;
975
+ }
976
+ else {
977
+ throw new Error('Unexpected response type from API');
978
+ }
979
+ }
980
+ /**
981
+ * Execute tools automatically if they are provided and have execute functions
982
+ * This is idempotent - multiple calls will return the same promise
983
+ */
984
+ async executeToolsIfNeeded() {
985
+ if (this.toolExecutionPromise) {
986
+ return this.toolExecutionPromise;
987
+ }
988
+ this.toolExecutionPromise = (async () => {
989
+ await this.initStream();
990
+ // If resuming from approval and still pending, don't continue
991
+ if (this.isResumingFromApproval && this.currentState?.status === 'awaiting_approval') {
992
+ return;
993
+ }
994
+ // Get initial response
995
+ let currentResponse = await this.getInitialResponse();
996
+ // Save initial response to state
997
+ await this.saveResponseToState(currentResponse);
998
+ // Check if tools should be executed
999
+ const hasToolCalls = currentResponse.output.some((item) => hasTypeProperty(item) && item.type === 'function_call');
1000
+ if (!this.options.tools?.length || !hasToolCalls) {
1001
+ this.finalResponse = currentResponse;
1002
+ await this.markStateComplete();
1003
+ return;
1004
+ }
1005
+ // Extract and check tool calls
1006
+ const toolCalls = extractToolCallsFromResponse(currentResponse);
1007
+ // Check for approval requirements
1008
+ if (await this.handleApprovalCheck(toolCalls, 0, currentResponse)) {
1009
+ return; // Paused for approval
1010
+ }
1011
+ if (!this.hasExecutableToolCalls(toolCalls)) {
1012
+ this.finalResponse = currentResponse;
1013
+ await this.markStateComplete();
1014
+ return;
1015
+ }
1016
+ // Main execution loop
1017
+ let currentRound = 0;
1018
+ while (true) {
1019
+ // Check for external interruption
1020
+ if (await this.checkForInterruption(currentResponse)) {
1021
+ return;
1022
+ }
1023
+ // Check stop conditions
1024
+ if (await this.shouldStopExecution()) {
1025
+ break;
1026
+ }
1027
+ const currentToolCalls = extractToolCallsFromResponse(currentResponse);
1028
+ if (currentToolCalls.length === 0) {
1029
+ break;
1030
+ }
1031
+ // Check for approval requirements
1032
+ if (await this.handleApprovalCheck(currentToolCalls, currentRound + 1, currentResponse)) {
1033
+ return;
1034
+ }
1035
+ if (!this.hasExecutableToolCalls(currentToolCalls)) {
1036
+ break;
1037
+ }
1038
+ // Build turn context
1039
+ const turnNumber = currentRound + 1;
1040
+ const turnContext = {
1041
+ numberOfTurns: turnNumber,
1042
+ };
1043
+ await this.options.onTurnStart?.(turnContext);
1044
+ // Resolve async functions for this turn
1045
+ await this.resolveAsyncFunctionsForTurn(turnContext);
1046
+ // Execute tools
1047
+ const toolResults = await this.executeToolRound(currentToolCalls, turnContext);
1048
+ // Track execution round
1049
+ this.allToolExecutionRounds.push({
1050
+ round: currentRound,
1051
+ toolCalls: currentToolCalls,
1052
+ response: currentResponse,
1053
+ toolResults,
1054
+ });
1055
+ // Save tool results to state
1056
+ await this.saveToolResultsToState(toolResults);
1057
+ // Apply nextTurnParams
1058
+ await this.applyNextTurnParams(currentToolCalls);
1059
+ currentResponse = await this.makeFollowupRequest(currentResponse, toolResults, turnNumber);
1060
+ await this.options.onTurnEnd?.(turnContext, currentResponse);
1061
+ // Save new response to state
1062
+ await this.saveResponseToState(currentResponse);
1063
+ currentRound++;
1064
+ }
1065
+ // Validate and finalize
1066
+ this.validateFinalResponse(currentResponse);
1067
+ this.finalResponse = currentResponse;
1068
+ await this.markStateComplete();
1069
+ })();
1070
+ return this.toolExecutionPromise;
1071
+ }
1072
+ /**
1073
+ * Internal helper to get the text after tool execution
1074
+ */
1075
+ async getTextInternal() {
1076
+ await this.executeToolsIfNeeded();
1077
+ if (!this.finalResponse) {
1078
+ throw new Error('Response not available');
1079
+ }
1080
+ return extractTextFromResponse(this.finalResponse);
1081
+ }
1082
+ /**
1083
+ * Get just the text content from the response.
1084
+ * This will consume the stream until completion, execute any tools, and extract the text.
1085
+ */
1086
+ getText() {
1087
+ if (this.textPromise) {
1088
+ return this.textPromise;
1089
+ }
1090
+ this.textPromise = this.getTextInternal();
1091
+ return this.textPromise;
1092
+ }
1093
+ /**
1094
+ * Get the complete response object including usage information.
1095
+ * This will consume the stream until completion and execute any tools.
1096
+ * Returns the full OpenResponsesResult with usage data (inputTokens, outputTokens, cachedTokens, etc.)
1097
+ */
1098
+ async getResponse() {
1099
+ await this.executeToolsIfNeeded();
1100
+ if (!this.finalResponse) {
1101
+ throw new Error('Response not available');
1102
+ }
1103
+ return this.finalResponse;
1104
+ }
1105
+ /**
1106
+ * Stream all response events as they arrive across all turns.
1107
+ * Multiple consumers can iterate over this stream concurrently.
1108
+ * Includes API events, tool events, and turn.start/turn.end delimiters.
1109
+ */
1110
+ getFullResponsesStream() {
1111
+ return async function* () {
1112
+ await this.initStream();
1113
+ if (!this.reusableStream && !this.finalResponse) {
1114
+ throw new Error('Stream not initialized');
1115
+ }
1116
+ if (!this.options.tools?.length) {
1117
+ if (this.reusableStream) {
1118
+ const consumer = this.reusableStream.createConsumer();
1119
+ for await (const event of consumer) {
1120
+ yield event;
1121
+ }
1122
+ }
1123
+ return;
1124
+ }
1125
+ const { consumer, executionPromise } = this.startTurnBroadcasterExecution();
1126
+ for await (const event of consumer) {
1127
+ yield event;
1128
+ }
1129
+ await executionPromise;
1130
+ }.call(this);
1131
+ }
1132
+ /**
1133
+ * Stream only text deltas as they arrive from all turns.
1134
+ * This filters the full event stream to only yield text content,
1135
+ * including text from follow-up responses in multi-turn tool loops.
1136
+ */
1137
+ getTextStream() {
1138
+ return async function* () {
1139
+ await this.initStream();
1140
+ if (!this.reusableStream && !this.finalResponse) {
1141
+ throw new Error('Stream not initialized');
1142
+ }
1143
+ if (!this.options.tools?.length) {
1144
+ if (this.reusableStream) {
1145
+ yield* extractTextDeltas(this.reusableStream);
1146
+ }
1147
+ return;
1148
+ }
1149
+ const { consumer, executionPromise } = this.startTurnBroadcasterExecution();
1150
+ for await (const event of consumer) {
1151
+ if (isOutputTextDeltaEvent(event)) {
1152
+ yield event.delta;
1153
+ }
1154
+ }
1155
+ await executionPromise;
1156
+ }.call(this);
1157
+ }
1158
+ /**
1159
+ * Stream all output items cumulatively as they arrive.
1160
+ * Items are emitted with the same ID but progressively updated content as streaming progresses.
1161
+ * Also yields tool results (function_call_output) after tool execution completes.
1162
+ *
1163
+ * Item types include:
1164
+ * - message: Assistant text responses (emitted cumulatively as text streams)
1165
+ * - function_call: Tool calls (emitted cumulatively as arguments stream)
1166
+ * - reasoning: Model reasoning (emitted cumulatively as thinking streams)
1167
+ * - web_search_call: Web search operations
1168
+ * - file_search_call: File search operations
1169
+ * - image_generation_call: Image generation operations
1170
+ * - function_call_output: Results from executed tools
1171
+ */
1172
+ getItemsStream() {
1173
+ return async function* () {
1174
+ await this.initStream();
1175
+ if (!this.reusableStream && !this.finalResponse) {
1176
+ throw new Error('Stream not initialized');
1177
+ }
1178
+ // No tools — stream single turn directly (no broadcaster needed)
1179
+ if (!this.options.tools?.length) {
1180
+ if (this.reusableStream) {
1181
+ yield* buildItemsStream(this.reusableStream);
1182
+ }
1183
+ return;
1184
+ }
1185
+ // Use turnBroadcaster (same pattern as getTextStream/getFullResponsesStream).
1186
+ // executeToolsIfNeeded() drives tool execution in the background while we
1187
+ // passively consume events from the broadcaster in real-time.
1188
+ const { consumer, executionPromise } = this.startTurnBroadcasterExecution();
1189
+ const itemsInProgress = new Map();
1190
+ for await (const event of consumer) {
1191
+ // Tool call outputs → yield directly as function_call_output items
1192
+ if (isToolCallOutputEvent(event)) {
1193
+ yield event.output;
1194
+ continue;
1195
+ }
1196
+ // Stream termination → reset items map for next turn
1197
+ if ('type' in event && streamTerminationEvents.has(event.type)) {
1198
+ itemsInProgress.clear();
1199
+ }
1200
+ // API stream events → dispatch through item handlers
1201
+ // Cast is necessary: TypeScript cannot narrow a union via Record key lookup,
1202
+ // but `event.type in itemsStreamHandlers` guarantees the event is an
1203
+ // StreamEvents whose type matches a handler key.
1204
+ if ('type' in event && event.type in itemsStreamHandlers) {
1205
+ const handler = itemsStreamHandlers[event.type];
1206
+ if (handler) {
1207
+ const result = handler(event, itemsInProgress);
1208
+ if (result) {
1209
+ yield result;
1210
+ }
1211
+ }
1212
+ }
1213
+ }
1214
+ await executionPromise;
1215
+ }.call(this);
1216
+ }
1217
+ /**
1218
+ * @deprecated Use `getItemsStream()` instead. This method only streams messages,
1219
+ * while `getItemsStream()` streams all output item types (messages, function_calls,
1220
+ * reasoning, etc.) with cumulative updates.
1221
+ *
1222
+ * Stream cumulative message snapshots as content is added in responses format.
1223
+ * Each iteration yields an updated version of the message with new content.
1224
+ * Also yields function_call items and FunctionCallOutputItem after tool execution completes.
1225
+ * Returns OutputMessage, OutputFunctionCallItem, or FunctionCallOutputItem
1226
+ * compatible with OpenAI Responses API format.
1227
+ */
1228
+ getNewMessagesStream() {
1229
+ return async function* () {
1230
+ await this.initStream();
1231
+ if (!this.reusableStream && !this.finalResponse) {
1232
+ throw new Error('Stream not initialized');
1233
+ }
1234
+ // First yield messages from the stream in responses format
1235
+ if (this.reusableStream) {
1236
+ yield* buildResponsesMessageStream(this.reusableStream);
1237
+ }
1238
+ // Execute tools if needed
1239
+ await this.executeToolsIfNeeded();
1240
+ // Yield function calls and their outputs for each executed tool
1241
+ for (const round of this.allToolExecutionRounds) {
1242
+ // First yield the function_call items from the response that triggered tool execution
1243
+ for (const item of round.response.output) {
1244
+ if (isFunctionCallItem(item)) {
1245
+ yield item;
1246
+ }
1247
+ }
1248
+ // Then yield the function_call_output results
1249
+ for (const toolResult of round.toolResults) {
1250
+ yield toolResult;
1251
+ }
1252
+ }
1253
+ // If tools were executed, yield the final message (if there is one)
1254
+ if (this.finalResponse && this.allToolExecutionRounds.length > 0) {
1255
+ // Check if the final response contains a message
1256
+ const hasMessage = this.finalResponse.output.some((item) => hasTypeProperty(item) && item.type === 'message');
1257
+ if (hasMessage) {
1258
+ yield extractResponsesMessageFromResponse(this.finalResponse);
1259
+ }
1260
+ }
1261
+ }.call(this);
1262
+ }
1263
+ /**
1264
+ * Stream only reasoning deltas as they arrive from all turns.
1265
+ * This filters the full event stream to only yield reasoning content,
1266
+ * including reasoning from follow-up responses in multi-turn tool loops.
1267
+ */
1268
+ getReasoningStream() {
1269
+ return async function* () {
1270
+ await this.initStream();
1271
+ if (!this.reusableStream && !this.finalResponse) {
1272
+ throw new Error('Stream not initialized');
1273
+ }
1274
+ if (!this.options.tools?.length) {
1275
+ if (this.reusableStream) {
1276
+ yield* extractReasoningDeltas(this.reusableStream);
1277
+ }
1278
+ return;
1279
+ }
1280
+ const { consumer, executionPromise } = this.startTurnBroadcasterExecution();
1281
+ for await (const event of consumer) {
1282
+ if (isReasoningDeltaEvent(event)) {
1283
+ yield event.delta;
1284
+ }
1285
+ }
1286
+ await executionPromise;
1287
+ }.call(this);
1288
+ }
1289
+ /**
1290
+ * Stream tool call argument deltas and preliminary results from all turns.
1291
+ * Preliminary results are streamed in REAL-TIME as generator tools yield.
1292
+ * - Tool call argument deltas as { type: "delta", content: string }
1293
+ * - Preliminary results as { type: "preliminary_result", toolCallId, result }
1294
+ */
1295
+ getToolStream() {
1296
+ return async function* () {
1297
+ await this.initStream();
1298
+ if (!this.reusableStream && !this.finalResponse) {
1299
+ throw new Error('Stream not initialized');
1300
+ }
1301
+ if (!this.options.tools?.length) {
1302
+ if (this.reusableStream) {
1303
+ for await (const delta of extractToolDeltas(this.reusableStream)) {
1304
+ yield {
1305
+ type: 'delta',
1306
+ content: delta,
1307
+ };
1308
+ }
1309
+ }
1310
+ return;
1311
+ }
1312
+ const { consumer, executionPromise } = this.startTurnBroadcasterExecution();
1313
+ for await (const event of consumer) {
1314
+ if (event.type === 'response.function_call_arguments.delta') {
1315
+ yield {
1316
+ type: 'delta',
1317
+ content: event.delta,
1318
+ };
1319
+ continue;
1320
+ }
1321
+ if (event.type === 'tool.preliminary_result') {
1322
+ yield {
1323
+ type: 'preliminary_result',
1324
+ toolCallId: event.toolCallId,
1325
+ result: event.result,
1326
+ };
1327
+ }
1328
+ }
1329
+ await executionPromise;
1330
+ }.call(this);
1331
+ }
1332
+ /**
1333
+ * Get all tool calls from the completed response (before auto-execution).
1334
+ * Note: If tools have execute functions, they will be automatically executed
1335
+ * and this will return the tool calls from the initial response.
1336
+ * Returns structured tool calls with parsed arguments.
1337
+ */
1338
+ async getToolCalls() {
1339
+ await this.initStream();
1340
+ // Handle non-streaming response case - use finalResponse directly
1341
+ if (this.finalResponse) {
1342
+ return extractToolCallsFromResponse(this.finalResponse);
1343
+ }
1344
+ if (!this.reusableStream) {
1345
+ throw new Error('Stream not initialized');
1346
+ }
1347
+ const completedResponse = await consumeStreamForCompletion(this.reusableStream);
1348
+ return extractToolCallsFromResponse(completedResponse);
1349
+ }
1350
+ /**
1351
+ * Stream structured tool call objects as they're completed.
1352
+ * Each iteration yields a complete tool call with parsed arguments.
1353
+ */
1354
+ getToolCallsStream() {
1355
+ return async function* () {
1356
+ await this.initStream();
1357
+ if (!this.reusableStream && !this.finalResponse) {
1358
+ throw new Error('Stream not initialized');
1359
+ }
1360
+ if (this.reusableStream) {
1361
+ yield* buildToolCallStream(this.reusableStream);
1362
+ }
1363
+ }.call(this);
1364
+ }
1365
+ /**
1366
+ * Returns an async iterable that emits a full context snapshot every time
1367
+ * any tool calls ctx.update(). Can be consumed concurrently with getText(),
1368
+ * getToolStream(), etc.
1369
+ *
1370
+ * @example
1371
+ * ```typescript
1372
+ * for await (const snapshot of result.getContextUpdates()) {
1373
+ * console.log('Context changed:', snapshot);
1374
+ * }
1375
+ * ```
1376
+ */
1377
+ async *getContextUpdates() {
1378
+ // Ensure stream is initialized (which creates the context store)
1379
+ await this.initStream();
1380
+ if (!this.contextStore) {
1381
+ return;
1382
+ }
1383
+ const store = this.contextStore;
1384
+ const queue = [];
1385
+ let resolve = null;
1386
+ let done = false;
1387
+ const unsubscribe = store.subscribe((snapshot) => {
1388
+ queue.push(snapshot);
1389
+ if (resolve) {
1390
+ resolve();
1391
+ resolve = null;
1392
+ }
1393
+ });
1394
+ // Signal completion when tool execution finishes
1395
+ this.executeToolsIfNeeded().then(() => {
1396
+ done = true;
1397
+ if (resolve) {
1398
+ resolve();
1399
+ resolve = null;
1400
+ }
1401
+ }, () => {
1402
+ done = true;
1403
+ if (resolve) {
1404
+ resolve();
1405
+ resolve = null;
1406
+ }
1407
+ });
1408
+ try {
1409
+ while (!done) {
1410
+ if (queue.length > 0) {
1411
+ yield queue.shift();
1412
+ }
1413
+ else {
1414
+ // Wait for next update or completion
1415
+ await new Promise((r) => {
1416
+ resolve = r;
1417
+ });
1418
+ }
1419
+ }
1420
+ // Drain any remaining queued snapshots
1421
+ while (queue.length > 0) {
1422
+ yield queue.shift();
1423
+ }
1424
+ }
1425
+ finally {
1426
+ unsubscribe();
1427
+ }
1428
+ }
1429
+ /**
1430
+ * Cancel the underlying stream and all consumers
1431
+ */
1432
+ async cancel() {
1433
+ if (this.reusableStream) {
1434
+ await this.reusableStream.cancel();
1435
+ }
1436
+ }
1437
+ // =========================================================================
1438
+ // Multi-Turn Conversation State Methods
1439
+ // =========================================================================
1440
+ /**
1441
+ * Check if the conversation requires human approval to continue.
1442
+ * Returns true if there are pending tool calls awaiting approval.
1443
+ */
1444
+ async requiresApproval() {
1445
+ await this.initStream();
1446
+ // If we have pending tool calls in state, approval is required
1447
+ if (this.currentState?.status === 'awaiting_approval') {
1448
+ return true;
1449
+ }
1450
+ // Also check if pendingToolCalls is populated
1451
+ return (this.currentState?.pendingToolCalls?.length ?? 0) > 0;
1452
+ }
1453
+ /**
1454
+ * Get the pending tool calls that require approval.
1455
+ * Returns empty array if no approvals needed.
1456
+ */
1457
+ async getPendingToolCalls() {
1458
+ await this.initStream();
1459
+ // Try to trigger tool execution to populate pending calls
1460
+ if (!this.isResumingFromApproval) {
1461
+ await this.executeToolsIfNeeded();
1462
+ }
1463
+ return (this.currentState?.pendingToolCalls ?? []);
1464
+ }
1465
+ /**
1466
+ * Get the current conversation state.
1467
+ * Useful for inspection, debugging, or custom persistence.
1468
+ * Note: This returns the raw ConversationState for inspection only.
1469
+ * To resume a conversation, use the StateAccessor pattern.
1470
+ */
1471
+ async getState() {
1472
+ await this.initStream();
1473
+ // Ensure tool execution has been attempted (to populate final state)
1474
+ if (!this.isResumingFromApproval) {
1475
+ await this.executeToolsIfNeeded();
1476
+ }
1477
+ if (!this.currentState) {
1478
+ throw new Error('State not initialized. Make sure a StateAccessor was provided to callModel.');
1479
+ }
1480
+ return this.currentState;
1481
+ }
1482
+ }
1483
+ //# sourceMappingURL=model-result.js.map