Unverified 提交 005b72bd authored 作者: wwwillchen-bot's avatar wwwillchen-bot 提交者: GitHub

fix: increase tool call limit to 50 and show clear pause message (#2828)

## Summary - Increase MAX_TOOL_CALL_STEPS from 25 to 50 to allow longer multi-step tasks - Add step limit detection to track total steps across all passes - Show a clear `<dyad-step-limit>` message when the limit is reached, instructing users to send "continue" to resume - Create DyadStepLimit component for displaying the pause notification Fixes #2754 ## Test plan - Run the local agent and perform a task that requires many tool calls - Verify the agent pauses at 50 tool calls instead of 25 - Verify a clear message is shown explaining why it paused and how to continue - Type "continue" to verify the agent resumes working 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2828" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> --------- Co-authored-by: 's avatarWill Chen <willchen90@gmail.com> Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 b517b457
import type {
LocalAgentFixture,
Turn,
} from "../../../../testing/fake-llm-server/localAgentTypes";
/**
* Fixture that triggers the step limit by generating 50 tool call turns.
* The AI SDK's stepCountIs(50) will stop after 50 steps, and the handler
* will append a <dyad-step-limit> notice to the response.
*/
const toolCallTurns: Turn[] = Array.from({ length: 50 }, (_, i) => ({
text: `Step ${i + 1}: reading file.`,
toolCalls: [
{
name: "read_file",
args: { path: "package.json" },
},
],
}));
// Final text-only turn (won't be reached because stepCountIs(50) stops first)
const finalTurn: Turn = {
text: "All steps completed.",
};
export const fixture: LocalAgentFixture = {
description:
"Triggers step limit by making 50+ tool call rounds, causing a pause notification",
turns: [...toolCallTurns, finalTurn],
};
import { expect } from "@playwright/test";
import { Timeout, testSkipIfWindows } from "./helpers/test_helper";
/**
* E2E test for the step limit feature.
* When the local agent hits 50 tool call steps, it pauses and shows
* a <dyad-step-limit> notification card.
*/
testSkipIfWindows("local-agent - step limit pause", async ({ po }) => {
await po.setUpDyadPro({ localAgent: true });
await po.importApp("minimal");
await po.chatActions.selectLocalAgentMode();
await po.sendPrompt("tc=local-agent/step-limit");
// Verify the step limit card is visible
await expect(
po.page.getByText("Paused after 50 tool calls", { exact: true }),
).toBeVisible({
timeout: Timeout.EXTRA_LONG,
});
// Verify the "Continue" button is shown
await expect(po.page.getByRole("button", { name: "Continue" })).toBeVisible({
timeout: Timeout.MEDIUM,
});
// Click the "Continue" button
await po.page.getByRole("button", { name: "Continue" }).click();
await po.snapshotMessages();
});
- 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\./
- button "file1.txt file1.txt Edit":
- img
- text: ""
- button "Edit":
- img
- text: ""
- img
- paragraph: More EOM
- button "Copy":
- img
- img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- img
- text: (1 files changed)
- button "Copy Request ID":
- img
- text: ""
- paragraph: tc=local-agent/step-limit
- paragraph: "Step 1: reading file."
- img
- text: Read package.json
- paragraph: "Step 2: reading file."
- img
- text: Read package.json
- paragraph: "Step 3: reading file."
- img
- text: Read package.json
- paragraph: "Step 4: reading file."
- img
- text: Read package.json
- paragraph: "Step 5: reading file."
- img
- text: Read package.json
- paragraph: "Step 6: reading file."
- img
- text: Read package.json
- paragraph: "Step 7: reading file."
- img
- text: Read package.json
- paragraph: "Step 8: reading file."
- img
- text: Read package.json
- paragraph: "Step 9: reading file."
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- paragraph: "/Step \\d+: reading file\\./"
- img
- text: Read package.json
- img
- text: /Paused after \d+ tool calls/
- button "Continue":
- img
- text: ""
- text: /Automatically paused after \d+ tool calls\./
- button "Copy":
- img
- img
- text: Approved
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Copy Request ID":
- img
- text: ""
- paragraph: Continue
- button "file1.txt file1.txt Edit":
- img
- text: ""
- button "Edit":
- img
- text: ""
- img
- paragraph: More EOM
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Copy Request ID":
- img
- text: ""
- button "Undo":
- img
- text: ""
- button "Retry":
- img
- text: ""
\ No newline at end of file
{
"name": "dyad",
"version": "0.38.0-beta.1",
"version": "0.39.0-beta.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dyad",
"version": "0.38.0-beta.1",
"version": "0.39.0-beta.1",
"license": "MIT",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.46",
......
......@@ -40,6 +40,7 @@ import { DyadCompaction } from "./DyadCompaction";
import { DyadWritePlan } from "./DyadWritePlan";
import { DyadExitPlan } from "./DyadExitPlan";
import { DyadQuestionnaire } from "./DyadQuestionnaire";
import { DyadStepLimit } from "./DyadStepLimit";
import { mapActionToButton } from "./ChatInput";
import { SuggestedAction } from "@/lib/schemas";
import { FixAllErrorsButton } from "./FixAllErrorsButton";
......@@ -82,6 +83,8 @@ const DYAD_CUSTOM_TAGS = [
"dyad-write-plan",
"dyad-exit-plan",
"dyad-questionnaire",
// Step limit notification
"dyad-step-limit",
];
interface DyadMarkdownParserProps {
......@@ -802,6 +805,21 @@ function renderCustomTag(
case "dyad-questionnaire":
return <DyadQuestionnaire>{content}</DyadQuestionnaire>;
case "dyad-step-limit":
return (
<DyadStepLimit
node={{
properties: {
steps: attributes.steps,
limit: attributes.limit,
state: getState({ isStreaming, inProgress }),
},
}}
>
{content}
</DyadStepLimit>
);
default:
return null;
}
......
import React, { useState } from "react";
import { useAtomValue } from "jotai";
import { CustomTagState } from "./stateTypes";
import {
DyadCard,
DyadCardHeader,
DyadCardContent,
} from "./DyadCardPrimitives";
import { PauseCircle, Play, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useStreamChat } from "@/hooks/useStreamChat";
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
interface DyadStepLimitProps {
node: {
properties: {
steps?: string;
limit?: string;
state?: CustomTagState;
};
};
children?: React.ReactNode;
}
export function DyadStepLimit({ node, children }: DyadStepLimitProps) {
const { steps = "50", limit: _limit = "50", state } = node.properties;
const isFinished = state === "finished";
const content = typeof children === "string" ? children : "";
const chatId = useAtomValue(selectedChatIdAtom);
const { streamMessage } = useStreamChat();
const [isLoading, setIsLoading] = useState(false);
const handleContinue = () => {
if (!chatId) return;
setIsLoading(true);
streamMessage({
prompt: "Continue",
chatId,
onSettled: () => setIsLoading(false),
});
};
return (
<DyadCard state={state} accentColor="amber" isExpanded={true}>
<DyadCardHeader icon={<PauseCircle size={15} />} accentColor="amber">
<span className="font-medium text-sm text-foreground">
Paused after {steps} tool calls
</span>
{isFinished && (
<Button
variant="outline"
size="sm"
disabled={isLoading}
onClick={handleContinue}
className="ml-auto hover:cursor-pointer"
>
{isLoading ? (
<Loader2 size={14} className="mr-1 animate-spin" />
) : (
<Play size={14} className="mr-1" />
)}
Continue
</Button>
)}
</DyadCardHeader>
<DyadCardContent isExpanded={true}>
{content && (
<div className="p-3 text-sm text-muted-foreground">{content}</div>
)}
</DyadCardContent>
</DyadCard>
);
}
......@@ -83,6 +83,11 @@ const MAX_TERMINATED_STREAM_RETRIES = 2;
const STREAM_RETRY_BASE_DELAY_MS = 400;
const STREAM_CONTINUE_MESSAGE =
"[System] Your previous response stream was interrupted by a transient network error. Continue from exactly where you left off and do not repeat text that has already been sent.";
/**
* Maximum number of tool call steps before pausing.
* This prevents runaway loops while allowing complex multi-step tasks.
*/
const MAX_TOOL_CALL_STEPS = 50;
// ============================================================================
// Tool Streaming State Management
......@@ -566,6 +571,8 @@ export async function handleLocalAgentStream(
let hasInjectedPlanningQuestionnaireReflection = false;
let currentMessageHistory = messageHistory;
const accumulatedAiMessages: ModelMessage[] = [];
// Track total steps across all passes to detect step limit
let totalStepsExecuted = 0;
// If there are persisted todos from a previous turn, inject a synthetic
// user message so the LLM is aware of them. Inserted BEFORE the user's
......@@ -661,7 +668,7 @@ export async function handleLocalAgentStream(
messages: attemptMessages,
tools: allTools,
stopWhen: [
stepCountIs(25),
stepCountIs(MAX_TOOL_CALL_STEPS),
hasToolCall(addIntegrationTool.name),
// In plan mode, also stop after writing a plan or exiting plan mode.
...(planModeOnly
......@@ -1062,6 +1069,9 @@ export async function handleLocalAgentStream(
break;
}
// Track total steps for step limit detection
totalStepsExecuted += steps.length;
if (responseMessages.length > 0) {
// For mid-turn compaction, slice off pre-compaction messages
const messagesToAccumulate =
......@@ -1133,6 +1143,24 @@ export async function handleLocalAgentStream(
return false; // Cancelled - don't consume quota
}
// Check if we hit the step limit and append a notice to the response
if (totalStepsExecuted >= MAX_TOOL_CALL_STEPS) {
logger.info(
`Chat ${req.chatId} hit step limit of ${MAX_TOOL_CALL_STEPS} steps`,
);
const stepLimitMessage = `\n\n<dyad-step-limit steps="${totalStepsExecuted}" limit="${MAX_TOOL_CALL_STEPS}">Automatically paused after ${totalStepsExecuted} tool calls.</dyad-step-limit>`;
fullResponse += stepLimitMessage;
await updateResponseInDb(placeholderMessageId, fullResponse);
sendResponseChunk(
event,
req.chatId,
chat,
fullResponse,
placeholderMessageId,
hiddenMessageIdsForStreaming,
);
}
// Save the AI SDK messages for multi-turn tool call preservation
try {
const aiMessagesJson = getAiMessagesJsonIfWithinLimit(
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论