@openrouter/sdk 0.3.14 → 0.3.16

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 (136) hide show
  1. package/esm/funcs/analyticsGetUserActivity.d.ts +1 -1
  2. package/esm/funcs/analyticsGetUserActivity.js +1 -1
  3. package/esm/funcs/apiKeysCreate.d.ts +3 -0
  4. package/esm/funcs/apiKeysCreate.js +3 -0
  5. package/esm/funcs/apiKeysDelete.d.ts +3 -0
  6. package/esm/funcs/apiKeysDelete.js +3 -0
  7. package/esm/funcs/apiKeysGet.d.ts +3 -0
  8. package/esm/funcs/apiKeysGet.js +3 -0
  9. package/esm/funcs/apiKeysList.d.ts +3 -0
  10. package/esm/funcs/apiKeysList.js +3 -0
  11. package/esm/funcs/apiKeysUpdate.d.ts +3 -0
  12. package/esm/funcs/apiKeysUpdate.js +3 -0
  13. package/esm/funcs/call-model.js +9 -6
  14. package/esm/funcs/creditsGetCredits.d.ts +1 -1
  15. package/esm/funcs/creditsGetCredits.js +1 -1
  16. package/esm/funcs/guardrailsBulkAssignKeys.d.ts +18 -0
  17. package/esm/funcs/guardrailsBulkAssignKeys.js +89 -0
  18. package/esm/funcs/guardrailsBulkAssignMembers.d.ts +18 -0
  19. package/esm/funcs/guardrailsBulkAssignMembers.js +89 -0
  20. package/esm/funcs/guardrailsBulkUnassignKeys.d.ts +18 -0
  21. package/esm/funcs/guardrailsBulkUnassignKeys.js +89 -0
  22. package/esm/funcs/guardrailsBulkUnassignMembers.d.ts +18 -0
  23. package/esm/funcs/guardrailsBulkUnassignMembers.js +89 -0
  24. package/esm/funcs/guardrailsCreate.d.ts +18 -0
  25. package/esm/funcs/guardrailsCreate.js +83 -0
  26. package/esm/funcs/guardrailsDelete.d.ts +18 -0
  27. package/esm/funcs/guardrailsDelete.js +88 -0
  28. package/esm/funcs/{parametersGetParameters.d.ts → guardrailsGet.d.ts} +6 -3
  29. package/esm/funcs/guardrailsGet.js +88 -0
  30. package/esm/funcs/guardrailsList.d.ts +18 -0
  31. package/esm/funcs/guardrailsList.js +87 -0
  32. package/esm/funcs/guardrailsListGuardrailKeyAssignments.d.ts +18 -0
  33. package/esm/funcs/guardrailsListGuardrailKeyAssignments.js +93 -0
  34. package/esm/funcs/guardrailsListGuardrailMemberAssignments.d.ts +18 -0
  35. package/esm/funcs/guardrailsListGuardrailMemberAssignments.js +93 -0
  36. package/esm/funcs/guardrailsListKeyAssignments.d.ts +18 -0
  37. package/esm/funcs/guardrailsListKeyAssignments.js +87 -0
  38. package/esm/funcs/guardrailsListMemberAssignments.d.ts +18 -0
  39. package/esm/funcs/guardrailsListMemberAssignments.js +87 -0
  40. package/esm/funcs/guardrailsUpdate.d.ts +18 -0
  41. package/esm/funcs/{parametersGetParameters.js → guardrailsUpdate.js} +24 -32
  42. package/esm/index.d.ts +4 -3
  43. package/esm/index.js +3 -1
  44. package/esm/lib/async-params.d.ts +46 -6
  45. package/esm/lib/async-params.js +10 -2
  46. package/esm/lib/config.d.ts +2 -4
  47. package/esm/lib/config.js +2 -2
  48. package/esm/lib/conversation-state.d.ts +61 -0
  49. package/esm/lib/conversation-state.js +207 -0
  50. package/esm/lib/model-result.d.ts +179 -4
  51. package/esm/lib/model-result.js +719 -191
  52. package/esm/lib/tool-executor.js +37 -13
  53. package/esm/lib/tool-types.d.ts +135 -2
  54. package/esm/lib/tool-types.js +19 -0
  55. package/esm/lib/tool.d.ts +21 -1
  56. package/esm/lib/tool.js +7 -0
  57. package/esm/models/assistantmessage.d.ts +31 -0
  58. package/esm/models/assistantmessage.js +43 -0
  59. package/esm/models/chatmessagecontentitemimage.d.ts +8 -8
  60. package/esm/models/chatmessagecontentitemimage.js +8 -9
  61. package/esm/models/chatresponsechoice.d.ts +0 -2
  62. package/esm/models/chatresponsechoice.js +0 -3
  63. package/esm/models/chatstreamingmessagechunk.d.ts +2 -2
  64. package/esm/models/chatstreamingmessagechunk.js +2 -2
  65. package/esm/models/index.d.ts +1 -1
  66. package/esm/models/index.js +1 -1
  67. package/esm/models/model.d.ts +4 -0
  68. package/esm/models/model.js +2 -0
  69. package/esm/models/operations/bulkassignkeystoguardrail.d.ts +44 -0
  70. package/esm/models/operations/bulkassignkeystoguardrail.js +42 -0
  71. package/esm/models/operations/bulkassignmemberstoguardrail.d.ts +44 -0
  72. package/esm/models/operations/bulkassignmemberstoguardrail.js +42 -0
  73. package/esm/models/operations/bulkunassignkeysfromguardrail.d.ts +44 -0
  74. package/esm/models/operations/bulkunassignkeysfromguardrail.js +42 -0
  75. package/esm/models/operations/bulkunassignmembersfromguardrail.d.ts +44 -0
  76. package/esm/models/operations/bulkunassignmembersfromguardrail.js +42 -0
  77. package/esm/models/operations/createguardrail.d.ts +136 -0
  78. package/esm/models/operations/createguardrail.js +85 -0
  79. package/esm/models/operations/deleteguardrail.d.ts +29 -0
  80. package/esm/models/operations/deleteguardrail.js +21 -0
  81. package/esm/models/operations/getguardrail.d.ts +92 -0
  82. package/esm/models/operations/getguardrail.js +60 -0
  83. package/esm/models/operations/getmodels.d.ts +28 -1
  84. package/esm/models/operations/getmodels.js +22 -1
  85. package/esm/models/operations/index.d.ts +13 -1
  86. package/esm/models/operations/index.js +13 -1
  87. package/esm/models/operations/listguardrailkeyassignments.d.ts +76 -0
  88. package/esm/models/operations/listguardrailkeyassignments.js +51 -0
  89. package/esm/models/operations/listguardrailmemberassignments.d.ts +72 -0
  90. package/esm/models/operations/listguardrailmemberassignments.js +49 -0
  91. package/esm/models/operations/listguardrails.d.ts +98 -0
  92. package/esm/models/operations/listguardrails.js +66 -0
  93. package/esm/models/operations/listkeyassignments.d.ts +71 -0
  94. package/esm/models/operations/listkeyassignments.js +50 -0
  95. package/esm/models/operations/listmemberassignments.d.ts +67 -0
  96. package/esm/models/operations/listmemberassignments.js +48 -0
  97. package/esm/models/operations/updateguardrail.d.ts +151 -0
  98. package/esm/models/operations/updateguardrail.js +97 -0
  99. package/esm/models/outputmodality.d.ts +1 -0
  100. package/esm/models/outputmodality.js +1 -0
  101. package/esm/models/percentilelatencycutoffs.js +1 -1
  102. package/esm/models/percentilestats.js +1 -1
  103. package/esm/models/percentilethroughputcutoffs.js +1 -1
  104. package/esm/models/preferredmaxlatency.js +1 -1
  105. package/esm/models/preferredminthroughput.js +1 -1
  106. package/esm/models/publicendpoint.d.ts +8 -0
  107. package/esm/models/publicendpoint.js +4 -0
  108. package/esm/models/publicpricing.d.ts +4 -0
  109. package/esm/models/publicpricing.js +2 -0
  110. package/esm/models/responseinputvideo.js +1 -1
  111. package/esm/models/responsesoutputmodality.js +1 -1
  112. package/esm/models/schema2.d.ts +92 -0
  113. package/esm/models/schema2.js +109 -0
  114. package/esm/sdk/analytics.d.ts +1 -1
  115. package/esm/sdk/analytics.js +1 -1
  116. package/esm/sdk/apikeys.d.ts +15 -0
  117. package/esm/sdk/apikeys.js +15 -0
  118. package/esm/sdk/credits.d.ts +1 -1
  119. package/esm/sdk/credits.js +1 -1
  120. package/esm/sdk/guardrails.d.ts +96 -0
  121. package/esm/sdk/guardrails.js +139 -0
  122. package/esm/sdk/sdk.d.ts +3 -3
  123. package/esm/sdk/sdk.js +4 -4
  124. package/esm/types/index.d.ts +2 -0
  125. package/esm/types/index.js +1 -0
  126. package/esm/types/models.d.ts +25 -0
  127. package/esm/types/models.js +10 -0
  128. package/jsr.json +1 -1
  129. package/package.json +12 -10
  130. package/scripts/check-types.js +127 -0
  131. package/esm/models/operations/getparameters.d.ts +0 -87
  132. package/esm/models/operations/getparameters.js +0 -73
  133. package/esm/models/schema3.d.ts +0 -51
  134. package/esm/models/schema3.js +0 -62
  135. package/esm/sdk/parameters.d.ts +0 -9
  136. package/esm/sdk/parameters.js +0 -16
@@ -1,20 +1,34 @@
1
1
  import { ToolEventBroadcaster } from './tool-event-broadcaster.js';
2
2
  import { betaResponsesSend } from '../funcs/betaResponsesSend.js';
3
3
  import { hasAsyncFunctions, resolveAsyncFunctions, } from './async-params.js';
4
+ import { appendToMessages, createInitialState, createRejectedResult, createUnsentResult, extractTextFromResponse as extractTextFromResponseState, partitionToolCalls, unsentResultsToAPIFormat, updateState, } from './conversation-state.js';
4
5
  import { ReusableReadableStream } from './reusable-stream.js';
5
6
  import { buildResponsesMessageStream, buildToolCallStream, consumeStreamForCompletion, extractReasoningDeltas, extractResponsesMessageFromResponse, extractTextDeltas, extractTextFromResponse, extractToolCallsFromResponse, extractToolDeltas, } from './stream-transformers.js';
6
7
  import { executeTool } from './tool-executor.js';
7
8
  import { executeNextTurnParamsFunctions, applyNextTurnParamsToRequest } from './next-turn-params.js';
8
9
  import { hasExecuteFunction } from './tool-types.js';
9
- import { isStopConditionMet } from './stop-conditions.js';
10
+ import { isStopConditionMet, stepCountIs } from './stop-conditions.js';
11
+ /**
12
+ * Default maximum number of tool execution steps if no stopWhen is specified.
13
+ * This prevents infinite loops in tool execution.
14
+ */
15
+ const DEFAULT_MAX_STEPS = 5;
10
16
  /**
11
17
  * Type guard for stream event with toReadableStream method
18
+ * Checks constructor name, prototype, and method availability
12
19
  */
13
20
  function isEventStream(value) {
14
- return (value !== null &&
15
- typeof value === 'object' &&
16
- 'toReadableStream' in value &&
17
- typeof value.toReadableStream === 'function');
21
+ if (value === null || typeof value !== 'object') {
22
+ return false;
23
+ }
24
+ // Check constructor name for EventStream
25
+ const constructorName = Object.getPrototypeOf(value)?.constructor?.name;
26
+ if (constructorName === 'EventStream') {
27
+ return true;
28
+ }
29
+ // Fallback: check for toReadableStream method (may be on prototype)
30
+ const maybeStream = value;
31
+ return typeof maybeStream.toReadableStream === 'function';
18
32
  }
19
33
  /**
20
34
  * Type guard for output items with a type property
@@ -47,7 +61,6 @@ function hasTypeProperty(item) {
47
61
  export class ModelResult {
48
62
  constructor(options) {
49
63
  this.reusableStream = null;
50
- this.streamPromise = null;
51
64
  this.textPromise = null;
52
65
  this.initPromise = null;
53
66
  this.toolExecutionPromise = null;
@@ -56,11 +69,31 @@ export class ModelResult {
56
69
  this.allToolExecutionRounds = [];
57
70
  // Track resolved request after async function resolution
58
71
  this.resolvedRequest = null;
72
+ // State management for multi-turn conversations
73
+ this.stateAccessor = null;
74
+ this.currentState = null;
75
+ this.requireApprovalFn = null;
76
+ this.approvedToolCalls = [];
77
+ this.rejectedToolCalls = [];
78
+ this.isResumingFromApproval = false;
59
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 ?? [];
60
92
  }
61
93
  /**
62
94
  * Get or create the tool event broadcaster (lazy initialization).
63
95
  * Ensures only one broadcaster exists for the lifetime of this ModelResult.
96
+ * Broadcasts both preliminary results and final tool results.
64
97
  */
65
98
  ensureBroadcaster() {
66
99
  if (!this.toolEventBroadcaster) {
@@ -70,15 +103,389 @@ export class ModelResult {
70
103
  }
71
104
  /**
72
105
  * Type guard to check if a value is a non-streaming response
106
+ * Only requires 'output' field and absence of 'toReadableStream' method
73
107
  */
74
108
  isNonStreamingResponse(value) {
75
109
  return (value !== null &&
76
110
  typeof value === 'object' &&
77
- 'id' in value &&
78
- 'object' in value &&
79
111
  'output' in value &&
80
112
  !('toReadableStream' in value));
81
113
  }
114
+ // =========================================================================
115
+ // Extracted Helper Methods for executeToolsIfNeeded
116
+ // =========================================================================
117
+ /**
118
+ * Get initial response from stream or cached final response.
119
+ * Consumes the stream to completion if needed to extract the response.
120
+ *
121
+ * @returns The complete non-streaming response
122
+ * @throws Error if neither stream nor response has been initialized
123
+ */
124
+ async getInitialResponse() {
125
+ if (this.finalResponse) {
126
+ return this.finalResponse;
127
+ }
128
+ if (this.reusableStream) {
129
+ return consumeStreamForCompletion(this.reusableStream);
130
+ }
131
+ throw new Error('Neither stream nor response initialized');
132
+ }
133
+ /**
134
+ * Save response output to state.
135
+ * Appends the response output to the message history and records the response ID.
136
+ *
137
+ * @param response - The API response to save
138
+ */
139
+ async saveResponseToState(response) {
140
+ if (!this.stateAccessor || !this.currentState)
141
+ return;
142
+ const outputItems = Array.isArray(response.output)
143
+ ? response.output
144
+ : [response.output];
145
+ await this.saveStateSafely({
146
+ messages: appendToMessages(this.currentState.messages, outputItems),
147
+ previousResponseId: response.id,
148
+ });
149
+ }
150
+ /**
151
+ * Mark state as complete.
152
+ * Sets the conversation status to 'complete' indicating no further tool execution is needed.
153
+ */
154
+ async markStateComplete() {
155
+ await this.saveStateSafely({ status: 'complete' });
156
+ }
157
+ /**
158
+ * Save tool results to state.
159
+ * Appends tool execution results to the message history for multi-turn context.
160
+ *
161
+ * @param toolResults - The tool execution results to save
162
+ */
163
+ async saveToolResultsToState(toolResults) {
164
+ if (!this.currentState)
165
+ return;
166
+ await this.saveStateSafely({
167
+ messages: appendToMessages(this.currentState.messages, toolResults),
168
+ });
169
+ }
170
+ /**
171
+ * Check if execution should be interrupted by external signal.
172
+ * Polls the state accessor for interruption flags set by external processes.
173
+ *
174
+ * @param currentResponse - The current response to save as partial state
175
+ * @returns True if interrupted and caller should exit, false to continue
176
+ */
177
+ async checkForInterruption(currentResponse) {
178
+ if (!this.stateAccessor)
179
+ return false;
180
+ const freshState = await this.stateAccessor.load();
181
+ if (!freshState?.interruptedBy)
182
+ return false;
183
+ // Save partial state
184
+ if (this.currentState) {
185
+ const currentToolCalls = extractToolCallsFromResponse(currentResponse);
186
+ await this.saveStateSafely({
187
+ status: 'interrupted',
188
+ partialResponse: {
189
+ text: extractTextFromResponseState(currentResponse),
190
+ toolCalls: currentToolCalls,
191
+ },
192
+ });
193
+ }
194
+ this.finalResponse = currentResponse;
195
+ return true;
196
+ }
197
+ /**
198
+ * Check if stop conditions are met.
199
+ * Returns true if execution should stop.
200
+ *
201
+ * @remarks
202
+ * Default: stepCountIs(DEFAULT_MAX_STEPS) if no stopWhen is specified.
203
+ * This evaluates stop conditions against the complete step history.
204
+ */
205
+ async shouldStopExecution() {
206
+ const stopWhen = this.options.stopWhen ?? stepCountIs(DEFAULT_MAX_STEPS);
207
+ const stopConditions = Array.isArray(stopWhen)
208
+ ? stopWhen
209
+ : [stopWhen];
210
+ return isStopConditionMet({
211
+ stopConditions,
212
+ steps: this.allToolExecutionRounds.map((round) => ({
213
+ stepType: 'continue',
214
+ text: extractTextFromResponse(round.response),
215
+ toolCalls: round.toolCalls,
216
+ toolResults: round.toolResults.map((tr) => ({
217
+ toolCallId: tr.callId,
218
+ toolName: round.toolCalls.find((tc) => tc.id === tr.callId)?.name ?? '',
219
+ result: JSON.parse(tr.output),
220
+ })),
221
+ response: round.response,
222
+ usage: round.response.usage,
223
+ finishReason: undefined,
224
+ })),
225
+ });
226
+ }
227
+ /**
228
+ * Check if any tool calls have execute functions.
229
+ * Used to determine if automatic tool execution should be attempted.
230
+ *
231
+ * @param toolCalls - The tool calls to check
232
+ * @returns True if at least one tool call has an executable function
233
+ */
234
+ hasExecutableToolCalls(toolCalls) {
235
+ return toolCalls.some((toolCall) => {
236
+ const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
237
+ return tool && hasExecuteFunction(tool);
238
+ });
239
+ }
240
+ /**
241
+ * Execute tools that can auto-execute (don't require approval).
242
+ * Processes tool calls that are approved for automatic execution.
243
+ *
244
+ * @param toolCalls - The tool calls to execute
245
+ * @param turnContext - The current turn context
246
+ * @returns Array of unsent tool results for later submission
247
+ */
248
+ async executeAutoApproveTools(toolCalls, turnContext) {
249
+ const results = [];
250
+ for (const tc of toolCalls) {
251
+ const tool = this.options.tools?.find(t => t.function.name === tc.name);
252
+ if (!tool || !hasExecuteFunction(tool))
253
+ continue;
254
+ const result = await executeTool(tool, tc, turnContext);
255
+ if (result.error) {
256
+ results.push(createRejectedResult(tc.id, String(tc.name), result.error.message));
257
+ }
258
+ else {
259
+ results.push(createUnsentResult(tc.id, String(tc.name), result.result));
260
+ }
261
+ }
262
+ return results;
263
+ }
264
+ /**
265
+ * Check for tools requiring approval and handle accordingly.
266
+ * Partitions tool calls into those needing approval and those that can auto-execute.
267
+ *
268
+ * @param toolCalls - The tool calls to check
269
+ * @param currentRound - The current execution round (1-indexed)
270
+ * @param currentResponse - The current response to save if pausing
271
+ * @returns True if execution should pause for approval, false to continue
272
+ * @throws Error if approval is required but no state accessor is configured
273
+ */
274
+ async handleApprovalCheck(toolCalls, currentRound, currentResponse) {
275
+ if (!this.options.tools)
276
+ return false;
277
+ const turnContext = { numberOfTurns: currentRound };
278
+ const { requiresApproval: needsApproval, autoExecute } = await partitionToolCalls(toolCalls, this.options.tools, turnContext, this.requireApprovalFn ?? undefined);
279
+ if (needsApproval.length === 0)
280
+ return false;
281
+ // Validate: approval requires state accessor
282
+ if (!this.stateAccessor) {
283
+ const toolNames = needsApproval.map(tc => tc.name).join(', ');
284
+ throw new Error(`Tool(s) require approval but no state accessor is configured: ${toolNames}. ` +
285
+ 'Provide a StateAccessor via the "state" parameter to enable approval workflows.');
286
+ }
287
+ // Execute auto-approve tools
288
+ const unsentResults = await this.executeAutoApproveTools(autoExecute, turnContext);
289
+ // Save state with pending approvals
290
+ const stateUpdates = {
291
+ pendingToolCalls: needsApproval,
292
+ status: 'awaiting_approval',
293
+ };
294
+ if (unsentResults.length > 0) {
295
+ stateUpdates.unsentToolResults = unsentResults;
296
+ }
297
+ await this.saveStateSafely(stateUpdates);
298
+ this.finalResponse = currentResponse;
299
+ return true; // Pause for approval
300
+ }
301
+ /**
302
+ * Execute all tools in a single round.
303
+ * Runs each tool call sequentially and collects results for API submission.
304
+ * Emits tool.result events after each tool execution completes.
305
+ *
306
+ * @param toolCalls - The tool calls to execute
307
+ * @param turnContext - The current turn context
308
+ * @returns Array of function call outputs formatted for the API
309
+ */
310
+ async executeToolRound(toolCalls, turnContext) {
311
+ const toolResults = [];
312
+ for (const toolCall of toolCalls) {
313
+ const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
314
+ if (!tool || !hasExecuteFunction(tool))
315
+ continue;
316
+ // Track preliminary results for this specific tool call
317
+ const preliminaryResultsForCall = [];
318
+ // Create callback for real-time preliminary results
319
+ const onPreliminaryResult = this.toolEventBroadcaster
320
+ ? (callId, resultValue) => {
321
+ // Track preliminary results for the tool.result event
322
+ const typedResult = resultValue;
323
+ preliminaryResultsForCall.push(typedResult);
324
+ // Emit preliminary result event
325
+ this.toolEventBroadcaster?.push({
326
+ type: 'preliminary_result',
327
+ toolCallId: callId,
328
+ result: typedResult,
329
+ });
330
+ }
331
+ : undefined;
332
+ const result = await executeTool(tool, toolCall, turnContext, onPreliminaryResult);
333
+ // Emit tool.result event with final result and any preliminary results
334
+ if (this.toolEventBroadcaster) {
335
+ const toolResultEvent = {
336
+ type: 'tool_result',
337
+ toolCallId: toolCall.id,
338
+ result: (result.error ? { error: result.error.message } : result.result),
339
+ ...(preliminaryResultsForCall.length > 0 && { preliminaryResults: preliminaryResultsForCall }),
340
+ };
341
+ this.toolEventBroadcaster.push(toolResultEvent);
342
+ }
343
+ toolResults.push({
344
+ type: 'function_call_output',
345
+ id: `output_${toolCall.id}`,
346
+ callId: toolCall.id,
347
+ output: result.error
348
+ ? JSON.stringify({ error: result.error.message })
349
+ : JSON.stringify(result.result),
350
+ });
351
+ }
352
+ return toolResults;
353
+ }
354
+ /**
355
+ * Resolve async functions for the current turn.
356
+ * Updates the resolved request with turn-specific parameter values.
357
+ *
358
+ * @param turnContext - The turn context for parameter resolution
359
+ */
360
+ async resolveAsyncFunctionsForTurn(turnContext) {
361
+ if (hasAsyncFunctions(this.options.request)) {
362
+ const resolved = await resolveAsyncFunctions(this.options.request, turnContext);
363
+ this.resolvedRequest = { ...resolved, stream: false };
364
+ }
365
+ }
366
+ /**
367
+ * Apply nextTurnParams from executed tools.
368
+ * Allows tools to modify request parameters for subsequent turns.
369
+ *
370
+ * @param toolCalls - The tool calls that were just executed
371
+ */
372
+ async applyNextTurnParams(toolCalls) {
373
+ if (!this.options.tools || toolCalls.length === 0 || !this.resolvedRequest) {
374
+ return;
375
+ }
376
+ const computedParams = await executeNextTurnParamsFunctions(toolCalls, this.options.tools, this.resolvedRequest);
377
+ if (Object.keys(computedParams).length > 0) {
378
+ this.resolvedRequest = applyNextTurnParamsToRequest(this.resolvedRequest, computedParams);
379
+ }
380
+ }
381
+ /**
382
+ * Make a follow-up API request with tool results.
383
+ * Continues the conversation after tool execution.
384
+ *
385
+ * @param currentResponse - The response that contained tool calls
386
+ * @param toolResults - The results from executing those tools
387
+ * @returns The new response from the API
388
+ */
389
+ async makeFollowupRequest(currentResponse, toolResults) {
390
+ // Build new input with tool results
391
+ const newInput = [
392
+ ...(Array.isArray(currentResponse.output)
393
+ ? currentResponse.output
394
+ : [currentResponse.output]),
395
+ ...toolResults,
396
+ ];
397
+ if (!this.resolvedRequest) {
398
+ throw new Error('Request not initialized');
399
+ }
400
+ const newRequest = {
401
+ ...this.resolvedRequest,
402
+ input: newInput,
403
+ stream: false,
404
+ };
405
+ const newResult = await betaResponsesSend(this.options.client, newRequest, this.options.options);
406
+ if (!newResult.ok) {
407
+ throw newResult.error;
408
+ }
409
+ // Handle streaming or non-streaming response
410
+ const value = newResult.value;
411
+ if (isEventStream(value)) {
412
+ const stream = new ReusableReadableStream(value);
413
+ return consumeStreamForCompletion(stream);
414
+ }
415
+ else if (this.isNonStreamingResponse(value)) {
416
+ return value;
417
+ }
418
+ else {
419
+ throw new Error('Unexpected response type from API');
420
+ }
421
+ }
422
+ /**
423
+ * Validate the final response has required fields.
424
+ *
425
+ * @param response - The response to validate
426
+ * @throws Error if response is missing required fields or has invalid output
427
+ */
428
+ validateFinalResponse(response) {
429
+ if (!response?.id || !response?.output) {
430
+ throw new Error('Invalid final response: missing required fields');
431
+ }
432
+ if (!Array.isArray(response.output) || response.output.length === 0) {
433
+ throw new Error('Invalid final response: empty or invalid output');
434
+ }
435
+ }
436
+ /**
437
+ * Resolve async functions in the request for a given turn context.
438
+ * Extracts non-function fields and resolves any async parameter functions.
439
+ *
440
+ * @param context - The turn context for parameter resolution
441
+ * @returns The resolved request without async functions
442
+ */
443
+ async resolveRequestForContext(context) {
444
+ if (hasAsyncFunctions(this.options.request)) {
445
+ return resolveAsyncFunctions(this.options.request, context);
446
+ }
447
+ // Already resolved, extract non-function fields
448
+ // Filter out stopWhen and state-related fields that aren't part of the API request
449
+ const { stopWhen: _, state: _s, requireApproval: _r, approveToolCalls: _a, rejectToolCalls: _rj, ...rest } = this.options.request;
450
+ return rest;
451
+ }
452
+ /**
453
+ * Safely persist state with error handling.
454
+ * Wraps state save operations to ensure failures are properly reported.
455
+ *
456
+ * @param updates - Optional partial state updates to apply before saving
457
+ * @throws Error if state persistence fails
458
+ */
459
+ async saveStateSafely(updates) {
460
+ if (!this.stateAccessor || !this.currentState)
461
+ return;
462
+ if (updates) {
463
+ this.currentState = updateState(this.currentState, updates);
464
+ }
465
+ try {
466
+ await this.stateAccessor.save(this.currentState);
467
+ }
468
+ catch (error) {
469
+ const message = error instanceof Error ? error.message : String(error);
470
+ throw new Error(`Failed to persist conversation state: ${message}`);
471
+ }
472
+ }
473
+ /**
474
+ * Remove optional properties from state when they should be cleared.
475
+ * Uses delete to properly remove optional properties rather than setting undefined.
476
+ *
477
+ * @param props - Array of property names to remove from current state
478
+ */
479
+ clearOptionalStateProperties(props) {
480
+ if (!this.currentState)
481
+ return;
482
+ for (const prop of props) {
483
+ delete this.currentState[prop];
484
+ }
485
+ }
486
+ // =========================================================================
487
+ // Core Methods
488
+ // =========================================================================
82
489
  /**
83
490
  * Initialize the stream if not already started
84
491
  * This is idempotent - multiple calls will return the same promise
@@ -88,23 +495,57 @@ export class ModelResult {
88
495
  return this.initPromise;
89
496
  }
90
497
  this.initPromise = (async () => {
498
+ // Load or create state if accessor provided
499
+ if (this.stateAccessor) {
500
+ const loadedState = await this.stateAccessor.load();
501
+ if (loadedState) {
502
+ this.currentState = loadedState;
503
+ // Check if we're resuming from awaiting_approval with decisions
504
+ if (loadedState.status === 'awaiting_approval' &&
505
+ (this.approvedToolCalls.length > 0 || this.rejectedToolCalls.length > 0)) {
506
+ this.isResumingFromApproval = true;
507
+ await this.processApprovalDecisions();
508
+ return; // Skip normal initialization, we're resuming
509
+ }
510
+ // Check for interruption flag and handle
511
+ if (loadedState.interruptedBy) {
512
+ // Clear interruption flag and continue from saved state
513
+ this.currentState = updateState(loadedState, { status: 'in_progress' });
514
+ this.clearOptionalStateProperties(['interruptedBy']);
515
+ await this.saveStateSafely();
516
+ }
517
+ }
518
+ else {
519
+ this.currentState = createInitialState();
520
+ }
521
+ // Update status to in_progress
522
+ await this.saveStateSafely({ status: 'in_progress' });
523
+ }
91
524
  // Resolve async functions before initial request
92
525
  // Build initial turn context (turn 0 for initial request)
93
526
  const initialContext = {
94
527
  numberOfTurns: 0,
95
528
  };
96
529
  // Resolve any async functions first
97
- let baseRequest;
98
- if (hasAsyncFunctions(this.options.request)) {
99
- baseRequest = await resolveAsyncFunctions(this.options.request, initialContext);
100
- }
101
- else {
102
- // Already resolved, extract non-function fields
103
- // Since request is CallModelInput, we need to filter out stopWhen
104
- // Note: tools are already in API format at this point (converted in callModel())
105
- const { stopWhen: _, ...rest } = this.options.request;
106
- // Cast to ResolvedCallModelInput - we know it's resolved if hasAsyncFunctions returned false
107
- baseRequest = rest;
530
+ let baseRequest = await this.resolveRequestForContext(initialContext);
531
+ // If we have state with existing messages, use those as input
532
+ if (this.currentState && this.currentState.messages &&
533
+ Array.isArray(this.currentState.messages) && this.currentState.messages.length > 0) {
534
+ // Append new input to existing messages
535
+ const newInput = baseRequest.input;
536
+ if (newInput) {
537
+ const inputArray = Array.isArray(newInput) ? newInput : [newInput];
538
+ baseRequest = {
539
+ ...baseRequest,
540
+ input: appendToMessages(this.currentState.messages, inputArray),
541
+ };
542
+ }
543
+ else {
544
+ baseRequest = {
545
+ ...baseRequest,
546
+ input: this.currentState.messages,
547
+ };
548
+ }
108
549
  }
109
550
  // Store resolved request with stream mode
110
551
  this.resolvedRequest = {
@@ -113,22 +554,145 @@ export class ModelResult {
113
554
  };
114
555
  // Force stream mode for initial request
115
556
  const request = this.resolvedRequest;
116
- // Create the stream promise
117
- this.streamPromise = betaResponsesSend(this.options.client, request, this.options.options).then((result) => {
118
- if (!result.ok) {
119
- throw result.error;
120
- }
121
- // When stream: true, the API returns EventStream
122
- // TypeScript can't narrow the union type based on runtime parameter values,
123
- // so we assert the type here based on our knowledge that stream=true
124
- return result.value;
125
- });
126
- // Wait for the stream and create the reusable stream
127
- const eventStream = await this.streamPromise;
128
- this.reusableStream = new ReusableReadableStream(eventStream);
557
+ // Make the API request
558
+ const apiResult = await betaResponsesSend(this.options.client, request, this.options.options);
559
+ if (!apiResult.ok) {
560
+ throw apiResult.error;
561
+ }
562
+ // Handle both streaming and non-streaming responses
563
+ // The API may return a non-streaming response even when stream: true is requested
564
+ if (isEventStream(apiResult.value)) {
565
+ this.reusableStream = new ReusableReadableStream(apiResult.value);
566
+ }
567
+ else if (this.isNonStreamingResponse(apiResult.value)) {
568
+ // API returned a complete response directly - use it as the final response
569
+ this.finalResponse = apiResult.value;
570
+ }
571
+ else {
572
+ throw new Error('Unexpected response type from API');
573
+ }
129
574
  })();
130
575
  return this.initPromise;
131
576
  }
577
+ /**
578
+ * Process approval/rejection decisions and resume execution
579
+ */
580
+ async processApprovalDecisions() {
581
+ if (!this.currentState || !this.stateAccessor) {
582
+ throw new Error('Cannot process approval decisions without state');
583
+ }
584
+ const pendingCalls = this.currentState.pendingToolCalls ?? [];
585
+ const unsentResults = [...(this.currentState.unsentToolResults ?? [])];
586
+ // Build turn context - numberOfTurns represents the current turn (1-indexed after initial)
587
+ const turnContext = {
588
+ numberOfTurns: this.allToolExecutionRounds.length + 1,
589
+ };
590
+ // Process approvals - execute the approved tools
591
+ for (const callId of this.approvedToolCalls) {
592
+ const toolCall = pendingCalls.find(tc => tc.id === callId);
593
+ if (!toolCall)
594
+ continue;
595
+ const tool = this.options.tools?.find(t => t.function.name === toolCall.name);
596
+ if (!tool || !hasExecuteFunction(tool)) {
597
+ // Can't execute, create error result
598
+ unsentResults.push(createRejectedResult(callId, String(toolCall.name), 'Tool not found or not executable'));
599
+ continue;
600
+ }
601
+ const result = await executeTool(tool, toolCall, turnContext);
602
+ if (result.error) {
603
+ unsentResults.push(createRejectedResult(callId, String(toolCall.name), result.error.message));
604
+ }
605
+ else {
606
+ unsentResults.push(createUnsentResult(callId, String(toolCall.name), result.result));
607
+ }
608
+ }
609
+ // Process rejections
610
+ for (const callId of this.rejectedToolCalls) {
611
+ const toolCall = pendingCalls.find(tc => tc.id === callId);
612
+ if (!toolCall)
613
+ continue;
614
+ unsentResults.push(createRejectedResult(callId, String(toolCall.name), 'Rejected by user'));
615
+ }
616
+ // Remove processed calls from pending
617
+ const processedIds = new Set([...this.approvedToolCalls, ...this.rejectedToolCalls]);
618
+ const remainingPending = pendingCalls.filter(tc => !processedIds.has(tc.id));
619
+ // Update state - conditionally include optional properties only if they have values
620
+ const stateUpdates = {
621
+ status: remainingPending.length > 0 ? 'awaiting_approval' : 'in_progress',
622
+ };
623
+ if (remainingPending.length > 0) {
624
+ stateUpdates.pendingToolCalls = remainingPending;
625
+ }
626
+ if (unsentResults.length > 0) {
627
+ stateUpdates.unsentToolResults = unsentResults;
628
+ }
629
+ await this.saveStateSafely(stateUpdates);
630
+ // Clear optional properties if they should be empty
631
+ const propsToClear = [];
632
+ if (remainingPending.length === 0)
633
+ propsToClear.push('pendingToolCalls');
634
+ if (unsentResults.length === 0)
635
+ propsToClear.push('unsentToolResults');
636
+ if (propsToClear.length > 0) {
637
+ this.clearOptionalStateProperties(propsToClear);
638
+ await this.saveStateSafely();
639
+ }
640
+ // If we still have pending approvals, stop here
641
+ if (remainingPending.length > 0) {
642
+ return;
643
+ }
644
+ // Otherwise, continue with tool execution using unsent results
645
+ await this.continueWithUnsentResults();
646
+ }
647
+ /**
648
+ * Continue execution with unsent tool results
649
+ */
650
+ async continueWithUnsentResults() {
651
+ if (!this.currentState || !this.stateAccessor)
652
+ return;
653
+ const unsentResults = this.currentState.unsentToolResults ?? [];
654
+ if (unsentResults.length === 0)
655
+ return;
656
+ // Convert to API format
657
+ const toolOutputs = unsentResultsToAPIFormat(unsentResults);
658
+ // Build new input with tool results
659
+ const currentMessages = this.currentState.messages;
660
+ const newInput = appendToMessages(currentMessages, toolOutputs);
661
+ // Clear unsent results from state
662
+ this.currentState = updateState(this.currentState, {
663
+ messages: newInput,
664
+ });
665
+ this.clearOptionalStateProperties(['unsentToolResults']);
666
+ await this.saveStateSafely();
667
+ // Build request with the updated input
668
+ // numberOfTurns represents the current turn number (1-indexed after initial)
669
+ const turnContext = {
670
+ numberOfTurns: this.allToolExecutionRounds.length + 1,
671
+ };
672
+ const baseRequest = await this.resolveRequestForContext(turnContext);
673
+ // Create request with the accumulated messages
674
+ const request = {
675
+ ...baseRequest,
676
+ input: newInput,
677
+ stream: true,
678
+ };
679
+ this.resolvedRequest = request;
680
+ // Make the API request
681
+ const apiResult = await betaResponsesSend(this.options.client, request, this.options.options);
682
+ if (!apiResult.ok) {
683
+ throw apiResult.error;
684
+ }
685
+ // Handle both streaming and non-streaming responses
686
+ if (isEventStream(apiResult.value)) {
687
+ this.reusableStream = new ReusableReadableStream(apiResult.value);
688
+ }
689
+ else if (this.isNonStreamingResponse(apiResult.value)) {
690
+ this.finalResponse = apiResult.value;
691
+ }
692
+ else {
693
+ throw new Error('Unexpected response type from API');
694
+ }
695
+ }
132
696
  /**
133
697
  * Execute tools automatically if they are provided and have execute functions
134
698
  * This is idempotent - multiple calls will return the same promise
@@ -139,179 +703,81 @@ export class ModelResult {
139
703
  }
140
704
  this.toolExecutionPromise = (async () => {
141
705
  await this.initStream();
142
- if (!this.reusableStream) {
143
- throw new Error('Stream not initialized');
706
+ // If resuming from approval and still pending, don't continue
707
+ if (this.isResumingFromApproval && this.currentState?.status === 'awaiting_approval') {
708
+ return;
144
709
  }
145
- // Note: Async functions already resolved in initStream()
146
- // Get the initial response
147
- const initialResponse = await consumeStreamForCompletion(this.reusableStream);
148
- // Check if we have tools and if auto-execution is enabled
149
- const shouldAutoExecute = this.options.tools &&
150
- this.options.tools.length > 0 &&
151
- initialResponse.output.some((item) => hasTypeProperty(item) && item.type === 'function_call');
152
- if (!shouldAutoExecute) {
153
- // No tools to execute, use initial response
154
- this.finalResponse = initialResponse;
710
+ // Get initial response
711
+ let currentResponse = await this.getInitialResponse();
712
+ // Save initial response to state
713
+ await this.saveResponseToState(currentResponse);
714
+ // Check if tools should be executed
715
+ const hasToolCalls = currentResponse.output.some((item) => hasTypeProperty(item) && item.type === 'function_call');
716
+ if (!this.options.tools?.length || !hasToolCalls) {
717
+ this.finalResponse = currentResponse;
718
+ await this.markStateComplete();
155
719
  return;
156
720
  }
157
- // Extract tool calls
158
- const toolCalls = extractToolCallsFromResponse(initialResponse);
159
- // Check if any have execute functions
160
- const executableTools = toolCalls.filter((toolCall) => {
161
- const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
162
- return tool && hasExecuteFunction(tool);
163
- });
164
- if (executableTools.length === 0) {
165
- // No executable tools, use initial response
166
- this.finalResponse = initialResponse;
721
+ // Extract and check tool calls
722
+ const toolCalls = extractToolCallsFromResponse(currentResponse);
723
+ // Check for approval requirements
724
+ if (await this.handleApprovalCheck(toolCalls, 0, currentResponse)) {
725
+ return; // Paused for approval
726
+ }
727
+ if (!this.hasExecutableToolCalls(toolCalls)) {
728
+ this.finalResponse = currentResponse;
729
+ await this.markStateComplete();
167
730
  return;
168
731
  }
169
- let currentResponse = initialResponse;
732
+ // Main execution loop
170
733
  let currentRound = 0;
171
734
  while (true) {
172
- // Check stopWhen conditions
173
- if (this.options.stopWhen) {
174
- const stopConditions = Array.isArray(this.options.stopWhen)
175
- ? this.options.stopWhen
176
- : [this.options.stopWhen];
177
- const shouldStop = await isStopConditionMet({
178
- stopConditions,
179
- steps: this.allToolExecutionRounds.map((round) => ({
180
- stepType: 'continue',
181
- text: extractTextFromResponse(round.response),
182
- toolCalls: round.toolCalls,
183
- toolResults: round.toolResults.map((tr) => ({
184
- toolCallId: tr.callId,
185
- toolName: round.toolCalls.find((tc) => tc.id === tr.callId)?.name ?? '',
186
- result: JSON.parse(tr.output),
187
- })),
188
- response: round.response,
189
- usage: round.response.usage,
190
- finishReason: undefined, // OpenResponsesNonStreamingResponse doesn't have finishReason
191
- })),
192
- });
193
- if (shouldStop) {
194
- break;
195
- }
735
+ // Check for external interruption
736
+ if (await this.checkForInterruption(currentResponse)) {
737
+ return;
738
+ }
739
+ // Check stop conditions
740
+ if (await this.shouldStopExecution()) {
741
+ break;
196
742
  }
197
743
  const currentToolCalls = extractToolCallsFromResponse(currentResponse);
198
744
  if (currentToolCalls.length === 0) {
199
745
  break;
200
746
  }
201
- const hasExecutable = currentToolCalls.some((toolCall) => {
202
- const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
203
- return tool && hasExecuteFunction(tool);
204
- });
205
- if (!hasExecutable) {
747
+ // Check for approval requirements
748
+ if (await this.handleApprovalCheck(currentToolCalls, currentRound + 1, currentResponse)) {
749
+ return;
750
+ }
751
+ if (!this.hasExecutableToolCalls(currentToolCalls)) {
206
752
  break;
207
753
  }
208
- // Build turn context for this round (for async parameter resolution only)
209
- const turnContext = {
210
- numberOfTurns: currentRound + 1, // 1-indexed
211
- };
754
+ // Build turn context
755
+ const turnContext = { numberOfTurns: currentRound + 1 };
212
756
  // Resolve async functions for this turn
213
- if (hasAsyncFunctions(this.options.request)) {
214
- const resolved = await resolveAsyncFunctions(this.options.request, turnContext);
215
- // Update resolved request with new values
216
- this.resolvedRequest = {
217
- ...resolved,
218
- stream: false, // Tool execution turns don't need streaming
219
- };
220
- }
221
- // Execute all tool calls
222
- const toolResults = [];
223
- for (const toolCall of currentToolCalls) {
224
- const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
225
- if (!tool || !hasExecuteFunction(tool)) {
226
- continue;
227
- }
228
- // Create callback for real-time preliminary results
229
- const onPreliminaryResult = this.toolEventBroadcaster
230
- ? (callId, resultValue) => {
231
- this.toolEventBroadcaster?.push({
232
- type: 'preliminary_result',
233
- toolCallId: callId,
234
- result: resultValue,
235
- });
236
- }
237
- : undefined;
238
- const result = await executeTool(tool, toolCall, turnContext, onPreliminaryResult);
239
- toolResults.push({
240
- type: 'function_call_output',
241
- id: `output_${toolCall.id}`,
242
- callId: toolCall.id,
243
- output: result.error
244
- ? JSON.stringify({
245
- error: result.error.message,
246
- })
247
- : JSON.stringify(result.result),
248
- });
249
- }
250
- // Store execution round info including tool results
757
+ await this.resolveAsyncFunctionsForTurn(turnContext);
758
+ // Execute tools
759
+ const toolResults = await this.executeToolRound(currentToolCalls, turnContext);
760
+ // Track execution round
251
761
  this.allToolExecutionRounds.push({
252
762
  round: currentRound,
253
763
  toolCalls: currentToolCalls,
254
764
  response: currentResponse,
255
765
  toolResults,
256
766
  });
257
- // Execute nextTurnParams functions for tools that were called
258
- if (this.options.tools && currentToolCalls.length > 0) {
259
- if (!this.resolvedRequest) {
260
- throw new Error('Request not initialized');
261
- }
262
- const computedParams = await executeNextTurnParamsFunctions(currentToolCalls, this.options.tools, this.resolvedRequest);
263
- // Apply computed parameters to the resolved request for next turn
264
- if (Object.keys(computedParams).length > 0) {
265
- this.resolvedRequest = applyNextTurnParamsToRequest(this.resolvedRequest, computedParams);
266
- }
267
- }
268
- // Build new input with tool results
269
- // For the Responses API, we need to include the tool results in the input
270
- const newInput = [
271
- ...(Array.isArray(currentResponse.output)
272
- ? currentResponse.output
273
- : [
274
- currentResponse.output,
275
- ]),
276
- ...toolResults,
277
- ];
278
- // Make new request with tool results
279
- if (!this.resolvedRequest) {
280
- throw new Error('Request not initialized');
281
- }
282
- const newRequest = {
283
- ...this.resolvedRequest,
284
- input: newInput,
285
- stream: false,
286
- };
287
- const newResult = await betaResponsesSend(this.options.client, newRequest, this.options.options);
288
- if (!newResult.ok) {
289
- throw newResult.error;
290
- }
291
- // Handle the result - it might be a stream or a response
292
- const value = newResult.value;
293
- if (isEventStream(value)) {
294
- // It's a stream, consume it
295
- const stream = new ReusableReadableStream(value);
296
- currentResponse = await consumeStreamForCompletion(stream);
297
- }
298
- else if (this.isNonStreamingResponse(value)) {
299
- currentResponse = value;
300
- }
301
- else {
302
- throw new Error('Unexpected response type from API');
303
- }
767
+ // Save tool results to state
768
+ await this.saveToolResultsToState(toolResults);
769
+ // Apply nextTurnParams
770
+ await this.applyNextTurnParams(currentToolCalls);
771
+ // Make follow-up request
772
+ currentResponse = await this.makeFollowupRequest(currentResponse, toolResults);
773
+ // Save new response to state
774
+ await this.saveResponseToState(currentResponse);
304
775
  currentRound++;
305
776
  }
306
- // Validate the final response has required fields
307
- if (!currentResponse || !currentResponse.id || !currentResponse.output) {
308
- throw new Error('Invalid final response: missing required fields');
309
- }
310
- // Ensure the response is in a completed state (has output content)
311
- if (!Array.isArray(currentResponse.output) || currentResponse.output.length === 0) {
312
- throw new Error('Invalid final response: empty or invalid output');
313
- }
777
+ // Validate and finalize
778
+ this.validateFinalResponse(currentResponse);
314
779
  this.finalResponse = currentResponse;
780
+ await this.markStateComplete();
315
781
  })();
316
782
  return this.toolExecutionPromise;
317
783
  }
@@ -351,7 +817,7 @@ export class ModelResult {
351
817
  /**
352
818
  * Stream all response events as they arrive.
353
819
  * Multiple consumers can iterate over this stream concurrently.
354
- * Preliminary tool results are streamed in REAL-TIME as generator tools yield.
820
+ * Preliminary tool results and tool results are streamed in REAL-TIME as generator tools yield.
355
821
  */
356
822
  getFullResponsesStream() {
357
823
  return async function* () {
@@ -371,14 +837,25 @@ export class ModelResult {
371
837
  for await (const event of consumer) {
372
838
  yield event;
373
839
  }
374
- // Yield tool preliminary results as they arrive (real-time!)
840
+ // Yield tool events as they arrive (real-time!)
375
841
  for await (const event of toolEventConsumer) {
376
- yield {
377
- type: 'tool.preliminary_result',
378
- toolCallId: event.toolCallId,
379
- result: event.result,
380
- timestamp: Date.now(),
381
- };
842
+ if (event.type === 'preliminary_result') {
843
+ yield {
844
+ type: 'tool.preliminary_result',
845
+ toolCallId: event.toolCallId,
846
+ result: event.result,
847
+ timestamp: Date.now(),
848
+ };
849
+ }
850
+ else if (event.type === 'tool_result') {
851
+ yield {
852
+ type: 'tool.result',
853
+ toolCallId: event.toolCallId,
854
+ result: event.result,
855
+ timestamp: Date.now(),
856
+ ...(event.preliminaryResults && { preliminaryResults: event.preliminaryResults }),
857
+ };
858
+ }
382
859
  }
383
860
  // Ensure execution completed (handles errors)
384
861
  await executionPromise;
@@ -468,9 +945,11 @@ export class ModelResult {
468
945
  content: delta,
469
946
  };
470
947
  }
471
- // Yield tool events as they arrive (real-time!)
948
+ // Yield only preliminary_result events (filter out tool_result events)
472
949
  for await (const event of toolEventConsumer) {
473
- yield event;
950
+ if (event.type === 'preliminary_result') {
951
+ yield event;
952
+ }
474
953
  }
475
954
  // Ensure execution completed (handles errors)
476
955
  await executionPromise;
@@ -484,6 +963,10 @@ export class ModelResult {
484
963
  */
485
964
  async getToolCalls() {
486
965
  await this.initStream();
966
+ // Handle non-streaming response case - use finalResponse directly
967
+ if (this.finalResponse) {
968
+ return extractToolCallsFromResponse(this.finalResponse);
969
+ }
487
970
  if (!this.reusableStream) {
488
971
  throw new Error('Stream not initialized');
489
972
  }
@@ -511,5 +994,50 @@ export class ModelResult {
511
994
  await this.reusableStream.cancel();
512
995
  }
513
996
  }
997
+ // =========================================================================
998
+ // Multi-Turn Conversation State Methods
999
+ // =========================================================================
1000
+ /**
1001
+ * Check if the conversation requires human approval to continue.
1002
+ * Returns true if there are pending tool calls awaiting approval.
1003
+ */
1004
+ async requiresApproval() {
1005
+ await this.initStream();
1006
+ // If we have pending tool calls in state, approval is required
1007
+ if (this.currentState?.status === 'awaiting_approval') {
1008
+ return true;
1009
+ }
1010
+ // Also check if pendingToolCalls is populated
1011
+ return (this.currentState?.pendingToolCalls?.length ?? 0) > 0;
1012
+ }
1013
+ /**
1014
+ * Get the pending tool calls that require approval.
1015
+ * Returns empty array if no approvals needed.
1016
+ */
1017
+ async getPendingToolCalls() {
1018
+ await this.initStream();
1019
+ // Try to trigger tool execution to populate pending calls
1020
+ if (!this.isResumingFromApproval) {
1021
+ await this.executeToolsIfNeeded();
1022
+ }
1023
+ return (this.currentState?.pendingToolCalls ?? []);
1024
+ }
1025
+ /**
1026
+ * Get the current conversation state.
1027
+ * Useful for inspection, debugging, or custom persistence.
1028
+ * Note: This returns the raw ConversationState for inspection only.
1029
+ * To resume a conversation, use the StateAccessor pattern.
1030
+ */
1031
+ async getState() {
1032
+ await this.initStream();
1033
+ // Ensure tool execution has been attempted (to populate final state)
1034
+ if (!this.isResumingFromApproval) {
1035
+ await this.executeToolsIfNeeded();
1036
+ }
1037
+ if (!this.currentState) {
1038
+ throw new Error('State not initialized. Make sure a StateAccessor was provided to callModel.');
1039
+ }
1040
+ return this.currentState;
1041
+ }
514
1042
  }
515
1043
  //# sourceMappingURL=model-result.js.map