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