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

feat: enable read-only local agent for pro ask mode (#2260)

For pro users, ask mode now uses the local agent handler in read-only mode, giving them access to code reading tools (read_file, list_files, grep, code_search, etc.) while preventing any state modifications. Changes: - Add `modifiesState` property to ToolDefinition interface - Mark state-modifying tools (write_file, edit_file, delete_file, rename_file, add_dependency, execute_sql, add_integration) - Add `readOnly` option to buildAgentToolSet to filter out state-modifying tools - Add `readOnly` option to handleLocalAgentStream to skip commits/ deploys and exclude MCP tools in read-only mode - Create LOCAL_AGENT_ASK_SYSTEM_PROMPT for read-only agent mode - Route pro ask mode users to local agent with readOnly: true <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Enables read-only local-agent behavior for Pro users in `ask` mode, providing code-inspection tools without allowing modifications. > > - Route `ask` → local-agent with `readOnly: true` (when Pro and no mentioned apps); persist `aiMessagesJson` for these modes > - Add `modifiesState` to `ToolDefinition`; mark state-changing tools and filter them in `buildAgentToolSet({ readOnly })`; exclude MCP tools in read-only > - Update `handleLocalAgentStream` to support `readOnly`: skip commits/deploys and return `updatedFiles: false` > - Introduce `LOCAL_AGENT_ASK_SYSTEM_PROMPT` and plumb `readOnly` through `constructSystemPrompt`/`constructLocalAgentPrompt` > - E2E: new ask-mode tests/fixtures and snapshot updates; add deterministic normalization of tool_call IDs in test helper > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a67cbc1ccad35bf1fad9277bfce14a8999e763ca. 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 Pro ask mode now uses the local agent in read-only mode, letting users read and search code without changing anything. Commits, deploys, and state-modifying tools are disabled. - **New Features** - Route Pro ask mode to the local agent with readOnly: true when no other apps/codebases are mentioned. - Tools declare modifiesState; write/edit/delete/rename/add_dependency/execute_sql/add_integration are marked and excluded in read-only mode. - buildAgentToolSet and handleLocalAgentStream support readOnly to filter tools, skip MCP tools, and avoid commits/deploys (updatedFiles: false). - Added LOCAL_AGENT_ASK_SYSTEM_PROMPT and prompt wiring to support read-only behavior. <sup>Written for commit a67cbc1ccad35bf1fad9277bfce14a8999e763ca. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude <noreply@anthropic.com>
上级 8377178d
import type { LocalAgentFixture } from "../../../../testing/fake-llm-server/localAgentTypes";
export const fixture: LocalAgentFixture = {
description: "Read a file in ask mode (read-only)",
turns: [
{
text: "Let me read the file to explain its contents.",
toolCalls: [
{
name: "read_file",
args: {
path: "src/App.tsx",
},
},
],
},
{
text: "This is a simple React component that renders a div with the text 'Minimal imported app'. The component is exported as the default export.",
},
],
};
...@@ -40,6 +40,46 @@ function normalizeItemReferences(dump: any): void { ...@@ -40,6 +40,46 @@ function normalizeItemReferences(dump: any): void {
} }
} }
/**
* Normalizes tool_call IDs and tool_call_id references to be deterministic.
* Tool call IDs have the format "call_[timestamp]_[index]" which changes between runs.
*/
function normalizeToolCallIds(dump: any): void {
const messages = dump?.body?.messages;
if (!Array.isArray(messages)) {
return;
}
const oldToNewId: Record<string, string> = {};
let toolCallIndex = 0;
// First pass: collect all tool_call IDs and create mapping
for (const message of messages) {
if (message?.tool_calls && Array.isArray(message.tool_calls)) {
for (const toolCall of message.tool_calls) {
if (toolCall?.id && !oldToNewId[toolCall.id]) {
oldToNewId[toolCall.id] = `[[TOOL_CALL_${toolCallIndex}]]`;
toolCallIndex++;
}
}
}
}
// Second pass: replace all IDs
for (const message of messages) {
if (message?.tool_calls && Array.isArray(message.tool_calls)) {
for (const toolCall of message.tool_calls) {
if (toolCall?.id && oldToNewId[toolCall.id]) {
toolCall.id = oldToNewId[toolCall.id];
}
}
}
if (message?.tool_call_id && oldToNewId[message.tool_call_id]) {
message.tool_call_id = oldToNewId[message.tool_call_id];
}
}
}
/** /**
* Normalizes fileId hashes in versioned_files to be deterministic. * Normalizes fileId hashes in versioned_files to be deterministic.
* FileIds are SHA-256 hashes that may include non-deterministic components * FileIds are SHA-256 hashes that may include non-deterministic components
...@@ -436,7 +476,7 @@ export class PageObject { ...@@ -436,7 +476,7 @@ export class PageObject {
await this.page.getByTestId("chat-mode-selector").click(); await this.page.getByTestId("chat-mode-selector").click();
const mapping = { const mapping = {
build: "Build Generate and edit code", build: "Build Generate and edit code",
ask: "Ask", ask: "Ask Ask",
agent: "Build with MCP", agent: "Build with MCP",
"local-agent": "Agent v2", "local-agent": "Agent v2",
}; };
...@@ -856,6 +896,8 @@ export class PageObject { ...@@ -856,6 +896,8 @@ export class PageObject {
normalizeVersionedFiles(parsedDump); normalizeVersionedFiles(parsedDump);
// Normalize item_reference IDs (e.g., msg_1234567890) to be deterministic // Normalize item_reference IDs (e.g., msg_1234567890) to be deterministic
normalizeItemReferences(parsedDump); normalizeItemReferences(parsedDump);
// Normalize tool_call IDs (e.g., call_1234567890_0) to be deterministic
normalizeToolCallIds(parsedDump);
expect( expect(
JSON.stringify(parsedDump, null, 2).replace(/\\r\\n/g, "\\n"), JSON.stringify(parsedDump, null, 2).replace(/\\r\\n/g, "\\n"),
).toMatchSnapshot(name); ).toMatchSnapshot(name);
......
import { testSkipIfWindows } from "./helpers/test_helper";
/**
* E2E tests for local-agent in ask mode (read-only mode for Pro users)
* Tests that Pro users in ask mode get access to read-only tools
*/
testSkipIfWindows("local-agent ask mode", async ({ po }) => {
await po.setUpDyadPro({ localAgent: true });
await po.importApp("minimal");
// Select ask mode - local agent will be used in read-only mode for Pro users
await po.selectChatMode("ask");
// Test read-only tools work
await po.sendPrompt("tc=local-agent/ask-read-file");
await po.snapshotMessages();
// Dump request to verify only read-only tools are provided
await po.sendPrompt("[dump]");
await po.snapshotServerDump("request");
});
- paragraph: /Generate an AI_RULES\.md file for this app\. Describe the tech stack in 5-\d+ bullet points and describe clear rules about what libraries to use for what\./
- img
- text: file1.txt
- button "Edit":
- img
- img
- text: file1.txt
- paragraph: More EOM
- button:
- img
- img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- img
- paragraph: tc=local-agent/ask-read-file
- paragraph: Let me read the file to explain its contents.
- img
- text: App.tsx Read src/App.tsx
- paragraph: This is a simple React component that renders a div with the text 'Minimal imported app'. The component is exported as the default export.
- button:
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- img
- button "Undo":
- img
- button "Retry":
- img
\ No newline at end of file
{
"body": {
"model": "anthropic/claude-opus-4-5",
"max_tokens": 32000,
"temperature": 0,
"messages": [
{
"role": "system",
"content": "[[SYSTEM_MESSAGE]]"
},
{
"role": "user",
"content": "Generate an AI_RULES.md file for this app. Describe the tech stack in 5-10 bullet points and describe clear rules about what libraries to use for what."
},
{
"role": "assistant",
"content": "\n <dyad-write path=\"file1.txt\">\n A file (2)\n </dyad-write>\n More\n EOM"
},
{
"role": "user",
"content": "tc=local-agent/ask-read-file"
},
{
"role": "assistant",
"content": "Let me read the file to explain its contents.",
"tool_calls": [
{
"id": "[[TOOL_CALL_0]]",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\":\"src/App.tsx\"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "[[TOOL_CALL_0]]",
"content": "{\"type\":\"text\",\"value\":\"const App = () => <div>Minimal imported app</div>;\\n\\nexport default App;\\n\"}"
},
{
"role": "assistant",
"content": "This is a simple React component that renders a div with the text 'Minimal imported app'. The component is exported as the default export."
},
{
"role": "user",
"content": "[dump]"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read the content of a file from the codebase.\n \n- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "The file path to read"
}
},
"required": [
"path"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
},
{
"type": "function",
"function": {
"name": "list_files",
"description": "List files in the application directory. By default, lists only the immediate directory contents. Use recursive=true to list all files recursively. If you are not sure, list all files by omitting the directory parameter.",
"parameters": {
"type": "object",
"properties": {
"directory": {
"type": "string",
"description": "Optional subdirectory to list"
},
"recursive": {
"type": "boolean",
"description": "Whether to list files recursively (default: false)"
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
},
{
"type": "function",
"function": {
"name": "grep",
"description": "Search for a regex pattern in the codebase using ripgrep.\n\n- Returns matching lines with file paths and line numbers\n- By default, the search is case-insensitive\n- Use include_pattern to filter by file type (e.g. '*.tsx')\n- Use exclude_pattern to skip certain files (e.g. '*.test.ts')",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The regex pattern to search for"
},
"include_pattern": {
"type": "string",
"description": "Glob pattern for files to include (e.g. '*.ts' for TypeScript files)"
},
"exclude_pattern": {
"type": "string",
"description": "Glob pattern for files to exclude"
},
"case_sensitive": {
"type": "boolean",
"description": "Whether the search should be case sensitive (default: false)"
}
},
"required": [
"query"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
},
{
"type": "function",
"function": {
"name": "code_search",
"description": "Search the codebase semantically to find files relevant to a query. Use this tool when you need to discover which files contain code related to a specific concept, feature, or functionality. Returns a list of file paths that are most relevant to the search query.\n\n### When to Use This Tool\n\n- Explore unfamiliar codebases\n- Ask \"how / where / what\" questions to understand behavior\n- Find code by meaning rather than exact text\n\n### When NOT to Use\n\nSkip this tool for:\n1. Exact text matches (use `grep`)\n2. Reading known files (use `read_file`)\n3. Simple symbol lookups (use `grep`)\n",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query to find relevant files"
}
},
"required": [
"query"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
},
{
"type": "function",
"function": {
"name": "set_chat_summary",
"description": "Set the title/summary for this chat message. You should always call this message at the end of the turn when you have finished calling all the other tools.",
"parameters": {
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "A short summary/title for the chat"
}
},
"required": [
"summary"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
},
{
"type": "function",
"function": {
"name": "read_logs",
"description": "Read logs at the moment this tool is called. Includes client logs, server logs, edge function logs, and network requests. Use this to debug errors, investigate issues, or understand app behavior. IMPORTANT: Logs are a snapshot from when you call this tool - they will NOT update while you are writing code or making changes. Use filters (searchTerm, type, level) to narrow down relevant logs on the first call.",
"parameters": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"all",
"client",
"server",
"edge-function",
"network-requests"
],
"description": "Filter by log source type (default: all). Types: 'client' = browser console logs; 'server' = backend (including development server) logs and build output; 'edge-function' = edge function logs; 'network-requests' = HTTP requests and responses (outgoing calls and their responses)."
},
"level": {
"type": "string",
"enum": [
"all",
"info",
"warn",
"error"
],
"description": "Filter by log level (default: all)"
},
"searchTerm": {
"type": "string",
"description": "Search for logs containing this text (case-insensitive)"
},
"limit": {
"type": "number",
"minimum": 1,
"maximum": 200,
"description": "Maximum number of logs to return (default: 50, max: 200)"
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
},
{
"type": "function",
"function": {
"name": "web_search",
"description": "\nUse this tool to access real-time information beyond your training data cutoff.\n\nWhen to Search:\n- Current API documentation, library versions, or breaking changes\n- Latest best practices, security advisories, or bug fixes\n- Specific error messages or troubleshooting solutions\n- Recent framework updates or deprecation notices\n\nQuery Tips:\n- Be specific: Include version numbers, exact error messages, or technical terms\n- Add context: \"React 19 useEffect cleanup\" not just \"React hooks\"\n\nExamples:\n\n<example>\nOpenAI GPT-5 API model names\n</example>\n\n<example>\nNextJS 14 app router middleware auth\n</example>\n",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to look up on the web"
}
},
"required": [
"query"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
},
{
"type": "function",
"function": {
"name": "web_crawl",
"description": "\nYou can crawl a website so you can clone it.\n\n### When You MUST Trigger a Crawl\nTrigger a crawl ONLY if BOTH conditions are true:\n\n1. The user's message shows intent to CLONE / COPY / REPLICATE / RECREATE / DUPLICATE / MIMIC a website.\n - Keywords include: clone, copy, replicate, recreate, duplicate, mimic, build the same, make the same.\n\n2. The user's message contains a URL or something that appears to be a domain name.\n - e.g. \"example.com\", \"https://example.com\"\n - Do not require 'http://' or 'https://'.\n",
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "URL to crawl"
}
},
"required": [
"url"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
},
{
"type": "function",
"function": {
"name": "update_todos",
"description": "\n### When to Use This Tool\n\nUse proactively for:\n1. Complex multi-step tasks (3+ distinct steps)\n2. Non-trivial tasks requiring careful planning\n3. User explicitly requests todo list\n4. User provides multiple tasks (numbered/comma-separated)\n5. After completing tasks - mark complete with merge=true and add follow-ups\n6. When starting new tasks - mark as in_progress (ideally only one at a time)\n\n### When NOT to Use\n\nSkip for:\n1. Single, straightforward tasks\n2. Trivial tasks with no organizational benefit\n3. Tasks completable in < 3 trivial steps\n4. Purely conversational/informational requests\n5. Todo items should NOT include operational actions done in service of higher-level tasks.\n\nNEVER INCLUDE THESE IN TODOS: linting; testing; searching or examining the codebase.\n\n### Examples\n\n<example>\nUser: Add dark mode toggle to settings\nAssistant:\n- *Creates todo list:*\n1. Add state management [in_progress]\n2. Implement styles\n3. Create toggle component\n4. Update components\n- [Immediately begins working on todo 1 in the same tool call batch]\n<reasoning>\nMulti-step feature with dependencies.\n</reasoning>\n</example>\n\n<example>\n// User: Implement user registration, product catalog, shopping cart, checkout flow.\nAssistant: *Creates todo list breaking down each feature into specific tasks*\n<reasoning>\nMultiple complex features provided as list requiring organized task management.\n</reasoning>\n</example>\n\n### Task States and Management\n\n1. **Task States:**\n- pending: Not yet started\n- in_progress: Currently working on\n- completed: Finished successfully\n\n2. **Task Management:**\n- Update status in real-time\n- Mark complete IMMEDIATELY after finishing\n- Only ONE task in_progress at a time\n- Complete current tasks before starting new ones\n\n3. **Task Breakdown:**\n- Create specific, actionable items\n- Break complex tasks into manageable steps\n- Use clear, descriptive names\n\n4. **Parallel Todo Writes:**\n- Prefer creating the first todo as in_progress\n- Start working on todos by using tool calls in the same tool call batch as the todo write\n- Batch todo updates with other tool calls for better latency and lower costs for the user\n",
"parameters": {
"type": "object",
"properties": {
"merge": {
"type": "boolean",
"description": "Whether to merge the todos with the existing todos. If true, the todos will be merged into the existing todos based on the id field. You can leave unchanged properties undefined. If false, the new todos will replace the existing todos."
},
"todos": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the todo item"
},
"content": {
"type": "string",
"description": "The description/content of the todo item"
},
"status": {
"type": "string",
"enum": [
"pending",
"in_progress",
"completed"
],
"description": "The current status of the todo item"
}
},
"required": [
"id"
],
"additionalProperties": false
},
"description": "Array of todo items. When merge is true, only include todos that need updates. When merge is false, this is the complete list."
}
},
"required": [
"merge",
"todos"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
},
{
"type": "function",
"function": {
"name": "run_type_checks",
"description": "Run TypeScript type checks on the current workspace. You can provide paths to specific files or directories, or omit the argument to get diagnostics for all files.\n\n- If a file path is provided, returns diagnostics for that file only\n- If a directory path is provided, returns diagnostics for all files within that directory\n- If no path is provided, returns diagnostics for all files in the workspace\n- This tool can return type errors that were already present before your edits, so avoid calling it with a very wide scope of files\n- NEVER call this tool on a file unless you've edited it or are about to edit it",
"parameters": {
"type": "object",
"properties": {
"paths": {
"type": "array",
"items": {
"type": "string"
},
"description": "Optional. An array of paths to files or directories to read type errors for. If provided, returns diagnostics for the specified files/directories only. If not provided, returns diagnostics for all files in the workspace."
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
}
],
"tool_choice": "auto",
"stream": true
},
"headers": {
"authorization": "Bearer testdyadkey"
}
}
\ No newline at end of file
...@@ -82,7 +82,11 @@ import { inArray } from "drizzle-orm"; ...@@ -82,7 +82,11 @@ import { inArray } from "drizzle-orm";
import { replacePromptReference } from "../utils/replacePromptReference"; import { replacePromptReference } from "../utils/replacePromptReference";
import { mcpManager } from "../utils/mcp_manager"; import { mcpManager } from "../utils/mcp_manager";
import z from "zod"; import z from "zod";
import { isSupabaseConnected, isTurboEditsV2Enabled } from "@/lib/schemas"; import {
isDyadProEnabled,
isSupabaseConnected,
isTurboEditsV2Enabled,
} from "@/lib/schemas";
import { AI_STREAMING_ERROR_MESSAGE_PREFIX } from "@/shared/texts"; import { AI_STREAMING_ERROR_MESSAGE_PREFIX } from "@/shared/texts";
import { getCurrentCommitHash } from "../utils/git_utils"; import { getCurrentCommitHash } from "../utils/git_utils";
import { import {
...@@ -806,7 +810,14 @@ This conversation includes one or more image attachments. When the user uploads ...@@ -806,7 +810,14 @@ This conversation includes one or more image attachments. When the user uploads
attachmentPaths, attachmentPaths,
); );
} }
if (settings.selectedChatMode === "local-agent") { // Save aiMessagesJson for modes that use handleLocalAgentStream
// (which reads from DB and needs structured image content)
const willUseLocalAgentStream =
settings.selectedChatMode === "local-agent" ||
(settings.selectedChatMode === "ask" &&
isDyadProEnabled(settings) &&
!mentionedAppsCodebases.length);
if (willUseLocalAgentStream) {
// Insert into DB (with size guard) // Insert into DB (with size guard)
const userAiMessagesJson = getAiMessagesJsonIfWithinLimit([ const userAiMessagesJson = getAiMessagesJsonIfWithinLimit([
chatMessages[lastUserIndex], chatMessages[lastUserIndex],
...@@ -996,6 +1007,31 @@ This conversation includes one or more image attachments. When the user uploads ...@@ -996,6 +1007,31 @@ This conversation includes one or more image attachments. When the user uploads
return fullResponse; return fullResponse;
}; };
// Handle pro ask mode: use local-agent in read-only mode
// This gives pro users access to code reading tools while in ask mode
if (
settings.selectedChatMode === "ask" &&
isDyadProEnabled(settings) &&
!mentionedAppsCodebases.length
) {
// Reconstruct system prompt for local-agent read-only mode
const readOnlySystemPrompt = constructSystemPrompt({
aiRules,
chatMode: "local-agent",
enableTurboEditsV2: false,
themePrompt,
readOnly: true,
});
await handleLocalAgentStream(event, req, abortController, {
placeholderMessageId: placeholderAssistantMessage.id,
systemPrompt: readOnlySystemPrompt,
dyadRequestId: dyadRequestId ?? "[no-request-id]",
readOnly: true,
});
return;
}
// Handle local-agent mode (Agent v2) // Handle local-agent mode (Agent v2)
// Mentioned apps can't be handled by the local agent (defer to balanced smart context // Mentioned apps can't be handled by the local agent (defer to balanced smart context
// in build mode) // in build mode)
......
...@@ -107,10 +107,16 @@ export async function handleLocalAgentStream( ...@@ -107,10 +107,16 @@ export async function handleLocalAgentStream(
placeholderMessageId, placeholderMessageId,
systemPrompt, systemPrompt,
dyadRequestId, dyadRequestId,
readOnly = false,
}: { }: {
placeholderMessageId: number; placeholderMessageId: number;
systemPrompt: string; systemPrompt: string;
dyadRequestId: string; dyadRequestId: string;
/**
* If true, the agent operates in read-only mode (e.g., ask mode).
* State-modifying tools are disabled, and no commits/deploys are made.
*/
readOnly?: boolean;
}, },
): Promise<void> { ): Promise<void> {
const settings = readSettings(); const settings = readSettings();
...@@ -216,8 +222,10 @@ export async function handleLocalAgentStream( ...@@ -216,8 +222,10 @@ export async function handleLocalAgentStream(
}; };
// Build tool set (agent tools + MCP tools) // Build tool set (agent tools + MCP tools)
const agentTools = buildAgentToolSet(ctx); // In read-only mode, only include read-only tools and skip MCP tools
const mcpTools = await getMcpTools(event, ctx); // (since we can't determine if MCP tools modify state)
const agentTools = buildAgentToolSet(ctx, { readOnly });
const mcpTools = readOnly ? {} : await getMcpTools(event, ctx);
const allTools: ToolSet = { ...agentTools, ...mcpTools }; const allTools: ToolSet = { ...agentTools, ...mcpTools };
// Prepare message history with graceful fallback // Prepare message history with graceful fallback
...@@ -413,6 +421,8 @@ export async function handleLocalAgentStream( ...@@ -413,6 +421,8 @@ export async function handleLocalAgentStream(
logger.warn("Failed to save AI messages JSON:", err); logger.warn("Failed to save AI messages JSON:", err);
} }
// In read-only mode, skip deploys and commits
if (!readOnly) {
// Deploy all Supabase functions if shared modules changed // Deploy all Supabase functions if shared modules changed
await deployAllFunctionsIfNeeded(ctx); await deployAllFunctionsIfNeeded(ctx);
...@@ -425,6 +435,7 @@ export async function handleLocalAgentStream( ...@@ -425,6 +435,7 @@ export async function handleLocalAgentStream(
.set({ commitHash: commitResult.commitHash }) .set({ commitHash: commitResult.commitHash })
.where(eq(messages.id, placeholderMessageId)); .where(eq(messages.id, placeholderMessageId));
} }
}
// Mark as approved (auto-approve for local-agent) // Mark as approved (auto-approve for local-agent)
await db await db
...@@ -435,7 +446,7 @@ export async function handleLocalAgentStream( ...@@ -435,7 +446,7 @@ export async function handleLocalAgentStream(
// Send completion // Send completion
safeSend(event.sender, "chat:response:end", { safeSend(event.sender, "chat:response:end", {
chatId: req.chatId, chatId: req.chatId,
updatedFiles: true, updatedFiles: !readOnly,
} satisfies ChatResponseEnd); } satisfies ChatResponseEnd);
return; return;
......
...@@ -255,10 +255,21 @@ function convertToolResultForAiSdk( ...@@ -255,10 +255,21 @@ function convertToolResultForAiSdk(
throw new Error(`Unsupported tool result type: ${typeof result}`); throw new Error(`Unsupported tool result type: ${typeof result}`);
} }
export interface BuildAgentToolSetOptions {
/**
* If true, exclude tools that modify state (files, database, etc.).
* Used for read-only modes like "ask" mode.
*/
readOnly?: boolean;
}
/** /**
* Build ToolSet for AI SDK from tool definitions * Build ToolSet for AI SDK from tool definitions
*/ */
export function buildAgentToolSet(ctx: AgentContext) { export function buildAgentToolSet(
ctx: AgentContext,
options: BuildAgentToolSetOptions = {},
) {
const toolSet: Record<string, any> = {}; const toolSet: Record<string, any> = {};
for (const tool of TOOL_DEFINITIONS) { for (const tool of TOOL_DEFINITIONS) {
...@@ -267,6 +278,11 @@ export function buildAgentToolSet(ctx: AgentContext) { ...@@ -267,6 +278,11 @@ export function buildAgentToolSet(ctx: AgentContext) {
continue; continue;
} }
// In read-only mode, skip tools that modify state
if (options.readOnly && tool.modifiesState) {
continue;
}
if (tool.isEnabled && !tool.isEnabled(ctx)) { if (tool.isEnabled && !tool.isEnabled(ctx)) {
continue; continue;
} }
......
...@@ -16,6 +16,7 @@ export const addDependencyTool: ToolDefinition< ...@@ -16,6 +16,7 @@ export const addDependencyTool: ToolDefinition<
description: "Install npm packages", description: "Install npm packages",
inputSchema: addDependencySchema, inputSchema: addDependencySchema,
defaultConsent: "ask", defaultConsent: "ask",
modifiesState: true,
getConsentPreview: (args) => `Install ${args.packages.join(", ")}`, getConsentPreview: (args) => `Install ${args.packages.join(", ")}`,
......
...@@ -17,6 +17,7 @@ export const addIntegrationTool: ToolDefinition< ...@@ -17,6 +17,7 @@ export const addIntegrationTool: ToolDefinition<
"Add an integration provider to the app (e.g., Supabase for auth, database, or server-side functions). Once you have called this tool, stop and do not call any more tools because you need to wait for the user to set up the integration.", "Add an integration provider to the app (e.g., Supabase for auth, database, or server-side functions). Once you have called this tool, stop and do not call any more tools because you need to wait for the user to set up the integration.",
inputSchema: addIntegrationSchema, inputSchema: addIntegrationSchema,
defaultConsent: "always", defaultConsent: "always",
modifiesState: true,
isEnabled: (ctx) => !ctx.supabaseProjectId, isEnabled: (ctx) => !ctx.supabaseProjectId,
getConsentPreview: (args) => `Add ${args.provider} integration`, getConsentPreview: (args) => `Add ${args.provider} integration`,
......
...@@ -27,6 +27,7 @@ export const deleteFileTool: ToolDefinition<z.infer<typeof deleteFileSchema>> = ...@@ -27,6 +27,7 @@ export const deleteFileTool: ToolDefinition<z.infer<typeof deleteFileSchema>> =
description: "Delete a file from the codebase", description: "Delete a file from the codebase",
inputSchema: deleteFileSchema, inputSchema: deleteFileSchema,
defaultConsent: "always", defaultConsent: "always",
modifiesState: true,
getConsentPreview: (args) => `Delete ${args.path}`, getConsentPreview: (args) => `Delete ${args.path}`,
......
...@@ -134,6 +134,7 @@ export const editFileTool: ToolDefinition<z.infer<typeof editFileSchema>> = { ...@@ -134,6 +134,7 @@ export const editFileTool: ToolDefinition<z.infer<typeof editFileSchema>> = {
description: DESCRIPTION, description: DESCRIPTION,
inputSchema: editFileSchema, inputSchema: editFileSchema,
defaultConsent: "always", defaultConsent: "always",
modifiesState: true,
getConsentPreview: (args) => `Edit ${args.path}`, getConsentPreview: (args) => `Edit ${args.path}`,
......
...@@ -15,6 +15,7 @@ export const executeSqlTool: ToolDefinition<z.infer<typeof executeSqlSchema>> = ...@@ -15,6 +15,7 @@ export const executeSqlTool: ToolDefinition<z.infer<typeof executeSqlSchema>> =
description: "Execute SQL on the Supabase database", description: "Execute SQL on the Supabase database",
inputSchema: executeSqlSchema, inputSchema: executeSqlSchema,
defaultConsent: "ask", defaultConsent: "ask",
modifiesState: true,
isEnabled: (ctx) => !!ctx.supabaseProjectId, isEnabled: (ctx) => !!ctx.supabaseProjectId,
getConsentPreview: (args) => getConsentPreview: (args) =>
......
...@@ -31,6 +31,7 @@ export const renameFileTool: ToolDefinition<z.infer<typeof renameFileSchema>> = ...@@ -31,6 +31,7 @@ export const renameFileTool: ToolDefinition<z.infer<typeof renameFileSchema>> =
description: "Rename or move a file in the codebase", description: "Rename or move a file in the codebase",
inputSchema: renameFileSchema, inputSchema: renameFileSchema,
defaultConsent: "always", defaultConsent: "always",
modifiesState: true,
getConsentPreview: (args) => `Rename ${args.from} to ${args.to}`, getConsentPreview: (args) => `Rename ${args.from} to ${args.to}`,
......
...@@ -123,6 +123,11 @@ export interface ToolDefinition<T = any> { ...@@ -123,6 +123,11 @@ export interface ToolDefinition<T = any> {
readonly description: string; readonly description: string;
readonly inputSchema: z.ZodType<T>; readonly inputSchema: z.ZodType<T>;
readonly defaultConsent: AgentToolConsent; readonly defaultConsent: AgentToolConsent;
/**
* If true, this tool modifies state (files, database, etc.).
* Used to filter out state-modifying tools in read-only mode (e.g., ask mode).
*/
readonly modifiesState?: boolean;
execute: (args: T, ctx: AgentContext) => Promise<ToolResult>; execute: (args: T, ctx: AgentContext) => Promise<ToolResult>;
/** /**
......
...@@ -27,6 +27,7 @@ export const writeFileTool: ToolDefinition<z.infer<typeof writeFileSchema>> = { ...@@ -27,6 +27,7 @@ export const writeFileTool: ToolDefinition<z.infer<typeof writeFileSchema>> = {
description: "Create or completely overwrite a file in the codebase", description: "Create or completely overwrite a file in the codebase",
inputSchema: writeFileSchema, inputSchema: writeFileSchema,
defaultConsent: "always", defaultConsent: "always",
modifiesState: true,
getConsentPreview: (args) => `Write to ${args.path}`, getConsentPreview: (args) => `Write to ${args.path}`,
......
...@@ -3,6 +3,53 @@ ...@@ -3,6 +3,53 @@
* Tool-based agent with parallel execution support * Tool-based agent with parallel execution support
*/ */
/**
* System prompt for Local Agent v2 in Ask Mode (read-only)
* The agent can read and analyze code, but cannot make changes
*/
export const LOCAL_AGENT_ASK_SYSTEM_PROMPT = `
<role>
You are Dyad, an AI assistant that helps users understand their web applications. You assist users by answering questions about their code, explaining concepts, and providing guidance. You can read and analyze code in the codebase to provide accurate, context-aware answers.
You are friendly and helpful, always aiming to provide clear explanations. You take pride in giving thorough, accurate answers based on the actual code.
</role>
<important_constraints>
**CRITICAL: You are in READ-ONLY mode.**
- You can read files, search code, and analyze the codebase
- You MUST NOT modify any files, create new files, or make any changes
- You MUST NOT suggest using write_file, edit_file, delete_file, rename_file, add_dependency, or execute_sql tools
- Focus on explaining, answering questions, and providing guidance
- If the user asks you to make changes, politely explain that you're in Ask mode and can only provide explanations and guidance
</important_constraints>
<general_guidelines>
- Always reply to the user in the same language they are using.
- Use your tools to read and understand the codebase before answering questions
- Provide clear, accurate explanations based on the actual code
- When explaining code, reference specific files and line numbers when helpful
- If you're not sure about something, read the relevant files to find out
- Keep explanations clear and focused on what the user is asking about
</general_guidelines>
<tool_calling>
You have READ-ONLY tools at your disposal to understand the codebase. Follow these rules:
1. ALWAYS follow the tool call schema exactly as specified and make sure to provide all necessary parameters.
2. **NEVER refer to tool names when speaking to the USER.** Instead, just say what you're doing in natural language (e.g., "Let me look at that file" instead of "I'll use read_file").
3. Use tools proactively to gather information and provide accurate answers.
4. You can call multiple tools in parallel for independent operations like reading multiple files at once.
5. If you are not sure about file content or codebase structure pertaining to the user's request, use your tools to read files and gather the relevant information: do NOT guess or make up an answer.
</tool_calling>
<workflow>
1. **Understand the question:** Think about what the user is asking and what information you need
2. **Gather context:** Use your tools to read relevant files and understand the codebase
3. **Analyze:** Think through the code and how it relates to the user's question
4. **Explain:** Provide a clear, accurate answer based on what you found
</workflow>
[[AI_RULES]]
`;
export const LOCAL_AGENT_SYSTEM_PROMPT = ` export const LOCAL_AGENT_SYSTEM_PROMPT = `
<role> <role>
You are Dyad, an AI assistant that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes. You are Dyad, an AI assistant that creates and modifies web applications. You assist users by chatting with them and making changes to their code in real-time. You understand that users can see a live preview of their application in an iframe on the right side of the screen while you make code changes.
...@@ -90,11 +137,14 @@ Available packages and libraries: ...@@ -90,11 +137,14 @@ Available packages and libraries:
export function constructLocalAgentPrompt( export function constructLocalAgentPrompt(
aiRules: string | undefined, aiRules: string | undefined,
themePrompt?: string, themePrompt?: string,
options?: { readOnly?: boolean },
): string { ): string {
let prompt = LOCAL_AGENT_SYSTEM_PROMPT.replace( // Use ask mode prompt if read-only, otherwise use the regular local agent prompt
"[[AI_RULES]]", const basePrompt = options?.readOnly
aiRules ?? DEFAULT_AI_RULES, ? LOCAL_AGENT_ASK_SYSTEM_PROMPT
); : LOCAL_AGENT_SYSTEM_PROMPT;
let prompt = basePrompt.replace("[[AI_RULES]]", aiRules ?? DEFAULT_AI_RULES);
// Append theme prompt if provided // Append theme prompt if provided
if (themePrompt) { if (themePrompt) {
......
...@@ -509,14 +509,17 @@ export const constructSystemPrompt = ({ ...@@ -509,14 +509,17 @@ export const constructSystemPrompt = ({
chatMode = "build", chatMode = "build",
enableTurboEditsV2, enableTurboEditsV2,
themePrompt, themePrompt,
readOnly,
}: { }: {
aiRules: string | undefined; aiRules: string | undefined;
chatMode?: "build" | "ask" | "agent" | "local-agent"; chatMode?: "build" | "ask" | "agent" | "local-agent";
enableTurboEditsV2: boolean; enableTurboEditsV2: boolean;
themePrompt?: string; themePrompt?: string;
/** If true, use read-only mode for local-agent (ask mode with tools) */
readOnly?: boolean;
}) => { }) => {
if (chatMode === "local-agent") { if (chatMode === "local-agent") {
return constructLocalAgentPrompt(aiRules, themePrompt); return constructLocalAgentPrompt(aiRules, themePrompt, { readOnly });
} }
let systemPrompt = getSystemPromptForChatMode({ let systemPrompt = getSystemPromptForChatMode({
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论