feat: add ultra mode

This commit is contained in:
hetao
2026-02-06 15:42:53 +08:00
parent f9811671d8
commit 96baab12a2
18 changed files with 272 additions and 41 deletions

View File

@@ -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,
)

View File

@@ -3,6 +3,8 @@ from datetime import datetime
from src.skills import load_skills
SUBAGENT_SECTION = """<subagent_system>
**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(

View File

@@ -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)

View File

@@ -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")

View File

@@ -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(

View File

@@ -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)")

View File

@@ -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
# ============================================================================

View File

@@ -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!));

View File

@@ -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" && (
<GraduationCapIcon className="size-3" />
)}
{context.mode === "ultra" && (
<RocketIcon className="size-3" />
)}
</div>
<div className="text-xs font-normal">
{(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)}
</div>
</PromptInputActionMenuTrigger>
<PromptInputActionMenuContent className="w-80">
@@ -306,6 +311,34 @@ export function InputBox({
<div className="ml-auto size-4" />
)}
</PromptInputActionMenuItem>
<PromptInputActionMenuItem
className={cn(
context.mode === "ultra"
? "text-accent-foreground"
: "text-muted-foreground/65",
)}
onSelect={() => handleModeSelect("ultra")}
>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-1 font-bold">
<RocketIcon
className={cn(
"mr-2 size-4",
context.mode === "ultra" && "text-accent-foreground",
)}
/>
{t.inputBox.ultraMode}
</div>
<div className="pl-7 text-xs">
{t.inputBox.ultraModeDescription}
</div>
</div>
{context.mode === "ultra" ? (
<CheckIcon className="ml-auto size-4" />
) : (
<div className="ml-auto size-4" />
)}
</PromptInputActionMenuItem>
</PromptInputActionMenu>
</DropdownMenuGroup>
</PromptInputActionMenuContent>

View File

@@ -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 <CheckCircleIcon className="size-4 text-green-600" />;
}
if (isFailed) {
return <XCircleIcon className="size-4 text-red-600" />;
}
if (isRunning) {
return <Loader2Icon className="size-4 animate-spin text-blue-600" />;
}
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 (
<div className={cn(
"rounded-lg border-l-2 p-4 transition-colors space-y-3",
borderColorClass
)}>
{/* Header */}
<div className="flex items-start gap-2">
<div className="mt-0.5 flex-shrink-0">
<IconComponent className="size-4" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{label}</span>
{getStatusIcon()}
</div>
{prompt && (
<div className="mt-1 text-xs text-muted-foreground">
{prompt}
</div>
)}
</div>
</div>
{/* Status message for running state */}
{isRunning && !state?.result && (
<div className="text-sm text-muted-foreground ml-6">
{t.subagents.running}
</div>
)}
{/* Result */}
{state?.result && (
<div className="ml-6 text-sm">
<MessageResponse>{state.result}</MessageResponse>
</div>
)}
{/* Error */}
{state?.status === "failed" && state.error && (
<div className="ml-6 rounded-md bg-red-50 p-3 text-sm text-red-700 border border-red-200">
<div className="font-medium">{t.subagents.failed}</div>
<div className="mt-1 text-xs">{state.error}</div>
</div>
)}
</div>
);
}

View File

@@ -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",

View File

@@ -62,6 +62,8 @@ export interface Translations {
reasoningModeDescription: string;
proMode: string;
proModeDescription: string;
ultraMode: string;
ultraModeDescription: string;
searchModels: string;
surpriseMe: string;
surpriseMePrompt: string;

View File

@@ -77,6 +77,8 @@ export const zhCN: Translations = {
reasoningModeDescription: "思考后再行动,在时间与准确性之间取得平衡",
proMode: "专业",
proModeDescription: "思考、计划再执行,获得更精准的结果,可能需要更多时间",
ultraMode: "超级",
ultraModeDescription: "专业模式加子代理,适用于复杂的多步骤任务,功能最强大",
searchModels: "搜索模型...",
surpriseMe: "小惊喜",
surpriseMePrompt: "给我一个小惊喜吧",

View File

@@ -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;

View File

@@ -0,0 +1,13 @@
import { createContext, useContext } from "react";
import type { SubagentState } from "../threads/types";
export const SubagentContext = createContext<Map<string, SubagentState>>(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;
}

View File

@@ -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<Map<string, SubagentState>>(new Map());
const subagentsRef = useRef<Map<string, SubagentState>>(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,
};
}

View File

@@ -0,0 +1,2 @@
export { useSubagentStates } from "./hooks";
export { SubagentContext, useSubagentContext } from "./context";

View File

@@ -17,4 +17,5 @@ export interface AgentThreadContext extends Record<string, unknown> {
model_name: string | undefined;
thinking_enabled: boolean;
is_plan_mode: boolean;
subagent_enabled: boolean;
}