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
// Tests
// ============================================================================
const dyadRequestId = "test-request-id";
describe("handleLocalAgentStream", () => {
beforeEach(() => {
vi.clearAllMocks();
......@@ -285,7 +286,11 @@ describe("handleLocalAgentStream", () => {
event,
{ chatId: 1, prompt: "test" },
new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" },
{
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
);
// Assert
......@@ -310,7 +315,11 @@ describe("handleLocalAgentStream", () => {
event,
{ chatId: 1, prompt: "test" },
new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" },
{
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
);
// Assert
......@@ -332,7 +341,11 @@ describe("handleLocalAgentStream", () => {
event,
{ chatId: 999, prompt: "test" },
new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" },
{
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
),
).rejects.toThrow("Chat not found: 999");
});
......@@ -349,7 +362,11 @@ describe("handleLocalAgentStream", () => {
event,
{ chatId: 1, prompt: "test" },
new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" },
{
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
),
).rejects.toThrow("Chat not found: 1");
});
......@@ -373,7 +390,11 @@ describe("handleLocalAgentStream", () => {
event,
{ chatId: 1, prompt: "test" },
new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" },
{
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
);
// Assert - check that chunks were sent
......@@ -418,7 +439,11 @@ describe("handleLocalAgentStream", () => {
event,
{ chatId: 1, prompt: "test" },
new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" },
{
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
);
// Assert - find the final content update
......@@ -451,7 +476,11 @@ describe("handleLocalAgentStream", () => {
event,
{ chatId: 1, prompt: "test" },
new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" },
{
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
);
// Assert
......@@ -500,7 +529,11 @@ describe("handleLocalAgentStream", () => {
event,
{ chatId: 1, prompt: "test" },
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)
......@@ -540,7 +573,11 @@ describe("handleLocalAgentStream", () => {
event,
{ chatId: 1, prompt: "test" },
abortController,
{ placeholderMessageId: 10, systemPrompt: "You are helpful" },
{
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
);
// Assert - should have saved cancellation message
......@@ -569,7 +606,11 @@ describe("handleLocalAgentStream", () => {
event,
{ chatId: 1, prompt: "test" },
new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" },
{
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
);
// Assert - commit hash should be saved
......@@ -594,7 +635,11 @@ describe("handleLocalAgentStream", () => {
event,
{ chatId: 1, prompt: "test" },
new AbortController(),
{ placeholderMessageId: 10, systemPrompt: "You are helpful" },
{
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
);
// Assert - approval state should be set
......
......@@ -1014,6 +1014,7 @@ This conversation includes one or more image attachments. When the user uploads
await handleLocalAgentStream(event, req, abortController, {
placeholderMessageId: placeholderAssistantMessage.id,
systemPrompt,
dyadRequestId: dyadRequestId ?? "[no-request-id]",
});
return;
}
......
......@@ -12,6 +12,7 @@ import {
type ToolExecutionOptions,
} from "ai";
import log from "electron-log";
import { db } from "@/db";
import { chats, messages } from "@/db/schema";
import { eq } from "drizzle-orm";
......@@ -103,7 +104,12 @@ export async function handleLocalAgentStream(
{
placeholderMessageId,
systemPrompt,
}: { placeholderMessageId: number; systemPrompt: string },
dyadRequestId,
}: {
placeholderMessageId: number;
systemPrompt: string;
dyadRequestId: string;
},
): Promise<void> {
const settings = readSettings();
......@@ -134,8 +140,6 @@ export async function handleLocalAgentStream(
const appPath = getDyadAppPath(chat.app.path);
// Generate request ID
// Send initial message update
safeSend(event.sender, "chat:response:chunk", {
chatId: req.chatId,
......@@ -167,6 +171,7 @@ export async function handleLocalAgentStream(
messageId: placeholderMessageId,
isSharedModulesChanged: false,
todos: [],
dyadRequestId,
onXmlStream: (accumulatedXml: string) => {
// Stream accumulated XML to UI without persisting
streamingPreview = accumulatedXml;
......@@ -225,6 +230,7 @@ export async function handleLocalAgentStream(
}),
providerOptions: getProviderOptions({
dyadAppId: chat.app.id,
dyadRequestId,
dyadDisableFiles: true, // Local agent uses tools, not file injection
files: [],
mentionedAppsCodebases: [],
......
......@@ -7,13 +7,10 @@ import {
escapeXmlContent,
} from "./types";
import { extractCodebase } from "../../../../../../utils/codebase";
import { readSettings } from "@/main/settings";
import { engineFetch } from "./engine_fetch";
const logger = log.scope("code_search");
const DYAD_ENGINE_URL =
process.env.DYAD_ENGINE_URL ?? "https://engine.dyad.sh/v1";
const codeSearchSchema = z.object({
query: z.string().describe("Search query to find relevant files"),
});
......@@ -34,22 +31,11 @@ async function callCodeSearch(
},
ctx: AgentContext,
): 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
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",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
query: params.query,
filesContext: params.filesContext,
......
......@@ -9,14 +9,11 @@ import {
isServerFunction,
isSharedServerModule,
} from "../../../../../../supabase_admin/supabase_utils";
import { readSettings } from "@/main/settings";
import { engineFetch } from "./engine_fetch";
const readFile = fs.promises.readFile;
const logger = log.scope("edit_file");
const DYAD_ENGINE_URL =
process.env.DYAD_ENGINE_URL ?? "https://engine.dyad.sh/v1";
const editFileSchema = z.object({
path: z.string().describe("The file path relative to the app root"),
content: z.string().describe("The updated code snippet to apply"),
......@@ -27,25 +24,17 @@ const turboFileEditResponseSchema = z.object({
result: z.string(),
});
async function callTurboFileEdit(params: {
path: string;
content: string;
originalContent: 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`, {
async function callTurboFileEdit(
params: {
path: string;
content: string;
originalContent: string;
description?: string;
},
ctx: AgentContext,
): Promise<string> {
const response = await engineFetch(ctx, "/tools/turbo-file-edit", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
path: params.path,
content: params.content,
......@@ -174,12 +163,15 @@ export const editFileTool: ToolDefinition<z.infer<typeof editFileSchema>> = {
const originalContent = await readFile(fullFilePath, "utf8");
// Call the turbo-file-edit endpoint
const newContent = await callTurboFileEdit({
path: args.path,
content: args.content,
originalContent,
description: args.description,
});
const newContent = await callTurboFileEdit(
{
path: args.path,
content: args.content,
originalContent,
description: args.description,
},
ctx,
);
if (!newContent) {
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 {
chatSummary?: string;
/** Turn-scoped todo list for agent task tracking */
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).
* Call this repeatedly with the full accumulated XML so far.
......
import { z } from "zod";
import log from "electron-log";
import { ToolDefinition, escapeXmlContent } from "./types";
import { readSettings } from "@/main/settings";
import { ToolDefinition, escapeXmlContent, AgentContext } from "./types";
import { engineFetch } from "./engine_fetch";
const logger = log.scope("web_crawl");
const DYAD_ENGINE_URL =
process.env.DYAD_ENGINE_URL ?? "https://engine.dyad.sh/v1";
const webCrawlSchema = z.object({
url: z.string().describe("URL to crawl"),
});
......@@ -59,20 +56,10 @@ Always include the placeholder.svg file in your output file tree.
async function callWebCrawl(
url: string,
ctx: Pick<AgentContext, "dyadRequestId">,
): Promise<z.infer<typeof webCrawlResponseSchema>> {
const settings = readSettings();
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`, {
const response = await engineFetch(ctx, "/tools/web-crawl", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({ url }),
});
......@@ -108,7 +95,7 @@ export const webCrawlTool: ToolDefinition<z.infer<typeof webCrawlSchema>> = {
execute: async (args, ctx) => {
logger.log(`Executing web crawl: ${args.url}`);
const result = await callWebCrawl(args.url);
const result = await callWebCrawl(args.url, ctx);
if (!result) {
throw new Error("Web crawl returned no results");
......
......@@ -6,13 +6,10 @@ import {
escapeXmlAttr,
escapeXmlContent,
} from "./types";
import { readSettings } from "@/main/settings";
import { engineFetch } from "./engine_fetch";
const logger = log.scope("web_search");
const DYAD_ENGINE_URL =
process.env.DYAD_ENGINE_URL ?? "https://engine.dyad.sh/v1";
const webSearchSchema = z.object({
query: z.string().describe("The search query to look up on the web"),
});
......@@ -102,20 +99,11 @@ async function callWebSearchSSE(
query: string,
ctx: AgentContext,
): 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)}">`);
const response = await fetch(`${DYAD_ENGINE_URL}/tools/web-search`, {
const response = await engineFetch(ctx, "/tools/web-search", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
Accept: "text/event-stream",
},
body: JSON.stringify({ query }),
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论