Checkpoint: Phase 7 complete: Orchestrator Agent добавлен в /agents с меткой CROWN/SYSTEM, кнопками Configure и Open Chat. /chat читает конфиг оркестратора из БД (модель, промпт, инструменты). AgentDetailModal поддерживает isOrchestrator. 24 теста пройдены.

This commit is contained in:
Manus
2026-03-20 17:48:21 -04:00
parent c2fdfdbf72
commit 7aa8eee2ca
11 changed files with 1339 additions and 128 deletions

View File

@@ -36,6 +36,11 @@ import {
Loader2,
AlertCircle,
BarChart2,
Crown,
MessageSquare,
Shield,
Wrench,
Code2,
} from "lucide-react";
import { motion } from "framer-motion";
import { useState } from "react";
@@ -50,6 +55,10 @@ const ROLE_ICONS: Record<string, any> = {
researcher: Brain,
executor: Zap,
monitor: Eye,
orchestrator: Crown,
browser: Globe,
tool_builder: Wrench,
agent_compiler: Code2,
};
function getStatusConfig(status: string) {
@@ -130,7 +139,7 @@ export default function Agents() {
<div>
<h2 className="text-xl font-bold text-foreground">Agent Fleet</h2>
<p className="text-sm text-muted-foreground font-mono mt-1">
{agents.length} total agents
{agents.filter((a: any) => !a.isOrchestrator).length} agents · {agents.filter((a: any) => a.isOrchestrator).length} orchestrator
</p>
</div>
<Button
@@ -162,83 +171,63 @@ export default function Agents() {
</CardContent>
</Card>
) : (
/* Agent cards grid */
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{agents.map((agent: any, i: number) => {
const sc = getStatusConfig(agent.status || "idle");
const Icon = ROLE_ICONS[agent.role] || Bot;
const temperature = typeof agent.temperature === "string" ? parseFloat(agent.temperature) : agent.temperature;
<div className="space-y-6">
{/* Orchestrator Section */}
{agents.filter((a: any) => a.isOrchestrator).map((agent: any) => {
const temperature = typeof agent.temperature === "string" ? parseFloat(agent.temperature) : (agent.temperature ?? 0.7);
return (
<motion.div
key={agent.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.08 }}
>
<Card className={`bg-card border-border/50 hover:border-primary/30 transition-all cursor-pointer ${sc.glow}`} onClick={() => handleEditAgent(agent)}>
<motion.div key={agent.id} initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}>
<Card className="bg-card border-cyan-500/40 hover:border-cyan-500/70 transition-all glow-cyan">
<CardContent className="p-5">
{/* Top row */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-secondary/50 border border-border/50 flex items-center justify-center">
<Icon className={`w-5 h-5 ${sc.color}`} />
<div className="w-12 h-12 rounded-lg bg-cyan-500/15 border border-cyan-500/40 flex items-center justify-center">
<Crown className="w-6 h-6 text-cyan-400" />
</div>
<div>
<h3 className="text-sm font-semibold text-foreground">{agent.name}</h3>
<p className="text-[11px] text-muted-foreground mt-0.5">{agent.description || "No description"}</p>
<div className="flex items-center gap-2">
<h3 className="text-base font-bold text-foreground">{agent.name}</h3>
<Badge className="text-[9px] font-mono bg-cyan-500/20 text-cyan-300 border-cyan-500/40 px-1.5 py-0">
<Crown className="w-2.5 h-2.5 mr-1" /> ORCHESTRATOR
</Badge>
<Badge className="text-[9px] font-mono bg-amber-500/20 text-amber-300 border-amber-500/40 px-1.5 py-0">
<Shield className="w-2.5 h-2.5 mr-1" /> SYSTEM
</Badge>
</div>
<p className="text-[11px] text-muted-foreground mt-0.5">{agent.description || "Main orchestrator — controls all agents, tools and system resources"}</p>
</div>
</div>
<Badge variant="outline" className={`text-[10px] font-mono ${sc.badge}`}>
<span className={`w-1.5 h-1.5 rounded-full ${sc.bg} mr-1.5 ${agent.isActive ? "pulse-indicator" : ""}`} />
{agent.isActive ? "ACTIVE" : "INACTIVE"}
<Badge variant="outline" className="text-[10px] font-mono bg-neon-green/15 text-neon-green border-neon-green/30">
<span className="w-1.5 h-1.5 rounded-full bg-neon-green mr-1.5 pulse-indicator" />
ACTIVE
</Badge>
</div>
{/* Model & Node info */}
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
<div className="flex items-center gap-1.5 mb-1">
<Brain className="w-3 h-3 text-primary" />
<span className="text-[10px] text-muted-foreground font-mono">MODEL</span>
</div>
<div className="text-xs font-mono font-medium text-primary">{agent.model}</div>
<div className="grid grid-cols-3 gap-3 mb-4">
<div className="p-2.5 rounded-md bg-cyan-500/10 border border-cyan-500/20">
<div className="text-[10px] text-muted-foreground font-mono mb-1">MODEL</div>
<div className="text-xs font-mono font-bold text-cyan-400">{agent.model}</div>
<div className="text-[10px] font-mono text-muted-foreground">{agent.provider}</div>
</div>
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
<div className="flex items-center gap-1.5 mb-1">
<Cpu className="w-3 h-3 text-primary" />
<span className="text-[10px] text-muted-foreground font-mono">CONFIG</span>
</div>
<div className="text-xs font-mono font-medium text-foreground">T: {temperature.toFixed(2)}</div>
<div className="text-[10px] text-muted-foreground font-mono mb-1">TEMPERATURE</div>
<div className="text-xs font-mono font-bold text-foreground">{temperature.toFixed(2)}</div>
<div className="text-[10px] font-mono text-muted-foreground">Tokens: {agent.maxTokens}</div>
</div>
</div>
{/* Metrics row */}
<div className="flex items-center gap-4 mb-4 text-[10px] font-mono">
<div className="flex items-center gap-1">
<Zap className="w-3 h-3 text-neon-amber" />
<span className="text-muted-foreground">Role:</span>
<span className="text-foreground font-medium capitalize">{agent.role}</span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3 text-primary" />
<span className="text-muted-foreground">Created:</span>
<span className="text-foreground font-medium">{new Date(agent.createdAt).toLocaleDateString()}</span>
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
<div className="text-[10px] text-muted-foreground font-mono mb-1">TOOLS</div>
<div className="text-xs font-mono font-bold text-foreground">
{agent.allowedTools?.length ?? 0} tools
</div>
<div className="text-[10px] font-mono text-muted-foreground">Full access</div>
</div>
</div>
{/* Tools */}
{agent.allowedTools && agent.allowedTools.length > 0 && (
<div className="mb-4">
<span className="text-[10px] text-muted-foreground font-mono block mb-1.5">TOOLS</span>
<div className="flex flex-wrap gap-1.5">
{agent.allowedTools.map((tool: string) => (
<span
key={tool}
className="px-2 py-0.5 rounded text-[10px] font-mono bg-primary/10 text-primary border border-primary/20"
>
<span key={tool} className="px-2 py-0.5 rounded text-[10px] font-mono bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
{tool}
</span>
))}
@@ -246,47 +235,167 @@ export default function Agents() {
</div>
)}
{/* Actions */}
<div className="flex items-center gap-2 pt-3 border-t border-border/30">
<div className="flex items-center gap-2 pt-3 border-t border-cyan-500/20">
<Button
size="sm"
className="h-7 text-[11px] bg-cyan-500/15 text-cyan-400 border border-cyan-500/30 hover:bg-cyan-500/25"
onClick={(e) => { e.stopPropagation(); navigate("/chat"); }}
>
<MessageSquare className="w-3 h-3 mr-1" /> Open Chat
</Button>
<Button
size="sm"
variant="outline"
className="h-7 text-[11px] text-primary border-primary/30 hover:bg-primary/10"
onClick={(e) => {
e.stopPropagation();
handleEditAgent(agent);
}}
onClick={(e) => { e.stopPropagation(); handleEditAgent(agent); }}
>
Edit
Configure
</Button>
<Button
size="sm"
variant="outline"
className="h-7 text-[11px] text-neon-amber border-neon-amber/30 hover:bg-neon-amber/10"
onClick={(e) => {
e.stopPropagation();
navigate(`/agents/${agent.id}/metrics`);
}}
onClick={(e) => { e.stopPropagation(); navigate(`/agents/${agent.id}/metrics`); }}
>
<BarChart2 className="w-3 h-3 mr-1" /> Metrics
</Button>
<Button
size="sm"
variant="outline"
className="h-7 text-[11px] text-neon-red border-neon-red/30 hover:bg-neon-red/10 ml-auto"
onClick={(e) => {
e.stopPropagation();
handleDeleteClick(agent.id);
}}
>
<Trash2 className="w-3 h-3 mr-1" /> Delete
</Button>
</div>
</CardContent>
</Card>
</motion.div>
);
})}
{/* Specialized Agents Section */}
{agents.filter((a: any) => !a.isOrchestrator).length > 0 && (
<div>
<h3 className="text-sm font-semibold text-muted-foreground font-mono mb-3 flex items-center gap-2">
<Bot className="w-4 h-4" /> SPECIALIZED AGENTS
</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{agents.filter((a: any) => !a.isOrchestrator).map((agent: any, i: number) => {
const sc = getStatusConfig(agent.status || "idle");
const Icon = ROLE_ICONS[agent.role] || Bot;
const temperature = typeof agent.temperature === "string" ? parseFloat(agent.temperature) : (agent.temperature ?? 0.7);
return (
<motion.div
key={agent.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.08 }}
>
<Card className={`bg-card border-border/50 hover:border-primary/30 transition-all cursor-pointer ${sc.glow}`} onClick={() => handleEditAgent(agent)}>
<CardContent className="p-5">
{/* Top row */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-secondary/50 border border-border/50 flex items-center justify-center">
<Icon className={`w-5 h-5 ${sc.color}`} />
</div>
<div>
<div className="flex items-center gap-1.5">
<h3 className="text-sm font-semibold text-foreground">{agent.name}</h3>
{agent.isSystem && (
<Badge className="text-[9px] font-mono bg-amber-500/15 text-amber-400 border-amber-500/30 px-1 py-0">
SYS
</Badge>
)}
</div>
<p className="text-[11px] text-muted-foreground mt-0.5">{agent.description || "No description"}</p>
</div>
</div>
<Badge variant="outline" className={`text-[10px] font-mono ${sc.badge}`}>
<span className={`w-1.5 h-1.5 rounded-full ${sc.bg} mr-1.5 ${agent.isActive ? "pulse-indicator" : ""}`} />
{agent.isActive ? "ACTIVE" : "INACTIVE"}
</Badge>
</div>
{/* Model & Config */}
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
<div className="flex items-center gap-1.5 mb-1">
<Brain className="w-3 h-3 text-primary" />
<span className="text-[10px] text-muted-foreground font-mono">MODEL</span>
</div>
<div className="text-xs font-mono font-medium text-primary">{agent.model}</div>
<div className="text-[10px] font-mono text-muted-foreground">{agent.provider}</div>
</div>
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
<div className="flex items-center gap-1.5 mb-1">
<Cpu className="w-3 h-3 text-primary" />
<span className="text-[10px] text-muted-foreground font-mono">CONFIG</span>
</div>
<div className="text-xs font-mono font-medium text-foreground">T: {temperature.toFixed(2)}</div>
<div className="text-[10px] font-mono text-muted-foreground">Tokens: {agent.maxTokens}</div>
</div>
</div>
{/* Role & Date */}
<div className="flex items-center gap-4 mb-4 text-[10px] font-mono">
<div className="flex items-center gap-1">
<Zap className="w-3 h-3 text-neon-amber" />
<span className="text-muted-foreground">Role:</span>
<span className="text-foreground font-medium capitalize">{agent.role}</span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3 text-primary" />
<span className="text-muted-foreground">Created:</span>
<span className="text-foreground font-medium">{new Date(agent.createdAt).toLocaleDateString()}</span>
</div>
</div>
{/* Tools */}
{agent.allowedTools && agent.allowedTools.length > 0 && (
<div className="mb-4">
<span className="text-[10px] text-muted-foreground font-mono block mb-1.5">TOOLS</span>
<div className="flex flex-wrap gap-1.5">
{agent.allowedTools.map((tool: string) => (
<span key={tool} className="px-2 py-0.5 rounded text-[10px] font-mono bg-primary/10 text-primary border border-primary/20">
{tool}
</span>
))}
</div>
</div>
)}
{/* Actions */}
<div className="flex items-center gap-2 pt-3 border-t border-border/30">
<Button
size="sm"
variant="outline"
className="h-7 text-[11px] text-primary border-primary/30 hover:bg-primary/10"
onClick={(e) => { e.stopPropagation(); handleEditAgent(agent); }}
>
Edit
</Button>
<Button
size="sm"
variant="outline"
className="h-7 text-[11px] text-neon-amber border-neon-amber/30 hover:bg-neon-amber/10"
onClick={(e) => { e.stopPropagation(); navigate(`/agents/${agent.id}/metrics`); }}
>
<BarChart2 className="w-3 h-3 mr-1" /> Metrics
</Button>
{!agent.isSystem && (
<Button
size="sm"
variant="outline"
className="h-7 text-[11px] text-neon-red border-neon-red/30 hover:bg-neon-red/10 ml-auto"
onClick={(e) => { e.stopPropagation(); handleDeleteClick(agent.id); }}
>
<Trash2 className="w-3 h-3 mr-1" /> Delete
</Button>
)}
</div>
</CardContent>
</Card>
</motion.div>
);
})}
</div>
</div>
)}
</div>
)}

View File

@@ -237,15 +237,7 @@ function MessageBubble({ msg }: { msg: ChatMessage }) {
// ─── Main Chat Component ──────────────────────────────────────────────────────
export default function Chat() {
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: "welcome",
role: "system",
content:
"GoClaw Orchestrator ready.\nI have access to all agents, tools, and skills.\nType a command or ask anything.",
timestamp: new Date().toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" }),
},
]);
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [conversationHistory, setConversationHistory] = useState<
Array<{ role: "user" | "assistant" | "system"; content: string }>
>([]);
@@ -256,6 +248,22 @@ export default function Chat() {
const agentsQuery = trpc.agents.list.useQuery(undefined, { refetchInterval: 30000 });
const orchestratorMutation = trpc.orchestrator.chat.useMutation();
const orchestratorConfigQuery = trpc.orchestrator.getConfig.useQuery();
// Initialize welcome message with orchestrator name from DB
useEffect(() => {
if (orchestratorConfigQuery.data && messages.length === 0) {
const cfg = orchestratorConfigQuery.data;
setMessages([
{
id: "welcome",
role: "system",
content: `${cfg.name} ready. Model: ${cfg.model}\nI have access to all agents, tools, and skills.\nType a command or ask anything.`,
timestamp: new Date().toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" }),
},
]);
}
}, [orchestratorConfigQuery.data]);
useEffect(() => {
if (scrollRef.current) {
@@ -364,7 +372,8 @@ export default function Chat() {
};
const agents = agentsQuery.data ?? [];
const activeAgents = agents.filter((a) => a.isActive);
const activeAgents = agents.filter((a) => a.isActive && !(a as any).isOrchestrator);
const orchConfig = orchestratorConfigQuery.data;
return (
<div className="h-full flex flex-col gap-3">
@@ -375,27 +384,44 @@ export default function Chat() {
<Bot className="w-4 h-4 text-cyan-400" />
</div>
<div>
<h2 className="text-lg font-bold text-foreground">GoClaw Orchestrator</h2>
<h2 className="text-lg font-bold text-foreground">
{orchConfig?.name ?? "GoClaw Orchestrator"}
</h2>
<p className="text-[11px] font-mono text-muted-foreground">
Main AI · {activeAgents.length} agents · {ORCHESTRATOR_TOOLS_COUNT} tools
{orchConfig ? (
<span>
<span className="text-cyan-400/80">{orchConfig.model}</span>
{" · "}{activeAgents.length} agents · {ORCHESTRATOR_TOOLS_COUNT} tools
</span>
) : (
`Main AI · ${activeAgents.length} agents · ${ORCHESTRATOR_TOOLS_COUNT} tools`
)}
</p>
</div>
</div>
{/* Active agents badges */}
<div className="flex items-center gap-1.5 flex-wrap justify-end">
{activeAgents.slice(0, 4).map((agent) => (
<Badge
key={agent.id}
variant="outline"
className="text-[9px] h-5 px-1.5 font-mono border-border/50 text-muted-foreground"
>
{agent.role === "browser" && <Globe className="w-2.5 h-2.5 mr-1 text-cyan-400" />}
{agent.role === "tool_builder" && <Wrench className="w-2.5 h-2.5 mr-1 text-orange-400" />}
{agent.role === "agent_compiler" && <Cpu className="w-2.5 h-2.5 mr-1 text-purple-400" />}
{agent.name}
</Badge>
))}
{/* Active agents badges + Configure link */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5 flex-wrap justify-end">
{activeAgents.slice(0, 3).map((agent) => (
<Badge
key={agent.id}
variant="outline"
className="text-[9px] h-5 px-1.5 font-mono border-border/50 text-muted-foreground"
>
{agent.role === "browser" && <Globe className="w-2.5 h-2.5 mr-1 text-cyan-400" />}
{agent.role === "tool_builder" && <Wrench className="w-2.5 h-2.5 mr-1 text-orange-400" />}
{agent.role === "agent_compiler" && <Cpu className="w-2.5 h-2.5 mr-1 text-purple-400" />}
{agent.name}
</Badge>
))}
</div>
<a
href="/agents"
className="text-[10px] font-mono px-2 py-1 rounded border border-cyan-500/30 text-cyan-400/70 hover:text-cyan-400 hover:border-cyan-500/60 transition-colors bg-cyan-500/5 shrink-0"
>
Configure
</a>
</div>
</div>