From 4dc93c9b36b0676635768495fd99a09b202ed6aa Mon Sep 17 00:00:00 2001 From: GabrielVasilescu04 Date: Wed, 22 Apr 2026 10:30:10 +0300 Subject: [PATCH] feat: support for IS resume triggers --- pyproject.toml | 2 +- src/uipath/runtime/__init__.py | 2 + src/uipath/runtime/resumable/__init__.py | 2 + src/uipath/runtime/resumable/runtime.py | 14 ++++--- src/uipath/runtime/resumable/trigger.py | 2 + tests/test_resumable.py | 48 ++++++++++++++++++++++++ uv.lock | 2 +- 7 files changed, 65 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7d1eb4e..2b8b615 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-runtime" -version = "0.10.1" +version = "0.10.2" description = "Runtime abstractions and interfaces for building agents and automation scripts in the UiPath ecosystem" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/src/uipath/runtime/__init__.py b/src/uipath/runtime/__init__.py index 55fc00d..1eed011 100644 --- a/src/uipath/runtime/__init__.py +++ b/src/uipath/runtime/__init__.py @@ -2,6 +2,7 @@ from uipath.core.triggers import ( UiPathApiTrigger, + UiPathIntegrationTrigger, UiPathResumeTrigger, UiPathResumeTriggerName, UiPathResumeTriggerType, @@ -60,6 +61,7 @@ "UiPathResumableStorageProtocol", "UiPathResumeTriggerProtocol", "UiPathApiTrigger", + "UiPathIntegrationTrigger", "UiPathResumeTrigger", "UiPathResumeTriggerType", "UiPathResumableRuntime", diff --git a/src/uipath/runtime/resumable/__init__.py b/src/uipath/runtime/resumable/__init__.py index 1582d2b..b78117b 100644 --- a/src/uipath/runtime/resumable/__init__.py +++ b/src/uipath/runtime/resumable/__init__.py @@ -2,6 +2,7 @@ from uipath.core.triggers import ( UiPathApiTrigger, + UiPathIntegrationTrigger, UiPathResumeTrigger, UiPathResumeTriggerType, ) @@ -19,6 +20,7 @@ "UiPathResumeTriggerReaderProtocol", "UiPathResumeTriggerProtocol", "UiPathApiTrigger", + "UiPathIntegrationTrigger", "UiPathResumeTrigger", "UiPathResumeTriggerType", ] diff --git a/src/uipath/runtime/resumable/runtime.py b/src/uipath/runtime/resumable/runtime.py index bd1c059..93bc2e5 100644 --- a/src/uipath/runtime/resumable/runtime.py +++ b/src/uipath/runtime/resumable/runtime.py @@ -145,9 +145,10 @@ async def stream( options.resume = True async def _get_fired_triggers(self) -> dict[str, Any] | None: - """Check stored triggers for any that have already fired (excluding API triggers). + """Check stored triggers for any that have already fired. - API triggers cannot be completed before suspending the job, so they are skipped. + Skips async-external triggers (API, Inbox) whose payloads only arrive + asynchronously and cannot be polled at suspend time. Returns: A resume map of {interrupt_id: resume_data} for fired triggers, or None. @@ -156,10 +157,13 @@ async def _get_fired_triggers(self) -> dict[str, Any] | None: if not triggers: return None - non_api_triggers = [ - t for t in triggers if t.trigger_type != UiPathResumeTriggerType.API + pollable_triggers = [ + t + for t in triggers + if t.trigger_type + not in (UiPathResumeTriggerType.API, UiPathResumeTriggerType.INBOX) ] - return await self._build_resume_map(non_api_triggers) + return await self._build_resume_map(pollable_triggers) async def _restore_resume_input( self, diff --git a/src/uipath/runtime/resumable/trigger.py b/src/uipath/runtime/resumable/trigger.py index 0a9ac45..9eb1412 100644 --- a/src/uipath/runtime/resumable/trigger.py +++ b/src/uipath/runtime/resumable/trigger.py @@ -2,6 +2,7 @@ from uipath.core.triggers import ( UiPathApiTrigger, + UiPathIntegrationTrigger, UiPathResumeTrigger, UiPathResumeTriggerName, UiPathResumeTriggerType, @@ -9,6 +10,7 @@ __all__ = [ "UiPathApiTrigger", + "UiPathIntegrationTrigger", "UiPathResumeTrigger", "UiPathResumeTriggerName", "UiPathResumeTriggerType", diff --git a/tests/test_resumable.py b/tests/test_resumable.py index 5a14bf8..f54d09c 100644 --- a/tests/test_resumable.py +++ b/tests/test_resumable.py @@ -499,6 +499,54 @@ async def read_trigger_impl(trigger: UiPathResumeTrigger) -> dict[str, Any]: # Delegate should have been executed only once (no auto-resume) assert runtime_impl.execution_count == 1 + @pytest.mark.asyncio + async def test_resumable_skips_inbox_triggers_on_auto_resume_check(self) -> None: + """Inbox triggers should be skipped when checking for auto-resume after suspension. + + Inbox triggers are async-external (payload delivered via Integration + Services), so calling read_trigger on them at suspend time would hit a + 404 and fault the run. They should behave like API triggers here. + """ + + runtime_impl = MultiTriggerMockRuntime() + storage = StatefulStorageMock() + trigger_manager = make_trigger_manager_mock() + + def create_inbox_trigger(data: dict[str, Any]) -> UiPathResumeTrigger: + return UiPathResumeTrigger( + interrupt_id="", # Will be set by resumable runtime + trigger_type=UiPathResumeTriggerType.INBOX, + payload=data, + ) + + trigger_manager.create_trigger = AsyncMock(side_effect=create_inbox_trigger) # type: ignore + + # Track whether read_trigger is ever called — it must NOT be, otherwise + # the filter is broken and we'd hit the payload endpoint prematurely. + trigger_manager.read_trigger = AsyncMock( + side_effect=AssertionError( + "read_trigger must not be called for Inbox triggers pre-resume" + ) + ) # type: ignore + + resumable = UiPathResumableRuntime( + delegate=runtime_impl, + storage=storage, + trigger_manager=trigger_manager, + runtime_id="runtime-1", + ) + + result = await resumable.execute({}) + + assert result.status == UiPathRuntimeStatus.SUSPENDED + assert result.triggers is not None + assert len(result.triggers) == 2 + assert all( + t.trigger_type == UiPathResumeTriggerType.INBOX for t in result.triggers + ) + trigger_manager.read_trigger.assert_not_called() + assert runtime_impl.execution_count == 1 + @pytest.mark.asyncio async def test_resumable_auto_resumes_task_triggers_but_not_api_triggers( self, diff --git a/uv.lock b/uv.lock index fa71a3a..59640e9 100644 --- a/uv.lock +++ b/uv.lock @@ -1005,7 +1005,7 @@ wheels = [ [[package]] name = "uipath-runtime" -version = "0.10.1" +version = "0.10.2" source = { editable = "." } dependencies = [ { name = "uipath-core" },