Skip to content

Commit dbe2ada

Browse files
committed
feat: implement agent orchestration pipeline with database persistence for task execution steps
1 parent d34ae11 commit dbe2ada

3 files changed

Lines changed: 224 additions & 20 deletions

File tree

backend/agents/controller/agent_controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ async def run_pipeline(self) -> dict:
7777
retried_any = False
7878

7979
for i, step in enumerate(steps):
80-
step_id = str(step.get("id", ""))
80+
step_id = str(step.get("step_id") or step.get("id") or "")
8181
if not failed_steps or step_id in failed_steps:
8282
step_results[i] = await self._execute_step(step, step_index=i)
8383
retried_any = True

backend/agents/executor/executor_agent.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,8 +283,10 @@ async def run(self, message: AgentMessage) -> AgentMessage:
283283
primary_output = code_artifacts[-1] if code_artifacts else final_message
284284

285285
# Build step result
286+
# Note: Planner emits steps with key 'step_id'; fall back to 'id' for
287+
# older plan formats, and finally to 'unknown' if neither is present.
286288
step_result = {
287-
"step_id": step.get("id", "unknown"),
289+
"step_id": step.get("step_id") or step.get("id") or "unknown",
288290
"description": step_description,
289291
"status": "completed",
290292
"output": primary_output,

frontend/src/pages/TaskDetailPage.tsx

Lines changed: 220 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ import {
2121
History,
2222
Timer,
2323
ArrowLeft,
24+
Terminal,
25+
Send,
26+
Inbox,
27+
Code2,
28+
Eye,
29+
EyeOff,
30+
Zap,
2431
} from 'lucide-react';
2532
import AgentFlowChart from '../components/task/AgentFlowChart';
2633
import TaskTimeline from '../components/task/TaskTimeline';
@@ -57,6 +64,7 @@ export default function TaskDetailPage() {
5764
const queryClient = useQueryClient();
5865
const { data: task, isLoading: isQueryLoading, error } = useTaskDetail(id);
5966
const [expandedSteps, setExpandedSteps] = useState<Record<string, boolean>>({});
67+
const [rawJsonSteps, setRawJsonSteps] = useState<Record<string, boolean>>({});
6068
const [elapsedTime, setElapsedTime] = useState(0);
6169

6270
const cancelMutation = useMutation({
@@ -128,6 +136,35 @@ export default function TaskDetailPage() {
128136
const toggleStep = (stepId: string) =>
129137
setExpandedSteps(prev => ({ ...prev, [stepId]: !prev[stepId] }));
130138

139+
const toggleRawJson = (e: React.MouseEvent, stepId: string) => {
140+
e.stopPropagation();
141+
setRawJsonSteps(prev => ({ ...prev, [stepId]: !prev[stepId] }));
142+
};
143+
144+
/* ── LLM I/O helpers ────────────────────────────────────────────────── */
145+
type StepResult = {
146+
trace?: string[];
147+
output?: string;
148+
summary?: string;
149+
step_id?: string;
150+
description?: string;
151+
tokens_used?: { in?: number; out?: number };
152+
tool_inputs?: Array<{ tool: string; args: Record<string, unknown> }>;
153+
tool_calls_used?: string[];
154+
code_artifacts?: string[];
155+
};
156+
157+
const getStepResult = (step: typeof task.steps[0]): StepResult | null => {
158+
const payload = step.output_payload as Record<string, unknown> | undefined;
159+
if (!payload) return null;
160+
return (payload.step_result as StepResult) ?? (payload.plan as StepResult) ?? null;
161+
};
162+
163+
const getPromptText = (sr: StepResult): string => {
164+
if (sr.trace && sr.trace.length > 0 && sr.trace[0]) return sr.trace[0];
165+
return sr.description ?? '';
166+
};
167+
131168
const isRunning =
132169
task.status === TaskStatus.PROCESSING || task.status === TaskStatus.RETRYING;
133170

@@ -434,16 +471,14 @@ export default function TaskDetailPage() {
434471
}
435472
</div>
436473

437-
{isExpanded && (
438-
<div className="px-5 pb-5 pl-[60px]">
439-
<div
440-
className="rounded-xl p-4 overflow-hidden"
441-
style={{ background: '#1b2026', border: '1px solid rgba(255,255,255,0.08)' }}
442-
>
443-
<div className="flex items-center justify-between mb-2">
444-
<p className="text-[10px] uppercase font-bold" style={{ color: '#848e9c' }}>
445-
Step Payload
446-
</p>
474+
{isExpanded && (() => {
475+
const sr = getStepResult(step);
476+
const showRaw = rawJsonSteps[step.id];
477+
return (
478+
<div className="px-5 pb-5 pl-[60px] space-y-3">
479+
480+
{/* ── Header bar: model pill + raw toggle ── */}
481+
<div className="flex items-center gap-2">
447482
{step.model_used && (
448483
<code
449484
className="text-[10px] font-mono px-2 py-0.5 rounded"
@@ -452,16 +487,120 @@ export default function TaskDetailPage() {
452487
{step.model_used}
453488
</code>
454489
)}
490+
{sr && (
491+
<>
492+
{sr.tokens_used && (
493+
<span className="text-[10px] font-mono px-2 py-0.5 rounded flex items-center gap-1"
494+
style={{ background: '#1e232a', color: '#848e9c' }}>
495+
<Zap className="w-2.5 h-2.5" />
496+
{sr.tokens_used.in ?? 0}{sr.tokens_used.out ?? 0}
497+
</span>
498+
)}
499+
<button
500+
onClick={e => toggleRawJson(e, step.id)}
501+
className="ml-auto flex items-center gap-1 text-[10px] font-semibold px-2 py-0.5 rounded transition-colors"
502+
style={{ background: '#1e232a', color: showRaw ? '#7ee787' : '#848e9c', border: '1px solid rgba(255,255,255,0.08)' }}
503+
>
504+
{showRaw ? <EyeOff className="w-3 h-3" /> : <Eye className="w-3 h-3" />}
505+
{showRaw ? 'Inspector' : 'Raw JSON'}
506+
</button>
507+
</>
508+
)}
455509
</div>
456-
<pre
457-
className="text-xs font-mono whitespace-pre-wrap max-h-60 overflow-y-auto"
458-
style={{ color: '#eaecef' }}
459-
>
460-
{JSON.stringify(step.output_payload, null, 2)}
461-
</pre>
510+
511+
{showRaw || !sr ? (
512+
/* ── Raw JSON fallback ── */
513+
<div className="rounded-xl p-4" style={{ background: '#1b2026', border: '1px solid rgba(255,255,255,0.08)' }}>
514+
<p className="text-[10px] uppercase font-bold mb-2" style={{ color: '#848e9c' }}>Raw Payload</p>
515+
<pre className="text-xs font-mono whitespace-pre-wrap max-h-72 overflow-y-auto" style={{ color: '#eaecef' }}>
516+
{JSON.stringify(step.output_payload, null, 2)}
517+
</pre>
518+
</div>
519+
) : (
520+
/* ── Structured LLM I/O Inspector ── */
521+
<div className="space-y-3">
522+
523+
{/* Prompt sent */}
524+
{getPromptText(sr) && (
525+
<div className="rounded-xl overflow-hidden" style={{ border: '1px solid rgba(159,232,112,0.2)' }}>
526+
<div className="flex items-center gap-2 px-3 py-2" style={{ background: '#0d1a0f' }}>
527+
<Send className="w-3 h-3" style={{ color: '#7ee787' }} />
528+
<span className="text-[10px] uppercase font-bold" style={{ color: '#7ee787' }}>Prompt Sent</span>
529+
</div>
530+
<pre className="text-xs font-mono whitespace-pre-wrap px-3 py-3 max-h-48 overflow-y-auto"
531+
style={{ background: '#0f1f12', color: '#c8e6c9' }}>
532+
{getPromptText(sr)}
533+
</pre>
534+
</div>
535+
)}
536+
537+
{/* Tool calls */}
538+
{sr.tool_inputs && sr.tool_inputs.length > 0 && (
539+
<div className="rounded-xl overflow-hidden" style={{ border: '1px solid rgba(255,200,60,0.2)' }}>
540+
<div className="flex items-center gap-2 px-3 py-2" style={{ background: '#1a1500' }}>
541+
<Code2 className="w-3 h-3" style={{ color: '#ffd11a' }} />
542+
<span className="text-[10px] uppercase font-bold" style={{ color: '#ffd11a' }}>Tool Calls ({sr.tool_inputs.length})</span>
543+
</div>
544+
<div className="divide-y" style={{ background: '#12100a', borderColor: 'rgba(255,200,60,0.1)' }}>
545+
{sr.tool_inputs.map((tc, ti) => (
546+
<div key={ti} className="px-3 py-3">
547+
<div className="flex items-center gap-2 mb-2">
548+
<span className="text-[10px] font-bold px-2 py-0.5 rounded"
549+
style={{ background: '#2a1f00', color: '#ffd11a' }}>
550+
{tc.tool}
551+
</span>
552+
<span className="text-[10px]" style={{ color: '#848e9c' }}>Call #{ti + 1}</span>
553+
</div>
554+
<pre className="text-xs font-mono whitespace-pre-wrap max-h-40 overflow-y-auto"
555+
style={{ color: '#e6d080' }}>
556+
{JSON.stringify(tc.args, null, 2)}
557+
</pre>
558+
</div>
559+
))}
560+
</div>
561+
</div>
562+
)}
563+
564+
{/* Raw output / LLM response */}
565+
{sr.output && (
566+
<div className="rounded-xl overflow-hidden" style={{ border: '1px solid rgba(130,140,160,0.2)' }}>
567+
<div className="flex items-center gap-2 px-3 py-2" style={{ background: '#13151a' }}>
568+
<Inbox className="w-3 h-3" style={{ color: '#848e9c' }} />
569+
<span className="text-[10px] uppercase font-bold" style={{ color: '#848e9c' }}>Raw Output</span>
570+
</div>
571+
<pre className="text-xs font-mono whitespace-pre-wrap px-3 py-3 max-h-60 overflow-y-auto"
572+
style={{ background: '#0e1014', color: '#b0b8c4' }}>
573+
{sr.output}
574+
</pre>
575+
</div>
576+
)}
577+
578+
{/* Trace messages if multiple (tool round-trips) */}
579+
{sr.trace && sr.trace.filter(Boolean).length > 2 && (
580+
<details className="group">
581+
<summary className="cursor-pointer flex items-center gap-2 text-[10px] uppercase font-bold py-1 select-none"
582+
style={{ color: '#848e9c' }}>
583+
<Terminal className="w-3 h-3" />
584+
Full Trace ({sr.trace.filter(Boolean).length} messages)
585+
</summary>
586+
<div className="mt-2 rounded-xl overflow-hidden" style={{ border: '1px solid rgba(255,255,255,0.06)' }}>
587+
{sr.trace.filter(Boolean).map((msg, mi) => (
588+
<div key={mi} className="px-3 py-2" style={{ background: mi % 2 === 0 ? '#0e1014' : '#111318', borderTop: mi > 0 ? '1px solid rgba(255,255,255,0.06)' : 'none' }}>
589+
<span className="text-[9px] font-bold uppercase mb-1 block" style={{ color: mi === 0 ? '#7ee787' : '#848e9c' }}>
590+
{mi === 0 ? 'prompt' : `msg ${mi}`}
591+
</span>
592+
<pre className="text-[11px] font-mono whitespace-pre-wrap max-h-32 overflow-y-auto" style={{ color: '#b0b8c4' }}>{msg}</pre>
593+
</div>
594+
))}
595+
</div>
596+
</details>
597+
)}
598+
599+
</div>
600+
)}
462601
</div>
463-
</div>
464-
)}
602+
);
603+
})()}
465604
</div>
466605
);
467606
})
@@ -569,6 +708,69 @@ export default function TaskDetailPage() {
569708
))}
570709
</div>
571710
</section>
711+
712+
{/* LLM Call Summary card */}
713+
{task.steps && task.steps.length > 0 && (() => {
714+
const execSteps = task.steps.filter(s => s.step_type === StepType.EXECUTE || s.step_type === StepType.PLAN || s.step_type === StepType.ANALYZE);
715+
if (execSteps.length === 0) return null;
716+
const totalIn = execSteps.reduce((s, st) => s + (st.tokens_in ?? 0), 0);
717+
const totalOut = execSteps.reduce((s, st) => s + (st.tokens_out ?? 0), 0);
718+
return (
719+
<section className="wise-card-dark-surface">
720+
<h3 className="font-semibold text-sm mb-4 flex items-center gap-2" style={{ color: '#eaecef' }}>
721+
<div className="w-6 h-6 rounded-lg flex items-center justify-center" style={{ background: '#1a1500' }}>
722+
<Zap className="w-3.5 h-3.5" style={{ color: '#ffd11a' }} />
723+
</div>
724+
LLM Call Summary
725+
</h3>
726+
727+
{/* Totals */}
728+
<div className="grid grid-cols-2 gap-2 mb-4">
729+
{[
730+
{ label: 'Total ↑ Tokens', val: totalIn.toLocaleString(), color: '#7ee787' },
731+
{ label: 'Total ↓ Tokens', val: totalOut.toLocaleString(), color: '#ffd11a' },
732+
].map(m => (
733+
<div key={m.label} className="p-2.5 rounded-xl text-center" style={{ background: '#1e232a' }}>
734+
<p className="text-[9px] uppercase font-bold mb-1" style={{ color: '#848e9c' }}>{m.label}</p>
735+
<p className="text-sm font-mono font-bold" style={{ color: m.color }}>{m.val}</p>
736+
</div>
737+
))}
738+
</div>
739+
740+
{/* Per-step rows */}
741+
<div className="space-y-2">
742+
{execSteps.map((st, i) => {
743+
const sr = getStepResult(st);
744+
const toolsUsed = sr?.tool_calls_used ?? [];
745+
const stepLabel = st.step_type === StepType.PLAN ? 'Plan' :
746+
st.step_type === StepType.ANALYZE ? 'Analyze' :
747+
`Step ${i}`;
748+
return (
749+
<div key={st.id} className="rounded-xl p-2.5" style={{ background: '#1b2026', border: '1px solid rgba(255,255,255,0.06)' }}>
750+
<div className="flex items-center justify-between mb-1.5">
751+
<span className="text-[11px] font-semibold" style={{ color: '#eaecef' }}>{stepLabel}</span>
752+
<span className="text-[10px] font-mono" style={{ color: '#848e9c' }}>
753+
{(st.tokens_in ?? 0)}{(st.tokens_out ?? 0)}
754+
</span>
755+
</div>
756+
<p className="text-[10px] truncate mb-1.5" style={{ color: '#848e9c' }}>{st.agent_name}</p>
757+
{toolsUsed.length > 0 && (
758+
<div className="flex flex-wrap gap-1">
759+
{toolsUsed.map(t => (
760+
<span key={t} className="text-[9px] font-mono px-1.5 py-0.5 rounded"
761+
style={{ background: '#2a1f00', color: '#ffd11a' }}>
762+
{t}
763+
</span>
764+
))}
765+
</div>
766+
)}
767+
</div>
768+
);
769+
})}
770+
</div>
771+
</section>
772+
);
773+
})()}
572774
</div>
573775
</div>
574776

0 commit comments

Comments
 (0)