Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } | |