Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-dev"
version = "0.0.63"
version = "0.0.64"
description = "UiPath Developer Console"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
93 changes: 46 additions & 47 deletions src/uipath/dev/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from uipath.dev.models.eval_data import EvalItemResult, EvalRunState
from uipath.dev.models.execution import ExecutionRun
from uipath.dev.server.debug_bridge import WebDebugBridge
from uipath.dev.services.agent_service import AgentService
from uipath.dev.services.agent import AgentService
from uipath.dev.services.eval_service import EvalService
from uipath.dev.services.run_service import RunService
from uipath.dev.services.skill_service import SkillService
Expand Down Expand Up @@ -102,13 +102,7 @@ def __init__(

self.agent_service = AgentService(
skill_service=self.skill_service,
on_status=self._on_agent_status,
on_text=self._on_agent_text,
on_plan=self._on_agent_plan,
on_tool_use=self._on_agent_tool_use,
on_tool_result=self._on_agent_tool_result,
on_tool_approval=self._on_agent_tool_approval,
on_error=self._on_agent_error,
on_event=self._on_agent_event,
)

def create_app(self) -> Any:
Expand Down Expand Up @@ -291,47 +285,52 @@ def _on_eval_run_completed(self, run: EvalRunState) -> None:
"""Broadcast eval run completed to all connected clients."""
self.connection_manager.broadcast_eval_run_completed(run)

def _on_agent_status(self, session_id: str, status: str) -> None:
"""Broadcast agent status to all connected clients."""
self.connection_manager.broadcast_agent_status(session_id, status)

def _on_agent_text(self, session_id: str, content: str, done: bool) -> None:
"""Broadcast agent text to all connected clients."""
self.connection_manager.broadcast_agent_text(session_id, content, done)

def _on_agent_plan(self, session_id: str, items: list[dict[str, str]]) -> None:
"""Broadcast agent plan to all connected clients."""
self.connection_manager.broadcast_agent_plan(session_id, items)

def _on_agent_tool_use(
self, session_id: str, tool: str, args: dict[str, Any]
) -> None:
"""Broadcast agent tool use to all connected clients."""
self.connection_manager.broadcast_agent_tool_use(session_id, tool, args)

def _on_agent_tool_result(
self, session_id: str, tool: str, result: str, is_error: bool
) -> None:
"""Broadcast agent tool result to all connected clients."""
self.connection_manager.broadcast_agent_tool_result(
session_id, tool, result, is_error
)

def _on_agent_tool_approval(
self,
session_id: str,
tool_call_id: str,
tool: str,
args: dict[str, Any],
) -> None:
"""Broadcast agent tool approval request to all connected clients."""
self.connection_manager.broadcast_agent_tool_approval(
session_id, tool_call_id, tool, args
def _on_agent_event(self, event: Any) -> None:
"""Route agent events to the appropriate broadcast method."""
from uipath.dev.services.agent import (
ErrorOccurred,
PlanUpdated,
StatusChanged,
TextDelta,
TextGenerated,
ThinkingGenerated,
TokenUsageUpdated,
ToolApprovalRequired,
ToolCompleted,
ToolStarted,
)

def _on_agent_error(self, session_id: str, message: str) -> None:
"""Broadcast agent error to all connected clients."""
self.connection_manager.broadcast_agent_error(session_id, message)
cm = self.connection_manager
match event:
case StatusChanged(session_id=sid, status=status):
cm.broadcast_agent_status(sid, status)
case TextGenerated(session_id=sid, content=content, done=done):
cm.broadcast_agent_text(sid, content, done)
case TextDelta(session_id=sid, delta=delta):
cm.broadcast_agent_text_delta(sid, delta)
case ThinkingGenerated(session_id=sid, content=content):
cm.broadcast_agent_thinking(sid, content)
case PlanUpdated(session_id=sid, items=items):
cm.broadcast_agent_plan(sid, items)
case ToolStarted(session_id=sid, tool=tool, args=args):
cm.broadcast_agent_tool_use(sid, tool, args)
case ToolCompleted(
session_id=sid, tool=tool, result=result, is_error=is_error
):
cm.broadcast_agent_tool_result(sid, tool, result, is_error)
case ToolApprovalRequired(
session_id=sid, tool_call_id=tcid, tool=tool, args=args
):
cm.broadcast_agent_tool_approval(sid, tcid, tool, args)
case ErrorOccurred(session_id=sid, message=message):
cm.broadcast_agent_error(sid, message)
case TokenUsageUpdated(
session_id=sid,
prompt_tokens=pt,
completion_tokens=ct,
total_session_tokens=total,
):
cm.broadcast_agent_token_usage(sid, pt, ct, total)

@staticmethod
def _find_free_port(host: str, start_port: int, max_attempts: int = 100) -> int:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ export default function AgentChatSidebar() {
});

const isBusy = status === "thinking" || status === "executing" || status === "planning";
const lastMsg = messages[messages.length - 1];
const isStreaming = isBusy && lastMsg?.role === "assistant" && !lastMsg.done;
const showBusyIndicator = isBusy && !isStreaming;

const textareaRef = useRef<HTMLTextAreaElement>(null);

Expand Down Expand Up @@ -191,7 +194,7 @@ export default function AgentChatSidebar() {
{messages.map((msg) => (
<AgentMessageComponent key={msg.id} message={msg} />
))}
{isBusy && (
{showBusyIndicator && (
<div className="py-1.5">
<div className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full animate-pulse" style={{ background: "var(--success)" }} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,39 @@ const ROLE_CONFIG: Record<string, { label: string; color: string }> = {
assistant: { label: "AI", color: "var(--success)" },
tool: { label: "Tool", color: "var(--warning)" },
plan: { label: "Plan", color: "var(--accent)" },
thinking: { label: "Reasoning", color: "var(--text-muted)" },
};

function ThinkingCard({ message }: Props) {
const [expanded, setExpanded] = useState(false);
return (
<div className="py-1.5">
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-1.5 mb-0.5 cursor-pointer"
style={{ background: "none", border: "none", padding: 0 }}
>
<div className="w-2 h-2 rounded-full" style={{ background: "var(--text-muted)", opacity: 0.5 }} />
<span className="text-[11px] font-semibold" style={{ color: "var(--text-muted)" }}>Reasoning</span>
<svg
width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" strokeWidth="2"
style={{ transform: expanded ? "rotate(180deg)" : "rotate(0deg)", transition: "transform 0.15s", marginLeft: 2 }}
>
<path d="M6 9l6 6 6-6" />
</svg>
</button>
{expanded && (
<div
className="text-[12px] leading-relaxed pl-2.5 max-w-prose whitespace-pre-wrap"
style={{ color: "var(--text-muted)", fontStyle: "italic" }}
>
{message.content}
</div>
)}
</div>
);
}

function PlanCard({ message }: Props) {
const items = message.planItems ?? [];
return (
Expand Down Expand Up @@ -234,6 +265,7 @@ function ToolCard({ message }: Props) {
}

export default function AgentMessageComponent({ message }: Props) {
if (message.role === "thinking") return <ThinkingCard message={message} />;
if (message.role === "plan") return <PlanCard message={message} />;
if (message.role === "tool") return <ToolCard message={message} />;

Expand Down
19 changes: 19 additions & 0 deletions src/uipath/dev/server/frontend/src/store/useAgentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface AgentStore {
addToolResult: (tool: string, result: string, isError: boolean) => void;
addToolApprovalRequest: (toolCallId: string, tool: string, args: Record<string, unknown>) => void;
resolveToolApproval: (toolCallId: string, approved: boolean) => void;
appendThinking: (content: string) => void;
addError: (message: string) => void;
setSessionId: (id: string) => void;
setModels: (models: AgentModel[]) => void;
Expand Down Expand Up @@ -188,6 +189,24 @@ export const useAgentStore = create<AgentStore>((set) => ({
return { messages: msgs };
}),

appendThinking: (content) =>
set((state) => {
const msgs = [...state.messages];
const last = msgs[msgs.length - 1];
if (last && last.role === "thinking") {
// Append to existing thinking message
msgs[msgs.length - 1] = { ...last, content: last.content + content };
} else {
msgs.push({
id: nextId(),
role: "thinking",
content,
timestamp: Date.now(),
});
}
return { messages: msgs };
}),

addError: (message) =>
set((state) => ({
status: "error" as AgentStatus,
Expand Down
16 changes: 16 additions & 0 deletions src/uipath/dev/server/frontend/src/store/useWebSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,22 @@ export function useWebSocket() {
agent.addToolApprovalRequest(tool_call_id, tool, args);
break;
}
case "agent.thinking": {
const { content } = msg.payload as { session_id: string; content: string };
useAgentStore.getState().appendThinking(content);
break;
}
case "agent.text_delta": {
const { session_id: deltaSid, delta } = msg.payload as { session_id: string; delta: string };
const agentDelta = useAgentStore.getState();
if (!agentDelta.sessionId) agentDelta.setSessionId(deltaSid);
agentDelta.appendAssistantText(delta, false);
break;
}
case "agent.token_usage": {
// Token usage received — could store for display if needed
break;
}
case "agent.error": {
const { message: errMsg } = msg.payload as { session_id: string; message: string };
useAgentStore.getState().addError(errMsg);
Expand Down
2 changes: 1 addition & 1 deletion src/uipath/dev/server/frontend/src/types/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface AgentToolCall {

export interface AgentMessage {
id: string;
role: "user" | "assistant" | "plan" | "tool";
role: "user" | "assistant" | "plan" | "tool" | "thinking";
content: string;
timestamp: number;
toolCall?: AgentToolCall;
Expand Down
5 changes: 4 additions & 1 deletion src/uipath/dev/server/frontend/src/types/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ export type ServerEventType =
| "agent.tool_use"
| "agent.tool_result"
| "agent.tool_approval"
| "agent.error";
| "agent.error"
| "agent.thinking"
| "agent.text_delta"
| "agent.token_usage";

export interface ServerMessage {
type: ServerEventType;
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/uipath/dev/server/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UiPath Developer Console</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<script type="module" crossorigin src="/assets/index-B2xfJE6O.js"></script>
<script type="module" crossorigin src="/assets/index-DYl0Xnov.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BN_uQvcy.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-reactflow-BP_V7ttx.js">
<link rel="stylesheet" crossorigin href="/assets/vendor-reactflow-B5DZHykP.css">
Expand Down
38 changes: 38 additions & 0 deletions src/uipath/dev/server/ws/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,44 @@ def broadcast_agent_error(self, session_id: str, message: str) -> None:
for ws in self._connections:
self._enqueue(ws, msg)

def broadcast_agent_thinking(self, session_id: str, content: str) -> None:
"""Broadcast agent thinking/reasoning to all connected clients."""
msg = server_message(
ServerEvent.AGENT_THINKING,
{"session_id": session_id, "content": content},
)
for ws in self._connections:
self._enqueue(ws, msg)

def broadcast_agent_text_delta(self, session_id: str, delta: str) -> None:
"""Broadcast a streaming text token to all connected clients."""
msg = server_message(
ServerEvent.AGENT_TEXT_DELTA,
{"session_id": session_id, "delta": delta},
)
for ws in self._connections:
self._enqueue(ws, msg)

def broadcast_agent_token_usage(
self,
session_id: str,
prompt_tokens: int,
completion_tokens: int,
total_session_tokens: int,
) -> None:
"""Broadcast token usage stats to all connected clients."""
msg = server_message(
ServerEvent.AGENT_TOKEN_USAGE,
{
"session_id": session_id,
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_session_tokens": total_session_tokens,
},
)
for ws in self._connections:
self._enqueue(ws, msg)

def _schedule_broadcast(self, run_id: str, message: dict[str, Any]) -> None:
"""Enqueue a message for all subscribers of a run."""
subscribers = self._subscriptions.get(run_id)
Expand Down
3 changes: 3 additions & 0 deletions src/uipath/dev/server/ws/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class ServerEvent(str, Enum):
AGENT_TOOL_RESULT = "agent.tool_result"
AGENT_TOOL_APPROVAL = "agent.tool_approval"
AGENT_ERROR = "agent.error"
AGENT_THINKING = "agent.thinking"
AGENT_TEXT_DELTA = "agent.text_delta"
AGENT_TOKEN_USAGE = "agent.token_usage"


class ClientCommand(str, Enum):
Expand Down
33 changes: 33 additions & 0 deletions src/uipath/dev/services/agent/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Agent harness — deep agent architecture with loop innovations."""

from uipath.dev.services.agent.events import (
AgentEvent,
ErrorOccurred,
PlanUpdated,
StatusChanged,
TextDelta,
TextGenerated,
ThinkingGenerated,
TokenUsageUpdated,
ToolApprovalRequired,
ToolCompleted,
ToolStarted,
)
from uipath.dev.services.agent.service import AgentService
from uipath.dev.services.agent.session import AgentSession

__all__ = [
"AgentEvent",
"AgentService",
"AgentSession",
"ErrorOccurred",
"PlanUpdated",
"StatusChanged",
"TextDelta",
"TextGenerated",
"ThinkingGenerated",
"TokenUsageUpdated",
"ToolApprovalRequired",
"ToolCompleted",
"ToolStarted",
]
Loading