feat(06): Dev Center — Fábrica de Agentes (Design → Assemble → Deploy + Galeria)

- Tabela arcadia_agent_defs (schema + migration)
- API CRUD /api/agent-defs com endpoints assemble/deploy/redraft/run/fork
- 4 novos tabs no DevCenter: Design (3 modos editor), Montar (polling Blackboard), Deploy (OrchestrateCenter), Galeria (grid + run/fork)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jonas Pacheco 2026-03-27 14:30:07 -03:00
parent b55295f9dd
commit ee74d014ee
6 changed files with 929 additions and 3 deletions

View File

@ -1,6 +1,6 @@
# State # State
## Current Phase: 5 — Automation Fabric ## Current Phase: 6 — Dev Center Completo
## Completed ## Completed
- Phase 1: submodules, tabelas, Neo4j, ReferenceParser - Phase 1: submodules, tabelas, Neo4j, ReferenceParser
@ -8,9 +8,10 @@
- Phase 3: miroflow_service.py, bridge TS + KG logging, MiroFlowControl.tsx + tab Científico - Phase 3: miroflow_service.py, bridge TS + KG logging, MiroFlowControl.tsx + tab Científico
- Phase 4: PatternDetector (cron 1h), OpenClaw routes (suggestions/patterns/accept/reject), tab Sugestões em /skills + badge contador - Phase 4: PatternDetector (cron 1h), OpenClaw routes (suggestions/patterns/accept/reject), tab Sugestões em /skills + badge contador
- Phase 5: 5 runtimes (WorkflowEngine/RuleEngine/AgentExecutor/ScheduleEngine/EventEngine), AutomationFabricService, /api/automation-fabric, AutomationCenter.tsx em /automations-center - Phase 5: 5 runtimes (WorkflowEngine/RuleEngine/AgentExecutor/ScheduleEngine/EventEngine), AutomationFabricService, /api/automation-fabric, AutomationCenter.tsx em /automations-center
- Phase 6: arcadia_agent_defs (schema+migration+API CRUD+assemble/deploy/run/fork), 4 tabs no DevCenter (Design/Montar/Deploy/Galeria)
## In Progress ## In Progress
- (nenhum — Phase 5 concluída, iniciar Phase 6) - (nenhum — Phase 6 concluída, iniciar Phase 7)
## Roadmap Evolution ## Roadmap Evolution
- Phase 7 added: Skill Fabric Expandido (compiladores, sandbox, 3 editor modes, validation pipeline) - Phase 7 added: Skill Fabric Expandido (compiladores, sandbox, 3 editor modes, validation pipeline)

View File

@ -61,13 +61,615 @@ import {
CheckSquare, CheckSquare,
Square, Square,
Fingerprint, Fingerprint,
PanelLeft PanelLeft,
Save
} from "lucide-react"; } from "lucide-react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { apiRequest } from "@/lib/queryClient"; import { apiRequest } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import DevHistory from "@/components/DevHistory"; import DevHistory from "@/components/DevHistory";
// ============================================================================
// PHASE 6: Agent Factory Tabs (Design, Assemble, Deploy, Galeria)
// ============================================================================
type AgentDef = {
id: number;
name: string;
description: string | null;
spec: { mode: "markdown" | "typescript" | "visual"; content: string } | null;
status: "draft" | "assembling" | "ready" | "deployed";
version: number;
lastTaskId: number | null;
tenantId: string | null;
userId: string | null;
createdAt: string;
updatedAt: string;
};
function AgentFactoryTabs() {
const { toast } = useToast();
const queryClient = useQueryClient();
// ---- Design tab state ----
const [selectedDefId, setSelectedDefId] = useState<number | null>(null);
const [editorMode, setEditorMode] = useState<"markdown" | "typescript" | "visual">("markdown");
const [editorContent, setEditorContent] = useState("");
const [defName, setDefName] = useState("");
const [defDescription, setDefDescription] = useState("");
const [isCreatingNew, setIsCreatingNew] = useState(false);
// ---- Assemble tab state ----
const [assemblePollingId, setAssemblePollingId] = useState<number | null>(null);
const [assembleTaskStatus, setAssembleTaskStatus] = useState<Record<number, string>>({});
// ---- Galeria filter ----
const [galFilter, setGalFilter] = useState("");
const { data: defsData, isLoading: defsLoading } = useQuery<{ success: boolean; data: AgentDef[] }>({
queryKey: ["/api/agent-defs"],
refetchInterval: assemblePollingId ? 3000 : false,
});
const defs = defsData?.data || [];
// Poll task status for assembling defs
useEffect(() => {
const assembling = defs.filter(d => d.status === "assembling" && d.lastTaskId);
if (assembling.length === 0) return;
const interval = setInterval(async () => {
for (const def of assembling) {
if (!def.lastTaskId) continue;
try {
const res = await fetch(`/api/blackboard/task/${def.lastTaskId}`, { credentials: "include" });
const data = await res.json();
const taskStatus = data?.task?.status;
if (taskStatus === "completed" || taskStatus === "failed") {
// Mark agent as ready or draft
await fetch(`/api/agent-defs/${def.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ status: taskStatus === "completed" ? "ready" : "draft" }),
});
queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] });
}
setAssembleTaskStatus(prev => ({ ...prev, [def.id]: taskStatus || "unknown" }));
} catch { /* ignore */ }
}
}, 3000);
return () => clearInterval(interval);
}, [defs, queryClient]);
const createMutation = useMutation({
mutationFn: async () => {
const res = await apiRequest("POST", "/api/agent-defs", {
name: defName,
description: defDescription,
spec: { mode: editorMode, content: editorContent },
});
return res.json();
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] });
setSelectedDefId(data.data.id);
setIsCreatingNew(false);
toast({ title: "Agente criado", description: data.data.name });
},
onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }),
});
const saveMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest("PATCH", `/api/agent-defs/${id}`, {
name: defName,
description: defDescription,
spec: { mode: editorMode, content: editorContent },
});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] });
toast({ title: "Salvo" });
},
onError: (e: any) => toast({ title: "Erro ao salvar", description: e.message, variant: "destructive" }),
});
const assembleMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest("POST", `/api/agent-defs/${id}/assemble`, {});
return res.json();
},
onSuccess: (data, id) => {
queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] });
setAssemblePollingId(id);
toast({ title: "Montagem iniciada", description: `Task #${data.taskId}` });
},
onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }),
});
const deployMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest("POST", `/api/agent-defs/${id}/deploy`, {});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] });
toast({ title: "Deploy realizado!" });
},
onError: (e: any) => toast({ title: "Erro no deploy", description: e.message, variant: "destructive" }),
});
const redraftMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest("POST", `/api/agent-defs/${id}/redraft`, {});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] });
toast({ title: "Volta para rascunho" });
},
onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }),
});
const runMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest("POST", `/api/agent-defs/${id}/run`, {});
return res.json();
},
onSuccess: (data) => {
toast({ title: "Execução iniciada", description: `Task #${data.taskId}` });
},
onError: (e: any) => toast({ title: "Erro ao executar", description: e.message, variant: "destructive" }),
});
const forkMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest("POST", `/api/agent-defs/${id}/fork`, {});
return res.json();
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] });
toast({ title: "Fork criado", description: data.data.name });
},
onError: (e: any) => toast({ title: "Erro ao fazer fork", description: e.message, variant: "destructive" }),
});
const deleteMutation = useMutation({
mutationFn: async (id: number) => {
const res = await apiRequest("DELETE", `/api/agent-defs/${id}`, undefined);
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/agent-defs"] });
setSelectedDefId(null);
toast({ title: "Excluído" });
},
onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }),
});
// Quando seleciona um def existente, carrega os campos
const selectDef = (def: AgentDef) => {
setSelectedDefId(def.id);
setDefName(def.name);
setDefDescription(def.description || "");
const spec = def.spec || { mode: "markdown", content: "" };
setEditorMode(spec.mode || "markdown");
setEditorContent(spec.content || "");
setIsCreatingNew(false);
};
const startNew = () => {
setSelectedDefId(null);
setDefName("");
setDefDescription("");
setEditorMode("markdown");
setEditorContent("");
setIsCreatingNew(true);
};
const statusColor: Record<string, string> = {
draft: "bg-zinc-500",
assembling: "bg-blue-500",
ready: "bg-green-500",
deployed: "bg-purple-500",
};
const statusLabel: Record<string, string> = {
draft: "Rascunho",
assembling: "Montando...",
ready: "Pronto",
deployed: "Implantado",
};
const galDefs = defs.filter(d =>
d.status === "deployed" &&
(d.name.toLowerCase().includes(galFilter.toLowerCase()) ||
(d.description || "").toLowerCase().includes(galFilter.toLowerCase()))
);
return (
<>
{/* ---- TAB: DESIGN ---- */}
<TabsContent value="design" className="space-y-4">
<div className="grid grid-cols-[260px_1fr] gap-4 h-[calc(100vh-240px)]">
{/* Lista de defs */}
<Card className="flex flex-col overflow-hidden">
<CardHeader className="border-b py-3 px-4">
<div className="flex items-center justify-between">
<CardTitle className="text-sm flex items-center gap-2">
<Layers className="w-4 h-4 text-purple-500" /> Agentes
</CardTitle>
<Button size="sm" variant="ghost" onClick={startNew} className="h-7 px-2 text-xs">
+ Novo
</Button>
</div>
</CardHeader>
<ScrollArea className="flex-1">
{defsLoading ? (
<div className="flex items-center justify-center h-24">
<Loader2 className="w-4 h-4 animate-spin text-zinc-400" />
</div>
) : defs.length === 0 ? (
<p className="text-xs text-zinc-500 text-center py-8">Nenhum agente ainda.<br />Clique em "+ Novo" para criar.</p>
) : (
<div className="divide-y">
{defs.map(def => (
<button
key={def.id}
onClick={() => selectDef(def)}
className={`w-full text-left px-4 py-3 hover:bg-zinc-50 transition-colors ${selectedDefId === def.id ? "bg-purple-50 border-l-2 border-purple-500" : ""}`}
>
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium truncate">{def.name}</span>
<span className={`text-[10px] px-1.5 py-0.5 rounded text-white shrink-0 ${statusColor[def.status] || "bg-zinc-400"}`}>
{statusLabel[def.status] || def.status}
</span>
</div>
{def.description && (
<p className="text-xs text-zinc-500 truncate mt-0.5">{def.description}</p>
)}
</button>
))}
</div>
)}
</ScrollArea>
</Card>
{/* Editor */}
<Card className="flex flex-col overflow-hidden">
{(selectedDefId || isCreatingNew) ? (
<>
<CardHeader className="border-b py-3 px-4">
<div className="flex items-center justify-between gap-4">
<div className="flex-1 space-y-1">
<Input
value={defName}
onChange={e => setDefName(e.target.value)}
placeholder="Nome do agente"
className="h-7 text-sm font-medium"
/>
<Input
value={defDescription}
onChange={e => setDefDescription(e.target.value)}
placeholder="Descrição (opcional)"
className="h-6 text-xs text-zinc-500"
/>
</div>
<div className="flex gap-1">
{(["markdown", "typescript", "visual"] as const).map(m => (
<Button
key={m}
size="sm"
variant={editorMode === m ? "default" : "outline"}
onClick={() => setEditorMode(m)}
className="h-7 px-2 text-xs capitalize"
>
{m === "markdown" ? "Markdown" : m === "typescript" ? "TypeScript" : "Visual"}
</Button>
))}
</div>
<div className="flex gap-1">
{isCreatingNew ? (
<Button
size="sm"
onClick={() => createMutation.mutate()}
disabled={!defName.trim() || createMutation.isPending}
className="h-7 px-3 text-xs"
>
{createMutation.isPending ? <Loader2 className="w-3 h-3 animate-spin" /> : <Save className="w-3 h-3 mr-1" />}
Criar
</Button>
) : (
<>
<Button
size="sm"
variant="outline"
onClick={() => saveMutation.mutate(selectedDefId!)}
disabled={saveMutation.isPending}
className="h-7 px-3 text-xs"
>
{saveMutation.isPending ? <Loader2 className="w-3 h-3 animate-spin" /> : <Save className="w-3 h-3 mr-1" />}
Salvar
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => deleteMutation.mutate(selectedDefId!)}
disabled={deleteMutation.isPending}
className="h-7 px-2 text-xs text-red-500 hover:text-red-600"
>
<X className="w-3 h-3" />
</Button>
</>
)}
</div>
</div>
</CardHeader>
<div className="flex-1 overflow-hidden">
{editorMode === "visual" ? (
<ScrollArea className="h-full p-4">
<p className="text-xs text-zinc-500 mb-3">Defina os nós do agente em JSON (name, type, connections):</p>
<Textarea
value={editorContent}
onChange={e => setEditorContent(e.target.value)}
placeholder={`[\n { "name": "Coletar dados", "type": "skill", "skill": "/skill/consulta_vendas" },\n { "name": "Analisar", "type": "skill", "skill": "/skill:system/analise_tendencia" },\n { "name": "Notificar", "type": "action", "tool": "/tool/whatsapp_send" }\n]`}
className="min-h-[400px] font-mono text-xs resize-none border-zinc-200"
/>
</ScrollArea>
) : (
<Textarea
value={editorContent}
onChange={e => setEditorContent(e.target.value)}
placeholder={
editorMode === "markdown"
? "# Nome do Agente\n\n## Objetivo\nDescreva o que este agente faz...\n\n## Skills utilizadas\n- /skill/consulta_vendas\n- /skill:system/analise_tendencia\n\n## Fluxo\n1. Coletar dados de vendas\n2. Analisar tendências\n3. Notificar gestor se necessário\n\n## Triggers\n- Schedule: toda segunda às 9h\n- Manual: via Dev Center"
: "// Especificação TypeScript do agente\nimport { BaseAgent } from '@arcadia/agent-sdk';\n\nexport class MeuAgente extends BaseAgent {\n // ...\n}"
}
className="h-full w-full resize-none border-0 rounded-none font-mono text-xs focus-visible:ring-0"
/>
)}
</div>
</>
) : (
<div className="flex-1 flex flex-col items-center justify-center text-zinc-400 gap-3">
<Layers className="w-12 h-12 opacity-30" />
<p className="text-sm">Selecione um agente ou crie um novo</p>
<Button size="sm" onClick={startNew} className="text-xs">
+ Novo Agente
</Button>
</div>
)}
</Card>
</div>
</TabsContent>
{/* ---- TAB: ASSEMBLE ---- */}
<TabsContent value="assemble" className="space-y-4">
<Card>
<CardHeader className="border-b py-4 px-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-full">
<Cpu className="w-5 h-5 text-blue-600" />
</div>
<div>
<CardTitle className="text-base">Linha de Montagem</CardTitle>
<CardDescription className="text-xs">Agentes em rascunho prontos para montagem via Blackboard</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
{defsLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-5 h-5 animate-spin text-zinc-400" />
</div>
) : defs.filter(d => d.status === "draft" || d.status === "assembling").length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-zinc-400 gap-2">
<CheckCircle className="w-10 h-10 opacity-30" />
<p className="text-sm">Nenhum agente em rascunho.</p>
<p className="text-xs">Crie agentes na aba Design e monte-os aqui.</p>
</div>
) : (
<div className="divide-y">
{defs.filter(d => d.status === "draft" || d.status === "assembling").map(def => {
const taskStatus = assembleTaskStatus[def.id];
return (
<div key={def.id} className="flex items-center gap-4 px-6 py-4 hover:bg-zinc-50">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{def.name}</span>
<Badge className={`text-[10px] px-1.5 py-0.5 text-white ${statusColor[def.status]}`}>
{statusLabel[def.status]}
</Badge>
{def.status === "assembling" && (
<Loader2 className="w-3 h-3 animate-spin text-blue-500" />
)}
</div>
{def.description && <p className="text-xs text-zinc-500 truncate mt-0.5">{def.description}</p>}
{def.lastTaskId && (
<p className="text-xs text-zinc-400 mt-0.5">
Task #{def.lastTaskId}
{taskStatus && `${taskStatus}`}
</p>
)}
</div>
<div className="flex gap-2 shrink-0">
{def.status === "draft" && (
<Button
size="sm"
onClick={() => assembleMutation.mutate(def.id)}
disabled={assembleMutation.isPending}
className="text-xs h-8"
>
{assembleMutation.isPending ? (
<Loader2 className="w-3 h-3 animate-spin mr-1" />
) : (
<Zap className="w-3 h-3 mr-1" />
)}
Montar
</Button>
)}
{def.status === "assembling" && (
<span className="text-xs text-blue-500 flex items-center gap-1">
<Loader2 className="w-3 h-3 animate-spin" /> Processando...
</span>
)}
</div>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* ---- TAB: DEPLOY ---- */}
<TabsContent value="deploy" className="space-y-4">
<Card>
<CardHeader className="border-b py-4 px-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-green-100 rounded-full">
<Rocket className="w-5 h-5 text-green-600" />
</div>
<div>
<CardTitle className="text-base">Orchestrate Center</CardTitle>
<CardDescription className="text-xs">Agentes prontos para deploy e implantados</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
{defsLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-5 h-5 animate-spin text-zinc-400" />
</div>
) : defs.filter(d => d.status === "ready" || d.status === "deployed").length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-zinc-400 gap-2">
<Rocket className="w-10 h-10 opacity-30" />
<p className="text-sm">Nenhum agente pronto para deploy.</p>
<p className="text-xs">Monte um agente na aba Montar para -lo aqui.</p>
</div>
) : (
<div className="divide-y">
{defs.filter(d => d.status === "ready" || d.status === "deployed").map(def => (
<div key={def.id} className="flex items-center gap-4 px-6 py-4 hover:bg-zinc-50">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{def.name}</span>
<Badge className={`text-[10px] px-1.5 py-0.5 text-white ${statusColor[def.status]}`}>
{statusLabel[def.status]}
</Badge>
<span className="text-[10px] text-zinc-400">v{def.version}</span>
</div>
{def.description && <p className="text-xs text-zinc-500 truncate mt-0.5">{def.description}</p>}
{def.lastTaskId && (
<p className="text-xs text-zinc-400 mt-0.5">Último artefato: Task #{def.lastTaskId}</p>
)}
</div>
<div className="flex gap-2 shrink-0">
{def.status === "ready" && (
<Button
size="sm"
onClick={() => deployMutation.mutate(def.id)}
disabled={deployMutation.isPending}
className="text-xs h-8 bg-purple-600 hover:bg-purple-700"
>
<Rocket className="w-3 h-3 mr-1" /> Deploy
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => redraftMutation.mutate(def.id)}
disabled={redraftMutation.isPending}
className="text-xs h-8"
>
<RotateCcw className="w-3 h-3 mr-1" /> Re-montar
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* ---- TAB: GALERIA ---- */}
<TabsContent value="galeria" className="space-y-4">
<div className="flex items-center justify-between">
<div className="relative flex-1 max-w-sm">
<Input
value={galFilter}
onChange={e => setGalFilter(e.target.value)}
placeholder="Buscar agentes..."
className="h-9 text-sm pl-8"
/>
<FolderSearch className="w-4 h-4 absolute left-2.5 top-2.5 text-zinc-400" />
</div>
<p className="text-xs text-zinc-500">{galDefs.length} agente{galDefs.length !== 1 ? "s" : ""} implantado{galDefs.length !== 1 ? "s" : ""}</p>
</div>
{defsLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-5 h-5 animate-spin text-zinc-400" />
</div>
) : galDefs.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-16 text-zinc-400 gap-3">
<Brain className="w-12 h-12 opacity-30" />
<p className="text-sm font-medium">Nenhum agente implantado</p>
<p className="text-xs">Crie um agente na aba Design e faça o deploy para -lo aqui.</p>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{galDefs.map(def => (
<Card key={def.id} className="flex flex-col hover:shadow-md transition-shadow">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<CardTitle className="text-sm truncate">{def.name}</CardTitle>
{def.description && (
<CardDescription className="text-xs mt-1 line-clamp-2">{def.description}</CardDescription>
)}
</div>
<Badge className="text-[10px] px-1.5 py-0.5 text-white bg-purple-500 shrink-0">
v{def.version}
</Badge>
</div>
</CardHeader>
<CardContent className="pt-0 pb-4 mt-auto flex gap-2">
<Button
size="sm"
onClick={() => runMutation.mutate(def.id)}
disabled={runMutation.isPending}
className="flex-1 text-xs h-8"
>
<Play className="w-3 h-3 mr-1" /> Executar
</Button>
<Button
size="sm"
variant="outline"
onClick={() => forkMutation.mutate(def.id)}
disabled={forkMutation.isPending}
className="text-xs h-8 px-3"
title="Fork"
>
<GitBranch className="w-3 h-3" />
</Button>
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
</>
);
}
// ============================================================================
const FILE_TO_ROUTE_MAP: Record<string, string> = { const FILE_TO_ROUTE_MAP: Record<string, string> = {
"Cockpit": "/", "Cockpit": "/",
@ -1860,6 +2462,18 @@ export default function DevCenter() {
<TabsTrigger value="history" data-testid="tab-history"> <TabsTrigger value="history" data-testid="tab-history">
<History className="w-4 h-4 mr-2" /> Histórico <History className="w-4 h-4 mr-2" /> Histórico
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="design" data-testid="tab-design">
<Layers className="w-4 h-4 mr-2" /> Design
</TabsTrigger>
<TabsTrigger value="assemble" data-testid="tab-assemble">
<Cpu className="w-4 h-4 mr-2" /> Montar
</TabsTrigger>
<TabsTrigger value="deploy" data-testid="tab-deploy">
<Rocket className="w-4 h-4 mr-2" /> Deploy
</TabsTrigger>
<TabsTrigger value="galeria" data-testid="tab-galeria">
<Brain className="w-4 h-4 mr-2" /> Galeria
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="develop" className="space-y-0"> <TabsContent value="develop" className="space-y-0">
@ -2351,6 +2965,12 @@ export default function DevCenter() {
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>
{/* ================================================================
PHASE 6: FÁBRICA DE AGENTES
================================================================ */}
<AgentFactoryTabs />
</Tabs> </Tabs>
{showCommitDialog && ( {showCommitDialog && (

View File

@ -0,0 +1,28 @@
-- Phase 6: Agent Definitions (Dev Center — Fábrica de Agentes)
-- Creates arcadia_agent_defs table for Design → Assemble → Deploy workflow
CREATE TABLE IF NOT EXISTS arcadia_agent_defs (
id SERIAL PRIMARY KEY,
tenant_id VARCHAR,
user_id VARCHAR REFERENCES users(id),
name VARCHAR(200) NOT NULL,
description TEXT,
-- spec: { mode: "markdown" | "typescript" | "visual", content: string }
spec JSONB NOT NULL DEFAULT '{}',
-- draft | assembling | ready | deployed
status VARCHAR(30) NOT NULL DEFAULT 'draft',
version INTEGER NOT NULL DEFAULT 1,
-- FK para blackboard_tasks (nullable)
last_task_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_agent_defs_tenant ON arcadia_agent_defs(tenant_id);
CREATE INDEX IF NOT EXISTS idx_agent_defs_status ON arcadia_agent_defs(status);
CREATE INDEX IF NOT EXISTS idx_agent_defs_user ON arcadia_agent_defs(user_id);

242
server/agent-defs/routes.ts Normal file
View File

@ -0,0 +1,242 @@
/**
* Agent Definitions Routes Phase 6
* Dev Center: Fábrica de Agentes (Design Assemble Deploy)
*/
import { Router, Request, Response } from "express";
import { db } from "../../db/index";
import { arcadiaAgentDefs, insertAgentDefSchema } from "@shared/schema";
import { eq, desc, and } from "drizzle-orm";
import { blackboardService } from "../blackboard/service";
const router = Router();
// GET /api/agent-defs — listar todos (filtro por status opcional)
router.get("/", async (req: Request, res: Response) => {
try {
const { status } = req.query;
const tenantId = (req.user as any)?.tenantId?.toString() || null;
let query = db.select().from(arcadiaAgentDefs).orderBy(desc(arcadiaAgentDefs.updatedAt));
const defs = await query;
const filtered = defs.filter(d => {
const tenantMatch = !tenantId || !d.tenantId || d.tenantId === tenantId;
const statusMatch = !status || d.status === status;
return tenantMatch && statusMatch;
});
res.json({ success: true, data: filtered });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
// GET /api/agent-defs/:id
router.get("/:id", async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const [def] = await db.select().from(arcadiaAgentDefs).where(eq(arcadiaAgentDefs.id, id));
if (!def) return res.status(404).json({ success: false, error: "Não encontrado" });
res.json({ success: true, data: def });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
// POST /api/agent-defs — criar
router.post("/", async (req: Request, res: Response) => {
try {
const userId = (req.user as any)?.id || "anonymous";
const tenantId = (req.user as any)?.tenantId?.toString() || null;
const parsed = insertAgentDefSchema.safeParse({ ...req.body, userId, tenantId });
if (!parsed.success) {
return res.status(400).json({ success: false, error: parsed.error.message });
}
const [created] = await db.insert(arcadiaAgentDefs).values(parsed.data).returning();
res.json({ success: true, data: created });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
// PATCH /api/agent-defs/:id — atualizar
router.patch("/:id", async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const { name, description, spec } = req.body;
const updates: any = { updatedAt: new Date() };
if (name !== undefined) updates.name = name;
if (description !== undefined) updates.description = description;
if (spec !== undefined) updates.spec = spec;
const [updated] = await db
.update(arcadiaAgentDefs)
.set(updates)
.where(eq(arcadiaAgentDefs.id, id))
.returning();
if (!updated) return res.status(404).json({ success: false, error: "Não encontrado" });
res.json({ success: true, data: updated });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
// DELETE /api/agent-defs/:id
router.delete("/:id", async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
await db.delete(arcadiaAgentDefs).where(eq(arcadiaAgentDefs.id, id));
res.json({ success: true });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
// POST /api/agent-defs/:id/assemble — montar via Blackboard
router.post("/:id/assemble", async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const userId = (req.user as any)?.id || "anonymous";
const [def] = await db.select().from(arcadiaAgentDefs).where(eq(arcadiaAgentDefs.id, id));
if (!def) return res.status(404).json({ success: false, error: "Não encontrado" });
const spec = def.spec as any;
const specContent = spec?.content || JSON.stringify(spec);
const task = await blackboardService.createMainTask(
`Montar agente: ${def.name}`,
specContent,
userId,
{ agentDefId: id, spec, source: "agent-factory" }
);
await blackboardService.createSubtask(
task.id,
"Projetar agente",
`Analisar spec e criar estrutura do agente: ${def.name}\n\n${specContent}`,
"architect",
[],
{ phase: "design", agentDefId: id }
);
const [updated] = await db
.update(arcadiaAgentDefs)
.set({ status: "assembling", lastTaskId: task.id, updatedAt: new Date() })
.where(eq(arcadiaAgentDefs.id, id))
.returning();
res.json({ success: true, data: updated, taskId: task.id });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
// POST /api/agent-defs/:id/deploy — marcar como deployed
router.post("/:id/deploy", async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const [def] = await db.select().from(arcadiaAgentDefs).where(eq(arcadiaAgentDefs.id, id));
if (!def) return res.status(404).json({ success: false, error: "Não encontrado" });
if (def.status !== "ready") {
return res.status(400).json({ success: false, error: "Agente precisa estar 'ready' para deploy" });
}
const [updated] = await db
.update(arcadiaAgentDefs)
.set({ status: "deployed", version: (def.version || 1) + 1, updatedAt: new Date() })
.where(eq(arcadiaAgentDefs.id, id))
.returning();
res.json({ success: true, data: updated });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
// POST /api/agent-defs/:id/redraft — volta para draft para remontar
router.post("/:id/redraft", async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const [updated] = await db
.update(arcadiaAgentDefs)
.set({ status: "draft", updatedAt: new Date() })
.where(eq(arcadiaAgentDefs.id, id))
.returning();
if (!updated) return res.status(404).json({ success: false, error: "Não encontrado" });
res.json({ success: true, data: updated });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
// POST /api/agent-defs/:id/run — executar agente via Blackboard
router.post("/:id/run", async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const userId = (req.user as any)?.id || "anonymous";
const [def] = await db.select().from(arcadiaAgentDefs).where(eq(arcadiaAgentDefs.id, id));
if (!def) return res.status(404).json({ success: false, error: "Não encontrado" });
const spec = def.spec as any;
const specContent = spec?.content || JSON.stringify(spec);
const task = await blackboardService.createMainTask(
`Executar agente: ${def.name}`,
specContent,
userId,
{ agentDefId: id, spec, source: "agent-run" }
);
await blackboardService.createSubtask(
task.id,
"Executar agente",
`Executar o agente conforme spec:\n\n${specContent}`,
"executor",
[],
{ phase: "execution", agentDefId: id }
);
res.json({ success: true, taskId: task.id });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
// POST /api/agent-defs/:id/fork — duplicar como novo draft
router.post("/:id/fork", async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id);
const userId = (req.user as any)?.id || "anonymous";
const tenantId = (req.user as any)?.tenantId?.toString() || null;
const [def] = await db.select().from(arcadiaAgentDefs).where(eq(arcadiaAgentDefs.id, id));
if (!def) return res.status(404).json({ success: false, error: "Não encontrado" });
const [forked] = await db.insert(arcadiaAgentDefs).values({
name: `${def.name} (cópia)`,
description: def.description,
spec: def.spec as any,
status: "draft",
version: 1,
tenantId,
userId,
}).returning();
res.json({ success: true, data: forked });
} catch (error: any) {
res.status(500).json({ success: false, error: error.message });
}
});
export default router;

View File

@ -68,6 +68,7 @@ import { startXosScheduler } from "./xos/scheduler";
import { registerOpenClawRoutes } from "./openclaw/routes"; import { registerOpenClawRoutes } from "./openclaw/routes";
import { startPatternDetector } from "./openclaw/pattern-detector"; import { startPatternDetector } from "./openclaw/pattern-detector";
import { registerAutomationFabricRoutes } from "./automation-fabric/routes"; import { registerAutomationFabricRoutes } from "./automation-fabric/routes";
import agentDefsRoutes from "./agent-defs/routes";
export async function registerRoutes( export async function registerRoutes(
httpServer: Server, httpServer: Server,
@ -170,6 +171,9 @@ export async function registerRoutes(
// Automation Fabric — runtime unificado (Phase 5) // Automation Fabric — runtime unificado (Phase 5)
registerAutomationFabricRoutes(app); registerAutomationFabricRoutes(app);
// Agent Defs — fábrica de agentes (Phase 6)
app.use("/api/agent-defs", agentDefsRoutes);
// Central de Protocolos (MCP, A2A, AP2, UCP) // Central de Protocolos (MCP, A2A, AP2, UCP)
app.use("/api", protocolsRoutes); app.use("/api", protocolsRoutes);

View File

@ -7589,3 +7589,34 @@ export type DetectedPattern = typeof detectedPatterns.$inferSelect;
export type InsertDetectedPattern = z.infer<typeof insertDetectedPatternSchema>; export type InsertDetectedPattern = z.infer<typeof insertDetectedPatternSchema>;
export type SkillSuggestionRecord = typeof skillSuggestions.$inferSelect; export type SkillSuggestionRecord = typeof skillSuggestions.$inferSelect;
export type InsertSkillSuggestion = z.infer<typeof insertSkillSuggestionSchema>; export type InsertSkillSuggestion = z.infer<typeof insertSkillSuggestionSchema>;
// ============================================================================
// PHASE 6: Agent Definitions (Dev Center — Fábrica de Agentes)
// ============================================================================
export const arcadiaAgentDefs = pgTable("arcadia_agent_defs", {
id: serial("id").primaryKey(),
tenantId: varchar("tenant_id"),
userId: varchar("user_id").references(() => users.id),
name: varchar("name", { length: 200 }).notNull(),
description: text("description"),
// spec: { mode: "markdown" | "typescript" | "visual", content: string }
spec: jsonb("spec").notNull().default({}),
// draft | assembling | ready | deployed
status: varchar("status", { length: 30 }).notNull().default("draft"),
version: integer("version").notNull().default(1),
// FK para blackboard_tasks (nullable — só após montar)
lastTaskId: integer("last_task_id"),
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});
export const insertAgentDefSchema = createInsertSchema(arcadiaAgentDefs).omit({ id: true, createdAt: true, updatedAt: true });
export type AgentDef = typeof arcadiaAgentDefs.$inferSelect;
export type InsertAgentDef = z.infer<typeof insertAgentDefSchema>;