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>

View File

@@ -0,0 +1,2 @@
ALTER TABLE `agents` ADD `isSystem` boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE `agents` ADD `isOrchestrator` boolean DEFAULT false;

View File

@@ -0,0 +1,874 @@
{
"version": "5",
"dialect": "mysql",
"id": "19c68417-6ca1-4df0-9f19-89364c61dd84",
"prevId": "c2d59f1f-0ab6-4daf-80f8-651cb95a4778",
"tables": {
"agentAccessControl": {
"name": "agentAccessControl",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"agentId": {
"name": "agentId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"tool": {
"name": "tool",
"type": "varchar(50)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"isAllowed": {
"name": "isAllowed",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": true
},
"maxExecutionsPerHour": {
"name": "maxExecutionsPerHour",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 100
},
"timeoutSeconds": {
"name": "timeoutSeconds",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 30
},
"allowedPatterns": {
"name": "allowedPatterns",
"type": "json",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "('[]')"
},
"blockedPatterns": {
"name": "blockedPatterns",
"type": "json",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "('[]')"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {
"agentAccessControl_agentId_tool_idx": {
"name": "agentAccessControl_agentId_tool_idx",
"columns": [
"agentId",
"tool"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"agentAccessControl_id": {
"name": "agentAccessControl_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"agentHistory": {
"name": "agentHistory",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"agentId": {
"name": "agentId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userMessage": {
"name": "userMessage",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"agentResponse": {
"name": "agentResponse",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"conversationId": {
"name": "conversationId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"messageIndex": {
"name": "messageIndex",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('pending','success','error')",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'pending'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {
"agentHistory_agentId_idx": {
"name": "agentHistory_agentId_idx",
"columns": [
"agentId"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"agentHistory_id": {
"name": "agentHistory_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"agentMetrics": {
"name": "agentMetrics",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"agentId": {
"name": "agentId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"requestId": {
"name": "requestId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userMessage": {
"name": "userMessage",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"agentResponse": {
"name": "agentResponse",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"inputTokens": {
"name": "inputTokens",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"outputTokens": {
"name": "outputTokens",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"totalTokens": {
"name": "totalTokens",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"processingTimeMs": {
"name": "processingTimeMs",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('success','error','timeout','rate_limited')",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"errorMessage": {
"name": "errorMessage",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"toolsCalled": {
"name": "toolsCalled",
"type": "json",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "('[]')"
},
"model": {
"name": "model",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"temperature": {
"name": "temperature",
"type": "decimal(3,2)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {
"agentMetrics_agentId_idx": {
"name": "agentMetrics_agentId_idx",
"columns": [
"agentId"
],
"isUnique": false
},
"agentMetrics_createdAt_idx": {
"name": "agentMetrics_createdAt_idx",
"columns": [
"createdAt"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"agentMetrics_id": {
"name": "agentMetrics_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"agentMetrics_requestId_unique": {
"name": "agentMetrics_requestId_unique",
"columns": [
"requestId"
]
}
},
"checkConstraint": {}
},
"agents": {
"name": "agents",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"userId": {
"name": "userId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "varchar(100)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"model": {
"name": "model",
"type": "varchar(100)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider": {
"name": "provider",
"type": "varchar(50)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"temperature": {
"name": "temperature",
"type": "decimal(3,2)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0.7'"
},
"maxTokens": {
"name": "maxTokens",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 2048
},
"topP": {
"name": "topP",
"type": "decimal(3,2)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'1.0'"
},
"frequencyPenalty": {
"name": "frequencyPenalty",
"type": "decimal(3,2)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0.0'"
},
"presencePenalty": {
"name": "presencePenalty",
"type": "decimal(3,2)",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'0.0'"
},
"systemPrompt": {
"name": "systemPrompt",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"allowedTools": {
"name": "allowedTools",
"type": "json",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "('[]')"
},
"allowedDomains": {
"name": "allowedDomains",
"type": "json",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "('[]')"
},
"maxRequestsPerHour": {
"name": "maxRequestsPerHour",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 100
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": true
},
"isPublic": {
"name": "isPublic",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
},
"isSystem": {
"name": "isSystem",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
},
"isOrchestrator": {
"name": "isOrchestrator",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
},
"tags": {
"name": "tags",
"type": "json",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "('[]')"
},
"metadata": {
"name": "metadata",
"type": "json",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "('{}')"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {
"agents_userId_idx": {
"name": "agents_userId_idx",
"columns": [
"userId"
],
"isUnique": false
},
"agents_model_idx": {
"name": "agents_model_idx",
"columns": [
"model"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"agents_id": {
"name": "agents_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"browserSessions": {
"name": "browserSessions",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"sessionId": {
"name": "sessionId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"agentId": {
"name": "agentId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"currentUrl": {
"name": "currentUrl",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"status": {
"name": "status",
"type": "enum('active','idle','closed','error')",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'idle'"
},
"screenshotUrl": {
"name": "screenshotUrl",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"lastActionAt": {
"name": "lastActionAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "(now())"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"closedAt": {
"name": "closedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"browserSessions_id": {
"name": "browserSessions_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"browserSessions_sessionId_unique": {
"name": "browserSessions_sessionId_unique",
"columns": [
"sessionId"
]
}
},
"checkConstraint": {}
},
"toolDefinitions": {
"name": "toolDefinitions",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"toolId": {
"name": "toolId",
"type": "varchar(100)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"category": {
"name": "category",
"type": "varchar(50)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'custom'"
},
"dangerous": {
"name": "dangerous",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
},
"parameters": {
"name": "parameters",
"type": "json",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"implementation": {
"name": "implementation",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": true
},
"createdBy": {
"name": "createdBy",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"toolDefinitions_id": {
"name": "toolDefinitions_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"toolDefinitions_toolId_unique": {
"name": "toolDefinitions_toolId_unique",
"columns": [
"toolId"
]
}
},
"checkConstraint": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"openId": {
"name": "openId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(320)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"loginMethod": {
"name": "loginMethod",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "enum('user','admin')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'user'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
},
"lastSignedIn": {
"name": "lastSignedIn",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"users_openId_unique": {
"name": "users_openId_unique",
"columns": [
"openId"
]
}
},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}

View File

@@ -22,6 +22,13 @@
"when": 1774042457262,
"tag": "0002_tricky_saracen",
"breakpoints": true
},
{
"idx": 3,
"version": "5",
"when": 1774043298939,
"tag": "0003_lazy_hitman",
"breakpoints": true
}
]
}

View File

@@ -57,6 +57,8 @@ export const agents = mysqlTable("agents", {
// Статус
isActive: boolean("isActive").default(true),
isPublic: boolean("isPublic").default(false),
isSystem: boolean("isSystem").default(false), // Системный агент (нельзя удалить)
isOrchestrator: boolean("isOrchestrator").default(false), // Главный оркестратор чата
// Метаданные
tags: json("tags").$type<string[]>().default([]),

View File

@@ -40,7 +40,7 @@ async function main() {
CONSTRAINT \`agentMetrics_requestId_unique\` UNIQUE(\`requestId\`)
)`,
// agents
// agents — full schema with isSystem and isOrchestrator
`CREATE TABLE IF NOT EXISTS \`agents\` (
\`id\` int AUTO_INCREMENT NOT NULL,
\`userId\` int NOT NULL,
@@ -60,6 +60,8 @@ async function main() {
\`maxRequestsPerHour\` int DEFAULT 100,
\`isActive\` boolean DEFAULT true,
\`isPublic\` boolean DEFAULT false,
\`isSystem\` boolean DEFAULT false,
\`isOrchestrator\` boolean DEFAULT false,
\`tags\` json,
\`metadata\` json,
\`createdAt\` timestamp NOT NULL DEFAULT (now()),
@@ -101,6 +103,19 @@ async function main() {
CONSTRAINT \`browserSessions_sessionId_unique\` UNIQUE(\`sessionId\`)
)`,
// agentHistory — conversation history per agent
`CREATE TABLE IF NOT EXISTS \`agentHistory\` (
\`id\` int AUTO_INCREMENT NOT NULL,
\`agentId\` int NOT NULL,
\`sessionId\` varchar(64),
\`role\` enum('user','assistant','system','tool') NOT NULL,
\`content\` text NOT NULL,
\`toolCalls\` json,
\`metadata\` json,
\`createdAt\` timestamp NOT NULL DEFAULT (now()),
CONSTRAINT \`agentHistory_id\` PRIMARY KEY(\`id\`)
)`,
// Indexes
`CREATE INDEX IF NOT EXISTS \`agentAccessControl_agentId_tool_idx\` ON \`agentAccessControl\` (\`agentId\`,\`tool\`)`,
`CREATE INDEX IF NOT EXISTS \`agentMetrics_agentId_idx\` ON \`agentMetrics\` (\`agentId\`)`,
@@ -108,6 +123,7 @@ async function main() {
`CREATE INDEX IF NOT EXISTS \`agents_userId_idx\` ON \`agents\` (\`userId\`)`,
`CREATE INDEX IF NOT EXISTS \`agents_model_idx\` ON \`agents\` (\`model\`)`,
`CREATE INDEX IF NOT EXISTS \`browserSessions_agentId_idx\` ON \`browserSessions\` (\`agentId\`)`,
`CREATE INDEX IF NOT EXISTS \`agentHistory_agentId_idx\` ON \`agentHistory\` (\`agentId\`)`,
];
for (const stmt of statements) {
@@ -118,12 +134,36 @@ async function main() {
} catch (e) {
if (e.code === 'ER_DUP_KEYNAME' || e.message.includes('Duplicate key name')) {
console.log('⚠ Index already exists (ok)');
} else if (e.message.includes('already exists')) {
console.log('⚠ Already exists (ok)');
} else {
console.error('✗ Error:', e.message.slice(0, 120));
}
}
}
// ALTER TABLE to add missing columns to existing tables
const alterStatements = [
`ALTER TABLE \`agents\` ADD COLUMN IF NOT EXISTS \`isSystem\` boolean DEFAULT false`,
`ALTER TABLE \`agents\` ADD COLUMN IF NOT EXISTS \`isOrchestrator\` boolean DEFAULT false`,
];
console.log('\n--- Applying ALTER TABLE migrations ---');
for (const stmt of alterStatements) {
try {
await conn.query(stmt);
const col = stmt.match(/ADD COLUMN.*?`(\w+)`/)?.[1] || 'column';
console.log('✓ Added column:', col);
} catch (e) {
if (e.message.includes('Duplicate column name') || e.message.includes('already exists')) {
const col = stmt.match(/ADD COLUMN.*?`(\w+)`/)?.[1] || 'column';
console.log('⚠ Column already exists:', col, '(ok)');
} else {
console.error('✗ ALTER error:', e.message.slice(0, 120));
}
}
}
const [tables] = await conn.query('SHOW TABLES');
console.log('\n✅ All tables:', tables.map(t => Object.values(t)[0]).join(', '));

View File

@@ -1,6 +1,7 @@
/**
* Seed script — creates 3 specialized agents in the database
* Seed script — creates Orchestrator + 3 specialized agents in the database
* Run: node scripts/seed-agents.mjs
* Run with --force to re-seed existing agents
*/
import mysql from "mysql2/promise";
import * as dotenv from "dotenv";
@@ -13,7 +14,6 @@ if (!DATABASE_URL) {
process.exit(1);
}
// Parse mysql2 connection string
function parseDbUrl(url) {
const u = new URL(url);
return {
@@ -26,7 +26,78 @@ function parseDbUrl(url) {
};
}
const AGENTS = [
const ORCHESTRATOR = {
name: "Orchestrator",
description:
"Main system agent that powers the /chat interface. Has full access to all system resources: shell commands, file system, Docker management, HTTP requests, and can delegate tasks to any other agent. Configure its model, system prompt, and tools here.",
role: "orchestrator",
model: "qwen2.5:7b",
provider: "ollama",
temperature: "0.5",
maxTokens: 8192,
topP: "0.9",
frequencyPenalty: "0.0",
presencePenalty: "0.0",
systemPrompt: `You are the GoClaw Orchestrator — the central AI agent of the GoClaw Control Center system.
You have FULL access to the system and can:
- Execute shell commands on the host system
- Read and write files anywhere on the filesystem
- Manage Docker containers and services
- Make HTTP requests to any URL
- Delegate tasks to specialized agents (Browser Agent, Tool Builder, Agent Compiler)
- List and manage all agents in the system
- Install and manage skills
- Access and modify the GoClaw codebase
When given a task:
1. Analyze what needs to be done
2. Choose the right approach: direct execution or delegation to a specialist agent
3. Use tools step by step, showing your reasoning
4. Report results clearly
Available specialized agents you can delegate to:
- Browser Agent: web browsing, scraping, research
- Tool Builder: create new tools from descriptions
- Agent Compiler: compile new agents from specifications (ТЗ)
System access tools: shell_exec, file_read, file_write, http_request, docker_ps, docker_restart
Agent management: delegate_to_agent, list_agents, list_skills, install_skill
Always be transparent about what you're doing and why.
Respond in the same language as the user's message.`,
allowedTools: JSON.stringify([
"shell_exec",
"file_read",
"file_write",
"http_request",
"docker_ps",
"docker_restart",
"delegate_to_agent",
"list_agents",
"list_skills",
"install_skill",
"read_logs",
"manage_agents",
]),
allowedDomains: JSON.stringify(["*"]),
maxRequestsPerHour: 1000,
isActive: 1,
isPublic: 0,
isSystem: 1,
isOrchestrator: 1,
tags: JSON.stringify(["orchestrator", "system", "core", "privileged"]),
metadata: JSON.stringify({
agentType: "orchestrator",
icon: "Crown",
color: "#FFD700",
seeded: true,
systemAgent: true,
privileged: true,
}),
};
const SPECIALIZED_AGENTS = [
{
name: "Browser Agent",
description:
@@ -62,6 +133,8 @@ Respond in the same language as the user's message.`,
maxRequestsPerHour: 100,
isActive: 1,
isPublic: 1,
isSystem: 1,
isOrchestrator: 0,
tags: JSON.stringify(["browser", "web", "scraping", "research"]),
metadata: JSON.stringify({
agentType: "browser",
@@ -111,6 +184,8 @@ Respond in the same language as the user's message.`,
maxRequestsPerHour: 50,
isActive: 1,
isPublic: 1,
isSystem: 1,
isOrchestrator: 0,
tags: JSON.stringify(["tools", "code", "builder", "automation"]),
metadata: JSON.stringify({
agentType: "tool_builder",
@@ -164,6 +239,8 @@ Respond in the same language as the user's message.`,
maxRequestsPerHour: 30,
isActive: 1,
isPublic: 1,
isSystem: 1,
isOrchestrator: 0,
tags: JSON.stringify(["compiler", "meta", "agent-factory", "automation"]),
metadata: JSON.stringify({
agentType: "agent_compiler",
@@ -174,41 +251,42 @@ Respond in the same language as the user's message.`,
},
];
const ALL_AGENTS = [ORCHESTRATOR, ...SPECIALIZED_AGENTS];
async function seed() {
const conn = await mysql.createConnection(parseDbUrl(DATABASE_URL));
console.log("Connected to DB");
try {
// Check if agents already seeded
// Check existing seeded agents
const [existing] = await conn.execute(
"SELECT id, name FROM agents WHERE JSON_CONTAINS(metadata, '\"seeded\"', '$.seeded') OR name IN (?, ?, ?)",
["Browser Agent", "Tool Builder", "Agent Compiler"]
"SELECT id, name FROM agents WHERE name IN (?, ?, ?, ?)",
["Orchestrator", "Browser Agent", "Tool Builder", "Agent Compiler"]
);
if (existing.length > 0) {
console.log("Agents already seeded:");
existing.forEach((a) => console.log(` - [${a.id}] ${a.name}`));
console.log("Skipping seed. Use --force to re-seed.");
if (!process.argv.includes("--force")) {
console.log("Skipping seed. Use --force to re-seed.");
await conn.end();
return;
}
// Delete existing seeded agents
const ids = existing.map((a) => a.id);
await conn.execute(`DELETE FROM agents WHERE id IN (${ids.join(",")})`);
console.log("Deleted existing seeded agents");
}
// Insert agents
for (const agent of AGENTS) {
// Insert all agents
for (const agent of ALL_AGENTS) {
const [result] = await conn.execute(
`INSERT INTO agents
(userId, name, description, role, model, provider, temperature, maxTokens, topP,
frequencyPenalty, presencePenalty, systemPrompt, allowedTools, allowedDomains,
maxRequestsPerHour, isActive, isPublic, tags, metadata, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
maxRequestsPerHour, isActive, isPublic, isSystem, isOrchestrator, tags, metadata, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
[
1, // SYSTEM_USER_ID
1,
agent.name,
agent.description,
agent.role,
@@ -225,21 +303,25 @@ async function seed() {
agent.maxRequestsPerHour,
agent.isActive,
agent.isPublic,
agent.isSystem,
agent.isOrchestrator,
agent.tags,
agent.metadata,
]
);
console.log(`✓ Created agent: ${agent.name} (id: ${result.insertId})`);
const badge = agent.isOrchestrator ? " [ORCHESTRATOR]" : agent.isSystem ? " [SYSTEM]" : "";
console.log(`✓ Created agent: ${agent.name}${badge} (id: ${result.insertId})`);
}
// Verify
const [agents] = await conn.execute(
"SELECT id, name, role, model, isActive FROM agents ORDER BY id"
"SELECT id, name, role, model, isSystem, isOrchestrator FROM agents ORDER BY id"
);
console.log("\nAll agents in DB:");
agents.forEach((a) =>
console.log(` [${a.id}] ${a.name} | role: ${a.role} | model: ${a.model} | active: ${a.isActive}`)
);
agents.forEach((a) => {
const flags = [a.isOrchestrator ? "ORCH" : "", a.isSystem ? "SYS" : ""].filter(Boolean).join(",");
console.log(` [${a.id}] ${a.name} | role: ${a.role} | model: ${a.model}${flags ? ` | ${flags}` : ""}`);
});
} finally {
await conn.end();
}

View File

@@ -443,13 +443,66 @@ Response style:
You are running on a Linux server with Node.js, Docker, and full internet access.`;
/**
* Load orchestrator config from DB.
* Returns { model, systemPrompt, allowedTools } from the agent with isOrchestrator=true.
* Falls back to defaults if not found.
*/
export async function getOrchestratorConfig(): Promise<{
id: number | null;
name: string;
model: string;
systemPrompt: string;
allowedTools: string[];
temperature: number;
maxTokens: number;
}> {
try {
const db = await getDb();
if (!db) throw new Error("DB not available");
const [orch] = await db
.select()
.from(agents)
.where(eq(agents.isOrchestrator, true))
.limit(1);
if (orch) {
return {
id: orch.id,
name: orch.name,
model: orch.model,
systemPrompt: orch.systemPrompt ?? ORCHESTRATOR_SYSTEM_PROMPT,
allowedTools: (orch.allowedTools as string[]) ?? [],
temperature: parseFloat(orch.temperature ?? "0.5"),
maxTokens: orch.maxTokens ?? 8192,
};
}
} catch (err) {
console.error("[Orchestrator] Failed to load config from DB:", err);
}
// Fallback defaults
return {
id: null,
name: "Orchestrator",
model: "qwen2.5:7b",
systemPrompt: ORCHESTRATOR_SYSTEM_PROMPT,
allowedTools: [],
temperature: 0.5,
maxTokens: 8192,
};
}
export async function orchestratorChat(
messages: OrchestratorMessage[],
model: string = "qwen2.5:7b",
model?: string,
maxToolIterations: number = 10
): Promise<OrchestratorResult> {
const toolCalls: ToolCallStep[] = [];
// Load config from DB — model and systemPrompt are configurable
const config = await getOrchestratorConfig();
const activeModel = model ?? config.model;
const activeSystemPrompt = config.systemPrompt;
// Build conversation with system prompt
const conversation: Array<{
role: "system" | "user" | "assistant" | "tool" | "function";
@@ -457,14 +510,14 @@ export async function orchestratorChat(
tool_call_id?: string;
name?: string;
}> = [
{ role: "system", content: ORCHESTRATOR_SYSTEM_PROMPT },
{ role: "system", content: activeSystemPrompt },
...messages.map((m) => ({ role: m.role, content: m.content })),
];
let iterations = 0;
let finalResponse = "";
let lastUsage: any;
let lastModel: string = model;
let lastModel: string = activeModel;
while (iterations < maxToolIterations) {
iterations++;
@@ -480,7 +533,7 @@ export async function orchestratorChat(
} catch (err: any) {
// Fallback: try without tools if LLM doesn't support them
try {
const fallbackResult = await chatCompletion(model, conversation as any, {
const fallbackResult = await chatCompletion(activeModel, conversation as any, {
temperature: 0.7,
max_tokens: 4096,
});

View File

@@ -466,6 +466,12 @@ export const appRouter = router({
* Orchestrator — main AI agent with tool-use loop
*/
orchestrator: router({
// Get orchestrator config from DB (model, systemPrompt, allowedTools)
getConfig: publicProcedure.query(async () => {
const { getOrchestratorConfig } = await import("./orchestrator");
return getOrchestratorConfig();
}),
chat: publicProcedure
.input(
z.object({
@@ -475,7 +481,7 @@ export const appRouter = router({
content: z.string(),
})
),
model: z.string().optional(),
model: z.string().optional(), // override model (optional, uses DB config by default)
maxIterations: z.number().min(1).max(20).optional(),
})
)
@@ -483,7 +489,7 @@ export const appRouter = router({
const { orchestratorChat } = await import("./orchestrator");
return orchestratorChat(
input.messages,
input.model ?? "qwen2.5:7b",
input.model, // undefined = use DB config model
input.maxIterations ?? 10
);
}),

10
todo.md
View File

@@ -105,3 +105,13 @@
- [x] Add /skills to sidebar navigation
- [x] Update /agents to show seeded agents with Chat button
- [ ] Write tests for orchestrator
## Phase 7: Orchestrator as Configurable System Agent
- [ ] Add isSystem + isOrchestrator fields to agents table (DB migration)
- [ ] Seed Orchestrator as system agent in DB (role=orchestrator, isSystem=true)
- [ ] Update orchestrator.ts to load model/systemPrompt/allowedTools from DB
- [ ] Update /chat to read orchestrator config from DB, show active model in header
- [ ] Update /agents to show Orchestrator with SYSTEM badge, Configure button, no delete
- [ ] AgentDetailModal: orchestrator gets extra tab with system tools (shell, docker, agents mgmt)
- [ ] Add system tools to orchestrator: docker_ps, docker_restart, manage_agents, read_logs
- [ ] /chat header: show current model name + link to Configure Orchestrator