Unverified 提交 eea719f0 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Inject web crawl user messages correctly (#2129)

<!-- CURSOR_SUMMARY --> > [!NOTE] > **Fix stable user-message injection across steps** > > - Extracts `prepareStepMessages` logic into `prepare_step_utils` with `transformContentPart`, `processPendingMessages`, and `injectMessagesAtPositions` to track injected messages with `insertAtIndex` and FIFO `sequence`, then re-inject them each step at the same positions > - Updates `local_agent_handler` to use `prepareStepMessages`, maintaining `pendingUserMessages` and accumulated `allInjectedMessages` > > **Web crawl behavior update** > > - `web_crawl` now requires a screenshot and injects only screenshot + markdown (HTML removed); clone instructions emphasize screenshot as primary visual reference > > **Tests** > > - Adds comprehensive unit tests for the new utilities and an integration-style multi-step simulation validating stable ordering and reinjection > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3b02b73cf25b497a2d09ab6239ec9e3598ae823e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Fixes incorrect injection of web-crawl user messages across agent steps. Messages are now re-injected at stable positions each step so screenshot and markdown context persist across tool rounds. - **Bug Fixes** - Track injected user messages with an insertion index and re-inject them every step (sorted in reverse) to keep order and prevent loss. - Web crawl now requires a screenshot and injects only screenshot + markdown (HTML removed). Updated clone instructions to emphasize the screenshot. <sup>Written for commit 3b02b73cf25b497a2d09ab6239ec9e3598ae823e. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. -->
上级 ddef8062
import { describe, it, expect } from "vitest";
import {
transformContentPart,
processPendingMessages,
injectMessagesAtPositions,
prepareStepMessages,
type InjectedMessage,
} from "@/pro/main/ipc/handlers/local_agent/prepare_step_utils";
import type { UserMessageContentPart } from "@/pro/main/ipc/handlers/local_agent/tools/types";
import { ImagePart, ModelMessage } from "node_modules/ai/dist";
describe("prepare_step_utils", () => {
describe("transformContentPart", () => {
it("transforms text parts correctly", () => {
const part: UserMessageContentPart = {
type: "text",
text: "Hello world",
};
const result = transformContentPart(part);
expect(result).toEqual({ type: "text", text: "Hello world" });
});
it("transforms image-url parts to image with URL object", () => {
const part: UserMessageContentPart = {
type: "image-url",
url: "https://example.com/image.png",
};
const result = transformContentPart(part);
expect(result.type).toBe("image");
expect((result as { type: "image"; image: URL }).image).toBeInstanceOf(
URL,
);
expect((result as { type: "image"; image: URL }).image.href).toBe(
"https://example.com/image.png",
);
});
it("handles URLs with query parameters", () => {
const part: UserMessageContentPart = {
type: "image-url",
url: "https://example.com/image.png?width=100&height=200",
};
const result = transformContentPart(part);
expect((result as { type: "image"; image: URL }).image.href).toBe(
"https://example.com/image.png?width=100&height=200",
);
});
});
describe("processPendingMessages", () => {
it("moves pending messages to injected list with correct insertion index", () => {
const pendingUserMessages: UserMessageContentPart[][] = [
[{ type: "text", text: "First message" }],
];
const allInjectedMessages: InjectedMessage[] = [];
const currentMessageCount = 5;
processPendingMessages(
pendingUserMessages,
allInjectedMessages,
currentMessageCount,
);
expect(pendingUserMessages).toHaveLength(0);
expect(allInjectedMessages).toHaveLength(1);
expect(allInjectedMessages[0].insertAtIndex).toBe(5);
expect(allInjectedMessages[0].message.role).toBe("user");
expect(allInjectedMessages[0].message.content).toHaveLength(1);
});
it("processes multiple pending messages in order", () => {
const pendingUserMessages: UserMessageContentPart[][] = [
[{ type: "text", text: "First" }],
[{ type: "text", text: "Second" }],
[{ type: "text", text: "Third" }],
];
const allInjectedMessages: InjectedMessage[] = [];
processPendingMessages(pendingUserMessages, allInjectedMessages, 10);
expect(pendingUserMessages).toHaveLength(0);
expect(allInjectedMessages).toHaveLength(3);
// All should have the same insertion index since they were processed in one call
expect(allInjectedMessages[0].insertAtIndex).toBe(10);
expect(allInjectedMessages[1].insertAtIndex).toBe(10);
expect(allInjectedMessages[2].insertAtIndex).toBe(10);
// But content order should be preserved
expect(allInjectedMessages[0].message.content[0]).toEqual({
type: "text",
text: "First",
});
expect(allInjectedMessages[1].message.content[0]).toEqual({
type: "text",
text: "Second",
});
});
it("handles mixed content types in a single message", () => {
const pendingUserMessages: UserMessageContentPart[][] = [
[
{ type: "text", text: "Check this image:" },
{ type: "image-url", url: "https://example.com/img.png" },
],
];
const allInjectedMessages: InjectedMessage[] = [];
processPendingMessages(pendingUserMessages, allInjectedMessages, 3);
expect(allInjectedMessages).toHaveLength(1);
expect(allInjectedMessages[0].message.content).toHaveLength(2);
expect(allInjectedMessages[0].message.content[0]).toEqual({
type: "text",
text: "Check this image:",
});
expect(
(allInjectedMessages[0].message.content[1] as ImagePart).type,
).toBe("image");
});
it("does nothing when no pending messages", () => {
const pendingUserMessages: UserMessageContentPart[][] = [];
const allInjectedMessages: InjectedMessage[] = [];
processPendingMessages(pendingUserMessages, allInjectedMessages, 5);
expect(allInjectedMessages).toHaveLength(0);
});
it("preserves existing injected messages", () => {
const existingInjected: InjectedMessage = {
insertAtIndex: 2,
sequence: 0,
message: {
role: "user",
content: [{ type: "text", text: "Existing" }],
},
};
const pendingUserMessages: UserMessageContentPart[][] = [
[{ type: "text", text: "New" }],
];
const allInjectedMessages: InjectedMessage[] = [existingInjected];
processPendingMessages(pendingUserMessages, allInjectedMessages, 7);
expect(allInjectedMessages).toHaveLength(2);
expect(allInjectedMessages[0]).toBe(existingInjected);
expect(allInjectedMessages[1].insertAtIndex).toBe(7);
expect(allInjectedMessages[1].sequence).toBe(1); // Sequence continues from existing
});
it("assigns incrementing sequence numbers", () => {
const pendingUserMessages: UserMessageContentPart[][] = [
[{ type: "text", text: "First" }],
[{ type: "text", text: "Second" }],
[{ type: "text", text: "Third" }],
];
const allInjectedMessages: InjectedMessage[] = [];
processPendingMessages(pendingUserMessages, allInjectedMessages, 5);
expect(allInjectedMessages[0].sequence).toBe(0);
expect(allInjectedMessages[1].sequence).toBe(1);
expect(allInjectedMessages[2].sequence).toBe(2);
});
});
describe("injectMessagesAtPositions", () => {
it("returns original messages when no injections", () => {
const messages = [{ id: 1 }, { id: 2 }, { id: 3 }];
const injected: InjectedMessage[] = [];
const result = injectMessagesAtPositions(messages, injected);
expect(result).toEqual(messages);
});
it("injects a single message at the correct position", () => {
const messages = [{ id: "a" }, { id: "b" }, { id: "c" }];
const injected: InjectedMessage[] = [
{
insertAtIndex: 1,
sequence: 0,
message: {
role: "user",
content: [{ type: "text", text: "injected" }],
},
},
];
const result = injectMessagesAtPositions(messages, injected);
expect(result).toHaveLength(4);
expect(result[0]).toEqual({ id: "a" });
expect(result[1]).toEqual(injected[0].message);
expect(result[2]).toEqual({ id: "b" });
expect(result[3]).toEqual({ id: "c" });
});
it("injects at the beginning (index 0)", () => {
const messages = [{ id: 1 }, { id: 2 }];
const injected: InjectedMessage[] = [
{
insertAtIndex: 0,
sequence: 0,
message: {
role: "user",
content: [{ type: "text", text: "first" }],
},
},
];
const result = injectMessagesAtPositions(messages, injected);
expect(result).toHaveLength(3);
expect(result[0]).toEqual(injected[0].message);
expect(result[1]).toEqual({ id: 1 });
});
it("injects at the end", () => {
const messages = [{ id: 1 }, { id: 2 }];
const injected: InjectedMessage[] = [
{
insertAtIndex: 2,
sequence: 0,
message: {
role: "user",
content: [{ type: "text", text: "last" }],
},
},
];
const result = injectMessagesAtPositions(messages, injected);
expect(result).toHaveLength(3);
expect(result[2]).toEqual(injected[0].message);
});
it("handles multiple injections at different positions", () => {
const messages = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }];
const injected: InjectedMessage[] = [
{
insertAtIndex: 1,
sequence: 0,
message: { role: "user", content: [{ type: "text", text: "at-1" }] },
},
{
insertAtIndex: 3,
sequence: 1,
message: { role: "user", content: [{ type: "text", text: "at-3" }] },
},
];
const result = injectMessagesAtPositions(messages, injected);
// Original: [a, b, c, d]
// After injections: [a, at-1, b, c, at-3, d]
expect(result).toHaveLength(6);
expect(
result.map((m) =>
"id" in m ? m.id : (m.content[0] as { text: string }).text,
),
).toEqual(["a", "at-1", "b", "c", "at-3", "d"]);
});
it("handles multiple injections at the same position (preserves FIFO order)", () => {
const messages = [{ id: "a" }, { id: "b" }];
const injected: InjectedMessage[] = [
{
insertAtIndex: 1,
sequence: 0, // Added first
message: { role: "user", content: [{ type: "text", text: "first" }] },
},
{
insertAtIndex: 1,
sequence: 1, // Added second
message: {
role: "user",
content: [{ type: "text", text: "second" }],
},
},
];
const result = injectMessagesAtPositions(messages, injected);
// With sequence-aware sorting, FIFO order is preserved:
// "first" (added first) appears before "second" (added second)
expect(result).toHaveLength(4);
expect(result[0]).toEqual({ id: "a" });
expect((result[1] as any).content[0].text).toBe("first");
expect((result[2] as any).content[0].text).toBe("second");
expect(result[3]).toEqual({ id: "b" });
});
it("does not mutate the original messages array", () => {
const messages = [{ id: 1 }, { id: 2 }];
const originalLength = messages.length;
const injected: InjectedMessage[] = [
{
insertAtIndex: 1,
sequence: 0,
message: { role: "user", content: [{ type: "text", text: "new" }] },
},
];
injectMessagesAtPositions(messages, injected);
expect(messages).toHaveLength(originalLength);
});
it("does not mutate the injected messages array", () => {
const messages = [{ id: 1 }];
const injected: InjectedMessage[] = [
{
insertAtIndex: 0,
sequence: 0,
message: { role: "user", content: [{ type: "text", text: "new" }] },
},
{
insertAtIndex: 1,
sequence: 1,
message: { role: "user", content: [{ type: "text", text: "other" }] },
},
];
const originalOrder = injected.map((i) => i.insertAtIndex);
injectMessagesAtPositions(messages, injected);
// Original array should not be sorted
expect(injected.map((i) => i.insertAtIndex)).toEqual(originalOrder);
});
});
describe("prepareStepMessages", () => {
it("returns undefined when no pending or injected messages", () => {
const options = {
messages: [{ role: "user", content: "Hello" }] satisfies ModelMessage[],
someOtherProp: true,
};
const pendingUserMessages: UserMessageContentPart[][] = [];
const allInjectedMessages: InjectedMessage[] = [];
const result = prepareStepMessages(
options,
pendingUserMessages,
allInjectedMessages,
);
expect(result).toBeUndefined();
});
it("processes pending messages and returns modified options", () => {
const options = {
messages: [
{ role: "user", content: "Original" },
] satisfies ModelMessage[],
temperature: 0.7,
};
const pendingUserMessages: UserMessageContentPart[][] = [
[{ type: "text", text: "Injected content" }],
];
const allInjectedMessages: InjectedMessage[] = [];
const result = prepareStepMessages(
options,
pendingUserMessages,
allInjectedMessages,
);
expect(result).toBeDefined();
expect(result!.messages).toHaveLength(2);
expect(result!.temperature).toBe(0.7);
expect(pendingUserMessages).toHaveLength(0);
expect(allInjectedMessages).toHaveLength(1);
});
it("re-injects all accumulated messages on subsequent steps", () => {
// Simulate multiple steps where injected messages accumulate
const allInjectedMessages: InjectedMessage[] = [];
const pendingUserMessages: UserMessageContentPart[][] = [];
// Step 1: Add first pending message
pendingUserMessages.push([{ type: "text", text: "Screenshot 1" }]);
const step1Messages: ModelMessage[] = [
{ role: "assistant", content: "Let me help" },
];
let result = prepareStepMessages(
{ messages: step1Messages },
pendingUserMessages,
allInjectedMessages,
);
expect(result!.messages).toHaveLength(2);
expect(allInjectedMessages).toHaveLength(1);
expect(allInjectedMessages[0].insertAtIndex).toBe(1);
// Step 2: AI added a new message, add another pending message
pendingUserMessages.push([{ type: "text", text: "Screenshot 2" }]);
const step2Messages: ModelMessage[] = [
{ role: "assistant", content: "Let me help" },
{ role: "assistant", content: "Tool result" },
];
result = prepareStepMessages(
{ messages: step2Messages },
pendingUserMessages,
allInjectedMessages,
);
// Should inject both accumulated messages
expect(result!.messages).toHaveLength(4); // 2 original + 2 injected
expect(allInjectedMessages).toHaveLength(2);
expect(allInjectedMessages[1].insertAtIndex).toBe(2);
});
it("preserves additional options properties", () => {
const options = {
messages: [{ role: "user", content: "test" }] satisfies ModelMessage[],
maxTokens: 1000,
model: "gpt-4",
tools: ["search", "write"],
};
const pendingUserMessages: UserMessageContentPart[][] = [
[{ type: "text", text: "test" }],
];
const allInjectedMessages: InjectedMessage[] = [];
const result = prepareStepMessages(
options,
pendingUserMessages,
allInjectedMessages,
);
expect(result).toBeDefined();
expect(result!.maxTokens).toBe(1000);
expect(result!.model).toBe("gpt-4");
expect(result!.tools).toEqual(["search", "write"]);
});
it("returns modified options when only previous injected messages exist", () => {
const existingInjected: InjectedMessage = {
insertAtIndex: 0,
sequence: 0,
message: {
role: "user",
content: [{ type: "text", text: "Previously injected" }],
},
};
const options = {
messages: [
{ role: "assistant", content: "Response" },
] satisfies ModelMessage[],
};
const pendingUserMessages: UserMessageContentPart[][] = [];
const allInjectedMessages: InjectedMessage[] = [existingInjected];
const result = prepareStepMessages(
options,
pendingUserMessages,
allInjectedMessages,
);
expect(result).toBeDefined();
expect(result!.messages).toHaveLength(2);
expect(result!.messages[0]).toEqual(existingInjected.message);
});
});
describe("integration: multi-step conversation simulation", () => {
it("correctly maintains message positions across multiple steps", () => {
const allInjectedMessages: InjectedMessage[] = [];
const pendingUserMessages: UserMessageContentPart[][] = [];
// Step 1: User sends initial prompt
let currentMessages: ModelMessage[] = [
{ role: "user", content: "Build a todo app" },
];
let result = prepareStepMessages(
{ messages: currentMessages },
pendingUserMessages,
allInjectedMessages,
);
expect(result).toBeUndefined(); // No injections yet
// Step 2: AI responds with tool call, tool adds screenshot
currentMessages = [
{ role: "user", content: "Build a todo app" },
{ role: "assistant", content: "Using web_crawl tool..." },
];
pendingUserMessages.push([
{ type: "text", text: "Screenshot of todo app reference:" },
{ type: "image-url", url: "https://example.com/screenshot.png" },
]);
pendingUserMessages.push([{ type: "text", text: "FOLLOWING" }]);
result = prepareStepMessages(
{ messages: currentMessages },
pendingUserMessages,
allInjectedMessages,
);
// With FIFO ordering fix, Screenshot (added first) appears before FOLLOWING (added second)
expect(result!.messages).toMatchInlineSnapshot(`
[
{
"content": "Build a todo app",
"role": "user",
},
{
"content": "Using web_crawl tool...",
"role": "assistant",
},
{
"content": [
{
"text": "Screenshot of todo app reference:",
"type": "text",
},
{
"image": "https://example.com/screenshot.png",
"type": "image",
},
],
"role": "user",
},
{
"content": [
{
"text": "FOLLOWING",
"type": "text",
},
],
"role": "user",
},
]
`);
// Screenshot should be inserted at position 2 (after the assistant message)
expect(allInjectedMessages[0].insertAtIndex).toBe(2);
// Step 3: AI continues, tool adds another screenshot
currentMessages = [
{ role: "user", content: "Build a todo app" },
{ role: "assistant", content: "Using web_crawl tool..." },
{ role: "assistant", content: "Analyzing the design..." },
];
pendingUserMessages.push([
{ type: "text", text: "Another screenshot:" },
{ type: "image-url", url: "https://example.com/screenshot2.png" },
]);
result = prepareStepMessages(
{ messages: currentMessages },
pendingUserMessages,
allInjectedMessages,
);
expect(result!.messages).toMatchInlineSnapshot(`
[
{
"content": "Build a todo app",
"role": "user",
},
{
"content": "Using web_crawl tool...",
"role": "assistant",
},
{
"content": [
{
"text": "Screenshot of todo app reference:",
"type": "text",
},
{
"image": "https://example.com/screenshot.png",
"type": "image",
},
],
"role": "user",
},
{
"content": [
{
"text": "FOLLOWING",
"type": "text",
},
],
"role": "user",
},
{
"content": "Analyzing the design...",
"role": "assistant",
},
{
"content": [
{
"text": "Another screenshot:",
"type": "text",
},
{
"image": "https://example.com/screenshot2.png",
"type": "image",
},
],
"role": "user",
},
]
`);
});
});
});
...@@ -47,6 +47,10 @@ import { ...@@ -47,6 +47,10 @@ import {
escapeXmlContent, escapeXmlContent,
UserMessageContentPart, UserMessageContentPart,
} from "./tools/types"; } from "./tools/types";
import {
prepareStepMessages,
type InjectedMessage,
} from "./prepare_step_utils";
import { TOOL_DEFINITIONS } from "./tool_definitions"; import { TOOL_DEFINITIONS } from "./tool_definitions";
import { parseAiMessagesJson } from "@/ipc/utils/ai_messages_utils"; import { parseAiMessagesJson } from "@/ipc/utils/ai_messages_utils";
import { parseMcpToolKey, sanitizeMcpName } from "@/ipc/utils/mcp_tool_utils"; import { parseMcpToolKey, sanitizeMcpName } from "@/ipc/utils/mcp_tool_utils";
...@@ -143,6 +147,8 @@ export async function handleLocalAgentStream( ...@@ -143,6 +147,8 @@ export async function handleLocalAgentStream(
// Track pending user messages to inject after tool results // Track pending user messages to inject after tool results
const pendingUserMessages: UserMessageContentPart[][] = []; const pendingUserMessages: UserMessageContentPart[][] = [];
// Store injected messages with their insertion index to re-inject at the same spot each step
const allInjectedMessages: InjectedMessage[] = [];
try { try {
// Get model client // Get model client
...@@ -227,28 +233,11 @@ export async function handleLocalAgentStream( ...@@ -227,28 +233,11 @@ export async function handleLocalAgentStream(
stopWhen: stepCountIs(25), // Allow multiple tool call rounds stopWhen: stepCountIs(25), // Allow multiple tool call rounds
abortSignal: abortController.signal, abortSignal: abortController.signal,
// Inject pending user messages (e.g., images from web_crawl) between steps // Inject pending user messages (e.g., images from web_crawl) between steps
prepareStep: ({ messages, ...rest }) => { // We must re-inject all accumulated messages each step because the AI SDK
if (pendingUserMessages.length === 0) { // doesn't persist dynamically injected messages in its internal state.
return undefined; // We track the insertion index so messages appear at the same position each step.
} prepareStep: (options) =>
// Build user messages from pending content prepareStepMessages(options, pendingUserMessages, allInjectedMessages),
const newMessages = [...messages];
for (const content of pendingUserMessages) {
newMessages.push({
role: "user" as const,
content: content.map((part) => {
if (part.type === "text") {
return { type: "text" as const, text: part.text };
}
// part.type === "image-url"
return { type: "image" as const, image: new URL(part.url) };
}),
});
}
// Clear pending messages after injection
pendingUserMessages.length = 0;
return { messages: newMessages, ...rest };
},
onFinish: async (response) => { onFinish: async (response) => {
const totalTokens = response.usage?.totalTokens; const totalTokens = response.usage?.totalTokens;
const inputTokens = response.usage?.inputTokens; const inputTokens = response.usage?.inputTokens;
......
/**
* Utility for preparing step messages with injected user content.
*
* This module contains pure functions extracted from the prepareStep callback
* in local_agent_handler.ts, enabling isolated unit testing.
*/
import {
ImagePart,
ModelMessage,
TextPart,
UserModelMessage,
} from "node_modules/ai/dist";
import type { UserMessageContentPart } from "./tools/types";
/**
* A message that has been processed and is ready to inject.
*/
export interface InjectedMessage {
insertAtIndex: number;
/** Sequence number to preserve FIFO order for same-index messages */
sequence: number;
message: UserModelMessage;
}
/**
* Transform a UserMessageContentPart to the format expected by the AI SDK.
*/
export function transformContentPart(
part: UserMessageContentPart,
): TextPart | ImagePart {
if (part.type === "text") {
return { type: "text", text: part.text };
}
// part.type === "image-url"
return { type: "image", image: new URL(part.url) };
}
/**
* Process pending user messages and add them to the injected messages list.
* Each message is recorded with the current message count as its insertion index.
*
* @param pendingUserMessages - Queue of pending messages (will be mutated/emptied)
* @param allInjectedMessages - List of already injected messages (will be mutated)
* @param currentMessageCount - The current number of messages in the conversation
*/
export function processPendingMessages(
pendingUserMessages: UserMessageContentPart[][],
allInjectedMessages: InjectedMessage[],
currentMessageCount: number,
): void {
while (pendingUserMessages.length > 0) {
const content = pendingUserMessages.shift()!;
allInjectedMessages.push({
insertAtIndex: currentMessageCount,
sequence: allInjectedMessages.length, // Track insertion order
message: {
role: "user" as const,
content: content.map(transformContentPart),
},
});
}
}
/**
* Build a new messages array with injected messages inserted at their recorded positions.
* Messages are processed in reverse order of insertion index to avoid shifting issues.
* For messages with the same index, we process in reverse sequence order to preserve FIFO.
*
* @param messages - The original messages array
* @param injectedMessages - Messages to inject with their target indices
* @returns New array with injected messages inserted at correct positions
*/
export function injectMessagesAtPositions<T>(
messages: T[],
injectedMessages: InjectedMessage[],
): (T | InjectedMessage["message"])[] {
if (injectedMessages.length === 0) {
return messages;
}
// Type as union from the start to allow inserting InjectedMessage["message"]
const newMessages: (T | InjectedMessage["message"])[] = [...messages];
// Sort by insertion index descending, then by sequence descending.
// The sequence descending ensures that for same-index messages,
// we splice the LAST-added first, so after all splices the FIRST-added
// ends up in front (preserving FIFO order).
const sortedInjections = [...injectedMessages].sort((a, b) => {
if (a.insertAtIndex !== b.insertAtIndex) {
return b.insertAtIndex - a.insertAtIndex;
}
return b.sequence - a.sequence;
});
for (const injection of sortedInjections) {
newMessages.splice(injection.insertAtIndex, 0, injection.message);
}
return newMessages;
}
/**
* The complete prepareStep logic as a pure function.
*
* @param options - The step options containing messages and other properties
* @param pendingUserMessages - Queue of pending messages to process
* @param allInjectedMessages - Accumulated list of injected messages
* @returns Modified options with injected messages, or undefined if no changes needed
*/
export function prepareStepMessages<
TMessage extends ModelMessage,
T extends { messages: TMessage[]; [key: string]: unknown },
>(
options: T,
pendingUserMessages: UserMessageContentPart[][],
allInjectedMessages: InjectedMessage[],
): (Omit<T, "messages"> & { messages: TMessage[] }) | undefined {
const { messages, ...rest } = options;
// Move any new pending messages to the permanent injected list
processPendingMessages(
pendingUserMessages,
allInjectedMessages,
messages.length,
);
// If no messages to inject, don't modify
if (allInjectedMessages.length === 0) {
return undefined;
}
// Build the new messages array with injections
// Cast is safe because InjectedMessage["message"] is a valid ModelMessage
const newMessages = injectMessagesAtPositions(
messages,
allInjectedMessages,
) as TMessage[];
return { messages: newMessages, ...rest };
}
...@@ -35,7 +35,7 @@ Trigger a crawl ONLY if BOTH conditions are true: ...@@ -35,7 +35,7 @@ Trigger a crawl ONLY if BOTH conditions are true:
const CLONE_INSTRUCTIONS = ` const CLONE_INSTRUCTIONS = `
Replicate the website from the provided HTML, markdown, and screenshot. Replicate the website from the provided screenshot image and markdown.
**Use the screenshot as your primary visual reference** to understand the layout, colors, typography, and overall design of the website. The screenshot shows exactly how the page should look. **Use the screenshot as your primary visual reference** to understand the layout, colors, typography, and overall design of the website. The screenshot shows exactly how the page should look.
...@@ -118,9 +118,6 @@ export const webCrawlTool: ToolDefinition<z.infer<typeof webCrawlSchema>> = { ...@@ -118,9 +118,6 @@ export const webCrawlTool: ToolDefinition<z.infer<typeof webCrawlSchema>> = {
throw new Error("No content available from web crawl"); throw new Error("No content available from web crawl");
} }
if (!result.html) {
throw new Error("No HTML available from web crawl");
}
if (!result.screenshot) { if (!result.screenshot) {
throw new Error("No screenshot available from web crawl"); throw new Error("No screenshot available from web crawl");
} }
...@@ -133,10 +130,6 @@ export const webCrawlTool: ToolDefinition<z.infer<typeof webCrawlSchema>> = { ...@@ -133,10 +130,6 @@ export const webCrawlTool: ToolDefinition<z.infer<typeof webCrawlSchema>> = {
type: "text", type: "text",
text: formatSnippet("Markdown snapshot:", result.markdown, "markdown"), text: formatSnippet("Markdown snapshot:", result.markdown, "markdown"),
}, },
{
type: "text",
text: formatSnippet("HTML snapshot:", result.html, "html"),
},
]); ]);
return "Web crawl completed."; return "Web crawl completed.";
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论