Skip to content

Commit 11cd329

Browse files
authored
fix: address PR #49 review feedback (#91)
2 parents 8f471e0 + 4aef98d commit 11cd329

File tree

2 files changed

+89
-33
lines changed

2 files changed

+89
-33
lines changed

src/lib/response-wrapper.ts

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,20 @@ export class ResponseWrapper {
6969
this.options = options;
7070
}
7171

72+
/**
73+
* Type guard to check if a value is a non-streaming response
74+
*/
75+
private isNonStreamingResponse(value: unknown): value is models.OpenResponsesNonStreamingResponse {
76+
return (
77+
value !== null &&
78+
typeof value === "object" &&
79+
"id" in value &&
80+
"object" in value &&
81+
"output" in value &&
82+
!("toReadableStream" in value)
83+
);
84+
}
85+
7286
/**
7387
* Initialize the stream if not already started
7488
* This is idempotent - multiple calls will return the same promise
@@ -284,19 +298,57 @@ export class ResponseWrapper {
284298
value as EventStream<models.OpenResponsesStreamEvent>,
285299
);
286300
currentResponse = await consumeStreamForCompletion(stream);
301+
} else if (this.isNonStreamingResponse(value)) {
302+
currentResponse = value;
287303
} else {
288-
currentResponse = value as models.OpenResponsesNonStreamingResponse;
304+
throw new Error("Unexpected response type from API");
289305
}
290306

291307
currentRound++;
292308
}
293309

310+
// Validate the final response has required fields
311+
if (!currentResponse || !currentResponse.id || !currentResponse.output) {
312+
throw new Error("Invalid final response: missing required fields");
313+
}
314+
315+
// Ensure the response is in a completed state (has output content)
316+
if (!Array.isArray(currentResponse.output) || currentResponse.output.length === 0) {
317+
throw new Error("Invalid final response: empty or invalid output");
318+
}
319+
294320
this.finalResponse = currentResponse;
295321
})();
296322

297323
return this.toolExecutionPromise;
298324
}
299325

326+
/**
327+
* Internal helper to get the message after tool execution
328+
*/
329+
private async getMessageInternal(): Promise<models.AssistantMessage> {
330+
await this.executeToolsIfNeeded();
331+
332+
if (!this.finalResponse) {
333+
throw new Error("Response not available");
334+
}
335+
336+
return extractMessageFromResponse(this.finalResponse);
337+
}
338+
339+
/**
340+
* Internal helper to get the text after tool execution
341+
*/
342+
private async getTextInternal(): Promise<string> {
343+
await this.executeToolsIfNeeded();
344+
345+
if (!this.finalResponse) {
346+
throw new Error("Response not available");
347+
}
348+
349+
return extractTextFromResponse(this.finalResponse);
350+
}
351+
300352
/**
301353
* Get the completed message from the response.
302354
* This will consume the stream until completion, execute any tools, and extract the first message.
@@ -307,16 +359,7 @@ export class ResponseWrapper {
307359
return this.messagePromise;
308360
}
309361

310-
this.messagePromise = (async (): Promise<models.AssistantMessage> => {
311-
await this.executeToolsIfNeeded();
312-
313-
if (!this.finalResponse) {
314-
throw new Error('Response not available');
315-
}
316-
317-
return extractMessageFromResponse(this.finalResponse);
318-
})();
319-
362+
this.messagePromise = this.getMessageInternal();
320363
return this.messagePromise;
321364
}
322365

@@ -329,16 +372,7 @@ export class ResponseWrapper {
329372
return this.textPromise;
330373
}
331374

332-
this.textPromise = (async () => {
333-
await this.executeToolsIfNeeded();
334-
335-
if (!this.finalResponse) {
336-
throw new Error('Response not available');
337-
}
338-
339-
return extractTextFromResponse(this.finalResponse);
340-
})();
341-
375+
this.textPromise = this.getTextInternal();
342376
return this.textPromise;
343377
}
344378

src/lib/tool-orchestrator.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,26 +80,23 @@ export async function executeToolLoop(
8080
break;
8181
}
8282

83-
// Execute all tool calls
84-
const roundResults: ToolExecutionResult[] = [];
85-
86-
for (const toolCall of toolCalls) {
83+
// Execute all tool calls in parallel (parallel tool calling)
84+
const toolCallPromises = toolCalls.map(async (toolCall) => {
8785
const tool = findToolByName(tools, toolCall.name);
8886

8987
if (!tool) {
9088
// Tool not found in definitions
91-
roundResults.push({
89+
return {
9290
toolCallId: toolCall.id,
9391
toolName: toolCall.name,
9492
result: null,
9593
error: new Error(`Tool "${toolCall.name}" not found in tool definitions`),
96-
});
97-
continue;
94+
} as ToolExecutionResult;
9895
}
9996

10097
if (!hasExecuteFunction(tool)) {
101-
// Tool has no execute function - skip
102-
continue;
98+
// Tool has no execute function - return null to filter out
99+
return null;
103100
}
104101

105102
// Build turn context
@@ -109,9 +106,34 @@ export async function executeToolLoop(
109106
};
110107

111108
// Execute the tool
112-
const result = await executeTool(tool, toolCall, turnContext, onPreliminaryResult);
113-
roundResults.push(result);
114-
}
109+
return executeTool(tool, toolCall, turnContext, onPreliminaryResult);
110+
});
111+
112+
// Wait for all tool executions to complete in parallel
113+
const settledResults = await Promise.allSettled(toolCallPromises);
114+
115+
// Process settled results, handling both fulfilled and rejected promises
116+
const roundResults: ToolExecutionResult[] = [];
117+
settledResults.forEach((settled, i) => {
118+
const toolCall = toolCalls[i];
119+
if (!toolCall) return;
120+
121+
if (settled.status === "fulfilled") {
122+
if (settled.value !== null) {
123+
roundResults.push(settled.value);
124+
}
125+
} else {
126+
// Promise rejected - create error result
127+
roundResults.push({
128+
toolCallId: toolCall.id,
129+
toolName: toolCall.name,
130+
result: null,
131+
error: settled.reason instanceof Error
132+
? settled.reason
133+
: new Error(String(settled.reason)),
134+
});
135+
}
136+
});
115137

116138
toolExecutionResults.push(...roundResults);
117139

0 commit comments

Comments
 (0)