Skip to content

Commit 78dd5a5

Browse files
committed
feat: include tool responses in getNewMessagesStream
Update getNewMessagesStream to yield ToolResponseMessage objects after tool execution completes, in addition to AssistantMessages. This allows consumers to receive the full message flow including tool call results.
1 parent cf0379d commit 78dd5a5

File tree

2 files changed

+71
-2
lines changed

2 files changed

+71
-2
lines changed

src/lib/response-wrapper.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,16 +379,45 @@ export class ResponseWrapper {
379379
/**
380380
* Stream incremental message updates as content is added.
381381
* Each iteration yields an updated version of the message with new content.
382-
* Returns AssistantMessage in chat format.
382+
* Also yields ToolResponseMessages after tool execution completes.
383+
* Returns AssistantMessage or ToolResponseMessage in chat format.
383384
*/
384-
getNewMessagesStream(): AsyncIterableIterator<models.AssistantMessage> {
385+
getNewMessagesStream(): AsyncIterableIterator<models.AssistantMessage | models.ToolResponseMessage> {
385386
return (async function* (this: ResponseWrapper) {
386387
await this.initStream();
387388
if (!this.reusableStream) {
388389
throw new Error("Stream not initialized");
389390
}
390391

392+
// First yield assistant messages from the stream
391393
yield* buildMessageStream(this.reusableStream);
394+
395+
// Execute tools if needed
396+
await this.executeToolsIfNeeded();
397+
398+
// Yield tool response messages for each executed tool
399+
for (const round of this.allToolExecutionRounds) {
400+
for (const toolCall of round.toolCalls) {
401+
// Find the tool to check if it was executed
402+
const tool = this.options.tools?.find((t) => t.function.name === toolCall.name);
403+
if (!tool || !hasExecuteFunction(tool)) {
404+
continue;
405+
}
406+
407+
// Get the result from preliminary results or construct from the response
408+
const prelimResults = this.preliminaryResults.get(toolCall.id);
409+
const result = prelimResults && prelimResults.length > 0
410+
? prelimResults[prelimResults.length - 1] // Last result is the final output
411+
: undefined;
412+
413+
// Yield tool response message
414+
yield {
415+
role: "tool" as const,
416+
content: result !== undefined ? JSON.stringify(result) : "",
417+
toolCallId: toolCall.id,
418+
} as models.ToolResponseMessage;
419+
}
420+
}
392421
}.call(this));
393422
}
394423

tests/e2e/callModel.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,46 @@ describe("callModel E2E Tests", () => {
108108

109109
const message = await response.getMessage();
110110

111+
// Ensure the message fully matches the OpenAI Chat API assistant message shape
112+
expect(message).toMatchObject({
113+
role: "assistant",
114+
content: expect.anything(),
115+
});
116+
// content can be string, array, or null according to OpenAI spec
117+
// Check rest of top-level shape
118+
expect(Object.keys(message)).toEqual(
119+
expect.arrayContaining([
120+
"role",
121+
"content",
122+
// Optionally some implementations may also include:
123+
// "tool_calls", "function_call", "tool_call_id", "name"
124+
])
125+
);
126+
// If content is array, match OpenAI content block shape
127+
if (Array.isArray(message.content)) {
128+
for (const block of message.content) {
129+
expect(block).toMatchObject({
130+
type: expect.any(String),
131+
// text blocks have 'text', others may have different keys
132+
});
133+
}
134+
}
135+
// If present, tool_calls in OpenAI schema must be an array of objects
136+
if (message.role === "assistant" && message.toolCalls) {
137+
expect(Array.isArray(message.toolCalls)).toBe(true);
138+
for (const call of message.toolCalls) {
139+
expect(call).toMatchObject({
140+
id: expect.any(String),
141+
type: expect.any(String),
142+
function: expect.any(Object),
143+
});
144+
expect(call.function).toMatchObject({
145+
name: expect.any(String),
146+
arguments: expect.any(String),
147+
});
148+
}
149+
}
150+
111151
expect(message).toBeDefined();
112152
expect(message.role).toBe("assistant");
113153
expect(message.content).toBeDefined();

0 commit comments

Comments
 (0)