From 449ffbad7539b2341c6ae3e0ae6f95486ea0936f Mon Sep 17 00:00:00 2001 From: hetao Date: Fri, 6 Feb 2026 15:42:53 +0800 Subject: [PATCH] feat: add ultra mode --- backend/src/agents/lead_agent/agent.py | 7 +- backend/src/agents/lead_agent/prompt.py | 10 +- backend/src/config/app_config.py | 2 - backend/src/config/subagents_config.py | 9 -- backend/src/tools/builtins/task_tool.py | 3 +- backend/src/tools/tools.py | 12 +- config.example.yaml | 9 -- .../app/workspace/chats/[thread_id]/page.tsx | 3 +- .../src/components/workspace/input-box.tsx | 45 ++++++- .../components/workspace/subagent-card.tsx | 117 ++++++++++++++++++ frontend/src/core/i18n/locales/en-US.ts | 3 + frontend/src/core/i18n/locales/types.ts | 2 + frontend/src/core/i18n/locales/zh-CN.ts | 2 + frontend/src/core/settings/local.ts | 4 +- frontend/src/core/subagents/context.ts | 13 ++ frontend/src/core/subagents/hooks.ts | 69 +++++++++++ frontend/src/core/subagents/index.ts | 2 + frontend/src/core/threads/types.ts | 1 + 18 files changed, 272 insertions(+), 41 deletions(-) delete mode 100644 backend/src/config/subagents_config.py create mode 100644 frontend/src/components/workspace/subagent-card.tsx create mode 100644 frontend/src/core/subagents/context.ts create mode 100644 frontend/src/core/subagents/hooks.ts create mode 100644 frontend/src/core/subagents/index.ts diff --git a/backend/src/agents/lead_agent/agent.py b/backend/src/agents/lead_agent/agent.py index 55f1707..cd51485 100644 --- a/backend/src/agents/lead_agent/agent.py +++ b/backend/src/agents/lead_agent/agent.py @@ -233,11 +233,12 @@ def make_lead_agent(config: RunnableConfig): thinking_enabled = config.get("configurable", {}).get("thinking_enabled", True) model_name = config.get("configurable", {}).get("model_name") or config.get("configurable", {}).get("model") is_plan_mode = config.get("configurable", {}).get("is_plan_mode", False) - print(f"thinking_enabled: {thinking_enabled}, model_name: {model_name}, is_plan_mode: {is_plan_mode}") + subagent_enabled = config.get("configurable", {}).get("subagent_enabled", False) + print(f"thinking_enabled: {thinking_enabled}, model_name: {model_name}, is_plan_mode: {is_plan_mode}, subagent_enabled: {subagent_enabled}") return create_agent( model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled), - tools=get_available_tools(model_name=model_name), + tools=get_available_tools(model_name=model_name, subagent_enabled=subagent_enabled), middleware=_build_middlewares(config), - system_prompt=apply_prompt_template(), + system_prompt=apply_prompt_template(subagent_enabled=subagent_enabled), state_schema=ThreadState, ) diff --git a/backend/src/agents/lead_agent/prompt.py b/backend/src/agents/lead_agent/prompt.py index 0a6fa7b..7719caf 100644 --- a/backend/src/agents/lead_agent/prompt.py +++ b/backend/src/agents/lead_agent/prompt.py @@ -3,6 +3,8 @@ from datetime import datetime from src.skills import load_skills SUBAGENT_SECTION = """ +**SUBAGENT MODE ENABLED**: You are running in subagent mode. Use the `task` tool proactively to delegate complex, multi-step tasks to specialized subagents. + You can delegate tasks to specialized subagents using the `task` tool. Subagents run in isolated context and return concise results. **Available Subagents:** @@ -258,7 +260,7 @@ def _get_memory_context() -> str: return "" -def apply_prompt_template() -> str: +def apply_prompt_template(subagent_enabled: bool = False) -> str: # Load only enabled skills skills = load_skills(enabled_only=True) @@ -268,11 +270,9 @@ def apply_prompt_template() -> str: config = get_app_config() container_base_path = config.skills.container_path - subagents_enabled = config.subagents.enabled except Exception: # Fallback to defaults if config fails container_base_path = "/mnt/skills" - subagents_enabled = True # Generate skills list XML with paths (path points to SKILL.md file) if skills: @@ -286,8 +286,8 @@ def apply_prompt_template() -> str: # Get memory context memory_context = _get_memory_context() - # Include subagent section only if enabled - subagent_section = SUBAGENT_SECTION if subagents_enabled else "" + # Include subagent section only if enabled (from runtime parameter) + subagent_section = SUBAGENT_SECTION if subagent_enabled else "" # Format the prompt with dynamic skills and memory prompt = SYSTEM_PROMPT_TEMPLATE.format( diff --git a/backend/src/config/app_config.py b/backend/src/config/app_config.py index a829659..d3886ea 100644 --- a/backend/src/config/app_config.py +++ b/backend/src/config/app_config.py @@ -11,7 +11,6 @@ from src.config.memory_config import load_memory_config_from_dict from src.config.model_config import ModelConfig from src.config.sandbox_config import SandboxConfig from src.config.skills_config import SkillsConfig -from src.config.subagents_config import SubagentsConfig from src.config.summarization_config import load_summarization_config_from_dict from src.config.title_config import load_title_config_from_dict from src.config.tool_config import ToolConfig, ToolGroupConfig @@ -27,7 +26,6 @@ class AppConfig(BaseModel): tools: list[ToolConfig] = Field(default_factory=list, description="Available tools") tool_groups: list[ToolGroupConfig] = Field(default_factory=list, description="Available tool groups") skills: SkillsConfig = Field(default_factory=SkillsConfig, description="Skills configuration") - subagents: SubagentsConfig = Field(default_factory=SubagentsConfig, description="Subagents configuration") extensions: ExtensionsConfig = Field(default_factory=ExtensionsConfig, description="Extensions configuration (MCP servers and skills state)") model_config = ConfigDict(extra="allow", frozen=False) diff --git a/backend/src/config/subagents_config.py b/backend/src/config/subagents_config.py deleted file mode 100644 index 2ccb47d..0000000 --- a/backend/src/config/subagents_config.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Configuration for subagents.""" - -from pydantic import BaseModel, Field - - -class SubagentsConfig(BaseModel): - """Configuration for subagents feature.""" - - enabled: bool = Field(default=True, description="Whether subagents are enabled") diff --git a/backend/src/tools/builtins/task_tool.py b/backend/src/tools/builtins/task_tool.py index e58b47c..a705eae 100644 --- a/backend/src/tools/builtins/task_tool.py +++ b/backend/src/tools/builtins/task_tool.py @@ -86,7 +86,8 @@ def task_tool( # Lazy import to avoid circular dependency from src.tools import get_available_tools - tools = get_available_tools(model_name=parent_model) + # Subagents should not have subagent tools enabled (prevent recursive nesting) + tools = get_available_tools(model_name=parent_model, subagent_enabled=False) # Create executor executor = SubagentExecutor( diff --git a/backend/src/tools/tools.py b/backend/src/tools/tools.py index b64e44c..1d4993e 100644 --- a/backend/src/tools/tools.py +++ b/backend/src/tools/tools.py @@ -19,7 +19,12 @@ SUBAGENT_TOOLS = [ ] -def get_available_tools(groups: list[str] | None = None, include_mcp: bool = True, model_name: str | None = None) -> list[BaseTool]: +def get_available_tools( + groups: list[str] | None = None, + include_mcp: bool = True, + model_name: str | None = None, + subagent_enabled: bool = False, +) -> list[BaseTool]: """Get all available tools from config. Note: MCP tools should be initialized at application startup using @@ -29,6 +34,7 @@ def get_available_tools(groups: list[str] | None = None, include_mcp: bool = Tru groups: Optional list of tool groups to filter by. include_mcp: Whether to include tools from MCP servers (default: True). model_name: Optional model name to determine if vision tools should be included. + subagent_enabled: Whether to include subagent tools (task, task_status). Returns: List of available tools. @@ -60,8 +66,8 @@ def get_available_tools(groups: list[str] | None = None, include_mcp: bool = Tru # Conditionally add tools based on config builtin_tools = BUILTIN_TOOLS.copy() - # Add subagent tools only if enabled - if config.subagents.enabled: + # Add subagent tools only if enabled via runtime parameter + if subagent_enabled: builtin_tools.extend(SUBAGENT_TOOLS) logger.info("Including subagent tools (task, task_status)") diff --git a/config.example.yaml b/config.example.yaml index 999e8f6..862dfe5 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -282,15 +282,6 @@ summarization: # # For more information, see: https://modelcontextprotocol.io -# ============================================================================ -# Subagents Configuration -# ============================================================================ -# Enable or disable the subagent (task tool) functionality -# Subagents allow delegating complex tasks to specialized agents - -subagents: - enabled: true # Set to false to disable subagents - # ============================================================================ # Memory Configuration # ============================================================================ diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index f8f0ed3..926334c 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -177,7 +177,8 @@ export default function ChatPage() { threadContext: { ...settings.context, thinking_enabled: settings.context.mode !== "flash", - is_plan_mode: settings.context.mode === "pro", + is_plan_mode: settings.context.mode === "pro" || settings.context.mode === "ultra", + subagent_enabled: settings.context.mode === "ultra", }, afterSubmit() { router.push(pathOfThread(threadId!)); diff --git a/frontend/src/components/workspace/input-box.tsx b/frontend/src/components/workspace/input-box.tsx index 4b5827f..7efc54a 100644 --- a/frontend/src/components/workspace/input-box.tsx +++ b/frontend/src/components/workspace/input-box.tsx @@ -8,6 +8,7 @@ import { PaperclipIcon, PlusIcon, SparklesIcon, + RocketIcon, ZapIcon, } from "lucide-react"; import { useSearchParams } from "next/navigation"; @@ -80,9 +81,9 @@ export function InputBox({ disabled?: boolean; context: Omit< AgentThreadContext, - "thread_id" | "is_plan_mode" | "thinking_enabled" + "thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled" > & { - mode: "flash" | "thinking" | "pro" | undefined; + mode: "flash" | "thinking" | "pro" | "ultra" | undefined; }; extraHeader?: React.ReactNode; isNewThread?: boolean; @@ -90,9 +91,9 @@ export function InputBox({ onContextChange?: ( context: Omit< AgentThreadContext, - "thread_id" | "is_plan_mode" | "thinking_enabled" + "thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled" > & { - mode: "flash" | "thinking" | "pro" | undefined; + mode: "flash" | "thinking" | "pro" | "ultra" | undefined; }, ) => void; onSubmit?: (message: PromptInputMessage) => void; @@ -131,7 +132,7 @@ export function InputBox({ [onContextChange, context], ); const handleModeSelect = useCallback( - (mode: "flash" | "thinking" | "pro") => { + (mode: "flash" | "thinking" | "pro" | "ultra") => { onContextChange?.({ ...context, mode, @@ -205,11 +206,15 @@ export function InputBox({ {context.mode === "pro" && ( )} + {context.mode === "ultra" && ( + + )}
{(context.mode === "flash" && t.inputBox.flashMode) || (context.mode === "thinking" && t.inputBox.reasoningMode) || - (context.mode === "pro" && t.inputBox.proMode)} + (context.mode === "pro" && t.inputBox.proMode) || + (context.mode === "ultra" && t.inputBox.ultraMode)}
@@ -306,6 +311,34 @@ export function InputBox({
)} + handleModeSelect("ultra")} + > +
+
+ + {t.inputBox.ultraMode} +
+
+ {t.inputBox.ultraModeDescription} +
+
+ {context.mode === "ultra" ? ( + + ) : ( +
+ )} + diff --git a/frontend/src/components/workspace/subagent-card.tsx b/frontend/src/components/workspace/subagent-card.tsx new file mode 100644 index 0000000..6fcc85d --- /dev/null +++ b/frontend/src/components/workspace/subagent-card.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { CheckCircleIcon, Loader2Icon, SquareTerminalIcon, WrenchIcon, XCircleIcon } from "lucide-react"; + +import { MessageResponse } from "@/components/ai-elements/message"; +import { useI18n } from "@/core/i18n/hooks"; +import { cn } from "@/lib/utils"; + +import type { SubagentState } from "@/core/threads/types"; + +interface SubagentCardProps { + subagentType: string; + state?: SubagentState; + isLoading?: boolean; + prompt?: string; +} + +export function SubagentCard({ subagentType, state, isLoading, prompt }: SubagentCardProps) { + const { t } = useI18n(); + + const getSubagentIcon = (type: string) => { + switch (type) { + case "bash": + return SquareTerminalIcon; + case "general-purpose": + return WrenchIcon; + default: + return WrenchIcon; + } + }; + + const getSubagentLabel = (type: string) => { + switch (type) { + case "bash": + return t.subagents.bash; + case "general-purpose": + return t.subagents.generalPurpose; + default: + return t.subagents.unknown; + } + }; + + const IconComponent = getSubagentIcon(subagentType); + const label = getSubagentLabel(subagentType); + + // Determine status based on state, not isLoading + const status = state?.status || "running"; + const isRunning = status === "running"; + const isCompleted = status === "completed"; + const isFailed = status === "failed"; + + const getStatusIcon = () => { + if (isCompleted) { + return ; + } + if (isFailed) { + return ; + } + if (isRunning) { + return ; + } + return null; + }; + + const borderColorClass = isCompleted + ? "border-green-200 bg-green-50/30" + : isFailed + ? "border-red-200 bg-red-50/30" + : "border-blue-200 bg-blue-50/30"; + + return ( +
+ {/* Header */} +
+
+ +
+
+
+ {label} + {getStatusIcon()} +
+ {prompt && ( +
+ {prompt} +
+ )} +
+
+ + {/* Status message for running state */} + {isRunning && !state?.result && ( +
+ {t.subagents.running} +
+ )} + + {/* Result */} + {state?.result && ( +
+ {state.result} +
+ )} + + {/* Error */} + {state?.status === "failed" && state.error && ( +
+
{t.subagents.failed}
+
{state.error}
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts index 9c3df0e..15475b9 100644 --- a/frontend/src/core/i18n/locales/en-US.ts +++ b/frontend/src/core/i18n/locales/en-US.ts @@ -79,6 +79,9 @@ export const enUS: Translations = { proMode: "Pro", proModeDescription: "Reasoning, planning and executing, get more accurate results, may take more time", + ultraMode: "Ultra", + ultraModeDescription: + "Pro mode with subagents enabled, maximum capability for complex multi-step tasks", searchModels: "Search models...", surpriseMe: "Surprise", surpriseMePrompt: "Surprise me", diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts index 8480289..58ebf09 100644 --- a/frontend/src/core/i18n/locales/types.ts +++ b/frontend/src/core/i18n/locales/types.ts @@ -62,6 +62,8 @@ export interface Translations { reasoningModeDescription: string; proMode: string; proModeDescription: string; + ultraMode: string; + ultraModeDescription: string; searchModels: string; surpriseMe: string; surpriseMePrompt: string; diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts index 567bd51..3ebd23d 100644 --- a/frontend/src/core/i18n/locales/zh-CN.ts +++ b/frontend/src/core/i18n/locales/zh-CN.ts @@ -77,6 +77,8 @@ export const zhCN: Translations = { reasoningModeDescription: "思考后再行动,在时间与准确性之间取得平衡", proMode: "专业", proModeDescription: "思考、计划再执行,获得更精准的结果,可能需要更多时间", + ultraMode: "超级", + ultraModeDescription: "专业模式加子代理,适用于复杂的多步骤任务,功能最强大", searchModels: "搜索模型...", surpriseMe: "小惊喜", surpriseMePrompt: "给我一个小惊喜吧", diff --git a/frontend/src/core/settings/local.ts b/frontend/src/core/settings/local.ts index c5e1242..9bdcf32 100644 --- a/frontend/src/core/settings/local.ts +++ b/frontend/src/core/settings/local.ts @@ -21,9 +21,9 @@ export interface LocalSettings { }; context: Omit< AgentThreadContext, - "thread_id" | "is_plan_mode" | "thinking_enabled" + "thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled" > & { - mode: "flash" | "thinking" | "pro" | undefined; + mode: "flash" | "thinking" | "pro" | "ultra" | undefined; }; layout: { sidebar_collapsed: boolean; diff --git a/frontend/src/core/subagents/context.ts b/frontend/src/core/subagents/context.ts new file mode 100644 index 0000000..da5d35d --- /dev/null +++ b/frontend/src/core/subagents/context.ts @@ -0,0 +1,13 @@ +import { createContext, useContext } from "react"; + +import type { SubagentState } from "../threads/types"; + +export const SubagentContext = createContext>(new Map()); + +export function useSubagentContext() { + const context = useContext(SubagentContext); + if (context === undefined) { + throw new Error("useSubagentContext must be used within a SubagentContext.Provider"); + } + return context; +} \ No newline at end of file diff --git a/frontend/src/core/subagents/hooks.ts b/frontend/src/core/subagents/hooks.ts new file mode 100644 index 0000000..c2b1133 --- /dev/null +++ b/frontend/src/core/subagents/hooks.ts @@ -0,0 +1,69 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +import type { SubagentProgressEvent, SubagentState } from "../threads/types"; + +export function useSubagentStates() { + const [subagents, setSubagents] = useState>(new Map()); + const subagentsRef = useRef>(new Map()); + + // 保持 ref 与 state 同步 + useEffect(() => { + subagentsRef.current = subagents; + }, [subagents]); + + const handleSubagentProgress = useCallback((event: SubagentProgressEvent) => { + console.log('[SubagentProgress] Received event:', event); + + const { task_id, trace_id, subagent_type, event_type, result, error } = event; + + setSubagents(prev => { + const newSubagents = new Map(prev); + const existingState = newSubagents.get(task_id) || { + task_id, + trace_id, + subagent_type, + status: "running" as const, + }; + + let newState = { ...existingState }; + + switch (event_type) { + case "started": + newState = { + ...newState, + status: "running", + }; + break; + + case "completed": + newState = { + ...newState, + status: "completed", + result, + }; + break; + + case "failed": + newState = { + ...newState, + status: "failed", + error, + }; + break; + } + + newSubagents.set(task_id, newState); + return newSubagents; + }); + }, []); + + const clearSubagents = useCallback(() => { + setSubagents(new Map()); + }, []); + + return { + subagents, + handleSubagentProgress, + clearSubagents, + }; +} \ No newline at end of file diff --git a/frontend/src/core/subagents/index.ts b/frontend/src/core/subagents/index.ts new file mode 100644 index 0000000..ef14e57 --- /dev/null +++ b/frontend/src/core/subagents/index.ts @@ -0,0 +1,2 @@ +export { useSubagentStates } from "./hooks"; +export { SubagentContext, useSubagentContext } from "./context"; \ No newline at end of file diff --git a/frontend/src/core/threads/types.ts b/frontend/src/core/threads/types.ts index 90232ba..106ef8a 100644 --- a/frontend/src/core/threads/types.ts +++ b/frontend/src/core/threads/types.ts @@ -17,4 +17,5 @@ export interface AgentThreadContext extends Record { model_name: string | undefined; thinking_enabled: boolean; is_plan_mode: boolean; + subagent_enabled: boolean; }