Skip to content

Commit 0e46d2c

Browse files
committed
Merge branch 'main' into releases/0.2.26
2 parents 34b1baf + 379355c commit 0e46d2c

File tree

4 files changed

+150
-6
lines changed

4 files changed

+150
-6
lines changed

apps/studio/electron/main/chat/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,18 @@ class LlmManager {
136136
return { message: 'Request aborted', type: 'error' };
137137
}
138138

139+
if ((error as Error).name === 'AbortError') {
140+
return { message: 'Request aborted', type: 'error' };
141+
}
142+
139143
return { message: JSON.stringify(error), type: 'error' };
140144
} catch (parseError) {
141145
console.error('Error parsing error', parseError);
142146
return { message: JSON.stringify(parseError), type: 'error' };
147+
} finally {
148+
this.abortController?.abort();
149+
this.abortController = null;
143150
}
144-
} finally {
145-
this.abortController?.abort();
146-
this.abortController = null;
147151
}
148152
}
149153

apps/studio/src/lib/editor/engine/chat/conversation/conversation.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ChatMessageRole, type ChatConversation, type TokenUsage } from '@onlook/models/chat';
22
import { MAX_NAME_LENGTH } from '@onlook/models/constants';
3-
import type { CoreMessage } from 'ai';
3+
import type { CoreMessage, ToolCallPart, ToolResultPart } from 'ai';
44
import { makeAutoObservable } from 'mobx';
55
import { nanoid } from 'nanoid/non-secure';
66
import { AssistantChatMessageImpl } from '../message/assistant';
@@ -89,7 +89,78 @@ export class ChatConversationImpl implements ChatConversation {
8989
} else {
9090
messages.push(...this.messages.map((m) => m.toCoreMessage()));
9191
}
92-
return messages;
92+
93+
return this.validateAndFixToolMessages(messages);
94+
}
95+
96+
/**
97+
* Validates that each tool_use message has a corresponding tool_result message after it.
98+
* If not, a stub tool_result message will be created to prevent API errors.
99+
*/
100+
private validateAndFixToolMessages(messages: CoreMessage[]): CoreMessage[] {
101+
try {
102+
const result: CoreMessage[] = [];
103+
104+
for (let i = 0; i < messages.length; i++) {
105+
const currentMessage = messages[i];
106+
result.push(currentMessage);
107+
108+
if (currentMessage.role === 'assistant' && Array.isArray(currentMessage.content)) {
109+
const toolCallParts = currentMessage.content.filter(
110+
(part): part is ToolCallPart => part.type === 'tool-call',
111+
);
112+
113+
if (toolCallParts.length > 0) {
114+
const nextMessage = i + 1 < messages.length ? messages[i + 1] : null;
115+
const missingToolResults: ToolResultPart[] = [];
116+
117+
for (const toolCall of toolCallParts) {
118+
let hasCorrespondingResult = false;
119+
120+
if (
121+
nextMessage?.role === 'tool' &&
122+
Array.isArray(nextMessage.content)
123+
) {
124+
hasCorrespondingResult = nextMessage.content.some(
125+
(part): part is ToolResultPart =>
126+
part.type === 'tool-result' &&
127+
part.toolCallId === toolCall.toolCallId,
128+
);
129+
}
130+
131+
if (!hasCorrespondingResult) {
132+
console.error(
133+
`Missing tool_result for tool call ${toolCall.toolCallId} (${toolCall.toolName}). Adding stub result.`,
134+
);
135+
missingToolResults.push({
136+
type: 'tool-result',
137+
toolCallId: toolCall.toolCallId,
138+
toolName: toolCall.toolName,
139+
result: 'success',
140+
isError: true,
141+
});
142+
}
143+
}
144+
145+
if (missingToolResults.length > 0) {
146+
console.warn(
147+
`Adding ${missingToolResults.length} stub tool result(s) for message without corresponding tool_result`,
148+
);
149+
150+
result.push({
151+
role: 'tool',
152+
content: missingToolResults,
153+
});
154+
}
155+
}
156+
}
157+
}
158+
159+
return result;
160+
} catch (error) {
161+
console.error('Error validating and fixing tool messages', error);
162+
return messages;
163+
}
93164
}
94165

95166
setSummaryMessage(content: string) {

apps/studio/src/lib/editor/engine/chat/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export class ChatManager {
107107
this.stream.clearBeforeSend();
108108
this.isWaiting = true;
109109
const messages = this.conversation.current.getMessagesForStream();
110+
110111
const res: CompletedStreamResponse | null = await this.sendStreamRequest(
111112
messages,
112113
requestType,

apps/web/client/src/components/store/editor/engine/chat/conversation/conversation.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ChatMessageRole, type ChatConversation, type TokenUsage } from '@onlook/models/chat';
22
import { MAX_NAME_LENGTH } from '@onlook/models/constants';
33
import type { CoreMessage } from 'ai';
4+
import type { ToolCallPart, ToolResultPart } from 'ai';
45
import { makeAutoObservable } from 'mobx';
56
import { nanoid } from 'nanoid/non-secure';
67
import { AssistantChatMessageImpl } from '../message/assistant';
@@ -89,7 +90,74 @@ export class ChatConversationImpl implements ChatConversation {
8990
} else {
9091
messages.push(...this.messages.map((m) => m.toCoreMessage()));
9192
}
92-
return messages;
93+
94+
return this.validateAndFixToolMessages(messages);
95+
}
96+
97+
/**
98+
* Validates that each tool_use message has a corresponding tool_result message after it.
99+
* If not, a stub tool_result message will be created to prevent API errors.
100+
*/
101+
private validateAndFixToolMessages(messages: CoreMessage[]): CoreMessage[] {
102+
const result: CoreMessage[] = [];
103+
104+
for (let i = 0; i < messages.length; i++) {
105+
const currentMessage = messages[i];
106+
if (!currentMessage) continue;
107+
108+
result.push(currentMessage);
109+
110+
if (currentMessage.role === 'assistant' &&
111+
Array.isArray(currentMessage.content)) {
112+
113+
const toolCallParts = currentMessage.content.filter(
114+
(part): part is ToolCallPart => part.type === 'tool-call'
115+
);
116+
117+
if (toolCallParts.length > 0) {
118+
const nextMessage = i + 1 < messages.length ? messages[i + 1] : null;
119+
const missingToolResults: ToolResultPart[] = [];
120+
121+
for (const toolCall of toolCallParts) {
122+
let hasCorrespondingResult = false;
123+
124+
if (nextMessage?.role === 'tool' && Array.isArray(nextMessage.content)) {
125+
hasCorrespondingResult = nextMessage.content.some(
126+
(part): part is ToolResultPart =>
127+
part.type === 'tool-result' &&
128+
part.toolCallId === toolCall.toolCallId
129+
);
130+
}
131+
132+
if (!hasCorrespondingResult) {
133+
console.error(
134+
`Missing tool_result for tool call ${toolCall.toolCallId} (${toolCall.toolName}). Adding stub result.`
135+
);
136+
missingToolResults.push({
137+
type: 'tool-result',
138+
toolCallId: toolCall.toolCallId,
139+
toolName: toolCall.toolName,
140+
result: "success",
141+
isError: true
142+
});
143+
}
144+
}
145+
146+
if (missingToolResults.length > 0) {
147+
console.warn(
148+
`Adding ${missingToolResults.length} stub tool result(s) for message without corresponding tool_result`
149+
);
150+
151+
result.push({
152+
role: 'tool',
153+
content: missingToolResults
154+
});
155+
}
156+
}
157+
}
158+
}
159+
160+
return result;
93161
}
94162

95163
setSummaryMessage(content: string) {

0 commit comments

Comments
 (0)