gMAS / web_ui /frontend /src /components /config /RunnerConfigForm.tsx
Артём Боярских
fix: fixed some bugs
81f5c1c
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Plus, Trash2 } from "lucide-react";
import { useConfigStore } from "@/stores/configStore";
import type { EarlyStopCondition, EarlyStopType, TopologyHookConfig, TopologyHookType } from "@/types/execution";
export function RunnerConfigForm() {
const { runnerConfig, updateRunnerConfig } = useConfigStore();
// ── Early-stop helpers ──────────────────────────────────────────────
const addEarlyStop = () => {
updateRunnerConfig({
early_stop_conditions: [
...runnerConfig.early_stop_conditions,
{ type: "keyword", keyword: "FINAL ANSWER" },
],
});
};
const removeEarlyStop = (idx: number) => {
updateRunnerConfig({
early_stop_conditions: runnerConfig.early_stop_conditions.filter((_, i) => i !== idx),
});
};
const updateEarlyStop = (idx: number, patch: Partial<EarlyStopCondition>) => {
updateRunnerConfig({
early_stop_conditions: runnerConfig.early_stop_conditions.map((c, i) =>
i === idx ? { ...c, ...patch } : c
),
});
};
// ── Topology-hook helpers ───────────────────────────────────────────
const addTopologyHook = () => {
updateRunnerConfig({
topology_hooks: [
...runnerConfig.topology_hooks,
{ type: "stop_on_keyword", keyword: "TASK_COMPLETE" },
],
});
};
const removeTopologyHook = (idx: number) => {
updateRunnerConfig({
topology_hooks: runnerConfig.topology_hooks.filter((_, i) => i !== idx),
});
};
const updateTopologyHook = (idx: number, patch: Partial<TopologyHookConfig>) => {
updateRunnerConfig({
topology_hooks: runnerConfig.topology_hooks.map((h, i) =>
i === idx ? { ...h, ...patch } : h
),
});
};
const hasDynamicFeatures =
runnerConfig.enable_dynamic_topology ||
runnerConfig.early_stop_conditions.length > 0 ||
runnerConfig.topology_hooks.length > 0;
return (
<div className="space-y-6">
{/* ── General settings ─────────────────────────────────────── */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Runner Configuration</CardTitle>
<CardDescription>Configure the MACPRunner execution settings.</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>Timeout (seconds)</Label>
<Input
type="number"
value={runnerConfig.timeout}
onChange={(e) => updateRunnerConfig({ timeout: parseFloat(e.target.value) || 60 })}
/>
</div>
<div className="space-y-2">
<Label>Max Retries</Label>
<Input
type="number"
value={runnerConfig.max_retries}
onChange={(e) => updateRunnerConfig({ max_retries: parseInt(e.target.value) || 2 })}
/>
</div>
<div className="space-y-2">
<Label>Max Parallel Size</Label>
<Input
type="number"
value={runnerConfig.max_parallel_size}
onChange={(e) => updateRunnerConfig({ max_parallel_size: parseInt(e.target.value) || 5 })}
/>
</div>
<div className="space-y-2">
<Label>Max Tool Iterations</Label>
<Input
type="number"
value={runnerConfig.max_tool_iterations}
onChange={(e) => updateRunnerConfig({ max_tool_iterations: parseInt(e.target.value) || 3 })}
/>
</div>
<div className="space-y-2">
<Label>Memory Context Limit</Label>
<Input
type="number"
value={runnerConfig.memory_context_limit}
onChange={(e) => updateRunnerConfig({ memory_context_limit: parseInt(e.target.value) || 5 })}
/>
</div>
<div className="sm:col-span-2 flex items-start justify-between gap-4 rounded-md border p-3">
<div className="space-y-0.5">
<Label>Adaptive Mode</Label>
<p className="text-xs text-muted-foreground">Weight-aware scheduling that dynamically reorders agents based on edge weights and intermediate results.</p>
</div>
<Switch
className="shrink-0"
checked={runnerConfig.adaptive}
onCheckedChange={(v) => updateRunnerConfig({ adaptive: v })}
/>
</div>
<div className="sm:col-span-2 flex items-start justify-between gap-4 rounded-md border p-3">
<div className="space-y-0.5">
<Label>Enable Parallel</Label>
<p className="text-xs text-muted-foreground">Allow agents without dependencies to execute concurrently, improving throughput for fan-out topologies.</p>
</div>
<Switch
className="shrink-0"
checked={runnerConfig.enable_parallel}
onCheckedChange={(v) => updateRunnerConfig({ enable_parallel: v })}
/>
</div>
<div className="sm:col-span-2 flex items-start justify-between gap-4 rounded-md border p-3">
<div className="space-y-0.5">
<Label>Enable Memory</Label>
<p className="text-xs text-muted-foreground">Activate shared memory so agents can read and write persistent context across execution steps.</p>
</div>
<Switch
className="shrink-0"
checked={runnerConfig.enable_memory}
onCheckedChange={(v) => updateRunnerConfig({ enable_memory: v })}
/>
</div>
<div className="sm:col-span-2 flex items-start justify-between gap-4 rounded-md border p-3">
<div className="space-y-0.5">
<Label>Dynamic Topology</Label>
<p className="text-xs text-muted-foreground">Enable runtime graph modifications via early stopping conditions and topology hooks defined below.</p>
</div>
<Switch
className="shrink-0"
checked={hasDynamicFeatures}
onCheckedChange={(v) => updateRunnerConfig({ enable_dynamic_topology: v })}
/>
</div>
<div className="sm:col-span-2 flex items-start justify-between gap-4 rounded-md border p-3">
<div className="space-y-0.5">
<Label>Broadcast Task to All</Label>
<p className="text-xs text-muted-foreground">Send the task query to every agent in the graph, not just the start node. Useful when all agents need full context.</p>
</div>
<Switch
className="shrink-0"
checked={runnerConfig.broadcast_task_to_all}
onCheckedChange={(v) => updateRunnerConfig({ broadcast_task_to_all: v })}
/>
</div>
</CardContent>
</Card>
{/* ── Early Stopping ───────────────────────────────────────── */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Early Stopping</CardTitle>
<CardDescription>
Halt execution when specific conditions are met. Saves tokens by stopping early.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{runnerConfig.early_stop_conditions.map((cond, idx) => (
<div key={idx} className="flex items-end gap-2 rounded-md border p-3">
<div className="space-y-1 w-40">
<Label className="text-xs">Type</Label>
<Select
value={cond.type}
onValueChange={(v) => updateEarlyStop(idx, { type: v as EarlyStopType })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="keyword">Keyword</SelectItem>
<SelectItem value="token_limit">Token Limit</SelectItem>
<SelectItem value="agent_count">Agent Count</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex-1 space-y-1">
{cond.type === "keyword" && (
<>
<Label className="text-xs">Keyword</Label>
<Input
className="h-8 text-xs"
placeholder="FINAL ANSWER"
value={cond.keyword || ""}
onChange={(e) => updateEarlyStop(idx, { keyword: e.target.value })}
/>
</>
)}
{cond.type === "token_limit" && (
<>
<Label className="text-xs">Max Tokens</Label>
<Input
className="h-8 text-xs"
type="number"
placeholder="10000"
value={cond.max_tokens ?? ""}
onChange={(e) => updateEarlyStop(idx, { max_tokens: parseInt(e.target.value) || null })}
/>
</>
)}
{cond.type === "agent_count" && (
<>
<Label className="text-xs">Max Agents</Label>
<Input
className="h-8 text-xs"
type="number"
placeholder="5"
value={cond.max_agents ?? ""}
onChange={(e) => updateEarlyStop(idx, { max_agents: parseInt(e.target.value) || null })}
/>
</>
)}
</div>
<Button variant="ghost" size="icon" className="h-8 w-8 shrink-0" onClick={() => removeEarlyStop(idx)}>
<Trash2 className="h-3.5 w-3.5 text-muted-foreground" />
</Button>
</div>
))}
<Button variant="outline" size="sm" className="gap-1" onClick={addEarlyStop}>
<Plus className="h-3.5 w-3.5" />
Add Condition
</Button>
</CardContent>
</Card>
{/* ── Topology Hooks ───────────────────────────────────────── */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Topology Hooks</CardTitle>
<CardDescription>
Dynamically modify the graph during execution based on intermediate results.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{runnerConfig.topology_hooks.map((hook, idx) => (
<div key={idx} className="rounded-md border p-3 space-y-2">
<div className="flex items-end gap-2">
<div className="space-y-1 w-52">
<Label className="text-xs">Hook Type</Label>
<Select
value={hook.type}
onValueChange={(v) => {
const patch: Partial<TopologyHookConfig> = { type: v as TopologyHookType };
if (v === "stop_on_keyword") patch.keyword = "TASK_COMPLETE";
if (v === "skip_on_token_budget") patch.token_threshold = 5000;
if (v === "force_reviewer_on_error") patch.reviewer_agent_id = "";
if (v === "insert_chain_on_keyword") { patch.keyword = "NEEDS_REVIEW"; patch.source_agent = ""; patch.target_agent = ""; }
if (v === "add_edge_on_keyword") { patch.keyword = "ESCALATE"; patch.source_agent = ""; patch.target_agent = ""; patch.weight = 1.0; }
if (v === "redirect_end_on_keyword") { patch.keyword = "REDIRECT"; patch.target_agent = ""; }
if (v === "skip_agent_on_keyword") { patch.keyword = "SKIP"; patch.target_agent = ""; }
updateTopologyHook(idx, patch);
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="stop_on_keyword">Stop on keyword</SelectItem>
<SelectItem value="skip_on_token_budget">Skip on token budget</SelectItem>
<SelectItem value="force_reviewer_on_error">Force reviewer on error</SelectItem>
<SelectItem value="insert_chain_on_keyword">Insert chain on keyword</SelectItem>
<SelectItem value="add_edge_on_keyword">Add edge on keyword</SelectItem>
<SelectItem value="redirect_end_on_keyword">Redirect end on keyword</SelectItem>
<SelectItem value="skip_agent_on_keyword">Skip agent on keyword</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex-1" />
<Button variant="ghost" size="icon" className="h-8 w-8 shrink-0" onClick={() => removeTopologyHook(idx)}>
<Trash2 className="h-3.5 w-3.5 text-muted-foreground" />
</Button>
</div>
{/* Parameters row */}
<div className="grid gap-2 sm:grid-cols-2">
{/* Keyword field β€” used by most hook types */}
{["stop_on_keyword", "insert_chain_on_keyword", "add_edge_on_keyword", "redirect_end_on_keyword", "skip_agent_on_keyword"].includes(hook.type) && (
<div className="space-y-1">
<Label className="text-xs">Keyword</Label>
<Input
className="h-8 text-xs"
placeholder="TASK_COMPLETE"
value={hook.keyword || ""}
onChange={(e) => updateTopologyHook(idx, { keyword: e.target.value })}
/>
</div>
)}
{/* Token threshold */}
{hook.type === "skip_on_token_budget" && (
<div className="space-y-1">
<Label className="text-xs">Token Threshold</Label>
<Input
className="h-8 text-xs"
type="number"
placeholder="5000"
value={hook.token_threshold ?? ""}
onChange={(e) => updateTopologyHook(idx, { token_threshold: parseInt(e.target.value) || null })}
/>
</div>
)}
{/* Reviewer agent */}
{hook.type === "force_reviewer_on_error" && (
<div className="space-y-1">
<Label className="text-xs">Reviewer Agent ID</Label>
<Input
className="h-8 text-xs"
placeholder="reviewer"
value={hook.reviewer_agent_id || ""}
onChange={(e) => updateTopologyHook(idx, { reviewer_agent_id: e.target.value })}
/>
</div>
)}
{/* Source agent β€” for insert_chain and add_edge */}
{["insert_chain_on_keyword", "add_edge_on_keyword"].includes(hook.type) && (
<div className="space-y-1">
<Label className="text-xs">Source Agent ID</Label>
<Input
className="h-8 text-xs"
placeholder="agent_a"
value={hook.source_agent || ""}
onChange={(e) => updateTopologyHook(idx, { source_agent: e.target.value })}
/>
</div>
)}
{/* Target agent β€” for insert_chain, add_edge, redirect_end, skip_agent */}
{["insert_chain_on_keyword", "add_edge_on_keyword", "redirect_end_on_keyword", "skip_agent_on_keyword"].includes(hook.type) && (
<div className="space-y-1">
<Label className="text-xs">Target Agent ID</Label>
<Input
className="h-8 text-xs"
placeholder="agent_b"
value={hook.target_agent || ""}
onChange={(e) => updateTopologyHook(idx, { target_agent: e.target.value })}
/>
</div>
)}
{/* Weight β€” for add_edge */}
{hook.type === "add_edge_on_keyword" && (
<div className="space-y-1">
<Label className="text-xs">Edge Weight</Label>
<Input
className="h-8 text-xs"
type="number"
step="0.1"
min="0"
max="1"
placeholder="1.0"
value={hook.weight ?? 1.0}
onChange={(e) => updateTopologyHook(idx, { weight: parseFloat(e.target.value) || 1.0 })}
/>
</div>
)}
</div>
</div>
))}
<Button variant="outline" size="sm" className="gap-1" onClick={addTopologyHook}>
<Plus className="h-3.5 w-3.5" />
Add Hook
</Button>
</CardContent>
</Card>
</div>
);
}