From 4e76a93759263e2775a6ff83da72ca6f0a904d0e Mon Sep 17 00:00:00 2001 From: Sandeepan-Ghosh-0312 Date: Tue, 21 Jul 2026 16:29:37 +0530 Subject: [PATCH 1/4] feat: JIT escalation apps support --- .../agent/exceptions/exceptions.py | 3 + .../agent/tools/escalation_tool.py | 20 ++++- tests/agent/tools/test_escalation_tool.py | 83 +++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/src/uipath_langchain/agent/exceptions/exceptions.py b/src/uipath_langchain/agent/exceptions/exceptions.py index 65ebbaf81..6cb3abadc 100644 --- a/src/uipath_langchain/agent/exceptions/exceptions.py +++ b/src/uipath_langchain/agent/exceptions/exceptions.py @@ -42,6 +42,9 @@ class AgentRuntimeErrorCode(str, Enum): TERMINATION_GUARDRAIL_ERROR = "TERMINATION_GUARDRAIL_ERROR" TERMINATION_ESCALATION_REJECTED = "TERMINATION_ESCALATION_REJECTED" TERMINATION_ESCALATION_ERROR = "TERMINATION_ESCALATION_ERROR" + ESCALATION_JIT_DEBUG_MISSING_PROJECT_KEY = ( + "ESCALATION_JIT_DEBUG_MISSING_PROJECT_KEY" + ) # State STATE_ERROR = "STATE_ERROR" diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index a6cf27923..d2458ba9d 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -19,7 +19,7 @@ from uipath.eval.mocks import mockable from uipath.platform import UiPath from uipath.platform.action_center.tasks import Task, TaskRecipient -from uipath.platform.common import WaitEscalation +from uipath.platform.common import UiPathConfig, WaitEscalation from uipath.runtime.errors import UiPathErrorCategory from uipath_langchain._utils import ( @@ -310,6 +310,9 @@ async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: if channel.recipients else None ) + is_debug = UiPathConfig.is_rooted_to_debug_job() + jit_project_key = channel.properties.project_key + action_schema = channel.properties.action_schema task_title = "Escalation Task" if tool.metadata is not None: @@ -336,6 +339,17 @@ async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: "outcome": cached_result.outcome, } + if is_debug and not jit_project_key: + raise AgentRuntimeError( + code=AgentRuntimeErrorCode.ESCALATION_JIT_DEBUG_MISSING_PROJECT_KEY, + title="Unable to create the action task in debug mode", + detail=( + "The app project key is missing from the escalation resource " + "configuration, so the app cannot be resolved." + ), + category=UiPathErrorCategory.USER, + ) + @mockable( name=tool_name.lower(), description=resource.description, @@ -354,6 +368,10 @@ async def create_escalation_task(): data=serialized_data, recipient=recipient, folder_path=folder_path, + app_project_key=jit_project_key, + app_type=channel.properties.app_type, + is_debug=is_debug, + action_schema=action_schema, ) if created_task.id is not None: diff --git a/tests/agent/tools/test_escalation_tool.py b/tests/agent/tools/test_escalation_tool.py index 84cdc99b1..229c6e438 100644 --- a/tests/agent/tools/test_escalation_tool.py +++ b/tests/agent/tools/test_escalation_tool.py @@ -16,6 +16,7 @@ ) from uipath.platform.action_center.tasks import Task, TaskRecipient, TaskRecipientType +from uipath_langchain.agent.exceptions import AgentRuntimeError from uipath_langchain.agent.tools.escalation_memory import ( EscalationMemoryCachedResult, ) @@ -213,6 +214,88 @@ async def test_escalation_tool_metadata_recipient_none_when_no_recipients( assert tool.metadata is not None assert tool.metadata["recipient"] is None + @pytest.fixture + def escalation_resource_jit(self): + """Escalation resource carrying JIT (debug) project key and app type.""" + return AgentEscalationResourceConfig( + name="approval", + description="Request approval", + channels=[ + AgentEscalationChannel( + name="action_center", + type="actionCenter", + description="Action Center channel", + input_schema={"type": "object", "properties": {}}, + output_schema={"type": "object", "properties": {}}, + properties=AgentEscalationChannelProperties( + app_name="ApprovalApp", + app_version=1, + resource_key="test-key", + project_key="proj-key-abc", + app_type="Custom", + ), + recipients=[ + StandardRecipient( + type=AgentEscalationRecipientType.USER_EMAIL, + value="user@example.com", + ) + ], + ) + ], + ) + + @pytest.mark.asyncio + @patch.dict(os.environ, {"UIPATH_PROJECT_ID": "proj-1"}) + @patch("uipath_langchain.agent.tools.escalation_tool.UiPath") + @patch("uipath_langchain._utils.durable_interrupt.decorator.interrupt") + async def test_escalation_tool_passes_jit_fields_in_debug( + self, mock_interrupt, mock_uipath_class, escalation_resource_jit + ): + """In a debug (studio) run, the project key, app type and is_debug flag are forwarded.""" + mock_client = MagicMock() + mock_client.tasks.create_async = AsyncMock(return_value=_make_mock_task()) + mock_uipath_class.return_value = mock_client + + mock_result = MagicMock() + mock_result.action = None + mock_result.data = {} + mock_result.is_deleted = False + mock_interrupt.return_value = mock_result + + tool = create_escalation_tool(escalation_resource_jit) + call = ToolCall(args={}, id="test-call", name=tool.name) + await tool.awrapper(tool, call, {}) # type: ignore[attr-defined] + + kwargs = mock_client.tasks.create_async.call_args.kwargs + assert kwargs["app_project_key"] == "proj-key-abc" + assert kwargs["app_type"] == "Custom" + assert kwargs["is_debug"] is True + + @pytest.mark.asyncio + @patch.dict(os.environ, {"UIPATH_PROJECT_ID": "proj-1"}) + @patch("uipath_langchain.agent.tools.escalation_tool.UiPath") + @patch("uipath_langchain._utils.durable_interrupt.decorator.interrupt") + async def test_escalation_tool_raises_in_debug_without_project_key( + self, mock_interrupt, mock_uipath_class, escalation_resource + ): + """In a debug run the task cannot be created without a project key for the undeployed app.""" + mock_client = MagicMock() + mock_client.tasks.create_async = AsyncMock(return_value=_make_mock_task()) + mock_uipath_class.return_value = mock_client + + mock_result = MagicMock() + mock_result.action = None + mock_result.data = {} + mock_result.is_deleted = False + mock_interrupt.return_value = mock_result + + tool = create_escalation_tool(escalation_resource) + call = ToolCall(args={}, id="test-call", name=tool.name) + + with pytest.raises(AgentRuntimeError): + await tool.awrapper(tool, call, {}) # type: ignore[attr-defined] + mock_client.tasks.create_async.assert_not_called() + @pytest.mark.asyncio @patch("uipath_langchain.agent.tools.escalation_tool.UiPath") @patch("uipath_langchain._utils.durable_interrupt.decorator.interrupt") From 8e5e89306bf594fd3b59a6366792ee3e2ae8a795 Mon Sep 17 00:00:00 2001 From: Sandeepan-Ghosh-0312 Date: Wed, 22 Jul 2026 03:36:33 +0530 Subject: [PATCH 2/4] fixes --- .../agent/tools/escalation_tool.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index d2458ba9d..650d42bb9 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -211,6 +211,10 @@ async def create_task_for_channel( data: dict[str, Any], recipient: TaskRecipient | None, folder_path: str | None, + app_project_key: str | None = None, + app_type: str | None = None, + is_debug: bool = False, + action_schema: Any = None, ) -> Task: """Create the human task backing an escalation channel.""" if isinstance(channel, AgentQuickFormEscalationChannel): @@ -227,6 +231,7 @@ async def create_task_for_channel( labels=channel.labels, is_actionable_message_enabled=channel.properties.is_actionable_message_enabled, actionable_message_metadata=channel.properties.actionable_message_meta_data, + solution_id=channel.properties.solution_id, ) return await client.tasks.create_async( title=title, @@ -238,6 +243,11 @@ async def create_task_for_channel( labels=channel.labels, is_actionable_message_enabled=channel.properties.is_actionable_message_enabled, actionable_message_metadata=channel.properties.actionable_message_meta_data, + app_project_key=app_project_key, + app_type=app_type, + is_debug=is_debug, + action_schema=action_schema, + solution_id=channel.properties.solution_id, ) @@ -310,8 +320,10 @@ async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: if channel.recipients else None ) - is_debug = UiPathConfig.is_rooted_to_debug_job() - jit_project_key = channel.properties.project_key + is_debug = True + print('Mock debug', is_debug, 'folder path', folder_path) + app_project_key = channel.properties.project_key + app_version = channel.properties.app_version action_schema = channel.properties.action_schema task_title = "Escalation Task" @@ -339,7 +351,7 @@ async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: "outcome": cached_result.outcome, } - if is_debug and not jit_project_key: + if is_debug and app_version == 0 and not app_project_key: raise AgentRuntimeError( code=AgentRuntimeErrorCode.ESCALATION_JIT_DEBUG_MISSING_PROJECT_KEY, title="Unable to create the action task in debug mode", @@ -368,7 +380,7 @@ async def create_escalation_task(): data=serialized_data, recipient=recipient, folder_path=folder_path, - app_project_key=jit_project_key, + app_project_key=app_project_key, app_type=channel.properties.app_type, is_debug=is_debug, action_schema=action_schema, From a585915f6cde48d797e5e208fd26d9c7c029701e Mon Sep 17 00:00:00 2001 From: Sandeepan-Ghosh-0312 Date: Thu, 23 Jul 2026 02:21:34 +0530 Subject: [PATCH 3/4] fix debug resolution --- .../agent/tools/escalation_tool.py | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index 650d42bb9..ca6333d3d 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -203,6 +203,44 @@ def _try_get_channel_app_name(channel: EscalationChannel) -> str | None: ) +async def _resolve_is_debug_run() -> bool: + """Determine whether the current run is a debug run. + + Reads the running job key (``UIPATH_JOB_KEY``) from the runtime environment + via ``UiPathConfig``, then retrieves the job from Orchestrator (the SDK + builds the ``Jobs/...GetByKey`` URL and injects the ``x-uipath-folderkey`` + header from ``UIPATH_FOLDER_KEY`` on every request from its HTTP client). + + The job's ``ParentContext`` field is a JSON string such as + ``{"IsDebug": true}``; when ``IsDebug`` is truthy the run is a debug run. + + Returns: + True when the current job is a debug run, False otherwise (including + when the job key is unavailable or the parent context cannot be read). + """ + job_key = UiPathConfig.job_key + if not job_key: + return False + + client = UiPath() + job = await client.jobs.retrieve_async(job_key=job_key) + + parent_context_raw = job.parent_context + if not parent_context_raw: + return False + + try: + parent_context = json.loads(parent_context_raw) + except json.JSONDecodeError: + _escalation_logger.warning( + "Unable to parse job ParentContext to determine debug run: %r", + parent_context_raw, + ) + return False + + return bool(parent_context.get("IsDebug")) + + async def create_task_for_channel( client: UiPath, channel: EscalationChannel, @@ -320,8 +358,8 @@ async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: if channel.recipients else None ) - is_debug = True - print('Mock debug', is_debug, 'folder path', folder_path) + is_debug = await _resolve_is_debug_run() + print('Resolved debug run', is_debug) app_project_key = channel.properties.project_key app_version = channel.properties.app_version action_schema = channel.properties.action_schema From 35d7dd84bba6fb1db88c01f7040485136dee644c Mon Sep 17 00:00:00 2001 From: Sandeepan-Ghosh-0312 Date: Thu, 23 Jul 2026 03:41:08 +0530 Subject: [PATCH 4/4] fixes --- .../agent/tools/escalation_tool.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/uipath_langchain/agent/tools/escalation_tool.py b/src/uipath_langchain/agent/tools/escalation_tool.py index ca6333d3d..d9034bf2b 100644 --- a/src/uipath_langchain/agent/tools/escalation_tool.py +++ b/src/uipath_langchain/agent/tools/escalation_tool.py @@ -283,7 +283,6 @@ async def create_task_for_channel( actionable_message_metadata=channel.properties.actionable_message_meta_data, app_project_key=app_project_key, app_type=app_type, - is_debug=is_debug, action_schema=action_schema, solution_id=channel.properties.solution_id, ) @@ -358,12 +357,27 @@ async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: if channel.recipients else None ) - is_debug = await _resolve_is_debug_run() + + is_debug = UiPathConfig.is_rooted_to_debug_job + if not is_debug: + is_debug = await _resolve_is_debug_run() print('Resolved debug run', is_debug) - app_project_key = channel.properties.project_key + if is_debug: + app_project_key = channel.properties.project_key app_version = channel.properties.app_version action_schema = channel.properties.action_schema + if is_debug and app_version == 0 and not app_project_key: + raise AgentRuntimeError( + code=AgentRuntimeErrorCode.ESCALATION_JIT_DEBUG_MISSING_PROJECT_KEY, + title="Unable to create the escalation task in debug mode", + detail=( + "The app project key is missing from the escalation resource " + "configuration, so the app cannot be resolved in debug mode." + ), + category=UiPathErrorCategory.USER, + ) + task_title = "Escalation Task" if tool.metadata is not None: # Recipient requires runtime resolution, store in metadata after resolving @@ -389,17 +403,6 @@ async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]: "outcome": cached_result.outcome, } - if is_debug and app_version == 0 and not app_project_key: - raise AgentRuntimeError( - code=AgentRuntimeErrorCode.ESCALATION_JIT_DEBUG_MISSING_PROJECT_KEY, - title="Unable to create the action task in debug mode", - detail=( - "The app project key is missing from the escalation resource " - "configuration, so the app cannot be resolved." - ), - category=UiPathErrorCategory.USER, - ) - @mockable( name=tool_name.lower(), description=resource.description,