From d32b8fd9c0ae2ab9712edbd8ba598836444cf6f1 Mon Sep 17 00:00:00 2001 From: Pavlo Haidar Date: Thu, 5 Mar 2026 18:44:35 +0200 Subject: [PATCH 1/7] Unify output format --- .../src/narada_core/actions/models.py | 31 ++++++++++++++++++- .../narada-core/src/narada_core/models.py | 6 ++++ packages/narada-pyodide/src/narada/window.py | 16 ++++++---- packages/narada/src/narada/window.py | 18 ++++++----- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/packages/narada-core/src/narada_core/actions/models.py b/packages/narada-core/src/narada_core/actions/models.py index 71025c5..0bc8903 100644 --- a/packages/narada-core/src/narada_core/actions/models.py +++ b/packages/narada-core/src/narada_core/actions/models.py @@ -209,6 +209,12 @@ class ObjectSetPropertiesTrace(BaseModel): description: str +class ReturnTrace(BaseModel): + step_type: Literal["return"] + url: str + description: str + + ApaStepTrace = Annotated[ GoToUrlTrace | GetUrlTrace @@ -237,7 +243,8 @@ class ObjectSetPropertiesTrace(BaseModel): | WaitTrace | DataTableInsertRowTrace | DataTableUpdateCellValueTrace - | ObjectSetPropertiesTrace, + | ObjectSetPropertiesTrace + | ReturnTrace, Field(discriminator="step_type"), ] @@ -259,11 +266,33 @@ def parse_action_trace(trace_data: list[dict[str, Any] | Any]) -> ActionTrace: return _ApaActionTraceAdapter.validate_python(trace_data) +class TextOutput(BaseModel): + type: Literal["text"] + value: str + + +class CustomAgentOutput(BaseModel): + type: Literal["custom-agent-result"] + value: dict[str, Any] + + +class StructuredOutput(BaseModel, Generic[_MaybeStructuredOutput]): + type: Literal["structured-output"] + value: _MaybeStructuredOutput + + +AgentOutput = Annotated[ + TextOutput | CustomAgentOutput | StructuredOutput[Any], + Field(discriminator="type"), +] + + class AgentResponse(BaseModel, Generic[_MaybeStructuredOutput]): request_id: str status: Literal["success", "error", "input-required"] text: str structured_output: _MaybeStructuredOutput | None + output: AgentOutput | None = None usage: AgentUsage action_trace: ActionTrace | None = None diff --git a/packages/narada-core/src/narada_core/models.py b/packages/narada-core/src/narada_core/models.py index 692e19e..824ed45 100644 --- a/packages/narada-core/src/narada_core/models.py +++ b/packages/narada-core/src/narada_core/models.py @@ -176,6 +176,11 @@ class ReadCsvTrace(TypedDict): description: str +class ReturnTrace(TypedDict): + step_type: Literal["return"] + description: str + + class StartTrace(TypedDict): step_type: Literal["start"] url: str @@ -280,6 +285,7 @@ class ObjectSetPropertiesTrace(TypedDict): | GetSimplifiedHtmlTrace | GetScreenshotTrace | RunCustomAgentTrace + | ReturnTrace | IfTrace | SetVariableTrace | WaitTrace diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index 95aa4ba..fc6dc59 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -288,13 +288,16 @@ async def dispatch_request( if response_content is not None: # Populate the `structuredOutput` field. This is a client-side field # that's not directly returned by the API. - if output_schema is None: - response_content["structuredOutput"] = None - else: - structured_output = output_schema.model_validate_json( - response_content["text"] + output_data = response_content.get("output") + if ( + output_schema is not None + and output_data.get("type") == "structured-output" + ): + response_content["structuredOutput"] = ( + output_schema.model_validate(output_data["value"]) ) - response_content["structuredOutput"] = structured_output + else: + response_content["structuredOutput"] = None return response @@ -375,6 +378,7 @@ async def agent( request_id=remote_dispatch_response["requestId"], status=remote_dispatch_response["status"], text=response_content["text"], + output=response_content.get("output"), structured_output=response_content.get("structuredOutput"), usage=AgentUsage.model_validate(remote_dispatch_response["usage"]), action_trace=action_trace, diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index 1919839..b6e398b 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -260,15 +260,16 @@ async def dispatch_request( if response["status"] != "pending": 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. - if output_schema is None: - response_content["structuredOutput"] = None - else: - structured_output = output_schema.model_validate_json( - response_content["text"] + output_data = response_content.get("output") + if ( + output_schema is not None + and output_data.get("type") == "structured-output" + ): + response_content["structuredOutput"] = ( + output_schema.model_validate(output_data["value"]) ) - response_content["structuredOutput"] = structured_output + else: + response_content["structuredOutput"] = None return response @@ -353,6 +354,7 @@ async def agent( request_id=remote_dispatch_response["requestId"], status=remote_dispatch_response["status"], text=response_content["text"], + output=response_content["output"], structured_output=response_content.get("structuredOutput"), usage=AgentUsage.model_validate(remote_dispatch_response["usage"]), action_trace=action_trace, From 5b4e7e701e654eede3c04f5466bf638405d1d203 Mon Sep 17 00:00:00 2001 From: Pavlo Haidar Date: Fri, 6 Mar 2026 14:35:50 +0200 Subject: [PATCH 2/7] update output type --- .../narada-core/src/narada_core/actions/models.py | 11 +++-------- packages/narada-pyodide/src/narada/window.py | 2 +- packages/narada/src/narada/window.py | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/narada-core/src/narada_core/actions/models.py b/packages/narada-core/src/narada_core/actions/models.py index 0bc8903..aa3cfd6 100644 --- a/packages/narada-core/src/narada_core/actions/models.py +++ b/packages/narada-core/src/narada_core/actions/models.py @@ -271,18 +271,13 @@ class TextOutput(BaseModel): value: str -class CustomAgentOutput(BaseModel): - type: Literal["custom-agent-result"] +class StructuredOutput(BaseModel): + type: Literal["structured"] value: dict[str, Any] -class StructuredOutput(BaseModel, Generic[_MaybeStructuredOutput]): - type: Literal["structured-output"] - value: _MaybeStructuredOutput - - AgentOutput = Annotated[ - TextOutput | CustomAgentOutput | StructuredOutput[Any], + TextOutput | StructuredOutput, Field(discriminator="type"), ] diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index fc6dc59..6e56202 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -291,7 +291,7 @@ async def dispatch_request( output_data = response_content.get("output") if ( output_schema is not None - and output_data.get("type") == "structured-output" + and output_data.get("type") == "structured" ): response_content["structuredOutput"] = ( output_schema.model_validate(output_data["value"]) diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index b6e398b..f1dcbf4 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -263,7 +263,7 @@ async def dispatch_request( output_data = response_content.get("output") if ( output_schema is not None - and output_data.get("type") == "structured-output" + and output_data.get("type") == "structured" ): response_content["structuredOutput"] = ( output_schema.model_validate(output_data["value"]) From 239413bac13f570e0c9c9dd47a4ab4514f1538a0 Mon Sep 17 00:00:00 2001 From: Pavlo Haidar Date: Tue, 10 Mar 2026 13:36:32 +0200 Subject: [PATCH 3/7] update output type --- .../narada-core/src/narada_core/actions/models.py | 11 +++++------ packages/narada-core/src/narada_core/models.py | 6 +++--- packages/narada/src/narada/window.py | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/narada-core/src/narada_core/actions/models.py b/packages/narada-core/src/narada_core/actions/models.py index aa3cfd6..6fb9ac5 100644 --- a/packages/narada-core/src/narada_core/actions/models.py +++ b/packages/narada-core/src/narada_core/actions/models.py @@ -209,9 +209,8 @@ class ObjectSetPropertiesTrace(BaseModel): description: str -class ReturnTrace(BaseModel): - step_type: Literal["return"] - url: str +class OutputTrace(BaseModel): + step_type: Literal["output"] description: str @@ -244,7 +243,7 @@ class ReturnTrace(BaseModel): | DataTableInsertRowTrace | DataTableUpdateCellValueTrace | ObjectSetPropertiesTrace - | ReturnTrace, + | OutputTrace, Field(discriminator="step_type"), ] @@ -268,12 +267,12 @@ def parse_action_trace(trace_data: list[dict[str, Any] | Any]) -> ActionTrace: class TextOutput(BaseModel): type: Literal["text"] - value: str + content: str class StructuredOutput(BaseModel): type: Literal["structured"] - value: dict[str, Any] + content: dict[str, Any] AgentOutput = Annotated[ diff --git a/packages/narada-core/src/narada_core/models.py b/packages/narada-core/src/narada_core/models.py index 824ed45..0e3ca3b 100644 --- a/packages/narada-core/src/narada_core/models.py +++ b/packages/narada-core/src/narada_core/models.py @@ -176,8 +176,8 @@ class ReadCsvTrace(TypedDict): description: str -class ReturnTrace(TypedDict): - step_type: Literal["return"] +class OutputTrace(TypedDict): + step_type: Literal["output"] description: str @@ -285,7 +285,7 @@ class ObjectSetPropertiesTrace(TypedDict): | GetSimplifiedHtmlTrace | GetScreenshotTrace | RunCustomAgentTrace - | ReturnTrace + | OutputTrace | IfTrace | SetVariableTrace | WaitTrace diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index f1dcbf4..3e1d976 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -266,7 +266,7 @@ async def dispatch_request( and output_data.get("type") == "structured" ): response_content["structuredOutput"] = ( - output_schema.model_validate(output_data["value"]) + output_schema.model_validate(output_data["content"]) ) else: response_content["structuredOutput"] = None @@ -354,7 +354,7 @@ async def agent( request_id=remote_dispatch_response["requestId"], status=remote_dispatch_response["status"], text=response_content["text"], - output=response_content["output"], + output=response_content.get("output"), structured_output=response_content.get("structuredOutput"), usage=AgentUsage.model_validate(remote_dispatch_response["usage"]), action_trace=action_trace, From 45e2aed8b012a14df86e3266bc99707b6da3713c Mon Sep 17 00:00:00 2001 From: Pavlo Haidar Date: Tue, 10 Mar 2026 13:38:24 +0200 Subject: [PATCH 4/7] use content --- packages/narada-pyodide/src/narada/window.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index 6e56202..b2b0297 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -257,7 +257,8 @@ async def dispatch_request( if not fetch_response.ok: status = fetch_response.status text = await fetch_response.text() - raise NaradaError(f"Failed to dispatch request: {status} {text}") + raise NaradaError( + f"Failed to dispatch request: {status} {text}") request_id = (await fetch_response.json())["requestId"] @@ -278,7 +279,8 @@ async def dispatch_request( if not fetch_response.ok: status = fetch_response.status text = await fetch_response.text() - raise NaradaError(f"Failed to poll for response: {status} {text}") + raise NaradaError( + f"Failed to poll for response: {status} {text}") response = await fetch_response.json() response["requestId"] = request_id @@ -294,7 +296,8 @@ async def dispatch_request( and output_data.get("type") == "structured" ): response_content["structuredOutput"] = ( - output_schema.model_validate(output_data["value"]) + output_schema.model_validate( + output_data["content"]) ) else: response_content["structuredOutput"] = None @@ -584,7 +587,8 @@ async def _run_extension_action( elif not fetch_response.ok: status = fetch_response.status text = await fetch_response.text() - raise NaradaError(f"Failed to run extension action: {status} {text}") + raise NaradaError( + f"Failed to run extension action: {status} {text}") resp_json = await fetch_response.json() @@ -607,7 +611,8 @@ def __init__(self) -> None: super().__init__( api_key=os.environ.get("NARADA_API_KEY"), - base_url=os.getenv("NARADA_API_BASE_URL", "https://api.narada.ai/fast/v2"), + base_url=os.getenv("NARADA_API_BASE_URL", + "https://api.narada.ai/fast/v2"), user_id=os.environ.get("NARADA_USER_ID"), env=env, browser_window_id=os.environ["NARADA_BROWSER_WINDOW_ID"], @@ -621,7 +626,8 @@ class RemoteBrowserWindow(BaseBrowserWindow): def __init__(self, *, browser_window_id: str, api_key: str | None = None) -> None: super().__init__( api_key=api_key or os.environ["NARADA_API_KEY"], - base_url=os.getenv("NARADA_API_BASE_URL", "https://api.narada.ai/fast/v2"), + base_url=os.getenv("NARADA_API_BASE_URL", + "https://api.narada.ai/fast/v2"), user_id=None, env=None, browser_window_id=browser_window_id, From d2310259a746aa0f6e5ea6116e6d1e4d09f1a0b7 Mon Sep 17 00:00:00 2001 From: Pavlo Haidar Date: Tue, 10 Mar 2026 13:39:11 +0200 Subject: [PATCH 5/7] format --- packages/narada-pyodide/src/narada/window.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index b2b0297..2af7b6d 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -257,8 +257,7 @@ async def dispatch_request( if not fetch_response.ok: status = fetch_response.status text = await fetch_response.text() - raise NaradaError( - f"Failed to dispatch request: {status} {text}") + raise NaradaError(f"Failed to dispatch request: {status} {text}") request_id = (await fetch_response.json())["requestId"] @@ -279,8 +278,7 @@ async def dispatch_request( if not fetch_response.ok: status = fetch_response.status text = await fetch_response.text() - raise NaradaError( - f"Failed to poll for response: {status} {text}") + raise NaradaError(f"Failed to poll for response: {status} {text}") response = await fetch_response.json() response["requestId"] = request_id @@ -296,8 +294,7 @@ async def dispatch_request( and output_data.get("type") == "structured" ): response_content["structuredOutput"] = ( - output_schema.model_validate( - output_data["content"]) + output_schema.model_validate(output_data["content"]) ) else: response_content["structuredOutput"] = None @@ -587,8 +584,7 @@ async def _run_extension_action( elif not fetch_response.ok: status = fetch_response.status text = await fetch_response.text() - raise NaradaError( - f"Failed to run extension action: {status} {text}") + raise NaradaError(f"Failed to run extension action: {status} {text}") resp_json = await fetch_response.json() @@ -611,8 +607,7 @@ def __init__(self) -> None: super().__init__( api_key=os.environ.get("NARADA_API_KEY"), - base_url=os.getenv("NARADA_API_BASE_URL", - "https://api.narada.ai/fast/v2"), + base_url=os.getenv("NARADA_API_BASE_URL", "https://api.narada.ai/fast/v2"), user_id=os.environ.get("NARADA_USER_ID"), env=env, browser_window_id=os.environ["NARADA_BROWSER_WINDOW_ID"], @@ -626,8 +621,7 @@ class RemoteBrowserWindow(BaseBrowserWindow): def __init__(self, *, browser_window_id: str, api_key: str | None = None) -> None: super().__init__( api_key=api_key or os.environ["NARADA_API_KEY"], - base_url=os.getenv("NARADA_API_BASE_URL", - "https://api.narada.ai/fast/v2"), + base_url=os.getenv("NARADA_API_BASE_URL", "https://api.narada.ai/fast/v2"), user_id=None, env=None, browser_window_id=browser_window_id, From a886dc9059b771cf7f8cb3a3743b910f0b701440 Mon Sep 17 00:00:00 2001 From: Pavlo Haidar Date: Wed, 11 Mar 2026 15:35:56 +0200 Subject: [PATCH 6/7] Addressed comments --- .../src/narada_core/actions/models.py | 21 ++++++++----------- packages/narada-pyodide/src/narada/window.py | 3 ++- packages/narada/src/narada/window.py | 5 +++-- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/narada-core/src/narada_core/actions/models.py b/packages/narada-core/src/narada_core/actions/models.py index 6fb9ac5..6e68db6 100644 --- a/packages/narada-core/src/narada_core/actions/models.py +++ b/packages/narada-core/src/narada_core/actions/models.py @@ -17,7 +17,7 @@ # There is no `AgentRequest` because the `agent` action delegates to the `dispatch_request` method # under the hood. -_MaybeStructuredOutput = TypeVar("_MaybeStructuredOutput", bound=BaseModel | None) +_StructuredOutputT = TypeVar("_StructuredOutputT") class AgentUsage(BaseModel): @@ -270,23 +270,20 @@ class TextOutput(BaseModel): content: str -class StructuredOutput(BaseModel): +class StructuredOutput(BaseModel, Generic[_StructuredOutputT]): type: Literal["structured"] - content: dict[str, Any] + content: _StructuredOutputT -AgentOutput = Annotated[ - TextOutput | StructuredOutput, - Field(discriminator="type"), -] - - -class AgentResponse(BaseModel, Generic[_MaybeStructuredOutput]): +class AgentResponse(BaseModel, Generic[_StructuredOutputT]): request_id: str status: Literal["success", "error", "input-required"] text: str - structured_output: _MaybeStructuredOutput | None - output: AgentOutput | None = None + structured_output: _StructuredOutputT | None + output: Annotated[ + TextOutput | StructuredOutput[_StructuredOutputT], + Field(discriminator="type"), + ] usage: AgentUsage action_trace: ActionTrace | None = None diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index 2af7b6d..e7bd3dd 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -291,6 +291,7 @@ async def dispatch_request( 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"] = ( @@ -322,7 +323,7 @@ async def agent( mcp_servers: list[McpServer] | None = None, variables: dict[str, str] | None = None, timeout: int = 1000, - ) -> AgentResponse[None]: ... + ) -> AgentResponse[dict[str, Any]]: ... @overload async def agent( diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index 3e1d976..65e8e42 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -263,6 +263,7 @@ async def dispatch_request( 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"] = ( @@ -295,7 +296,7 @@ async def agent( mcp_servers: list[McpServer] | None = None, variables: dict[str, str] | None = None, timeout: int = 1000, - ) -> AgentResponse[None]: ... + ) -> AgentResponse[dict[str, Any]]: ... @overload async def agent( @@ -354,7 +355,7 @@ async def agent( request_id=remote_dispatch_response["requestId"], status=remote_dispatch_response["status"], text=response_content["text"], - output=response_content.get("output"), + output=response_content["output"], structured_output=response_content.get("structuredOutput"), usage=AgentUsage.model_validate(remote_dispatch_response["usage"]), action_trace=action_trace, From a4f6b8d0c5d323f8a4f8866fb0b4311c6b29028d Mon Sep 17 00:00:00 2001 From: Pavlo Haidar Date: Wed, 11 Mar 2026 19:11:59 +0200 Subject: [PATCH 7/7] revert comment --- packages/narada/src/narada/window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index 65e8e42..5a5a007 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -260,6 +260,8 @@ async def dispatch_request( if response["status"] != "pending": 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