From 0bc50b82a16a6aff473d9a62636d8ae904fdd43a Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Fri, 8 May 2026 16:14:37 -0700 Subject: [PATCH 1/9] Poll remote dispatch until completion timestamp --- .../narada-core/src/narada_core/models.py | 2 +- packages/narada-pyodide/src/narada/window.py | 8 ++- .../tests/test_cloud_browser.py | 58 +++++++++++++++++-- packages/narada/src/narada/window.py | 8 ++- 4 files changed, 68 insertions(+), 8 deletions(-) diff --git a/packages/narada-core/src/narada_core/models.py b/packages/narada-core/src/narada_core/models.py index 307c7a9..680a19c 100644 --- a/packages/narada-core/src/narada_core/models.py +++ b/packages/narada-core/src/narada_core/models.py @@ -345,7 +345,7 @@ class Usage(TypedDict): class Response(TypedDict, Generic[_MaybeStructuredOutput]): requestId: str - status: Literal["success", "error"] + status: Literal["success", "error", "input-required"] response: ResponseContent[_MaybeStructuredOutput] | None createdAt: str completedAt: str | None diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index 4c6809c..e2a3c3e 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -452,7 +452,7 @@ async def dispatch_request( response = await fetch_response.json() response["requestId"] = request_id - if response["status"] != "pending": + if response["completedAt"] is not None: response_content = response["response"] if response_content is not None: # Populate the `structuredOutput` field. This is a client-side field @@ -964,6 +964,12 @@ async def _run_extension_action( "action": request.model_dump(), "browserWindowId": self.browser_window_id, } + remote_dispatch_request_id = os.environ.get("NARADA_REMOTE_DISPATCH_REQUEST_ID") + if remote_dispatch_request_id is not None: + body["requestId"] = remote_dispatch_request_id + remote_dispatch_api_key_id = os.environ.get("NARADA_REMOTE_DISPATCH_API_KEY_ID") + if remote_dispatch_api_key_id is not None: + body["apiKeyId"] = remote_dispatch_api_key_id parent_run_ids = self._current_parent_run_ids() if parent_run_ids: body["parentRunIds"] = parent_run_ids diff --git a/packages/narada-pyodide/tests/test_cloud_browser.py b/packages/narada-pyodide/tests/test_cloud_browser.py index 02cf5a7..4e1d087 100644 --- a/packages/narada-pyodide/tests/test_cloud_browser.py +++ b/packages/narada-pyodide/tests/test_cloud_browser.py @@ -9,7 +9,7 @@ import pytest from packaging.version import InvalidVersion -PROJECT_ROOT = Path("/Users/zizheng/Projects/narada-python-sdk") +PROJECT_ROOT = Path(__file__).resolve().parents[3] PYODIDE_SRC = PROJECT_ROOT / "packages" / "narada-pyodide" / "src" CORE_SRC = PROJECT_ROOT / "packages" / "narada-core" / "src" @@ -253,7 +253,13 @@ async def test_cloud_browser_window_dispatch_request_omits_parent_run_ids( pyfetch = AsyncMock( side_effect=[ _FakeResponse(json_data={"requestId": "req-123"}), - _FakeResponse(json_data={"status": "success", "response": None}), + _FakeResponse( + json_data={ + "status": "success", + "completedAt": "2026-05-08T00:00:00+00:00", + "response": None, + } + ), ] ) _, _, window_module = _import_pyodide_narada(monkeypatch, pyfetch=pyfetch) @@ -284,7 +290,13 @@ async def test_cloud_browser_window_dispatch_request_preserves_current_file_vari pyfetch = AsyncMock( side_effect=[ _FakeResponse(json_data={"requestId": "req-123"}), - _FakeResponse(json_data={"status": "success", "response": None}), + _FakeResponse( + json_data={ + "status": "success", + "completedAt": "2026-05-08T00:00:00+00:00", + "response": None, + } + ), ] ) _, _, window_module = _import_pyodide_narada(monkeypatch, pyfetch=pyfetch) @@ -386,6 +398,30 @@ async def test_remote_browser_window_without_cloud_session_keeps_extension_actio assert "parentRunIds" not in payload +@pytest.mark.asyncio +async def test_extension_action_includes_remote_dispatch_context( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("NARADA_REMOTE_DISPATCH_REQUEST_ID", "request-123") + monkeypatch.setenv("NARADA_REMOTE_DISPATCH_API_KEY_ID", "api-key-123") + pyfetch = AsyncMock( + return_value=_FakeResponse(json_data={"status": "success", "data": None}) + ) + _, _, window_module = _import_pyodide_narada(monkeypatch, pyfetch=pyfetch) + + window = window_module.RemoteBrowserWindow( + browser_window_id="browser-window-123", + api_key="test-api-key", + ) + await window.close() + + call = pyfetch.await_args + assert call is not None + payload = json.loads(call.kwargs["body"]) + assert payload["requestId"] == "request-123" + assert payload["apiKeyId"] == "api-key-123" + + @pytest.mark.asyncio async def test_local_browser_window_dispatch_request_uses_latest_parent_run_ids( monkeypatch: pytest.MonkeyPatch, @@ -395,9 +431,21 @@ async def test_local_browser_window_dispatch_request_uses_latest_parent_run_ids( pyfetch = AsyncMock( side_effect=[ _FakeResponse(json_data={"requestId": "req-1"}), - _FakeResponse(json_data={"status": "success", "response": None}), + _FakeResponse( + json_data={ + "status": "success", + "completedAt": "2026-05-08T00:00:00+00:00", + "response": None, + } + ), _FakeResponse(json_data={"requestId": "req-2"}), - _FakeResponse(json_data={"status": "success", "response": None}), + _FakeResponse( + json_data={ + "status": "success", + "completedAt": "2026-05-08T00:00:00+00:00", + "response": None, + } + ), ] ) _, _, window_module = _import_pyodide_narada(monkeypatch, pyfetch=pyfetch) diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index c577951..1535757 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -467,7 +467,7 @@ async def dispatch_request( response["requestId"] = request_id - if response["status"] != "pending": + if response["completedAt"] is not None: response_content = response["response"] if response_content is not None: # Populate the `structuredOutput` field. This is a client-side field @@ -927,6 +927,12 @@ async def _run_extension_action( "action": request.model_dump(), "browserWindowId": self.browser_window_id, } + remote_dispatch_request_id = os.environ.get("NARADA_REMOTE_DISPATCH_REQUEST_ID") + if remote_dispatch_request_id is not None: + body["requestId"] = remote_dispatch_request_id + remote_dispatch_api_key_id = os.environ.get("NARADA_REMOTE_DISPATCH_API_KEY_ID") + if remote_dispatch_api_key_id is not None: + body["apiKeyId"] = remote_dispatch_api_key_id if timeout is not None: body["timeout"] = timeout From 02098f1c70ee786c5511036a321028862ffe8dcf Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Mon, 11 May 2026 18:18:05 -0700 Subject: [PATCH 2/9] Cover active input-required polling --- packages/narada-pyodide/src/narada/window.py | 8 +++- .../tests/test_cloud_browser.py | 43 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index e2a3c3e..ed40cee 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -964,10 +964,14 @@ async def _run_extension_action( "action": request.model_dump(), "browserWindowId": self.browser_window_id, } - remote_dispatch_request_id = os.environ.get("NARADA_REMOTE_DISPATCH_REQUEST_ID") + remote_dispatch_request_id = os.environ.get( + "NARADA_REMOTE_DISPATCH_REQUEST_ID" + ) if remote_dispatch_request_id is not None: body["requestId"] = remote_dispatch_request_id - remote_dispatch_api_key_id = os.environ.get("NARADA_REMOTE_DISPATCH_API_KEY_ID") + remote_dispatch_api_key_id = os.environ.get( + "NARADA_REMOTE_DISPATCH_API_KEY_ID" + ) if remote_dispatch_api_key_id is not None: body["apiKeyId"] = remote_dispatch_api_key_id parent_run_ids = self._current_parent_run_ids() diff --git a/packages/narada-pyodide/tests/test_cloud_browser.py b/packages/narada-pyodide/tests/test_cloud_browser.py index 4e1d087..8dbc735 100644 --- a/packages/narada-pyodide/tests/test_cloud_browser.py +++ b/packages/narada-pyodide/tests/test_cloud_browser.py @@ -283,6 +283,49 @@ async def test_cloud_browser_window_dispatch_request_omits_parent_run_ids( assert "parentRunIds" not in payload +@pytest.mark.asyncio +async def test_cloud_browser_window_dispatch_request_waits_through_active_input_required( + monkeypatch: pytest.MonkeyPatch, +) -> None: + pyfetch = AsyncMock( + side_effect=[ + _FakeResponse(json_data={"requestId": "req-123"}), + _FakeResponse( + json_data={ + "status": "input-required", + "completedAt": None, + "response": None, + } + ), + _FakeResponse( + json_data={ + "status": "success", + "completedAt": "2026-05-08T00:00:00+00:00", + "response": None, + } + ), + ] + ) + _, _, window_module = _import_pyodide_narada(monkeypatch, pyfetch=pyfetch) + sleep = AsyncMock() + monkeypatch.setattr(window_module.asyncio, "sleep", sleep) + + window = window_module.CloudBrowserWindow( + browser_window_id="browser-window-123", + session_id="session-123", + api_key="test-api-key", + ) + response = await window.dispatch_request(prompt="hello from cloud browser") + + assert response["status"] == "success" + assert pyfetch.await_count == 3 + assert sleep.await_count == 1 + first_poll_call = pyfetch.await_args_list[1] + second_poll_call = pyfetch.await_args_list[2] + assert first_poll_call.args[0].endswith("/remote-dispatch/responses/req-123") + assert second_poll_call.args[0].endswith("/remote-dispatch/responses/req-123") + + @pytest.mark.asyncio async def test_cloud_browser_window_dispatch_request_preserves_current_file_variable_shape( monkeypatch: pytest.MonkeyPatch, From 0cfb37a6b93aefa14b5ffd064a71196dbe60a081 Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Wed, 20 May 2026 17:53:49 -0700 Subject: [PATCH 3/9] Handle remote dispatch input-required callbacks --- .../src/narada_core/actions/models.py | 11 ++ .../narada-core/src/narada_core/models.py | 3 +- .../narada-pyodide/src/narada/__init__.py | 3 +- packages/narada-pyodide/src/narada/window.py | 141 ++++++++++++------ .../tests/test_cloud_browser.py | 51 ++++++- packages/narada/src/narada/__init__.py | 3 +- packages/narada/src/narada/window.py | 89 ++++++++--- packages/narada/tests/test_cloud_browser.py | 117 +++++++++++++++ 8 files changed, 342 insertions(+), 76 deletions(-) diff --git a/packages/narada-core/src/narada_core/actions/models.py b/packages/narada-core/src/narada_core/actions/models.py index 89b72c6..3e70ce0 100644 --- a/packages/narada-core/src/narada_core/actions/models.py +++ b/packages/narada-core/src/narada_core/actions/models.py @@ -420,6 +420,17 @@ class UserApprovalResponse(BaseModel): approved: bool +ActiveInputAction = Annotated[ + PromptForUserInputRequest | UserApprovalRequest, + Field(discriminator="name"), +] + + +class ActiveInputRequest(BaseModel): + input_id: str = Field(alias="inputId") + action: ActiveInputAction + + type ExtensionActionRequest = ( AgenticSelectorRequest | AgenticMouseActionRequest diff --git a/packages/narada-core/src/narada_core/models.py b/packages/narada-core/src/narada_core/models.py index 8b52d46..3a06410 100644 --- a/packages/narada-core/src/narada_core/models.py +++ b/packages/narada-core/src/narada_core/models.py @@ -1,7 +1,7 @@ from __future__ import annotations from enum import Enum, StrEnum -from typing import Annotated, Generic, Literal, NotRequired, TypedDict, TypeVar +from typing import Annotated, Any, Generic, Literal, NotRequired, TypedDict, TypeVar from pydantic import BaseModel, Field @@ -413,6 +413,7 @@ class Response(TypedDict, Generic[_MaybeStructuredOutput]): createdAt: str completedAt: str | None usage: Usage + activeInputRequest: NotRequired[dict[str, Any]] class File(TypedDict): diff --git a/packages/narada-pyodide/src/narada/__init__.py b/packages/narada-pyodide/src/narada/__init__.py index 1c5c77b..bfe0b01 100644 --- a/packages/narada-pyodide/src/narada/__init__.py +++ b/packages/narada-pyodide/src/narada/__init__.py @@ -1,4 +1,4 @@ -from narada_core.actions.models import CriticResult +from narada_core.actions.models import ActiveInputRequest, CriticResult from narada_core.errors import ( NaradaError, NaradaTimeoutError, @@ -23,6 +23,7 @@ __all__ = [ "__version__", + "ActiveInputRequest", "Agent", "CloudBrowserWindow", "CriticConfig", diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index 22328b7..14456f6 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -1,4 +1,5 @@ import asyncio +import inspect import json import logging import os @@ -10,6 +11,8 @@ IO, TYPE_CHECKING, Any, + Awaitable, + Callable, Literal, Optional, TypeVar, @@ -22,6 +25,7 @@ from js import AbortController, setTimeout # type: ignore from narada_core.actions.critic import run_critic from narada_core.actions.models import ( + ActiveInputRequest, AgenticMouseAction, AgenticMouseActionRequest, AgenticSelectorAction, @@ -109,6 +113,30 @@ async def _narada_get_id_token() -> str: ... _ResponseModel = TypeVar("_ResponseModel", bound=BaseModel) +type InputRequiredCallback = Callable[[ActiveInputRequest], Awaitable[None] | None] + + +async def _notify_input_required_callback( + callback: InputRequiredCallback | None, + response: dict[str, Any], + seen_input_ids: set[str], +) -> None: + if callback is None or response.get("status") != "input-required": + return + + active_input_request_data = response.get("activeInputRequest") + if active_input_request_data is None: + return + + active_input_request = ActiveInputRequest.model_validate(active_input_request_data) + if active_input_request.input_id in seen_input_ids: + return + + seen_input_ids.add(active_input_request.input_id) + callback_result = callback(active_input_request) + if inspect.isawaitable(callback_result): + await callback_result + def _trace_agent_type(agent: Agent | str) -> str: match agent: @@ -253,6 +281,7 @@ async def dispatch_request( callback_url: str | None = None, callback_secret: str | None = None, callback_headers: dict[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> Response[None]: ... @@ -277,6 +306,7 @@ async def dispatch_request( callback_url: str | None = None, callback_secret: str | None = None, callback_headers: dict[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> Response[_StructuredOutput]: ... @@ -301,6 +331,7 @@ async def dispatch_request( callback_url: str | None = None, callback_secret: str | None = None, callback_headers: dict[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> Response[None]: ... @@ -325,6 +356,7 @@ async def dispatch_request( callback_url: str | None = None, callback_secret: str | None = None, callback_headers: dict[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> Response[_StructuredOutput]: ... @@ -349,6 +381,7 @@ async def dispatch_request( callback_url: str | None = None, callback_secret: str | None = None, callback_headers: dict[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> Response: """Low-level API for invoking an agent in the Narada extension side panel chat. @@ -424,6 +457,7 @@ async def dispatch_request( body["reasoningMode"] = reasoning.value try: + seen_input_ids: set[str] = set() controller = AbortController.new() signal = controller.signal @@ -466,56 +500,60 @@ async def dispatch_request( response = await fetch_response.json() response["requestId"] = request_id - if response["completedAt"] is not None: - response_content = response["response"] - if response_content is not None: - # Populate the `structuredOutput` field. This is a client-side field - # that's not directly returned by the API. - output_data = response_content.get("output") - if ( - output_schema is not None - and output_data is not None - and output_data.get("type") == "structured" - ): - response_content["structuredOutput"] = ( - output_schema.model_validate(output_data["content"]) - ) - else: - response_content["structuredOutput"] = None - - trace_status: _trace.SubAgentCallStatus = ( - "error" if response["status"] == "error" else "success" - ) - trace_error: str | None = ( - response_content.get("text") - if response["status"] == "error" - and response_content is not None - else None + if response["completedAt"] is None: + await _notify_input_required_callback( + on_input_required, + response, + seen_input_ids, ) - trace_text: str | None = ( - response_content.get("text") - if response["status"] == "success" - and response_content is not None + # Poll every 3 seconds. + await asyncio.sleep(3) + continue + + response_content = response["response"] + if response_content is not None: + # Populate the `structuredOutput` field. This is a client-side field + # that's not directly returned by the API. + output_data = response_content.get("output") + if ( + output_schema is not None + and output_data is not None + and output_data.get("type") == "structured" + ): + response_content["structuredOutput"] = ( + output_schema.model_validate(output_data["content"]) + ) + else: + response_content["structuredOutput"] = None + + trace_status: _trace.SubAgentCallStatus = ( + "error" if response["status"] == "error" else "success" + ) + trace_error: str | None = ( + response_content.get("text") + if response["status"] == "error" and response_content is not None + else None + ) + trace_text: str | None = ( + response_content.get("text") + if response["status"] == "success" and response_content is not None + else None + ) + _trace.emit_sub_agent_call( + ts_start=trace_start_ms, + agent_type=agent_type_str, + prompt=prompt, + status=trace_status, + request_id=request_id, + text=trace_text, + error_message=trace_error, + action_trace_raw=( + response_content.get("actionTrace") + if response_content is not None else None - ) - _trace.emit_sub_agent_call( - ts_start=trace_start_ms, - agent_type=agent_type_str, - prompt=prompt, - status=trace_status, - request_id=request_id, - text=trace_text, - error_message=trace_error, - action_trace_raw=( - response_content.get("actionTrace") - if response_content is not None - else None - ), - ) - return response - - # Poll every 3 seconds. - await asyncio.sleep(3) + ), + ) + return response else: raise NaradaAgentTimeoutError_INTERNAL_DO_NOT_USE(timeout) @@ -563,6 +601,7 @@ async def agent( mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, input_variables: dict[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> AgentResponse[dict[str, Any]]: ... @@ -580,6 +619,7 @@ async def agent( mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, input_variables: dict[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> AgentResponse[_StructuredOutput]: ... @@ -596,6 +636,7 @@ async def agent( mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, input_variables: dict[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, critic: CriticConfig | None = None, timeout: int = 1000, ) -> AgentResponse[dict[str, Any]]: ... @@ -613,6 +654,7 @@ async def agent( mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, input_variables: dict[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, critic: CriticConfig | None = None, timeout: int = 1000, ) -> AgentResponse[_StructuredOutput]: ... @@ -630,6 +672,7 @@ async def agent( mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, input_variables: dict[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, critic: CriticConfig | None = None, timeout: int = 1000, ) -> AgentResponse: @@ -649,6 +692,7 @@ async def agent( mcp_servers=mcp_servers, secret_variables=secret_variables, input_variables=input_variables, + on_input_required=on_input_required, timeout=timeout, ) else: @@ -675,6 +719,7 @@ async def agent( mcp_servers=mcp_servers, secret_variables=secret_variables, input_variables=input_variables, + on_input_required=on_input_required, timeout=timeout, ) response_content = remote_dispatch_response["response"] diff --git a/packages/narada-pyodide/tests/test_cloud_browser.py b/packages/narada-pyodide/tests/test_cloud_browser.py index adec425..9ba1624 100644 --- a/packages/narada-pyodide/tests/test_cloud_browser.py +++ b/packages/narada-pyodide/tests/test_cloud_browser.py @@ -295,6 +295,41 @@ async def test_cloud_browser_window_dispatch_request_waits_through_active_input_ "status": "input-required", "completedAt": None, "response": None, + "activeInputRequest": { + "inputId": "input-123", + "action": { + "name": "prompt_for_user_input", + "step_id": "step-123", + "variables": [ + { + "name": "email", + "type": "string", + "required": True, + } + ], + }, + }, + } + ), + _FakeResponse( + json_data={ + "status": "input-required", + "completedAt": None, + "response": None, + "activeInputRequest": { + "inputId": "input-123", + "action": { + "name": "prompt_for_user_input", + "step_id": "step-123", + "variables": [ + { + "name": "email", + "type": "string", + "required": True, + } + ], + }, + }, } ), _FakeResponse( @@ -308,6 +343,7 @@ async def test_cloud_browser_window_dispatch_request_waits_through_active_input_ ) _, _, window_module = _import_pyodide_narada(monkeypatch, pyfetch=pyfetch) sleep = AsyncMock() + on_input_required = AsyncMock() monkeypatch.setattr(window_module.asyncio, "sleep", sleep) window = window_module.CloudBrowserWindow( @@ -315,13 +351,20 @@ async def test_cloud_browser_window_dispatch_request_waits_through_active_input_ session_id="session-123", api_key="test-api-key", ) - response = await window.dispatch_request(prompt="hello from cloud browser") + response = await window.dispatch_request( + prompt="hello from cloud browser", + on_input_required=on_input_required, + ) assert response["status"] == "success" - assert pyfetch.await_count == 3 - assert sleep.await_count == 1 + assert pyfetch.await_count == 4 + assert sleep.await_count == 2 + on_input_required.assert_awaited_once() + active_input_request = on_input_required.await_args.args[0] + assert active_input_request.input_id == "input-123" + assert active_input_request.action.name == "prompt_for_user_input" first_poll_call = pyfetch.await_args_list[1] - second_poll_call = pyfetch.await_args_list[2] + second_poll_call = pyfetch.await_args_list[3] assert first_poll_call.args[0].endswith("/remote-dispatch/responses/req-123") assert second_poll_call.args[0].endswith("/remote-dispatch/responses/req-123") diff --git a/packages/narada/src/narada/__init__.py b/packages/narada/src/narada/__init__.py index 719f28d..bf9c3a2 100644 --- a/packages/narada/src/narada/__init__.py +++ b/packages/narada/src/narada/__init__.py @@ -1,4 +1,4 @@ -from narada_core.actions.models import CriticResult +from narada_core.actions.models import ActiveInputRequest, CriticResult from narada_core.errors import ( NaradaError, NaradaExtensionMissingError, @@ -25,6 +25,7 @@ __all__ = [ "__version__", + "ActiveInputRequest", "Agent", "BrowserConfig", "CloudBrowserWindow", diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index 9dc9a8f..1d25f39 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -1,4 +1,5 @@ import asyncio +import inspect import logging import mimetypes import os @@ -11,6 +12,8 @@ from typing import ( IO, Any, + Awaitable, + Callable, Literal, Mapping, TypedDict, @@ -23,6 +26,7 @@ import aiohttp from narada_core.actions.critic import run_critic from narada_core.actions.models import ( + ActiveInputRequest, AgenticMouseAction, AgenticMouseActionRequest, AgenticSelectorAction, @@ -91,6 +95,30 @@ _ResponseModel = TypeVar("_ResponseModel", bound=BaseModel) +type InputRequiredCallback = Callable[[ActiveInputRequest], Awaitable[None] | None] + + +async def _notify_input_required_callback( + callback: InputRequiredCallback | None, + response: Mapping[str, Any], + seen_input_ids: set[str], +) -> None: + if callback is None or response.get("status") != "input-required": + return + + active_input_request_data = response.get("activeInputRequest") + if active_input_request_data is None: + return + + active_input_request = ActiveInputRequest.model_validate(active_input_request_data) + if active_input_request.input_id in seen_input_ids: + return + + seen_input_ids.add(active_input_request.input_id) + callback_result = callback(active_input_request) + if inspect.isawaitable(callback_result): + await callback_result + class _InputVariableFileReference(TypedDict): source: Literal["remoteDispatchUpload"] @@ -278,6 +306,7 @@ async def dispatch_request( callback_url: str | None = None, callback_secret: str | None = None, callback_headers: Mapping[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> Response[None]: ... @@ -303,6 +332,7 @@ async def dispatch_request( callback_url: str | None = None, callback_secret: str | None = None, callback_headers: Mapping[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> Response[_StructuredOutput]: ... @@ -328,6 +358,7 @@ async def dispatch_request( callback_url: str | None = None, callback_secret: str | None = None, callback_headers: Mapping[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> Response[None]: ... @@ -353,6 +384,7 @@ async def dispatch_request( callback_url: str | None = None, callback_secret: str | None = None, callback_headers: Mapping[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> Response[_StructuredOutput]: ... @@ -378,6 +410,7 @@ async def dispatch_request( callback_url: str | None = None, callback_secret: str | None = None, callback_headers: Mapping[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> Response: """Low-level API for invoking an agent in the Narada extension side panel chat. @@ -446,6 +479,7 @@ async def dispatch_request( body["reasoningMode"] = reasoning.value try: + seen_input_ids: set[str] = set() async with aiohttp.ClientSession() as session: async with session.post( f"{self._base_url}/remote-dispatch", @@ -467,27 +501,33 @@ async def dispatch_request( response["requestId"] = request_id - if response["completedAt"] is not None: - response_content = response["response"] - if response_content is not None: - # Populate the `structuredOutput` field. This is a client-side field - # that's not directly returned by the API. - output_data = response_content.get("output") - if ( - output_schema is not None - and output_data is not None - and output_data.get("type") == "structured" - ): - response_content["structuredOutput"] = ( - output_schema.model_validate(output_data["content"]) - ) - else: - response_content["structuredOutput"] = None - - return response - - # Poll every 3 seconds. - await asyncio.sleep(3) + if response["completedAt"] is None: + await _notify_input_required_callback( + on_input_required, + response, + seen_input_ids, + ) + # Poll every 3 seconds. + await asyncio.sleep(3) + continue + + response_content = response["response"] + if response_content is not None: + # Populate the `structuredOutput` field. This is a client-side field + # that's not directly returned by the API. + output_data = response_content.get("output") + if ( + output_schema is not None + and output_data is not None + and output_data.get("type") == "structured" + ): + response_content["structuredOutput"] = ( + output_schema.model_validate(output_data["content"]) + ) + else: + response_content["structuredOutput"] = None + + return response else: raise NaradaAgentTimeoutError_INTERNAL_DO_NOT_USE(timeout) @@ -511,6 +551,7 @@ async def agent( mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, input_variables: Mapping[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> AgentResponse[dict[str, Any]]: ... @@ -529,6 +570,7 @@ async def agent( mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, input_variables: Mapping[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, timeout: int = 1000, ) -> AgentResponse[_StructuredOutput]: ... @@ -546,6 +588,7 @@ async def agent( mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, input_variables: Mapping[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, critic: CriticConfig | None = None, timeout: int = 1000, ) -> AgentResponse[dict[str, Any]]: ... @@ -564,6 +607,7 @@ async def agent( mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, input_variables: Mapping[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, critic: CriticConfig | None = None, timeout: int = 1000, ) -> AgentResponse[_StructuredOutput]: ... @@ -582,6 +626,7 @@ async def agent( mcp_servers: list[McpServer] | None = None, secret_variables: dict[str, str] | None = None, input_variables: Mapping[str, Any] | None = None, + on_input_required: InputRequiredCallback | None = None, critic: CriticConfig | None = None, timeout: int = 1000, ) -> AgentResponse: @@ -602,6 +647,7 @@ async def agent( mcp_servers=mcp_servers, secret_variables=secret_variables, input_variables=input_variables, + on_input_required=on_input_required, timeout=timeout, ) else: @@ -629,6 +675,7 @@ async def agent( mcp_servers=mcp_servers, secret_variables=secret_variables, input_variables=input_variables, + on_input_required=on_input_required, timeout=timeout, ) response_content = remote_dispatch_response["response"] diff --git a/packages/narada/tests/test_cloud_browser.py b/packages/narada/tests/test_cloud_browser.py index db54062..6a466d3 100644 --- a/packages/narada/tests/test_cloud_browser.py +++ b/packages/narada/tests/test_cloud_browser.py @@ -4,6 +4,7 @@ import pytest from narada.client import Narada from narada.config import BrowserConfig +from narada.window import RemoteBrowserWindow from narada_core.errors import NaradaTimeoutError @@ -20,6 +21,9 @@ async def __aenter__(self): async def __aexit__(self, *args): pass + def raise_for_status(self): + return None + async def json(self): return self._payload @@ -43,6 +47,29 @@ def post(self, url: str, **kwargs): return _FakeResponse(self.payload) +class _RemoteDispatchFakeClientSession: + def __init__(self, poll_payloads: list[dict]) -> None: + self.poll_payloads = poll_payloads + self.dispatched_body: dict | None = None + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + pass + + def post(self, url: str, **kwargs): + if url.endswith("/remote-dispatch"): + self.dispatched_body = kwargs["json"] + return _FakeResponse({"requestId": "req-123"}) + raise AssertionError(f"Unexpected POST URL: {url}") + + def get(self, url: str, **kwargs): + if url.endswith("/remote-dispatch/responses/req-123"): + return _FakeResponse(self.poll_payloads.pop(0)) + raise AssertionError(f"Unexpected GET URL: {url}") + + def _build_client_with_cloud_page(page: AsyncMock) -> Narada: client = Narada(auth_headers={"x-api-key": "test-key"}) browser = SimpleNamespace(contexts=[SimpleNamespace(pages=[page])]) @@ -52,6 +79,96 @@ def _build_client_with_cloud_page(page: AsyncMock) -> Narada: return client +@pytest.mark.asyncio +async def test_dispatch_request_calls_input_required_callback_once_per_input_id( + monkeypatch: pytest.MonkeyPatch, +) -> None: + import narada.window as window_module + + fake_session = _RemoteDispatchFakeClientSession( + [ + { + "status": "input-required", + "response": None, + "usage": None, + "createdAt": "2026-01-01T00:00:00Z", + "completedAt": None, + "activeInputRequest": { + "inputId": "input-1", + "action": { + "name": "prompt_for_user_input", + "step_id": "prompt-1", + "variables": [ + {"name": "email", "type": "string", "required": True} + ], + }, + }, + }, + { + "status": "input-required", + "response": None, + "usage": None, + "createdAt": "2026-01-01T00:00:00Z", + "completedAt": None, + "activeInputRequest": { + "inputId": "input-1", + "action": { + "name": "prompt_for_user_input", + "step_id": "prompt-1", + "variables": [ + {"name": "email", "type": "string", "required": True} + ], + }, + }, + }, + { + "status": "input-required", + "response": None, + "usage": None, + "createdAt": "2026-01-01T00:00:00Z", + "completedAt": None, + "activeInputRequest": { + "inputId": "input-2", + "action": { + "name": "user_approval", + "step_id": "approval-1", + "prompt_message": "Approve?", + "approve_label": "Approve", + "reject_label": "Reject", + }, + }, + }, + { + "status": "success", + "response": {"text": "ok"}, + "usage": {"actions": 1, "credits": 1}, + "createdAt": "2026-01-01T00:00:00Z", + "completedAt": "2026-01-01T00:00:01Z", + }, + ] + ) + monkeypatch.setattr(window_module.aiohttp, "ClientSession", lambda: fake_session) + sleep = AsyncMock() + monkeypatch.setattr(window_module.asyncio, "sleep", sleep) + + observed_input_ids: list[str] = [] + + async def on_input_required(active_input_request) -> None: + observed_input_ids.append(active_input_request.input_id) + + window = RemoteBrowserWindow(browser_window_id="bw-1", api_key="test-key") + + response = await window.dispatch_request( + prompt="Summarize", + timeout=5, + on_input_required=on_input_required, + ) + + assert response["status"] == "success" + assert observed_input_ids == ["input-1", "input-2"] + assert sleep.await_count == 3 + + @pytest.mark.asyncio async def test_extensionless_cloud_browser_uses_backend_initialization( monkeypatch: pytest.MonkeyPatch, From 11daf190be9a1c4cd4b264a0a49c81c841da4e7f Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Thu, 21 May 2026 17:16:36 -0700 Subject: [PATCH 4/9] Type remote dispatch active input request --- packages/narada-core/src/narada_core/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/narada-core/src/narada_core/models.py b/packages/narada-core/src/narada_core/models.py index 3a06410..09db07e 100644 --- a/packages/narada-core/src/narada_core/models.py +++ b/packages/narada-core/src/narada_core/models.py @@ -1,10 +1,12 @@ from __future__ import annotations from enum import Enum, StrEnum -from typing import Annotated, Any, Generic, Literal, NotRequired, TypedDict, TypeVar +from typing import Annotated, Generic, Literal, NotRequired, TypedDict, TypeVar from pydantic import BaseModel, Field +from narada_core.actions.models import ActiveInputRequest + class Agent(Enum): PRODUCTIVITY = 1 @@ -413,7 +415,7 @@ class Response(TypedDict, Generic[_MaybeStructuredOutput]): createdAt: str completedAt: str | None usage: Usage - activeInputRequest: NotRequired[dict[str, Any]] + activeInputRequest: NotRequired[ActiveInputRequest] class File(TypedDict): From c3676c8d70bdbae2531eb92a5e861904838d6f68 Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Tue, 26 May 2026 15:51:27 -0700 Subject: [PATCH 5/9] Type remote dispatch active input request as nullable --- packages/narada-core/src/narada_core/models.py | 2 +- packages/narada-pyodide/tests/test_cloud_browser.py | 8 ++++++++ packages/narada/tests/test_cloud_browser.py | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/narada-core/src/narada_core/models.py b/packages/narada-core/src/narada_core/models.py index 09db07e..d6e53af 100644 --- a/packages/narada-core/src/narada_core/models.py +++ b/packages/narada-core/src/narada_core/models.py @@ -415,7 +415,7 @@ class Response(TypedDict, Generic[_MaybeStructuredOutput]): createdAt: str completedAt: str | None usage: Usage - activeInputRequest: NotRequired[ActiveInputRequest] + activeInputRequest: ActiveInputRequest | None class File(TypedDict): diff --git a/packages/narada-pyodide/tests/test_cloud_browser.py b/packages/narada-pyodide/tests/test_cloud_browser.py index a6bd398..4ebc734 100644 --- a/packages/narada-pyodide/tests/test_cloud_browser.py +++ b/packages/narada-pyodide/tests/test_cloud_browser.py @@ -258,6 +258,7 @@ async def test_cloud_browser_window_dispatch_request_omits_parent_run_ids( "status": "success", "completedAt": "2026-05-08T00:00:00+00:00", "response": None, + "activeInputRequest": None, } ), ] @@ -337,6 +338,7 @@ async def test_cloud_browser_window_dispatch_request_waits_through_active_input_ "status": "success", "completedAt": "2026-05-08T00:00:00+00:00", "response": None, + "activeInputRequest": None, } ), ] @@ -383,6 +385,7 @@ async def test_cloud_browser_window_dispatch_request_retries_poll_fetch_failures "status": "success", "completedAt": "2026-05-08T00:00:00+00:00", "response": None, + "activeInputRequest": None, } ), ] @@ -444,6 +447,7 @@ async def test_dispatch_request_emits_string_trace_agent_type_for_sdk_enum( "status": "success", "completedAt": "2026-05-08T00:00:00+00:00", "response": None, + "activeInputRequest": None, } ), ] @@ -490,6 +494,7 @@ async def test_dispatch_request_emits_success_text_in_sub_agent_trace( "text": "TRACE_CORE_AGENT_DONE", "actionTrace": [], }, + "activeInputRequest": None, } ), ] @@ -568,6 +573,7 @@ async def test_cloud_browser_window_dispatch_request_preserves_current_file_vari "status": "success", "completedAt": "2026-05-08T00:00:00+00:00", "response": None, + "activeInputRequest": None, } ), ] @@ -773,6 +779,7 @@ async def test_local_browser_window_dispatch_request_uses_latest_parent_run_ids( "status": "success", "completedAt": "2026-05-08T00:00:00+00:00", "response": None, + "activeInputRequest": None, } ), _FakeResponse(json_data={"requestId": "req-2"}), @@ -781,6 +788,7 @@ async def test_local_browser_window_dispatch_request_uses_latest_parent_run_ids( "status": "success", "completedAt": "2026-05-08T00:00:00+00:00", "response": None, + "activeInputRequest": None, } ), ] diff --git a/packages/narada/tests/test_cloud_browser.py b/packages/narada/tests/test_cloud_browser.py index 6a466d3..3f2fdb7 100644 --- a/packages/narada/tests/test_cloud_browser.py +++ b/packages/narada/tests/test_cloud_browser.py @@ -144,6 +144,7 @@ async def test_dispatch_request_calls_input_required_callback_once_per_input_id( "usage": {"actions": 1, "credits": 1}, "createdAt": "2026-01-01T00:00:00Z", "completedAt": "2026-01-01T00:00:01Z", + "activeInputRequest": None, }, ] ) From ec85822326ba41111a369181670312954a415188 Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Tue, 26 May 2026 16:04:59 -0700 Subject: [PATCH 6/9] Type internal remote dispatch poll responses --- packages/narada-core/src/narada_core/models.py | 12 +++++++++++- packages/narada-pyodide/src/narada/window.py | 7 ++++--- packages/narada/src/narada/window.py | 8 +++++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/narada-core/src/narada_core/models.py b/packages/narada-core/src/narada_core/models.py index d6e53af..91361dd 100644 --- a/packages/narada-core/src/narada_core/models.py +++ b/packages/narada-core/src/narada_core/models.py @@ -1,7 +1,7 @@ from __future__ import annotations from enum import Enum, StrEnum -from typing import Annotated, Generic, Literal, NotRequired, TypedDict, TypeVar +from typing import Annotated, Any, Generic, Literal, NotRequired, TypedDict, TypeVar from pydantic import BaseModel, Field @@ -418,6 +418,16 @@ class Response(TypedDict, Generic[_MaybeStructuredOutput]): activeInputRequest: ActiveInputRequest | None +class _RemoteDispatchPollResponse(TypedDict): + requestId: str + status: Literal["pending", "input-required", "success", "error"] + response: dict[str, Any] | None + createdAt: str + completedAt: str | None + usage: Usage | None + activeInputRequest: ActiveInputRequest | None + + class File(TypedDict): key: str diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index ceb6940..b19779e 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -79,6 +79,7 @@ RemoteDispatchChatHistoryItem, Response, UserResourceCredentials, + _RemoteDispatchPollResponse, ) from narada_core.tracing.model import parse_action_trace from pydantic import BaseModel @@ -119,7 +120,7 @@ async def _narada_get_id_token() -> str: ... async def _notify_input_required_callback( callback: InputRequiredCallback | None, - response: dict[str, Any], + response: _RemoteDispatchPollResponse, seen_input_ids: set[str], ) -> None: if callback is None or response.get("status") != "input-required": @@ -498,7 +499,7 @@ async def dispatch_request( text = await fetch_response.text() raise NaradaError(f"Failed to poll for response: {status} {text}") - response = await fetch_response.json() + response: _RemoteDispatchPollResponse = await fetch_response.json() response["requestId"] = request_id if response["completedAt"] is None: @@ -554,7 +555,7 @@ async def dispatch_request( else None ), ) - return response + return cast(Response, response) else: raise NaradaAgentTimeoutError_INTERNAL_DO_NOT_USE(timeout) diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index 363ef52..f11e4af 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -19,6 +19,7 @@ TypedDict, TypeGuard, TypeVar, + cast, overload, override, ) @@ -80,6 +81,7 @@ RemoteDispatchChatHistoryItem, Response, UserResourceCredentials, + _RemoteDispatchPollResponse, ) from narada_core.tracing.model import parse_action_trace from playwright.async_api import ( @@ -101,7 +103,7 @@ async def _notify_input_required_callback( callback: InputRequiredCallback | None, - response: Mapping[str, Any], + response: _RemoteDispatchPollResponse, seen_input_ids: set[str], ) -> None: if callback is None or response.get("status") != "input-required": @@ -498,7 +500,7 @@ async def dispatch_request( timeout=aiohttp.ClientTimeout(total=deadline - now), ) as resp: resp.raise_for_status() - response = await resp.json() + response: _RemoteDispatchPollResponse = await resp.json() response["requestId"] = request_id @@ -528,7 +530,7 @@ async def dispatch_request( else: response_content["structuredOutput"] = None - return response + return cast(Response, response) else: raise NaradaAgentTimeoutError_INTERNAL_DO_NOT_USE(timeout) From d3b1975b17054f7ac6c2097e1fb363f4c20f0b3d Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Tue, 26 May 2026 16:17:00 -0700 Subject: [PATCH 7/9] Document remote dispatch runtime env vars --- packages/narada-pyodide/src/narada/window.py | 10 ++++++++-- packages/narada/src/narada/window.py | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index b19779e..2e0e577 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -115,6 +115,12 @@ async def _narada_get_id_token() -> str: ... _ResponseModel = TypeVar("_ResponseModel", bound=BaseModel) +# Optional remote-dispatch context. In frontend Pyodide runs, these are generated +# by prepare-code.ts; extension-action calls forward them so the parent request +# can report active input-required status. +_REMOTE_DISPATCH_REQUEST_ID_ENV_VAR = "NARADA_REMOTE_DISPATCH_REQUEST_ID" +_REMOTE_DISPATCH_API_KEY_ID_ENV_VAR = "NARADA_REMOTE_DISPATCH_API_KEY_ID" + type InputRequiredCallback = Callable[[ActiveInputRequest], Awaitable[None] | None] @@ -1036,12 +1042,12 @@ async def _run_extension_action( "browserWindowId": self.browser_window_id, } remote_dispatch_request_id = os.environ.get( - "NARADA_REMOTE_DISPATCH_REQUEST_ID" + _REMOTE_DISPATCH_REQUEST_ID_ENV_VAR ) if remote_dispatch_request_id is not None: body["requestId"] = remote_dispatch_request_id remote_dispatch_api_key_id = os.environ.get( - "NARADA_REMOTE_DISPATCH_API_KEY_ID" + _REMOTE_DISPATCH_API_KEY_ID_ENV_VAR ) if remote_dispatch_api_key_id is not None: body["apiKeyId"] = remote_dispatch_api_key_id diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index f11e4af..9d0e65f 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -98,6 +98,12 @@ _ResponseModel = TypeVar("_ResponseModel", bound=BaseModel) +# Optional remote-dispatch context. In frontend Pyodide runs, these are generated +# by prepare-code.ts; extension-action calls forward them so the parent request +# can report active input-required status. +_REMOTE_DISPATCH_REQUEST_ID_ENV_VAR = "NARADA_REMOTE_DISPATCH_REQUEST_ID" +_REMOTE_DISPATCH_API_KEY_ID_ENV_VAR = "NARADA_REMOTE_DISPATCH_API_KEY_ID" + type InputRequiredCallback = Callable[[ActiveInputRequest], Awaitable[None] | None] @@ -980,10 +986,14 @@ async def _run_extension_action( "action": request.model_dump(), "browserWindowId": self.browser_window_id, } - remote_dispatch_request_id = os.environ.get("NARADA_REMOTE_DISPATCH_REQUEST_ID") + remote_dispatch_request_id = os.environ.get( + _REMOTE_DISPATCH_REQUEST_ID_ENV_VAR + ) if remote_dispatch_request_id is not None: body["requestId"] = remote_dispatch_request_id - remote_dispatch_api_key_id = os.environ.get("NARADA_REMOTE_DISPATCH_API_KEY_ID") + remote_dispatch_api_key_id = os.environ.get( + _REMOTE_DISPATCH_API_KEY_ID_ENV_VAR + ) if remote_dispatch_api_key_id is not None: body["apiKeyId"] = remote_dispatch_api_key_id if timeout is not None: From 63cf060e2486a50076b99aa0196f63c9cdfeee35 Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Tue, 26 May 2026 16:22:49 -0700 Subject: [PATCH 8/9] Format remote dispatch env var reads --- packages/narada/src/narada/window.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index 9d0e65f..43c3f56 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -986,14 +986,10 @@ async def _run_extension_action( "action": request.model_dump(), "browserWindowId": self.browser_window_id, } - remote_dispatch_request_id = os.environ.get( - _REMOTE_DISPATCH_REQUEST_ID_ENV_VAR - ) + remote_dispatch_request_id = os.environ.get(_REMOTE_DISPATCH_REQUEST_ID_ENV_VAR) if remote_dispatch_request_id is not None: body["requestId"] = remote_dispatch_request_id - remote_dispatch_api_key_id = os.environ.get( - _REMOTE_DISPATCH_API_KEY_ID_ENV_VAR - ) + remote_dispatch_api_key_id = os.environ.get(_REMOTE_DISPATCH_API_KEY_ID_ENV_VAR) if remote_dispatch_api_key_id is not None: body["apiKeyId"] = remote_dispatch_api_key_id if timeout is not None: From c7e3a2c84b2261a4bf393d78c36b18ee6c268b91 Mon Sep 17 00:00:00 2001 From: Arnav Brahmasandra Date: Thu, 28 May 2026 12:37:35 -0700 Subject: [PATCH 9/9] Fix SDK import order after merge --- packages/narada/src/narada/window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index be61378..b1eb2f2 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -29,10 +29,10 @@ from narada_core.actions.models import ( DEFAULT_HITL_TIMEOUT_SECONDS, ActiveInputRequest, - AgenticMouseAction, - AgenticMouseActionRequest, AgenticMatchingSelectorsFinderRequest, AgenticMatchingSelectorsFinderResponse, + AgenticMouseAction, + AgenticMouseActionRequest, AgenticSelectorAction, AgenticSelectorRequest, AgenticSelectorResponse,