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