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

Send dyad request id to engine fetch calls (#2181)

<!-- CURSOR_SUMMARY --> > [!NOTE] > Implements request-level tracing for local-agent by threading `dyadRequestId` through handlers and tool calls, and consolidates engine API requests. > > - Pass `dyadRequestId` from `chat_stream_handlers.ts` into `handleLocalAgentStream` and include in `providerOptions` > - Update `handleLocalAgentStream` signature and `AgentContext` to include `dyadRequestId`; propagate to `streamText` and tool execution context > - Introduce `pro/main/ipc/handlers/local_agent/tools/engine_fetch.ts` to centralize Dyad engine requests, automatically adding `Authorization` and `X-Dyad-Request-Id` headers > - Refactor tools (`code_search.ts`, `edit_file.ts`, `web_crawl.ts`, `web_search.ts`) to use `engineFetch` and remove per-file API key/URL handling > - Adjust tests to supply `dyadRequestId` and validate unchanged behaviors (errors, streaming, abort, commits, approvals) > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6a90d98fef23e459a0679c46b8eace8d907e0be9. 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 Pass the Dyad request ID through the local agent and include it on all engine tool calls to enable end-to-end request tracing. Centralizes engine API calls with a shared fetch wrapper. - **Refactors** - Added engine_fetch wrapper that sets Authorization and X-Dyad-Request-Id headers. - handleLocalAgentStream now accepts dyadRequestId and forwards it to AgentContext and provider options. - Updated tools (code_search, edit_file, web_search, web_crawl) to use engineFetch and removed duplicate URL/API key handling. - chat_stream_handlers forwards dyadRequestId (fallback: “[no-request-id]”). - Tests updated to include dyadRequestId in handler calls. <sup>Written for commit 6a90d98fef23e459a0679c46b8eace8d907e0be9. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. -->
上级 8d28d508
...@@ -264,6 +264,7 @@ import { handleLocalAgentStream } from "@/pro/main/ipc/handlers/local_agent/loca ...@@ -264,6 +264,7 @@ import { handleLocalAgentStream } from "@/pro/main/ipc/handlers/local_agent/loca
// Tests // Tests
// ============================================================================ // ============================================================================
const dyadRequestId = "test-request-id";
describe("handleLocalAgentStream", () => { describe("handleLocalAgentStream", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
...@@ -285,7 +286,11 @@ describe("handleLocalAgentStream", () => { ...@@ -285,7 +286,11 @@ describe("handleLocalAgentStream", () => {
event, event,
{ chatId: 1, prompt: "test" }, { chatId: 1, prompt: "test" },
new AbortController(), new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" }, {
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
); );
// Assert // Assert
...@@ -310,7 +315,11 @@ describe("handleLocalAgentStream", () => { ...@@ -310,7 +315,11 @@ describe("handleLocalAgentStream", () => {
event, event,
{ chatId: 1, prompt: "test" }, { chatId: 1, prompt: "test" },
new AbortController(), new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" }, {
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
); );
// Assert // Assert
...@@ -332,7 +341,11 @@ describe("handleLocalAgentStream", () => { ...@@ -332,7 +341,11 @@ describe("handleLocalAgentStream", () => {
event, event,
{ chatId: 999, prompt: "test" }, { chatId: 999, prompt: "test" },
new AbortController(), new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" }, {
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
), ),
).rejects.toThrow("Chat not found: 999"); ).rejects.toThrow("Chat not found: 999");
}); });
...@@ -349,7 +362,11 @@ describe("handleLocalAgentStream", () => { ...@@ -349,7 +362,11 @@ describe("handleLocalAgentStream", () => {
event, event,
{ chatId: 1, prompt: "test" }, { chatId: 1, prompt: "test" },
new AbortController(), new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" }, {
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
), ),
).rejects.toThrow("Chat not found: 1"); ).rejects.toThrow("Chat not found: 1");
}); });
...@@ -373,7 +390,11 @@ describe("handleLocalAgentStream", () => { ...@@ -373,7 +390,11 @@ describe("handleLocalAgentStream", () => {
event, event,
{ chatId: 1, prompt: "test" }, { chatId: 1, prompt: "test" },
new AbortController(), new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" }, {
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
); );
// Assert - check that chunks were sent // Assert - check that chunks were sent
...@@ -418,7 +439,11 @@ describe("handleLocalAgentStream", () => { ...@@ -418,7 +439,11 @@ describe("handleLocalAgentStream", () => {
event, event,
{ chatId: 1, prompt: "test" }, { chatId: 1, prompt: "test" },
new AbortController(), new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" }, {
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
); );
// Assert - find the final content update // Assert - find the final content update
...@@ -451,7 +476,11 @@ describe("handleLocalAgentStream", () => { ...@@ -451,7 +476,11 @@ describe("handleLocalAgentStream", () => {
event, event,
{ chatId: 1, prompt: "test" }, { chatId: 1, prompt: "test" },
new AbortController(), new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" }, {
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
); );
// Assert // Assert
...@@ -500,7 +529,11 @@ describe("handleLocalAgentStream", () => { ...@@ -500,7 +529,11 @@ describe("handleLocalAgentStream", () => {
event, event,
{ chatId: 1, prompt: "test" }, { chatId: 1, prompt: "test" },
abortController, abortController,
{ placeholderMessageId: 10, systemPrompt: "You are helpful" }, {
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
); );
// Assert - only first chunk should be processed (stream breaks on abort) // Assert - only first chunk should be processed (stream breaks on abort)
...@@ -540,7 +573,11 @@ describe("handleLocalAgentStream", () => { ...@@ -540,7 +573,11 @@ describe("handleLocalAgentStream", () => {
event, event,
{ chatId: 1, prompt: "test" }, { chatId: 1, prompt: "test" },
abortController, abortController,
{ placeholderMessageId: 10, systemPrompt: "You are helpful" }, {
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
); );
// Assert - should have saved cancellation message // Assert - should have saved cancellation message
...@@ -569,7 +606,11 @@ describe("handleLocalAgentStream", () => { ...@@ -569,7 +606,11 @@ describe("handleLocalAgentStream", () => {
event, event,
{ chatId: 1, prompt: "test" }, { chatId: 1, prompt: "test" },
new AbortController(), new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" }, {
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
); );
// Assert - commit hash should be saved // Assert - commit hash should be saved
...@@ -594,7 +635,11 @@ describe("handleLocalAgentStream", () => { ...@@ -594,7 +635,11 @@ describe("handleLocalAgentStream", () => {
event, event,
{ chatId: 1, prompt: "test" }, { chatId: 1, prompt: "test" },
new AbortController(), new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" }, {
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
); );
// Assert - approval state should be set // Assert - approval state should be set
......
...@@ -1014,6 +1014,7 @@ This conversation includes one or more image attachments. When the user uploads ...@@ -1014,6 +1014,7 @@ This conversation includes one or more image attachments. When the user uploads
await handleLocalAgentStream(event, req, abortController, { await handleLocalAgentStream(event, req, abortController, {
placeholderMessageId: placeholderAssistantMessage.id, placeholderMessageId: placeholderAssistantMessage.id,
systemPrompt, systemPrompt,
dyadRequestId: dyadRequestId ?? "[no-request-id]",
}); });
return; return;
} }
......
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
type ToolExecutionOptions, type ToolExecutionOptions,
} from "ai"; } from "ai";
import log from "electron-log"; import log from "electron-log";
import { db } from "@/db"; import { db } from "@/db";
import { chats, messages } from "@/db/schema"; import { chats, messages } from "@/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
...@@ -103,7 +104,12 @@ export async function handleLocalAgentStream( ...@@ -103,7 +104,12 @@ export async function handleLocalAgentStream(
{ {
placeholderMessageId, placeholderMessageId,
systemPrompt, systemPrompt,
}: { placeholderMessageId: number; systemPrompt: string }, dyadRequestId,
}: {
placeholderMessageId: number;
systemPrompt: string;
dyadRequestId: string;
},
): Promise<void> { ): Promise<void> {
const settings = readSettings(); const settings = readSettings();
...@@ -134,8 +140,6 @@ export async function handleLocalAgentStream( ...@@ -134,8 +140,6 @@ export async function handleLocalAgentStream(
const appPath = getDyadAppPath(chat.app.path); const appPath = getDyadAppPath(chat.app.path);
// Generate request ID
// Send initial message update // Send initial message update
safeSend(event.sender, "chat:response:chunk", { safeSend(event.sender, "chat:response:chunk", {
chatId: req.chatId, chatId: req.chatId,
...@@ -167,6 +171,7 @@ export async function handleLocalAgentStream( ...@@ -167,6 +171,7 @@ export async function handleLocalAgentStream(
messageId: placeholderMessageId, messageId: placeholderMessageId,
isSharedModulesChanged: false, isSharedModulesChanged: false,
todos: [], todos: [],
dyadRequestId,
onXmlStream: (accumulatedXml: string) => { onXmlStream: (accumulatedXml: string) => {
// Stream accumulated XML to UI without persisting // Stream accumulated XML to UI without persisting
streamingPreview = accumulatedXml; streamingPreview = accumulatedXml;
...@@ -225,6 +230,7 @@ export async function handleLocalAgentStream( ...@@ -225,6 +230,7 @@ export async function handleLocalAgentStream(
}), }),
providerOptions: getProviderOptions({ providerOptions: getProviderOptions({
dyadAppId: chat.app.id, dyadAppId: chat.app.id,
dyadRequestId,
dyadDisableFiles: true, // Local agent uses tools, not file injection dyadDisableFiles: true, // Local agent uses tools, not file injection
files: [], files: [],
mentionedAppsCodebases: [], mentionedAppsCodebases: [],
......
...@@ -7,13 +7,10 @@ import { ...@@ -7,13 +7,10 @@ import {
escapeXmlContent, escapeXmlContent,
} from "./types"; } from "./types";
import { extractCodebase } from "../../../../../../utils/codebase"; import { extractCodebase } from "../../../../../../utils/codebase";
import { readSettings } from "@/main/settings"; import { engineFetch } from "./engine_fetch";
const logger = log.scope("code_search"); const logger = log.scope("code_search");
const DYAD_ENGINE_URL =
process.env.DYAD_ENGINE_URL ?? "https://engine.dyad.sh/v1";
const codeSearchSchema = z.object({ const codeSearchSchema = z.object({
query: z.string().describe("Search query to find relevant files"), query: z.string().describe("Search query to find relevant files"),
}); });
...@@ -34,22 +31,11 @@ async function callCodeSearch( ...@@ -34,22 +31,11 @@ async function callCodeSearch(
}, },
ctx: AgentContext, ctx: AgentContext,
): Promise<string[]> { ): Promise<string[]> {
const settings = readSettings();
const apiKey = settings.providerSettings?.auto?.apiKey?.value;
if (!apiKey) {
throw new Error("Dyad Pro API key is required for code_search tool");
}
// Stream initial state to UI // Stream initial state to UI
ctx.onXmlStream(`<dyad-code-search query="${escapeXmlAttr(params.query)}">`); ctx.onXmlStream(`<dyad-code-search query="${escapeXmlAttr(params.query)}">`);
const response = await fetch(`${DYAD_ENGINE_URL}/tools/code-search`, { const response = await engineFetch(ctx, "/tools/code-search", {
method: "POST", method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({ body: JSON.stringify({
query: params.query, query: params.query,
filesContext: params.filesContext, filesContext: params.filesContext,
......
...@@ -9,14 +9,11 @@ import { ...@@ -9,14 +9,11 @@ import {
isServerFunction, isServerFunction,
isSharedServerModule, isSharedServerModule,
} from "../../../../../../supabase_admin/supabase_utils"; } from "../../../../../../supabase_admin/supabase_utils";
import { readSettings } from "@/main/settings"; import { engineFetch } from "./engine_fetch";
const readFile = fs.promises.readFile; const readFile = fs.promises.readFile;
const logger = log.scope("edit_file"); const logger = log.scope("edit_file");
const DYAD_ENGINE_URL =
process.env.DYAD_ENGINE_URL ?? "https://engine.dyad.sh/v1";
const editFileSchema = z.object({ const editFileSchema = z.object({
path: z.string().describe("The file path relative to the app root"), path: z.string().describe("The file path relative to the app root"),
content: z.string().describe("The updated code snippet to apply"), content: z.string().describe("The updated code snippet to apply"),
...@@ -27,25 +24,17 @@ const turboFileEditResponseSchema = z.object({ ...@@ -27,25 +24,17 @@ const turboFileEditResponseSchema = z.object({
result: z.string(), result: z.string(),
}); });
async function callTurboFileEdit(params: { async function callTurboFileEdit(
params: {
path: string; path: string;
content: string; content: string;
originalContent: string; originalContent: string;
description?: string; description?: string;
}): Promise<string> {
const settings = readSettings();
const apiKey = settings.providerSettings?.auto?.apiKey?.value;
if (!apiKey) {
throw new Error("Dyad Pro API key is required for edit_file tool");
}
const response = await fetch(`${DYAD_ENGINE_URL}/tools/turbo-file-edit`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
}, },
ctx: AgentContext,
): Promise<string> {
const response = await engineFetch(ctx, "/tools/turbo-file-edit", {
method: "POST",
body: JSON.stringify({ body: JSON.stringify({
path: params.path, path: params.path,
content: params.content, content: params.content,
...@@ -174,12 +163,15 @@ export const editFileTool: ToolDefinition<z.infer<typeof editFileSchema>> = { ...@@ -174,12 +163,15 @@ export const editFileTool: ToolDefinition<z.infer<typeof editFileSchema>> = {
const originalContent = await readFile(fullFilePath, "utf8"); const originalContent = await readFile(fullFilePath, "utf8");
// Call the turbo-file-edit endpoint // Call the turbo-file-edit endpoint
const newContent = await callTurboFileEdit({ const newContent = await callTurboFileEdit(
{
path: args.path, path: args.path,
content: args.content, content: args.content,
originalContent, originalContent,
description: args.description, description: args.description,
}); },
ctx,
);
if (!newContent) { if (!newContent) {
throw new Error( throw new Error(
......
/**
* Shared utility for making fetch requests to the Dyad engine API.
* Handles common headers including Authorization and X-Dyad-Request-Id.
*/
import { readSettings } from "@/main/settings";
import type { AgentContext } from "./types";
export const DYAD_ENGINE_URL =
process.env.DYAD_ENGINE_URL ?? "https://engine.dyad.sh/v1";
export interface EngineFetchOptions extends Omit<RequestInit, "headers"> {
/** Additional headers to include */
headers?: Record<string, string>;
}
/**
* Fetch wrapper for Dyad engine API calls.
* Automatically adds Authorization and X-Dyad-Request-Id headers.
*
* @param ctx - The agent context containing the request ID
* @param endpoint - The API endpoint path (e.g., "/tools/web-search")
* @param options - Fetch options (method, body, additional headers, etc.)
* @returns The fetch Response
* @throws Error if Dyad Pro API key is not configured
*/
export async function engineFetch(
ctx: Pick<AgentContext, "dyadRequestId">,
endpoint: string,
options: EngineFetchOptions = {},
): Promise<Response> {
const settings = readSettings();
const apiKey = settings.providerSettings?.auto?.apiKey?.value;
if (!apiKey) {
throw new Error("Dyad Pro API key is required");
}
const { headers: extraHeaders, ...restOptions } = options;
return fetch(`${DYAD_ENGINE_URL}${endpoint}`, {
...restOptions,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"X-Dyad-Request-Id": ctx.dyadRequestId,
...extraHeaders,
},
});
}
...@@ -42,6 +42,8 @@ export interface AgentContext { ...@@ -42,6 +42,8 @@ export interface AgentContext {
chatSummary?: string; chatSummary?: string;
/** Turn-scoped todo list for agent task tracking */ /** Turn-scoped todo list for agent task tracking */
todos: Todo[]; todos: Todo[];
/** Request ID for tracking requests to the Dyad engine */
dyadRequestId: string;
/** /**
* Streams accumulated XML to UI without persisting to DB (for live preview). * Streams accumulated XML to UI without persisting to DB (for live preview).
* Call this repeatedly with the full accumulated XML so far. * Call this repeatedly with the full accumulated XML so far.
......
import { z } from "zod"; import { z } from "zod";
import log from "electron-log"; import log from "electron-log";
import { ToolDefinition, escapeXmlContent } from "./types"; import { ToolDefinition, escapeXmlContent, AgentContext } from "./types";
import { readSettings } from "@/main/settings"; import { engineFetch } from "./engine_fetch";
const logger = log.scope("web_crawl"); const logger = log.scope("web_crawl");
const DYAD_ENGINE_URL =
process.env.DYAD_ENGINE_URL ?? "https://engine.dyad.sh/v1";
const webCrawlSchema = z.object({ const webCrawlSchema = z.object({
url: z.string().describe("URL to crawl"), url: z.string().describe("URL to crawl"),
}); });
...@@ -59,20 +56,10 @@ Always include the placeholder.svg file in your output file tree. ...@@ -59,20 +56,10 @@ Always include the placeholder.svg file in your output file tree.
async function callWebCrawl( async function callWebCrawl(
url: string, url: string,
ctx: Pick<AgentContext, "dyadRequestId">,
): Promise<z.infer<typeof webCrawlResponseSchema>> { ): Promise<z.infer<typeof webCrawlResponseSchema>> {
const settings = readSettings(); const response = await engineFetch(ctx, "/tools/web-crawl", {
const apiKey = settings.providerSettings?.auto?.apiKey?.value;
if (!apiKey) {
throw new Error("Dyad Pro API key is required for web_crawl tool");
}
const response = await fetch(`${DYAD_ENGINE_URL}/tools/web-crawl`, {
method: "POST", method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({ url }), body: JSON.stringify({ url }),
}); });
...@@ -108,7 +95,7 @@ export const webCrawlTool: ToolDefinition<z.infer<typeof webCrawlSchema>> = { ...@@ -108,7 +95,7 @@ export const webCrawlTool: ToolDefinition<z.infer<typeof webCrawlSchema>> = {
execute: async (args, ctx) => { execute: async (args, ctx) => {
logger.log(`Executing web crawl: ${args.url}`); logger.log(`Executing web crawl: ${args.url}`);
const result = await callWebCrawl(args.url); const result = await callWebCrawl(args.url, ctx);
if (!result) { if (!result) {
throw new Error("Web crawl returned no results"); throw new Error("Web crawl returned no results");
......
...@@ -6,13 +6,10 @@ import { ...@@ -6,13 +6,10 @@ import {
escapeXmlAttr, escapeXmlAttr,
escapeXmlContent, escapeXmlContent,
} from "./types"; } from "./types";
import { readSettings } from "@/main/settings"; import { engineFetch } from "./engine_fetch";
const logger = log.scope("web_search"); const logger = log.scope("web_search");
const DYAD_ENGINE_URL =
process.env.DYAD_ENGINE_URL ?? "https://engine.dyad.sh/v1";
const webSearchSchema = z.object({ const webSearchSchema = z.object({
query: z.string().describe("The search query to look up on the web"), query: z.string().describe("The search query to look up on the web"),
}); });
...@@ -102,20 +99,11 @@ async function callWebSearchSSE( ...@@ -102,20 +99,11 @@ async function callWebSearchSSE(
query: string, query: string,
ctx: AgentContext, ctx: AgentContext,
): Promise<string> { ): Promise<string> {
const settings = readSettings();
const apiKey = settings.providerSettings?.auto?.apiKey?.value;
if (!apiKey) {
throw new Error("Dyad Pro API key is required for web_search tool");
}
ctx.onXmlStream(`<dyad-web-search query="${escapeXmlAttr(query)}">`); ctx.onXmlStream(`<dyad-web-search query="${escapeXmlAttr(query)}">`);
const response = await fetch(`${DYAD_ENGINE_URL}/tools/web-search`, { const response = await engineFetch(ctx, "/tools/web-search", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
Accept: "text/event-stream", Accept: "text/event-stream",
}, },
body: JSON.stringify({ query }), body: JSON.stringify({ query }),
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论