From 261959104a4339e355bc414481b7c7a34c3214b1 Mon Sep 17 00:00:00 2001 From: anvyle Date: Mon, 6 Apr 2026 19:00:47 +0200 Subject: [PATCH 01/61] fix(backend/copilot): skip AI blocks without model property in fix_ai_model_parameter Some AI-category blocks do not expose a "model" input property in their inputSchema. The fixer was injecting a default model value into these blocks, which is incorrect. Now checks for the presence of "model" in inputSchema properties before attempting to set or validate the model field. Co-Authored-By: Claude Sonnet 4.6 --- .../copilot/tools/agent_generator/fixer.py | 10 +++++--- .../tools/agent_generator/fixer_test.py | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer.py b/autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer.py index 50d0e1925af8..adebd89bf17b 100644 --- a/autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer.py +++ b/autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer.py @@ -890,6 +890,12 @@ def fix_ai_model_parameter( ) if is_ai_block: + # Skip AI blocks that don't expose a "model" input property + # (some AI-category blocks have no model selector at all). + input_properties = block.get("inputSchema", {}).get("properties", {}) + if "model" not in input_properties: + continue + node_id = node.get("id") input_default = node.get("input_default", {}) current_model = input_default.get("model") @@ -898,9 +904,7 @@ def fix_ai_model_parameter( # Blocks with a block-specific enum on the model field (e.g. # PerplexityBlock) use their own enum values; others use the # generic set. - model_schema = ( - block.get("inputSchema", {}).get("properties", {}).get("model", {}) - ) + model_schema = input_properties.get("model", {}) block_model_enum = model_schema.get("enum") if block_model_enum: diff --git a/autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer_test.py b/autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer_test.py index 07d71a941cd7..2319ad676044 100644 --- a/autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer_test.py +++ b/autogpt_platform/backend/backend/copilot/tools/agent_generator/fixer_test.py @@ -580,6 +580,29 @@ def test_block_specific_enum_missing_model_gets_block_default(self): assert result["nodes"][0]["input_default"]["model"] == "perplexity/sonar" + def test_ai_block_without_model_property_is_skipped(self): + """AI-category blocks that have no 'model' input property should not + have a model injected — they simply don't expose a model selector.""" + fixer = AgentFixer() + block_id = generate_uuid() + node = _make_node(node_id="n1", block_id=block_id, input_default={}) + agent = _make_agent(nodes=[node]) + + blocks = [ + { + "id": block_id, + "name": "SomeAIBlock", + "categories": [{"category": "AI"}], + "inputSchema": { + "properties": {"prompt": {"type": "string"}}, + }, + } + ] + + result = fixer.fix_ai_model_parameter(agent, blocks) + + assert "model" not in result["nodes"][0]["input_default"] + class TestFixAgentExecutorBlocks: """Tests for fix_agent_executor_blocks.""" From 5bb919e7b5b609940ec73dbdfb57f2b917ce7cda Mon Sep 17 00:00:00 2001 From: anvyle Date: Wed, 8 Apr 2026 14:33:49 +0200 Subject: [PATCH 02/61] feat(copilot): add task decomposition for agent building Add a decompose_goal tool that breaks user goals into sub-instructions before building. Users see a plan checklist and can approve or modify before the agent is created, improving transparency and control. - Backend: DecomposeGoalTool, TaskDecompositionResponse model, system prompt update - Frontend: DecomposeGoal component with StepItem checklist, approve/modify buttons Co-Authored-By: Claude Opus 4.6 --- .../backend/backend/copilot/prompting.py | 11 ++ .../backend/backend/copilot/tools/__init__.py | 2 + .../backend/copilot/tools/decompose_goal.py | 130 +++++++++++++++ .../backend/backend/copilot/tools/models.py | 35 ++++ .../components/MessagePartRenderer.tsx | 3 + .../tools/DecomposeGoal/DecomposeGoal.tsx | 124 ++++++++++++++ .../DecomposeGoal/components/StepItem.tsx | 39 +++++ .../copilot/tools/DecomposeGoal/helpers.tsx | 152 ++++++++++++++++++ 8 files changed, 496 insertions(+) create mode 100644 autogpt_platform/backend/backend/copilot/tools/decompose_goal.py create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/components/StepItem.tsx create mode 100644 autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/helpers.tsx diff --git a/autogpt_platform/backend/backend/copilot/prompting.py b/autogpt_platform/backend/backend/copilot/prompting.py index dd630a2e9b06..3fdba6fc338e 100644 --- a/autogpt_platform/backend/backend/copilot/prompting.py +++ b/autogpt_platform/backend/backend/copilot/prompting.py @@ -127,6 +127,17 @@ non-overlapping scope to avoid redundant searches. +### Agent Building Workflow — ALWAYS follow this +When the user asks to create an agent, ALWAYS follow this workflow: +1. Analyze the goal and break it into logical sub-instructions. +2. Call `decompose_goal` with the steps (each step = one logical task like + "add input block", "add AI summarizer", "wire blocks together"). +3. Wait for user approval before proceeding. +4. After approval, call `create_agent` with the full agent JSON. + +For simple goals (1-2 blocks), keep the decomposition brief (2-3 steps). +For complex goals, decompose into 4-8 steps max. + ### Tool Discovery Priority When the user asks to interact with a service or API, follow this order: diff --git a/autogpt_platform/backend/backend/copilot/tools/__init__.py b/autogpt_platform/backend/backend/copilot/tools/__init__.py index 6d1a054c32f9..a15624776f1a 100644 --- a/autogpt_platform/backend/backend/copilot/tools/__init__.py +++ b/autogpt_platform/backend/backend/copilot/tools/__init__.py @@ -17,6 +17,7 @@ from .continue_run_block import ContinueRunBlockTool from .create_agent import CreateAgentTool from .customize_agent import CustomizeAgentTool +from .decompose_goal import DecomposeGoalTool from .edit_agent import EditAgentTool from .feature_requests import CreateFeatureRequestTool, SearchFeatureRequestsTool from .find_agent import FindAgentTool @@ -59,6 +60,7 @@ "ask_question": AskQuestionTool(), "create_agent": CreateAgentTool(), "customize_agent": CustomizeAgentTool(), + "decompose_goal": DecomposeGoalTool(), "edit_agent": EditAgentTool(), "find_agent": FindAgentTool(), "find_block": FindBlockTool(), diff --git a/autogpt_platform/backend/backend/copilot/tools/decompose_goal.py b/autogpt_platform/backend/backend/copilot/tools/decompose_goal.py new file mode 100644 index 000000000000..598b0c597b3e --- /dev/null +++ b/autogpt_platform/backend/backend/copilot/tools/decompose_goal.py @@ -0,0 +1,130 @@ +"""DecomposeGoalTool - Breaks agent-building goals into sub-instructions.""" + +import logging +from typing import Any + +from backend.copilot.model import ChatSession + +from .base import BaseTool +from .models import ( + DecompositionStepModel, + ErrorResponse, + TaskDecompositionResponse, + ToolResponseBase, +) + +logger = logging.getLogger(__name__) + +MAX_STEPS = 10 + + +class DecomposeGoalTool(BaseTool): + """Tool for decomposing an agent goal into sub-instructions.""" + + @property + def name(self) -> str: + return "decompose_goal" + + @property + def description(self) -> str: + return ( + "Break down an agent-building goal into logical sub-instructions. " + "Each step maps to one task (e.g. add a block, wire connections, " + "configure settings). ALWAYS call this before create_agent to show " + "the user your plan and get approval." + ) + + @property + def parameters(self) -> dict[str, Any]: + return { + "type": "object", + "properties": { + "goal": { + "type": "string", + "description": "The user's agent-building goal.", + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string", + "description": "Human-readable step description.", + }, + "action": { + "type": "string", + "description": ( + "Action type: 'add_block', 'connect_blocks', " + "'configure', 'add_input', 'add_output'." + ), + }, + "block_name": { + "type": "string", + "description": "Block name if adding a block.", + }, + }, + "required": ["description", "action"], + }, + "description": "List of sub-instructions for the plan.", + }, + "require_approval": { + "type": "boolean", + "description": "Whether to ask user for approval (default: true).", + "default": True, + }, + }, + "required": ["goal", "steps"], + } + + async def _execute( + self, + user_id: str | None, + session: ChatSession, + goal: str | None = None, + steps: list[dict[str, Any]] | None = None, + require_approval: bool = True, + **kwargs, + ) -> ToolResponseBase: + session_id = session.session_id if session else None + + if not goal: + return ErrorResponse( + message="Please provide a goal to decompose.", + error="missing_goal", + session_id=session_id, + ) + + if not steps: + return ErrorResponse( + message="Please provide at least one step in the plan.", + error="missing_steps", + session_id=session_id, + ) + + if len(steps) > MAX_STEPS: + return ErrorResponse( + message=f"Too many steps ({len(steps)}). Keep the plan to {MAX_STEPS} steps max.", + error="too_many_steps", + session_id=session_id, + ) + + decomposition_steps = [ + DecompositionStepModel( + step_id=f"step_{i + 1}", + description=step.get("description", ""), + action=step.get("action", "add_block"), + block_name=step.get("block_name"), + status="pending", + ) + for i, step in enumerate(steps) + ] + + return TaskDecompositionResponse( + message=f"Here's the plan to build your agent ({len(decomposition_steps)} steps):", + goal=goal, + steps=decomposition_steps, + step_count=len(decomposition_steps), + requires_approval=require_approval, + session_id=session_id, + ) diff --git a/autogpt_platform/backend/backend/copilot/tools/models.py b/autogpt_platform/backend/backend/copilot/tools/models.py index a0d1ad13ef05..6c65764469c2 100644 --- a/autogpt_platform/backend/backend/copilot/tools/models.py +++ b/autogpt_platform/backend/backend/copilot/tools/models.py @@ -36,6 +36,9 @@ class ResponseType(str, Enum): AGENT_BUILDER_VALIDATION_RESULT = "agent_builder_validation_result" AGENT_BUILDER_FIX_RESULT = "agent_builder_fix_result" + # Task decomposition (goal → sub-instructions) + TASK_DECOMPOSITION = "task_decomposition" + # Block BLOCK_LIST = "block_list" BLOCK_DETAILS = "block_details" @@ -688,3 +691,35 @@ class AgentsMovedToFolderResponse(ToolResponseBase): agent_names: list[str] = [] folder_id: str | None = None count: int = 0 + + +# Task decomposition models + + +class DecompositionStepModel(BaseModel): + """A single step in a decomposed agent-building plan.""" + + step_id: str = Field(description="Unique step identifier, e.g. 'step_1'") + description: str = Field(description="Human-readable step description") + action: str = Field( + description="Action type: 'add_block', 'connect_blocks', 'configure', etc." + ) + block_name: str | None = Field( + default=None, description="Block being added, if applicable" + ) + status: str = Field( + default="pending", + description="Step status: pending, in_progress, completed, failed", + ) + + +class TaskDecompositionResponse(ToolResponseBase): + """Response for decompose_goal tool — shows the plan to the user.""" + + type: ResponseType = ResponseType.TASK_DECOMPOSITION + goal: str = Field(description="The original user goal") + steps: list[DecompositionStepModel] + step_count: int + requires_approval: bool = True + + diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx index 5d129a0a7885..0668ea1e24d1 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx @@ -5,6 +5,7 @@ import { ToolUIPart, UIDataTypes, UIMessage, UITools } from "ai"; import { AskQuestionTool } from "../../../tools/AskQuestion/AskQuestion"; import { ConnectIntegrationTool } from "../../../tools/ConnectIntegrationTool/ConnectIntegrationTool"; import { CreateAgentTool } from "../../../tools/CreateAgent/CreateAgent"; +import { DecomposeGoalTool } from "../../../tools/DecomposeGoal/DecomposeGoal"; import { EditAgentTool } from "../../../tools/EditAgent/EditAgent"; import { CreateFeatureRequestTool, @@ -144,6 +145,8 @@ export function MessagePartRenderer({ case "tool-run_agent": case "tool-schedule_agent": return ; + case "tool-decompose_goal": + return ; case "tool-create_agent": return ; case "tool-edit_agent": diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx new file mode 100644 index 000000000000..18abeb9fbab0 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { Button } from "@/components/atoms/Button/Button"; +import { CheckIcon, PencilSimpleIcon } from "@phosphor-icons/react"; +import type { ToolUIPart } from "ai"; +import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions"; +import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; +import { + ContentGrid, + ContentHint, + ContentMessage, +} from "../../components/ToolAccordion/AccordionContent"; +import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion"; +import { ToolErrorCard } from "../../components/ToolErrorCard/ToolErrorCard"; +import { StepItem } from "./components/StepItem"; +import { + AccordionIcon, + getAnimationText, + getDecomposeGoalOutput, + isDecompositionOutput, + isErrorOutput, + ToolIcon, +} from "./helpers"; + +interface Props { + part: ToolUIPart; +} + +export function DecomposeGoalTool({ part }: Props) { + const text = getAnimationText(part); + const { onSend } = useCopilotChatActions(); + + const isStreaming = + part.state === "input-streaming" || part.state === "input-available"; + + const output = getDecomposeGoalOutput(part); + + const isError = + part.state === "output-error" || (!!output && isErrorOutput(output)); + + const isOperating = !output; + + function handleApprove() { + onSend("Approved. Please build the agent."); + } + + function handleModify() { + onSend("I'd like to modify the plan. Here are my changes: "); + } + + return ( +
+ {isOperating && ( +
+ + +
+ )} + + {isError && output && isErrorOutput(output) && ( + onSend("Please try decomposing the goal again."), + }, + ]} + /> + )} + + {output && isDecompositionOutput(output) && ( + } + title={`Build Plan — ${output.step_count} steps`} + description={output.goal} + defaultExpanded + > + + {output.message} + +
+ {output.steps.map((step, i) => ( + + ))} +
+ + {output.requires_approval && ( +
+ + +
+ )} + + + Review the plan above and approve to start building. + +
+
+ )} +
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/components/StepItem.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/components/StepItem.tsx new file mode 100644 index 000000000000..ac1305e70674 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/components/StepItem.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Text } from "@/components/atoms/Text/Text"; +import { CubeIcon } from "@phosphor-icons/react"; +import { StepStatusIcon } from "../helpers"; + +interface Props { + index: number; + description: string; + action: string; + blockName?: string | null; + status: string; +} + +export function StepItem({ index, description, blockName, status }: Props) { + return ( +
+
+ +
+
+ + {index + 1}. {description} + + {blockName && ( +
+ + + {blockName} + +
+ )} +
+
+ ); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/helpers.tsx new file mode 100644 index 000000000000..ac1111d1e6b4 --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/helpers.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { + CheckCircleIcon, + CircleDashedIcon, + ListChecksIcon, + SpinnerGapIcon, + WarningDiamondIcon, + XCircleIcon, +} from "@phosphor-icons/react"; +import type { ToolUIPart } from "ai"; +import { ScaleLoader } from "../../components/ScaleLoader/ScaleLoader"; + +interface DecompositionStep { + step_id: string; + description: string; + action: string; + block_name?: string | null; + status: string; +} + +export interface TaskDecompositionOutput { + type: string; + message: string; + goal: string; + steps: DecompositionStep[]; + step_count: number; + requires_approval: boolean; +} + +export interface DecomposeErrorOutput { + type: string; + error?: string; + message?: string; +} + +export type DecomposeGoalOutput = + | TaskDecompositionOutput + | DecomposeErrorOutput; + +function parseOutput(output: unknown): DecomposeGoalOutput | null { + if (!output) return null; + if (typeof output === "string") { + const trimmed = output.trim(); + if (!trimmed) return null; + try { + return parseOutput(JSON.parse(trimmed) as unknown); + } catch { + return null; + } + } + if (typeof output === "object") { + if ("steps" in output && "goal" in output) { + return output as TaskDecompositionOutput; + } + if ("error" in output) { + return output as DecomposeErrorOutput; + } + } + return null; +} + +export function getDecomposeGoalOutput( + part: unknown, +): DecomposeGoalOutput | null { + if (!part || typeof part !== "object") return null; + return parseOutput((part as { output?: unknown }).output); +} + +export function isDecompositionOutput( + output: DecomposeGoalOutput, +): output is TaskDecompositionOutput { + return "steps" in output && "goal" in output; +} + +export function isErrorOutput( + output: DecomposeGoalOutput, +): output is DecomposeErrorOutput { + return "error" in output; +} + +export function getAnimationText(part: { + state: ToolUIPart["state"]; + output?: unknown; +}): string { + switch (part.state) { + case "input-streaming": + case "input-available": + return "Analyzing your goal..."; + case "output-available": { + const output = parseOutput(part.output); + if (output && isDecompositionOutput(output)) + return `Plan ready (${output.step_count} steps)`; + return "Analyzing your goal..."; + } + case "output-error": + return "Error analyzing goal"; + default: + return "Analyzing your goal..."; + } +} + +export function ToolIcon({ + isStreaming, + isError, +}: { + isStreaming?: boolean; + isError?: boolean; +}) { + if (isError) { + return ( + + ); + } + if (isStreaming) { + return ; + } + return ( + + ); +} + +export function AccordionIcon() { + return ; +} + +export function StepStatusIcon({ status }: { status: string }) { + switch (status) { + case "completed": + return ( + + ); + case "in_progress": + return ( + + ); + case "failed": + return ; + default: + return ( + + ); + } +} From f330699a89c9dc12a4fe25f8156326e6caccea34 Mon Sep 17 00:00:00 2001 From: anvyle Date: Wed, 8 Apr 2026 23:36:17 +0200 Subject: [PATCH 03/61] =?UTF-8?q?fix(copilot):=20improve=20decompose=5Fgoa?= =?UTF-8?q?l=20UX=20=E2=80=94=20pin=20box=20post-stream,=20suppress=20comp?= =?UTF-8?q?anion=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move decomposition prompt from prompting.py to agent_generation_guide.md as a required pre-build gate - Add tool-decompose_goal to CUSTOM_TOOL_TYPES so it renders individually (not collapsed) - Add task_decomposition to INTERACTIVE_RESPONSE_TYPES so the box is pinned to response after streaming - Filter out text parts (table) from response when decompose_goal is pinned - Hide decompose_goal during streaming so the box only appears once all reasoning is complete and Approve is immediately actionable Co-Authored-By: Claude Sonnet 4.6 --- .../backend/backend/copilot/prompting.py | 11 ----------- .../backend/copilot/sdk/agent_generation_guide.md | 13 +++++++++++++ .../ChatMessagesContainer/ChatMessagesContainer.tsx | 7 ++++++- .../components/ChatMessagesContainer/helpers.ts | 11 ++++++++++- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/autogpt_platform/backend/backend/copilot/prompting.py b/autogpt_platform/backend/backend/copilot/prompting.py index 3fdba6fc338e..dd630a2e9b06 100644 --- a/autogpt_platform/backend/backend/copilot/prompting.py +++ b/autogpt_platform/backend/backend/copilot/prompting.py @@ -127,17 +127,6 @@ non-overlapping scope to avoid redundant searches. -### Agent Building Workflow — ALWAYS follow this -When the user asks to create an agent, ALWAYS follow this workflow: -1. Analyze the goal and break it into logical sub-instructions. -2. Call `decompose_goal` with the steps (each step = one logical task like - "add input block", "add AI summarizer", "wire blocks together"). -3. Wait for user approval before proceeding. -4. After approval, call `create_agent` with the full agent JSON. - -For simple goals (1-2 blocks), keep the decomposition brief (2-3 steps). -For complex goals, decompose into 4-8 steps max. - ### Tool Discovery Priority When the user asks to interact with a service or API, follow this order: diff --git a/autogpt_platform/backend/backend/copilot/sdk/agent_generation_guide.md b/autogpt_platform/backend/backend/copilot/sdk/agent_generation_guide.md index 28b6f1c7dc4d..00750e3b1f7b 100644 --- a/autogpt_platform/backend/backend/copilot/sdk/agent_generation_guide.md +++ b/autogpt_platform/backend/backend/copilot/sdk/agent_generation_guide.md @@ -26,6 +26,19 @@ Steps: **Skip this** when the goal already specifies all dimensions (e.g. "scrape prices from Amazon and email me daily"). +### Before Building: Goal Decomposition (REQUIRED) + +Before running the workflow below, ALWAYS decompose the goal first: + +1. Analyze the user's request and break it into logical build steps (e.g. + "add input block", "add AI summarizer", "wire blocks together"). +2. Call `decompose_goal` with those steps. +3. **STOP — do not proceed until the user approves the plan.** +4. Only after approval, continue with "Workflow for Creating/Editing Agents". + +For simple goals (1-2 blocks), keep steps brief (2-3 steps). +For complex goals, decompose into 4-8 steps max. + ### Workflow for Creating/Editing Agents 1. **If editing**: First narrow to the specific agent by UUID, then fetch its diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx index 205fa74bd0ce..89cad821ed06 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx @@ -214,7 +214,12 @@ export function ChatMessagesContainer({ message.role === "assistant" && !isCurrentlyStreaming; const { reasoning, response } = isFinalized ? splitReasoningAndResponse(message.parts) - : { reasoning: [] as MessagePart[], response: message.parts }; + : { + reasoning: [] as MessagePart[], + response: message.parts.filter( + (p) => p.type !== "tool-decompose_goal", + ), + }; const hasReasoning = reasoning.length > 0; // Note: when interactive tools are pinned from reasoning into response, diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/helpers.ts index c859ba791fe8..64425dd1622e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/helpers.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/helpers.ts @@ -29,6 +29,7 @@ const CUSTOM_TOOL_TYPES = new Set([ "tool-view_agent_output", "tool-search_feature_requests", "tool-create_feature_request", + "tool-decompose_goal", ]); const INTERACTIVE_RESPONSE_TYPES: ReadonlySet = new Set([ @@ -42,6 +43,7 @@ const INTERACTIVE_RESPONSE_TYPES: ReadonlySet = new Set([ ResponseType.suggested_goal, ResponseType.agent_builder_preview, ResponseType.agent_builder_saved, + ResponseType.task_decomposition, ]); export function isCompletedToolPart(part: MessagePart): part is ToolUIPart { @@ -148,9 +150,16 @@ export function splitReasoningAndResponse(parts: MessagePart[]): { } } + const hasDecomposeGoal = pinnedParts.some( + (p) => p.type === "tool-decompose_goal", + ); + const filteredResponse = hasDecomposeGoal + ? rawResponse.filter((p) => p.type !== "text") + : rawResponse; + return { reasoning, - response: [...pinnedParts, ...rawResponse], + response: [...pinnedParts, ...filteredResponse], }; } From 703d34364d30313759ecad38f63409df8368c764 Mon Sep 17 00:00:00 2001 From: anvyle Date: Wed, 8 Apr 2026 23:37:25 +0200 Subject: [PATCH 04/61] chore(frontend): update openapi.json snapshot Co-Authored-By: Claude Sonnet 4.6 --- autogpt_platform/frontend/src/app/api/openapi.json | 1 + 1 file changed, 1 insertion(+) diff --git a/autogpt_platform/frontend/src/app/api/openapi.json b/autogpt_platform/frontend/src/app/api/openapi.json index 3ca7e5707bc5..e183ee59487e 100644 --- a/autogpt_platform/frontend/src/app/api/openapi.json +++ b/autogpt_platform/frontend/src/app/api/openapi.json @@ -12100,6 +12100,7 @@ "agent_builder_clarification_needed", "agent_builder_validation_result", "agent_builder_fix_result", + "task_decomposition", "block_list", "block_details", "block_output", From 629fb4d3bb2732f5947ab545ad9eb06deade3a62 Mon Sep 17 00:00:00 2001 From: anvyle Date: Thu, 9 Apr 2026 12:27:05 +0200 Subject: [PATCH 05/61] fix(copilot): allow sub-instructions companion text and restore streaming render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revert ChatMessagesContainer streaming filter — decompose_goal now visible during stream - Remove text suppression in splitReasoningAndResponse — table message is allowed alongside sub-instructions box Co-Authored-By: Claude Sonnet 4.6 --- .../ChatMessagesContainer/ChatMessagesContainer.tsx | 7 +------ .../copilot/components/ChatMessagesContainer/helpers.ts | 9 +-------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx index 89cad821ed06..205fa74bd0ce 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx @@ -214,12 +214,7 @@ export function ChatMessagesContainer({ message.role === "assistant" && !isCurrentlyStreaming; const { reasoning, response } = isFinalized ? splitReasoningAndResponse(message.parts) - : { - reasoning: [] as MessagePart[], - response: message.parts.filter( - (p) => p.type !== "tool-decompose_goal", - ), - }; + : { reasoning: [] as MessagePart[], response: message.parts }; const hasReasoning = reasoning.length > 0; // Note: when interactive tools are pinned from reasoning into response, diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/helpers.ts index 64425dd1622e..94113b06860d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/helpers.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/helpers.ts @@ -150,16 +150,9 @@ export function splitReasoningAndResponse(parts: MessagePart[]): { } } - const hasDecomposeGoal = pinnedParts.some( - (p) => p.type === "tool-decompose_goal", - ); - const filteredResponse = hasDecomposeGoal - ? rawResponse.filter((p) => p.type !== "text") - : rawResponse; - return { reasoning, - response: [...pinnedParts, ...filteredResponse], + response: [...pinnedParts, ...rawResponse], }; } From 5fa33111de452912ff22bbf6ad000ec4e0666ff2 Mon Sep 17 00:00:00 2001 From: anvyle Date: Thu, 9 Apr 2026 21:50:43 +0200 Subject: [PATCH 06/61] feat(copilot): add auto-approve timer with editable steps to decompose_goal UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace static Approve/Modify buttons with a 99s countdown timer that auto-approves when it expires - Timer ring animates inline within "Starting in [N]s" text using SVG strokeDasharray; hover on the text swaps it to "Start now" via Tailwind named groups (group/label) - Clicking Modify stops the timer, enters editable mode where steps can be renamed, deleted, or inserted between existing steps - In edit mode only Approve is shown; timer and Modify are hidden - showActions gated on isLastMessage (server-derived) so the timer never re-appears when returning to a session with prior messages - Forward isLastMessage through ChatMessagesContainer → MessagePartRenderer Co-Authored-By: Claude Sonnet 4.6 --- .../ChatMessagesContainer.tsx | 4 + .../components/MessagePartRenderer.tsx | 10 +- .../tools/DecomposeGoal/DecomposeGoal.tsx | 274 +++++++++++++++--- 3 files changed, 250 insertions(+), 38 deletions(-) diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx index 5161103f4b4a..620c108388aa 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/ChatMessagesContainer.tsx @@ -50,6 +50,7 @@ function renderSegments( segments: RenderSegment[], messageID: string, onRetry?: () => void, + isLastMessage?: boolean, ): React.ReactNode[] { return segments.map((seg, segIdx) => { if (seg.kind === "collapsed-group") { @@ -62,6 +63,7 @@ function renderSegments( messageID={messageID} partIndex={seg.index} onRetry={onRetry} + isLastMessage={isLastMessage} /> ); }); @@ -372,6 +374,7 @@ export function ChatMessagesContainer({ responseSegments, message.id, isLastAssistant ? onRetry : undefined, + isLastAssistant, ) : message.parts.map((part, i) => ( ))} {isLastInTurn && !isCurrentlyStreaming && ( diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx index 136192a7d313..b86f73d86bc7 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/ChatMessagesContainer/components/MessagePartRenderer.tsx @@ -94,6 +94,7 @@ interface Props { messageID: string; partIndex: number; onRetry?: () => void; + isLastMessage?: boolean; } export function MessagePartRenderer({ @@ -101,6 +102,7 @@ export function MessagePartRenderer({ messageID, partIndex, onRetry, + isLastMessage, }: Props) { const key = `${messageID}-${partIndex}`; @@ -169,7 +171,13 @@ export function MessagePartRenderer({ case "tool-schedule_agent": return ; case "tool-decompose_goal": - return ; + return ( + + ); case "tool-create_agent": return ; case "tool-edit_agent": diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx index 18abeb9fbab0..fde9c67dc46d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/DecomposeGoal/DecomposeGoal.tsx @@ -1,13 +1,18 @@ "use client"; import { Button } from "@/components/atoms/Button/Button"; -import { CheckIcon, PencilSimpleIcon } from "@phosphor-icons/react"; +import { + CheckIcon, + PencilSimpleIcon, + PlusIcon, + TrashIcon, +} from "@phosphor-icons/react"; import type { ToolUIPart } from "ai"; +import { useEffect, useRef, useState } from "react"; import { useCopilotChatActions } from "../../components/CopilotChatActionsProvider/useCopilotChatActions"; import { MorphingTextAnimation } from "../../components/MorphingTextAnimation/MorphingTextAnimation"; import { ContentGrid, - ContentHint, ContentMessage, } from "../../components/ToolAccordion/AccordionContent"; import { ToolAccordion } from "../../components/ToolAccordion/ToolAccordion"; @@ -22,11 +27,24 @@ import { ToolIcon, } from "./helpers"; +const COUNTDOWN_SECONDS = 99; +const RADIUS = 15; +const CIRCUMFERENCE = 2 * Math.PI * RADIUS; + +interface EditableStep { + step_id: string; + description: string; + action: string; + block_name?: string | null; + status: string; +} + interface Props { part: ToolUIPart; + isLastMessage?: boolean; } -export function DecomposeGoalTool({ part }: Props) { +export function DecomposeGoalTool({ part, isLastMessage }: Props) { const text = getAnimationText(part); const { onSend } = useCopilotChatActions(); @@ -34,20 +52,102 @@ export function DecomposeGoalTool({ part }: Props) { part.state === "input-streaming" || part.state === "input-available"; const output = getDecomposeGoalOutput(part); - const isError = part.state === "output-error" || (!!output && isErrorOutput(output)); - const isOperating = !output; - function handleApprove() { - onSend("Approved. Please build the agent."); + const showActions = + !!isLastMessage && + !!output && + isDecompositionOutput(output) && + output.requires_approval; + + const [secondsLeft, setSecondsLeft] = useState(COUNTDOWN_SECONDS); + // timerActive becomes false when the user clicks Modify — stops countdown and auto-approve. + const [timerActive, setTimerActive] = useState(true); + const [isEditing, setIsEditing] = useState(false); + const [editableSteps, setEditableSteps] = useState([]); + + const approvedRef = useRef(false); + const onSendRef = useRef(onSend); + const isEditingRef = useRef(isEditing); + const editableStepsRef = useRef(editableSteps); + onSendRef.current = onSend; + isEditingRef.current = isEditing; + editableStepsRef.current = editableSteps; + + function buildMessage() { + if (isEditingRef.current && editableStepsRef.current.length > 0) { + const list = editableStepsRef.current + .map((s, i) => `${i + 1}. ${s.description}`) + .join("; "); + return `Approved with modifications. Please build the agent following these steps: ${list}`; + } + return "Approved. Please build the agent."; + } + + function approve() { + if (approvedRef.current) return; + approvedRef.current = true; + setIsEditing(false); + onSendRef.current(buildMessage()); } function handleModify() { - onSend("I'd like to modify the plan. Here are my changes: "); + if (!output || !isDecompositionOutput(output)) return; + setTimerActive(false); + setIsEditing(true); + setEditableSteps(output.steps.map((s) => ({ ...s }))); } + function handleStepChange(index: number, description: string) { + setEditableSteps((prev) => + prev.map((s, i) => (i === index ? { ...s, description } : s)), + ); + } + + function handleStepDelete(index: number) { + setEditableSteps((prev) => prev.filter((_, i) => i !== index)); + } + + // Insert a blank step after the given index (-1 = prepend). + function handleStepInsert(afterIndex: number) { + setEditableSteps((prev) => { + const next = [...prev]; + next.splice(afterIndex + 1, 0, { + step_id: `step_new_${Date.now()}`, + description: "", + action: "add_block", + status: "pending", + }); + return next; + }); + } + + // Tick down only while the timer is active. + useEffect(() => { + if (!showActions || !timerActive) return; + const interval = setInterval(() => { + setSecondsLeft((s) => Math.max(0, s - 1)); + }, 1000); + return () => clearInterval(interval); + }, [showActions, timerActive, part.toolCallId]); + + // Auto-approve when countdown reaches 0 (only if timer is still active). + useEffect(() => { + if (secondsLeft === 0 && timerActive) approve(); + // approve is stable via ref — intentionally omitted + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [secondsLeft, timerActive]); + + const progress = secondsLeft / COUNTDOWN_SECONDS; + const dashOffset = CIRCUMFERENCE * (1 - progress); + const stepCount = isEditing + ? editableSteps.length + : output && isDecompositionOutput(output) + ? output.step_count + : 0; + return (
{isOperating && ( @@ -76,49 +176,149 @@ export function DecomposeGoalTool({ part }: Props) { {output && isDecompositionOutput(output) && ( } - title={`Build Plan — ${output.step_count} steps`} + title={`Build Plan — ${stepCount} steps`} description={output.goal} defaultExpanded > {output.message} -
- {output.steps.map((step, i) => ( - - ))} +
+ {isEditing ? ( +
+ {/* Insert before the first step */} + handleStepInsert(-1)} /> + + {editableSteps.map((step, i) => ( +
+
+ + {i + 1}. + + handleStepChange(i, e.target.value)} + className="flex-1 rounded border border-slate-200 px-2 py-1 text-sm focus:border-neutral-400 focus:outline-none" + placeholder="Step description" + /> + +
+ {/* Insert after each step */} + handleStepInsert(i)} /> +
+ ))} +
+ ) : ( +
+ {output.steps.map((step, i) => ( + + ))} +
+ )}
- {output.requires_approval && ( + {showActions && (
- - + {isEditing ? ( + + ) : ( + <> + {/* Timer button — same ghost style as Modify, ring wraps the number inline */} + + | + + + )}
)} - - - Review the plan above and approve to start building. - )}
); } + +function InsertButton({ onClick }: { onClick: () => void }) { + return ( +
+
+ +
+
+ ); +} From b9d47a8cf59df1660bb212ae63f03cb23fa993e9 Mon Sep 17 00:00:00 2001 From: anvyle Date: Thu, 9 Apr 2026 22:10:51 +0200 Subject: [PATCH 07/61] fix(copilot): auto-size editable step textareas on initial render and input - Replace with