Skip to content

Commit 46220d1

Browse files
cristipufuclaude
andcommitted
feat: add phase lifecycle to UiPathRuntimeStateEvent
Introduce UiPathRuntimeStatePhase enum (started, updated, completed) to track node lifecycle in state events. Defaults to "updated" for backward compatibility. Bump version to 0.8.6. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8ff70cc commit 46220d1

File tree

6 files changed

+123
-5
lines changed

6 files changed

+123
-5
lines changed

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-runtime"
3-
version = "0.8.5"
3+
version = "0.8.6"
44
description = "Runtime abstractions and interfaces for building agents and automation scripts in the UiPath ecosystem"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/runtime/events/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
All events inherit from UiPathRuntimeEvent and can be filtered by execution_id.
99
"""
1010

11-
from uipath.runtime.events.base import UiPathRuntimeEvent, UiPathRuntimeEventType
11+
from uipath.runtime.events.base import (
12+
UiPathRuntimeEvent,
13+
UiPathRuntimeEventType,
14+
UiPathRuntimeStatePhase,
15+
)
1216
from uipath.runtime.events.state import (
1317
UiPathRuntimeMessageEvent,
1418
UiPathRuntimeStateEvent,
@@ -18,6 +22,7 @@
1822
# Base
1923
"UiPathRuntimeEvent",
2024
"UiPathRuntimeEventType",
25+
"UiPathRuntimeStatePhase",
2126
# Runtime events
2227
"UiPathRuntimeStateEvent",
2328
"UiPathRuntimeMessageEvent",

src/uipath/runtime/events/base.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ class UiPathRuntimeEventType(str, Enum):
1515
RUNTIME_RESULT = "runtime_result"
1616

1717

18+
class UiPathRuntimeStatePhase(str, Enum):
19+
"""Lifecycle phase of a state event."""
20+
21+
STARTED = "started"
22+
UPDATED = "updated"
23+
COMPLETED = "completed"
24+
25+
1826
class UiPathRuntimeEvent(BaseModel):
1927
"""Base class for all UiPath runtime events."""
2028

@@ -29,4 +37,4 @@ class UiPathRuntimeEvent(BaseModel):
2937
)
3038

3139

32-
__all__ = ["UiPathRuntimeEventType", "UiPathRuntimeEvent"]
40+
__all__ = ["UiPathRuntimeEventType", "UiPathRuntimeStatePhase", "UiPathRuntimeEvent"]

src/uipath/runtime/events/state.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44

55
from pydantic import Field
66

7-
from uipath.runtime.events.base import UiPathRuntimeEvent, UiPathRuntimeEventType
7+
from uipath.runtime.events.base import (
8+
UiPathRuntimeEvent,
9+
UiPathRuntimeEventType,
10+
UiPathRuntimeStatePhase,
11+
)
812

913

1014
class UiPathRuntimeMessageEvent(UiPathRuntimeEvent):
@@ -69,6 +73,10 @@ class UiPathRuntimeStateEvent(UiPathRuntimeEvent):
6973
default=None,
7074
description="Fully qualified node name including subgraph hierarchy prefix",
7175
)
76+
phase: UiPathRuntimeStatePhase = Field(
77+
default=UiPathRuntimeStatePhase.UPDATED,
78+
description="Lifecycle phase: started, updated, or completed",
79+
)
7280
event_type: UiPathRuntimeEventType = Field(
7381
default=UiPathRuntimeEventType.RUNTIME_STATE, frozen=True
7482
)

tests/test_events.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from typing import Any, AsyncGenerator
2+
3+
import pytest
4+
5+
from uipath.runtime.base import UiPathStreamOptions
6+
from uipath.runtime.events import (
7+
UiPathRuntimeEvent,
8+
UiPathRuntimeStateEvent,
9+
UiPathRuntimeStatePhase,
10+
)
11+
from uipath.runtime.result import UiPathRuntimeResult, UiPathRuntimeStatus
12+
13+
14+
def test_state_event_phase_defaults_to_updated() -> None:
15+
"""Phase should default to UPDATED for backward compatibility."""
16+
event = UiPathRuntimeStateEvent(payload={"messages": []})
17+
18+
assert event.phase == UiPathRuntimeStatePhase.UPDATED
19+
20+
21+
def test_state_event_phase_can_be_set_explicitly() -> None:
22+
"""Phase should accept any valid UiPathRuntimeStatePhase value."""
23+
for phase in UiPathRuntimeStatePhase:
24+
event = UiPathRuntimeStateEvent(payload={}, phase=phase)
25+
assert event.phase == phase
26+
27+
28+
class PhaseAwareMockRuntime:
29+
"""Mock runtime that emits started/updated/completed state events per node."""
30+
31+
def __init__(self, nodes: list[str]) -> None:
32+
self.nodes = nodes
33+
34+
async def stream(
35+
self,
36+
input: dict[str, Any] | None = None,
37+
options: UiPathStreamOptions | None = None,
38+
) -> AsyncGenerator[UiPathRuntimeEvent, None]:
39+
for node in self.nodes:
40+
yield UiPathRuntimeStateEvent(
41+
node_name=node,
42+
payload={},
43+
phase=UiPathRuntimeStatePhase.STARTED,
44+
)
45+
yield UiPathRuntimeStateEvent(
46+
node_name=node,
47+
payload={"key": f"{node}_value"},
48+
phase=UiPathRuntimeStatePhase.UPDATED,
49+
)
50+
yield UiPathRuntimeStateEvent(
51+
node_name=node,
52+
payload={"key": f"{node}_value"},
53+
phase=UiPathRuntimeStatePhase.COMPLETED,
54+
)
55+
56+
yield UiPathRuntimeResult(
57+
status=UiPathRuntimeStatus.SUCCESSFUL,
58+
output={"done": True},
59+
)
60+
61+
62+
@pytest.mark.asyncio
63+
async def test_runtime_stream_emits_phase_lifecycle() -> None:
64+
"""A runtime streaming started/updated/completed phases per node."""
65+
runtime = PhaseAwareMockRuntime(nodes=["planner", "executor"])
66+
67+
state_events: list[UiPathRuntimeStateEvent] = []
68+
result: UiPathRuntimeResult | None = None
69+
70+
async for event in runtime.stream({}):
71+
if isinstance(event, UiPathRuntimeResult):
72+
result = event
73+
elif isinstance(event, UiPathRuntimeStateEvent):
74+
state_events.append(event)
75+
76+
# 2 nodes x 3 phases = 6 state events
77+
assert len(state_events) == 6
78+
79+
# Verify per-node lifecycle ordering
80+
for i, node in enumerate(["planner", "executor"]):
81+
group = state_events[i * 3 : i * 3 + 3]
82+
assert group[0].node_name == node
83+
assert group[0].phase == UiPathRuntimeStatePhase.STARTED
84+
assert group[0].payload == {}
85+
86+
assert group[1].node_name == node
87+
assert group[1].phase == UiPathRuntimeStatePhase.UPDATED
88+
assert group[1].payload == {"key": f"{node}_value"}
89+
90+
assert group[2].node_name == node
91+
assert group[2].phase == UiPathRuntimeStatePhase.COMPLETED
92+
assert group[2].payload == {"key": f"{node}_value"}
93+
94+
# Final result
95+
assert result is not None
96+
assert result.status == UiPathRuntimeStatus.SUCCESSFUL
97+
assert result.output == {"done": True}

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)