Skip to content

Commit b1eb66f

Browse files
authored
feat: reduce agent overhead (#2953)
1 parent 53ec325 commit b1eb66f

File tree

32 files changed

+284
-886
lines changed

32 files changed

+284
-886
lines changed

apps/web/client/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
"preview:standalone": "bun run build:standalone && bun run start:standalone"
2929
},
3030
"dependencies": {
31-
"@ai-sdk/provider-utils": "^3.0.5",
32-
"@ai-sdk/react": "2.0.25",
31+
"@ai-sdk/provider-utils": "^3.0.10",
32+
"@ai-sdk/react": "2.0.60",
3333
"@codemirror/lang-css": "^6.2.0",
3434
"@codemirror/lang-html": "^6.4.7",
3535
"@codemirror/lang-javascript": "^6.2.3",

apps/web/client/public/onlook-preload-script.js

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/web/client/src/app/api/chat/helpers/stream.ts

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,3 @@
1-
import type { ToolCall } from '@ai-sdk/provider-utils';
2-
import { initModel } from '@onlook/ai';
3-
import { LLMProvider, OPENROUTER_MODELS } from '@onlook/models';
4-
import { generateObject, NoSuchToolError, type ToolSet } from 'ai';
5-
6-
7-
export const repairToolCall = async ({ toolCall, tools, error }: { toolCall: ToolCall<string, unknown>, tools: ToolSet, error: Error }) => {
8-
if (NoSuchToolError.isInstance(error)) {
9-
throw new Error(
10-
`Tool "${toolCall.toolName}" not found. Available tools: ${Object.keys(tools).join(', ')}`,
11-
);
12-
}
13-
const tool = tools[toolCall.toolName];
14-
15-
if (!tool?.inputSchema) {
16-
throw new Error(`Tool "${toolCall.toolName}" has no input schema`);
17-
}
18-
19-
console.warn(
20-
`Invalid parameter for tool ${toolCall.toolName} with args ${JSON.stringify(toolCall.input)}, attempting to fix`,
21-
);
22-
23-
const { model } = initModel({
24-
provider: LLMProvider.OPENROUTER,
25-
model: OPENROUTER_MODELS.OPEN_AI_GPT_5_NANO,
26-
});
27-
28-
const { object: repairedArgs } = await generateObject({
29-
model,
30-
schema: tool.inputSchema,
31-
prompt: [
32-
`The model tried to call the tool "${toolCall.toolName}"` +
33-
` with the following arguments:`,
34-
JSON.stringify(toolCall.input),
35-
`The tool accepts the following schema:`,
36-
JSON.stringify(tool?.inputSchema),
37-
'Please fix the inputs. Return the fixed inputs as a JSON object, DO NOT include any other text.',
38-
].join('\n'),
39-
});
40-
41-
return {
42-
type: 'tool-call' as const,
43-
toolCallId: toolCall.toolCallId,
44-
toolName: toolCall.toolName,
45-
input: JSON.stringify(repairedArgs),
46-
};
47-
}
48-
491
export function errorHandler(error: unknown) {
502
try {
513
console.error('Error in chat', error);

apps/web/client/src/app/api/chat/route.ts

Lines changed: 33 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { api } from '@/trpc/server';
22
import { trackEvent } from '@/utils/analytics/server';
3-
import { AgentStreamer, BaseAgent, RootAgent, UserAgent } from '@onlook/ai';
3+
import { createRootAgentStream } from '@onlook/ai';
44
import { toDbMessage } from '@onlook/db';
5-
import { AgentType, ChatType } from '@onlook/models';
5+
import { ChatType, type ChatMessage, type ChatMetadata } from '@onlook/models';
66
import { type NextRequest } from 'next/server';
77
import { v4 as uuidv4 } from 'uuid';
8-
import { checkMessageLimit, decrementUsage, errorHandler, getSupabaseUser, incrementUsage, repairToolCall } from './helpers';
9-
import { z } from 'zod';
8+
import { checkMessageLimit, decrementUsage, errorHandler, getSupabaseUser, incrementUsage } from './helpers';
109

1110
export async function POST(req: NextRequest) {
1211
try {
@@ -52,24 +51,14 @@ export async function POST(req: NextRequest) {
5251
}
5352
}
5453

55-
const streamResponseSchema = z.object({
56-
agentType: z.enum(AgentType).optional().default(AgentType.ROOT),
57-
messages: z.array(z.any()),
58-
chatType: z.enum(ChatType).optional(),
59-
conversationId: z.string(),
60-
projectId: z.string(),
61-
}).refine((data) => {
62-
// Only allow chatType if agentType is ROOT
63-
if (data.chatType !== undefined && data.agentType !== AgentType.ROOT) {
64-
return false;
65-
}
66-
return true;
67-
}, { message: "chatType is only allowed if agentType is root" });
68-
6954
export const streamResponse = async (req: NextRequest, userId: string) => {
7055
const body = await req.json();
71-
const { agentType, messages, chatType, conversationId, projectId } = streamResponseSchema.parse(body);
72-
56+
const { messages, chatType, conversationId, projectId } = body as {
57+
messages: ChatMessage[],
58+
chatType: ChatType,
59+
conversationId: string,
60+
projectId: string,
61+
};
7362
// Updating the usage record and rate limit is done here to avoid
7463
// abuse in the case where a single user sends many concurrent requests.
7564
// If the call below fails, the user will not be penalized.
@@ -82,53 +71,31 @@ export const streamResponse = async (req: NextRequest, userId: string) => {
8271
const lastUserMessage = messages.findLast((message) => message.role === 'user');
8372
const traceId = lastUserMessage?.id ?? uuidv4();
8473

85-
// Create RootAgent instance
86-
let agent: BaseAgent;
87-
if (agentType === AgentType.ROOT) {
88-
if (chatType === ChatType.EDIT) {
89-
usageRecord = await incrementUsage(req, traceId);
90-
}
91-
92-
agent = new RootAgent(chatType!);
93-
} else if (agentType === AgentType.USER) {
94-
agent = new UserAgent();
95-
} else {
96-
// agent = new WeatherAgent();
97-
throw new Error('Agent type not supported');
74+
if (chatType === ChatType.EDIT) {
75+
usageRecord = await incrementUsage(req, traceId);
9876
}
99-
const streamer = new AgentStreamer(agent, conversationId);
100-
101-
return streamer.streamText(messages, {
102-
streamTextConfig: {
103-
experimental_telemetry: {
104-
isEnabled: true,
105-
metadata: {
77+
const stream = createRootAgentStream({
78+
chatType,
79+
conversationId,
80+
projectId,
81+
userId,
82+
traceId,
83+
messages,
84+
});
85+
return stream.toUIMessageStreamResponse<ChatMessage>(
86+
{
87+
originalMessages: messages,
88+
generateMessageId: () => uuidv4(),
89+
messageMetadata: ({ part }) => {
90+
return {
91+
createdAt: new Date(),
10692
conversationId,
107-
projectId,
108-
userId,
109-
agentType: agentType ?? AgentType.ROOT,
110-
chatType: chatType ?? "null",
111-
tags: ['chat'],
112-
langfuseTraceId: traceId,
113-
sessionId: conversationId,
114-
},
115-
},
116-
experimental_repairToolCall: repairToolCall,
117-
onError: async (error) => {
118-
console.error('Error in chat stream call', error);
119-
// if there was an error with the API, do not penalize the user
120-
await decrementUsage(req, usageRecord);
121-
122-
// Ensure the stream stops on error by re-throwing
123-
if (error instanceof Error) {
124-
throw error;
125-
} else {
126-
const errorMessage = typeof error === 'string' ? error : JSON.stringify(error);
127-
throw new Error(errorMessage);
128-
}
93+
context: [],
94+
checkpoints: [],
95+
finishReason: part.type === 'finish-step' ? part.finishReason : undefined,
96+
usage: part.type === 'finish-step' ? part.usage : undefined,
97+
} satisfies ChatMetadata;
12998
},
130-
},
131-
toUIMessageStreamResponseConfig: {
13299
onFinish: async ({ messages: finalMessages }) => {
133100
const messagesToStore = finalMessages
134101
.filter(msg =>
@@ -142,8 +109,8 @@ export const streamResponse = async (req: NextRequest, userId: string) => {
142109
});
143110
},
144111
onError: errorHandler,
145-
},
146-
});
112+
}
113+
);
147114
} catch (error) {
148115
console.error('Error in streamResponse setup', error);
149116
// If there was an error setting up the stream and we incremented usage, revert it

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/chat-context.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { useMemo } from 'react';
1919
export const ChatContextWindow = ({ usage }: { usage: LanguageModelUsage }) => {
2020
const showCost = false;
2121
// Hardcoded for now, but should be dynamic based on the model used
22-
const maxTokens = MODEL_MAX_TOKENS[OPENROUTER_MODELS.CLAUDE_4_SONNET];
22+
const maxTokens = MODEL_MAX_TOKENS[OPENROUTER_MODELS.CLAUDE_4_5_SONNET];
2323
const usedTokens = useMemo(() => {
2424
if (!usage) return 0;
2525
const input = usage.inputTokens ?? 0;

apps/web/client/src/app/project/[id]/_hooks/use-chat/index.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useEditorEngine } from '@/components/store/editor';
44
import { handleToolCall } from '@/components/tools';
55
import { useChat as useAiChat } from '@ai-sdk/react';
6-
import { AgentType, ChatType, type ChatMessage, type MessageContext, type QueuedMessage } from '@onlook/models';
6+
import { ChatType, type ChatMessage, type MessageContext, type QueuedMessage } from '@onlook/models';
77
import { jsonClone } from '@onlook/utility';
88
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, type FinishReason } from 'ai';
99
import { usePostHog } from 'posthog-js/react';
@@ -31,7 +31,6 @@ interface UseChatProps {
3131
projectId: string;
3232
initialMessages: ChatMessage[];
3333
}
34-
const agentType = AgentType.ROOT;
3534

3635
export function useChat({ conversationId, projectId, initialMessages }: UseChatProps) {
3736
const editorEngine = useEditorEngine();
@@ -52,12 +51,11 @@ export function useChat({ conversationId, projectId, initialMessages }: UseChatP
5251
body: {
5352
conversationId,
5453
projectId,
55-
agentType,
5654
},
5755
}),
5856
onToolCall: async (toolCall) => {
5957
setIsExecutingToolCall(true);
60-
void handleToolCall(agentType, toolCall.toolCall, editorEngine, addToolResult).then(() => {
58+
void handleToolCall(toolCall.toolCall, editorEngine, addToolResult).then(() => {
6159
setIsExecutingToolCall(false);
6260
});
6361
},
@@ -90,7 +88,6 @@ export function useChat({ conversationId, projectId, initialMessages }: UseChatP
9088
chatType: type,
9189
conversationId,
9290
context: messageContext,
93-
agentType,
9491
},
9592
});
9693
void editorEngine.chat.conversation.generateTitle(content);
@@ -165,7 +162,6 @@ export function useChat({ conversationId, projectId, initialMessages }: UseChatP
165162
body: {
166163
chatType,
167164
conversationId,
168-
agentType,
169165
},
170166
});
171167

@@ -185,16 +181,16 @@ export function useChat({ conversationId, projectId, initialMessages }: UseChatP
185181

186182
const processNextInQueue = useCallback(async () => {
187183
if (isProcessingQueue.current || isStreaming || queuedMessages.length === 0) return;
188-
184+
189185
const nextMessage = queuedMessages[0];
190186
if (!nextMessage) return;
191-
187+
192188
isProcessingQueue.current = true;
193-
189+
194190
try {
195191
const refreshedContext = await editorEngine.chat.context.getRefreshedContext(nextMessage.context);
196192
await processMessage(nextMessage.content, nextMessage.type, refreshedContext);
197-
193+
198194
// Remove only after successful processing
199195
setQueuedMessages(prev => prev.slice(1));
200196
} catch (error) {

apps/web/client/src/components/tools/tools.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,29 @@
11
import type { EditorEngine } from '@/components/store/editor/engine';
22
import type { ToolCall } from '@ai-sdk/provider-utils';
3-
import type { AbstractChat } from 'ai';
4-
import { getAvailableTools, type OnToolCallHandler } from '@onlook/ai';
3+
import { getToolClassesFromType } from '@onlook/ai';
54
import { toast } from '@onlook/ui/sonner';
6-
import type { AgentType } from '@onlook/models';
75

8-
export async function handleToolCall(agentType: AgentType, toolCall: ToolCall<string, unknown>, editorEngine: EditorEngine, addToolResult: typeof AbstractChat.prototype.addToolResult) {
6+
export async function handleToolCall(toolCall: ToolCall<string, unknown>, editorEngine: EditorEngine, addToolResult: (toolResult: { tool: string, toolCallId: string, output: any }) => Promise<void>) {
97
const toolName = toolCall.toolName;
108
const currentChatMode = editorEngine.state.chatMode;
11-
const availableTools = getAvailableTools(agentType, currentChatMode) as any[];
12-
let output: any = null;
9+
const availableTools = getToolClassesFromType(currentChatMode);
10+
let output: unknown = null;
1311

1412
try {
15-
const tool = availableTools.find((tool: any) => tool.toolName === toolName);
13+
const tool = availableTools.find(tool => tool.toolName === toolName);
1614
if (!tool) {
1715
toast.error(`Tool "${toolName}" not available in ask mode`, {
1816
description: `Switch to build mode to use this tool.`,
1917
duration: 2000,
2018
});
2119

22-
throw new Error(`Tool "${toolName}" is not available in ${currentChatMode} mode!!!!`);
20+
throw new Error(`Tool "${toolName}" is not available in ${currentChatMode} mode`);
2321
}
2422
// Parse the input to the tool parameters. Throws if invalid.
2523
const validatedInput = tool.parameters.parse(toolCall.input);
2624
const toolInstance = new tool();
27-
const getOnToolCall: OnToolCallHandler = (subAgentType, addSubAgentToolResult) => (toolCall) =>
28-
void handleToolCall(subAgentType, toolCall.toolCall, editorEngine, addSubAgentToolResult);
29-
3025
// Can force type with as any because we know the input is valid.
31-
output = await toolInstance.handle(validatedInput as any, editorEngine, getOnToolCall);
26+
output = await toolInstance.handle(validatedInput as any, editorEngine);
3227
} catch (error) {
3328
output = 'error handling tool call ' + error;
3429
} finally {

apps/web/client/src/server/api/routers/chat/conversation.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,7 @@ export const conversationRouter = createTRPCRouter({
3636
upsert: protectedProcedure
3737
.input(conversationInsertSchema)
3838
.mutation(async ({ ctx, input }) => {
39-
const [conversation] = await ctx.db.insert(conversations).values(input).onConflictDoUpdate({
40-
target: [conversations.id],
41-
set: {
42-
...input,
43-
updatedAt: new Date(),
44-
},
45-
}).returning();
39+
const [conversation] = await ctx.db.insert(conversations).values(input).returning();
4640
if (!conversation) {
4741
throw new Error('Conversation not created');
4842
}
@@ -51,11 +45,10 @@ export const conversationRouter = createTRPCRouter({
5145
update: protectedProcedure
5246
.input(conversationUpdateSchema)
5347
.mutation(async ({ ctx, input }) => {
54-
const [conversation] = await ctx.db.update(conversations)
55-
.set({
56-
...input,
57-
updatedAt: new Date(),
58-
})
48+
const [conversation] = await ctx.db.update({
49+
...conversations,
50+
updatedAt: new Date(),
51+
}).set(input)
5952
.where(eq(conversations.id, input.id)).returning();
6053
if (!conversation) {
6154
throw new Error('Conversation not updated');

0 commit comments

Comments
 (0)