Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/narada-core/src/narada_core/actions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@
override,
)

from pydantic import (
BaseModel,
Field,
)
from pydantic import BaseModel, ConfigDict, Field

from narada_core.tracing import model as tracing_model

Expand Down Expand Up @@ -50,6 +47,8 @@ class CriticResult(BaseModel):


class AgentResponse(BaseModel, Generic[_StructuredOutputT]):
model_config = ConfigDict(populate_by_name=True)

request_id: str
status: Literal["success", "error", "input-required"]
text: str
Expand All @@ -60,6 +59,7 @@ class AgentResponse(BaseModel, Generic[_StructuredOutputT]):
]
usage: AgentUsage
action_trace: tracing_model.ActionTrace | None = None
workflow_trace: dict[str, Any] | None = Field(default=None, alias="workflowTrace")
critic_result: CriticResult | None = None


Expand Down Expand Up @@ -458,3 +458,5 @@ class ExtensionActionResponse(BaseModel):
status: Literal["success", "error", "aborted"]
error: str | None = None
data: str | None = None
action_trace: tracing_model.ActionTrace | None = None
workflowTrace: dict[str, Any] | None = None
Comment thread
sebzhao marked this conversation as resolved.
Comment thread
sebzhao marked this conversation as resolved.
3 changes: 2 additions & 1 deletion packages/narada-core/src/narada_core/models.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -399,6 +399,7 @@ class ResponseContent(TypedDict, Generic[_MaybeStructuredOutput]):
text: str
structuredOutput: _MaybeStructuredOutput
actionTrace: NotRequired[ActionTrace]
workflowTrace: NotRequired[dict[str, Any]]


class Usage(TypedDict):
Expand Down
7 changes: 7 additions & 0 deletions packages/narada-core/src/narada_core/tracing/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ class RunCustomAgentTrace(BaseModel):
workflow_name: str
status: Literal["success", "error"]
error_message: str | None = None
subWorkflow: dict[str, Any] | None = None
children: ActionTrace | None = None


Expand Down Expand Up @@ -302,6 +303,11 @@ def _check_ts_ordering(self) -> PythonExtensionActionEvent:
return self


class PythonSubWorkflowEvent(BaseModel):
kind: Literal["subWorkflow"] = "subWorkflow"
workflowTrace: dict[str, Any]
Comment thread
sebzhao marked this conversation as resolved.
Comment thread
sebzhao marked this conversation as resolved.
Comment thread
sebzhao marked this conversation as resolved.


class PythonSideEffectEvent(BaseModel):
kind: Literal["sideEffect"] = "sideEffect"
ts: int
Expand All @@ -314,6 +320,7 @@ class PythonSideEffectEvent(BaseModel):
| PythonStderrEvent
| PythonSubAgentCallEvent
| PythonExtensionActionEvent
| PythonSubWorkflowEvent
| PythonSideEffectEvent,
Field(discriminator="kind"),
]
Expand Down
9 changes: 9 additions & 0 deletions packages/narada-pyodide/src/narada/_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ def emit_extension_action(
emit_trace_event(event)


def emit_sub_workflow(*, workflow_trace: dict[str, Any]) -> None:
emit_trace_event(
{
"kind": "subWorkflow",
"workflowTrace": workflow_trace,
}
)


def emit_side_effect(*, effect_type: SideEffectType, description: str) -> None:
emit_trace_event(
{
Expand Down
34 changes: 32 additions & 2 deletions packages/narada-pyodide/src/narada/window.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import builtins
import json
import logging
import os
Expand All @@ -23,10 +24,10 @@
from narada_core.actions.critic import run_critic
from narada_core.actions.models import (
DEFAULT_HITL_TIMEOUT_SECONDS,
AgenticMouseAction,
AgenticMouseActionRequest,
AgenticMatchingSelectorsFinderRequest,
AgenticMatchingSelectorsFinderResponse,
AgenticMouseAction,
AgenticMouseActionRequest,
AgenticSelectorAction,
AgenticSelectorRequest,
AgenticSelectorResponse,
Expand Down Expand Up @@ -103,10 +104,20 @@ def _parent_run_ids() -> list[str]:
)


def _parent_request_id() -> str | None:
parent_request_id = getattr(builtins, "_narada_request_id", None)
if isinstance(parent_request_id, str):
return parent_request_id
parent_request_id = globals().get("_narada_request_id")
return parent_request_id if isinstance(parent_request_id, str) else None


if TYPE_CHECKING:
# Magic function injected by the JavaScript harness to get the current user's ID token.
async def _narada_get_id_token() -> str: ...

_narada_request_id: str | None


_StructuredOutput = TypeVar("_StructuredOutput", bound=BaseModel)

Expand Down Expand Up @@ -215,6 +226,10 @@ def _current_parent_run_ids(self) -> list[str] | None:
"""
return None

def _current_parent_request_id(self) -> str | None:
"""Returns the remote-dispatch request that owns the current Python execution."""
return _parent_request_id()

async def _get_auth_headers(self) -> dict[str, str]:
return await _build_auth_headers(
api_key=self._api_key,
Expand Down Expand Up @@ -387,6 +402,9 @@ async def dispatch_request(
parent_run_ids = self._current_parent_run_ids()
if parent_run_ids:
body["parentRunIds"] = parent_run_ids
parent_request_id = self._current_parent_request_id()
if parent_request_id is not None:
body["parentRequestId"] = parent_request_id
cloud_browser_session_id = self.cloud_browser_session_id
if cloud_browser_session_id is not None:
body["cloudBrowserSessionId"] = cloud_browser_session_id
Expand Down Expand Up @@ -689,6 +707,12 @@ async def agent(
if action_trace_raw is not None
else None
)
workflow_trace = response_content.get("workflowTrace")
parent_request_id = self._current_parent_request_id()
# Preserve the response contract for direct callers, but avoid adding a second
# child node when the backend will stitch the child request into the parent row.
if workflow_trace is not None and parent_request_id is None:
_trace.emit_sub_workflow(workflow_trace=workflow_trace)

critic_result: CriticResult | None = None
if critic is not None:
Expand All @@ -710,6 +734,7 @@ async def agent(
structured_output=response_content.get("structuredOutput"),
usage=AgentUsage.model_validate(remote_dispatch_response["usage"]),
action_trace=action_trace,
workflow_trace=workflow_trace,
critic_result=critic_result,
)

Expand Down Expand Up @@ -1008,6 +1033,9 @@ async def _run_extension_action(
parent_run_ids = self._current_parent_run_ids()
if parent_run_ids:
body["parentRunIds"] = parent_run_ids
parent_request_id = self._current_parent_request_id()
if parent_request_id is not None:
body["requestId"] = parent_request_id
if timeout is not None:
body["timeout"] = timeout

Expand All @@ -1029,6 +1057,8 @@ async def _run_extension_action(
resp_json = await fetch_response.json()

response = ExtensionActionResponse.model_validate(resp_json)
if response.workflowTrace is not None:
Comment thread
sebzhao marked this conversation as resolved.
_trace.emit_sub_workflow(workflow_trace=response.workflowTrace)
Comment thread
sebzhao marked this conversation as resolved.
if response.status == "error":
raise NaradaError(response.error)
if response.status == "aborted":
Expand Down
Loading
Loading