Skip to content

Commit f3719dd

Browse files
authored
Merge pull request #91 from UiPath/fix/update_agent_harness
fix: improve agent
2 parents 5f6c432 + e39db74 commit f3719dd

33 files changed

Lines changed: 1479 additions & 360 deletions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-dev"
3-
version = "0.0.64"
3+
version = "0.0.65"
44
description = "UiPath Developer Console"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/dev/server/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ def _on_agent_event(self, event: Any) -> None:
298298
ToolApprovalRequired,
299299
ToolCompleted,
300300
ToolStarted,
301+
UserQuestionAsked,
301302
)
302303

303304
cm = self.connection_manager
@@ -322,6 +323,10 @@ def _on_agent_event(self, event: Any) -> None:
322323
session_id=sid, tool_call_id=tcid, tool=tool, args=args
323324
):
324325
cm.broadcast_agent_tool_approval(sid, tcid, tool, args)
326+
case UserQuestionAsked(
327+
session_id=sid, question_id=qid, question=q, options=opts
328+
):
329+
cm.broadcast_agent_question(sid, qid, q, opts)
325330
case ErrorOccurred(session_id=sid, message=message):
326331
cm.broadcast_agent_error(sid, message)
327332
case TokenUsageUpdated(

src/uipath/dev/server/frontend/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ export default function App() {
315315
const onMove = (ev: MouseEvent | TouchEvent) => {
316316
const clientX = "touches" in ev ? ev.touches[0].clientX : ev.clientX;
317317
// Dragging left increases width (panel is on the right)
318-
const newW = Math.max(280, Math.min(500, startW - (clientX - startX)));
318+
const newW = Math.max(280, Math.min(700, startW - (clientX - startX)));
319319
setAgentWidth(newW);
320320
};
321321

src/uipath/dev/server/frontend/src/api/agent-client.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AgentModel, AgentSkill } from "../types/agent";
1+
import type { AgentModel, AgentSessionState, AgentSkill } from "../types/agent";
22

33
const BASE = "/api";
44

@@ -11,6 +11,21 @@ export async function listAgentModels(): Promise<AgentModel[]> {
1111
return res.json();
1212
}
1313

14+
export async function getAgentSessionDiagnostics(sessionId: string): Promise<Record<string, unknown>> {
15+
const res = await fetch(`${BASE}/agent/session/${sessionId}/diagnostics`);
16+
if (!res.ok) {
17+
throw new Error(`HTTP ${res.status}`);
18+
}
19+
return res.json();
20+
}
21+
22+
export async function getAgentSessionState(sessionId: string): Promise<AgentSessionState | null> {
23+
const res = await fetch(`${BASE}/agent/session/${sessionId}/state`);
24+
if (res.status === 404) return null;
25+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
26+
return res.json();
27+
}
28+
1429
export async function listAgentSkills(): Promise<AgentSkill[]> {
1530
const res = await fetch(`${BASE}/agent/skills`);
1631
if (!res.ok) {

src/uipath/dev/server/frontend/src/api/websocket.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,12 @@ export class WsClient {
144144
approved,
145145
});
146146
}
147+
148+
sendQuestionResponse(sessionId: string, questionId: string, answer: string): void {
149+
this.send("agent.question_response", {
150+
session_id: sessionId,
151+
question_id: questionId,
152+
answer,
153+
});
154+
}
147155
}

src/uipath/dev/server/frontend/src/components/agent/AgentChatSidebar.tsx

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { useCallback, useEffect, useRef, useState } from "react";
22
import { useAgentStore } from "../../store/useAgentStore";
33
import { useAuthStore } from "../../store/useAuthStore";
4-
import { listAgentModels, listAgentSkills } from "../../api/agent-client";
4+
import { getAgentSessionState, listAgentModels, listAgentSkills } from "../../api/agent-client";
55
import { getWs } from "../../store/useWebSocket";
66
import AgentMessageComponent from "./AgentMessage";
7-
import type { AgentSkill } from "../../types/agent";
7+
import QuestionCard from "./QuestionCard";
8+
import type { AgentPlanItem, AgentSkill } from "../../types/agent";
89

910
export default function AgentChatSidebar() {
1011
const ws = useRef(getWs()).current;
@@ -20,6 +21,7 @@ export default function AgentChatSidebar() {
2021
sessionId,
2122
status,
2223
messages,
24+
plan,
2325
models,
2426
selectedModel,
2527
modelsLoading,
@@ -34,6 +36,7 @@ export default function AgentChatSidebar() {
3436
toggleSkill,
3537
setSkillsLoading,
3638
addUserMessage,
39+
hydrateSession,
3740
clearSession,
3841
} = useAgentStore();
3942

@@ -67,6 +70,24 @@ export default function AgentChatSidebar() {
6770
.finally(() => setSkillsLoading(false));
6871
}, [skills.length, setSkills, setSelectedSkillIds, setSkillsLoading]);
6972

73+
// Hydrate session from sessionStorage on mount
74+
useEffect(() => {
75+
if (sessionId) return; // Already have a session
76+
const storedId = sessionStorage.getItem("agent_session_id");
77+
if (!storedId) return;
78+
getAgentSessionState(storedId)
79+
.then((state) => {
80+
if (state) {
81+
hydrateSession(state);
82+
} else {
83+
sessionStorage.removeItem("agent_session_id");
84+
}
85+
})
86+
.catch(() => {
87+
sessionStorage.removeItem("agent_session_id");
88+
});
89+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
90+
7091
const [showScrollTop, setShowScrollTop] = useState(false);
7192

7293
const handleScroll = () => {
@@ -84,7 +105,7 @@ export default function AgentChatSidebar() {
84105
}
85106
});
86107

87-
const isBusy = status === "thinking" || status === "executing" || status === "planning";
108+
const isBusy = status === "thinking" || status === "executing" || status === "planning" || status === "awaiting_input";
88109
const lastMsg = messages[messages.length - 1];
89110
const isStreaming = isBusy && lastMsg?.role === "assistant" && !lastMsg.done;
90111
const showBusyIndicator = isBusy && !isStreaming;
@@ -173,6 +194,9 @@ export default function AgentChatSidebar() {
173194
isBusy={isBusy}
174195
/>
175196

197+
{/* Sticky Plan */}
198+
<StickyPlan plan={plan} />
199+
176200
{/* Messages */}
177201
<div className="relative flex-1 overflow-hidden">
178202
<div
@@ -191,15 +215,15 @@ export default function AgentChatSidebar() {
191215
</div>
192216
</div>
193217
)}
194-
{messages.map((msg) => (
218+
{messages.filter((msg) => msg.role !== "plan").map((msg) => (
195219
<AgentMessageComponent key={msg.id} message={msg} />
196220
))}
197221
{showBusyIndicator && (
198222
<div className="py-1.5">
199223
<div className="flex items-center gap-1.5">
200224
<div className="w-2 h-2 rounded-full animate-pulse" style={{ background: "var(--success)" }} />
201225
<span className="text-[11px] font-semibold" style={{ color: "var(--success)" }}>
202-
{status === "thinking" ? "Thinking..." : status === "executing" ? "Executing..." : "Planning..."}
226+
{status === "thinking" ? "Thinking..." : status === "executing" ? "Executing..." : status === "awaiting_input" ? "Waiting for answer..." : "Planning..."}
203227
</span>
204228
</div>
205229
</div>
@@ -219,6 +243,9 @@ export default function AgentChatSidebar() {
219243
)}
220244
</div>
221245

246+
{/* Question card */}
247+
<QuestionCard />
248+
222249
{/* Input */}
223250
<div
224251
className="flex items-end gap-2 px-3 py-2 border-t"
@@ -414,3 +441,88 @@ function Header({
414441
</div>
415442
);
416443
}
444+
445+
const MAX_VISIBLE_PLAN_ITEMS = 10;
446+
447+
function StickyPlan({ plan }: { plan: AgentPlanItem[] }) {
448+
const completed = plan.filter((t) => t.status === "completed").length;
449+
const uncompleted = plan.filter((t) => t.status !== "completed");
450+
const allDone = plan.length > 0 && completed === plan.length;
451+
const [collapsed, setCollapsed] = useState(false);
452+
const prevUncompletedCount = useRef(uncompleted.length);
453+
454+
// Auto-collapse when all tasks complete
455+
useEffect(() => {
456+
if (allDone) setCollapsed(true);
457+
}, [allDone]);
458+
459+
// Auto-expand when new uncompleted items appear
460+
useEffect(() => {
461+
if (uncompleted.length > prevUncompletedCount.current) {
462+
setCollapsed(false);
463+
}
464+
prevUncompletedCount.current = uncompleted.length;
465+
}, [uncompleted.length]);
466+
467+
if (plan.length === 0) return null;
468+
469+
// Show all uncompleted + fill remaining slots with most recent completed
470+
const completedItems = plan.filter((t) => t.status === "completed");
471+
const remainingSlots = Math.max(0, MAX_VISIBLE_PLAN_ITEMS - uncompleted.length);
472+
const recentCompleted = completedItems.slice(-remainingSlots);
473+
// Preserve original order: show recent completed first, then uncompleted
474+
const visibleItems = [...recentCompleted, ...uncompleted];
475+
const hiddenCount = plan.length - visibleItems.length;
476+
477+
return (
478+
<div
479+
className="shrink-0 border-b"
480+
style={{ borderColor: "var(--border)", background: "var(--bg-secondary)" }}
481+
>
482+
<button
483+
onClick={() => setCollapsed(!collapsed)}
484+
className="w-full flex items-center gap-2 px-3 py-1.5 cursor-pointer"
485+
style={{ background: "none", border: "none" }}
486+
>
487+
<div className="w-2 h-2 rounded-full" style={{ background: "var(--accent)" }} />
488+
<span className="text-[11px] font-semibold" style={{ color: "var(--accent)" }}>
489+
Plan ({completed}/{plan.length} completed)
490+
</span>
491+
<svg
492+
width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" strokeWidth="2"
493+
className="ml-auto"
494+
style={{ transform: collapsed ? "rotate(0deg)" : "rotate(180deg)", transition: "transform 0.15s" }}
495+
>
496+
<path d="M6 9l6 6 6-6" />
497+
</svg>
498+
</button>
499+
{!collapsed && (
500+
<div className="px-3 pb-2 space-y-1">
501+
{hiddenCount > 0 && (
502+
<div className="text-[11px]" style={{ color: "var(--text-muted)" }}>
503+
{hiddenCount} earlier completed task{hiddenCount !== 1 ? "s" : ""} hidden
504+
</div>
505+
)}
506+
{visibleItems.map((item, i) => (
507+
<div key={i} className="flex items-center gap-2 text-sm">
508+
{item.status === "completed" ? (
509+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--success)" strokeWidth="2.5" strokeLinecap="round"><path d="M20 6L9 17l-5-5" /></svg>
510+
) : item.status === "in_progress" ? (
511+
<span className="w-3.5 h-3.5 flex items-center justify-center">
512+
<span className="w-2 h-2 rounded-full animate-pulse" style={{ background: "var(--accent)" }} />
513+
</span>
514+
) : (
515+
<span className="w-3.5 h-3.5 flex items-center justify-center">
516+
<span className="w-2 h-2 rounded-full" style={{ background: "var(--text-muted)", opacity: 0.4 }} />
517+
</span>
518+
)}
519+
<span style={{ color: item.status === "completed" ? "var(--text-muted)" : "var(--text-primary)", textDecoration: item.status === "completed" ? "line-through" : "none" }}>
520+
{item.title}
521+
</span>
522+
</div>
523+
))}
524+
</div>
525+
)}
526+
</div>
527+
);
528+
}

0 commit comments

Comments
 (0)