narada 0.2.7__tar.gz → 0.2.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: narada
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary: Python client SDK for Narada
5
5
  Project-URL: Homepage, https://github.com/NaradaAI/narada-python-sdk/narada
6
6
  Project-URL: Repository, https://github.com/NaradaAI/narada-python-sdk
@@ -9,7 +9,7 @@ Author-email: Narada <support@narada.ai>
9
9
  License-Expression: Apache-2.0
10
10
  Requires-Python: >=3.12
11
11
  Requires-Dist: aiohttp>=3.12.13
12
- Requires-Dist: narada-core==0.1.4
12
+ Requires-Dist: narada-core==0.1.6
13
13
  Requires-Dist: packaging==24.2
14
14
  Requires-Dist: playwright>=1.53.0
15
15
  Requires-Dist: rich>=14.0.0
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "narada"
3
- version = "0.2.7"
3
+ version = "0.2.9"
4
4
  description = "Python client SDK for Narada"
5
5
  license = "Apache-2.0"
6
6
  readme = "README.md"
7
7
  authors = [{ name = "Narada", email = "support@narada.ai" }]
8
8
  requires-python = ">=3.12"
9
9
  dependencies = [
10
- "narada-core==0.1.4",
10
+ "narada-core==0.1.6",
11
11
  "aiohttp>=3.12.13",
12
12
  "playwright>=1.53.0",
13
13
  "rich>=14.0.0",
@@ -81,8 +81,6 @@ class Agent(Generic[_StructuredOutput]):
81
81
  self.environment = environment
82
82
  self.kind = kind
83
83
 
84
- # `reasoning` is only valid with the Core Agent; these two overloads make
85
- # that constraint type-checkable when callers construct a core-agent instance.
86
84
  @overload
87
85
  async def run(
88
86
  self,
@@ -160,6 +158,12 @@ class Agent(Generic[_StructuredOutput]):
160
158
  timeout: int = 1000,
161
159
  ) -> AgentResponse:
162
160
  """Invokes an agent in the bound Narada environment."""
161
+ if reasoning is not None and not isinstance(self.kind, AgentKind):
162
+ raise ValueError(
163
+ "`reasoning` is only supported for built-in `AgentKind` values; "
164
+ "named Agent Studio agents own reasoning per workflow step"
165
+ )
166
+
163
167
  remote_dispatch_response = await self._dispatch_request(
164
168
  prompt=prompt,
165
169
  clear_chat=clear_chat,
@@ -246,70 +250,29 @@ class Agent(Generic[_StructuredOutput]):
246
250
  timeout: int = 1000,
247
251
  ) -> Response:
248
252
  dispatch_agent = self.kind if agent is None else agent
249
- # Branch on `reasoning` so each call site binds a single, typed overload
250
- # of `_dispatch_request`. The validation also lives in `_dispatch_request`
251
- # itself (defense in depth + reachable when callers go straight to the
252
- # low-level API), so the redundancy here is intentional.
253
- if reasoning is None:
254
- remote_dispatch_response = await self.environment._dispatch_request(
255
- prompt=prompt,
256
- agent=dispatch_agent,
257
- clear_chat=clear_chat,
258
- generate_gif=generate_gif,
259
- output_schema=output_schema,
260
- previous_request_id=previous_request_id,
261
- chat_history=chat_history,
262
- additional_context=additional_context,
263
- attachment=attachment,
264
- time_zone=time_zone,
265
- user_resource_credentials=user_resource_credentials,
266
- mcp_servers=mcp_servers,
267
- secret_variables=secret_variables,
268
- input_variables=input_variables,
269
- callback_url=callback_url,
270
- callback_secret=callback_secret,
271
- callback_headers=callback_headers,
272
- on_input_required=on_input_required,
273
- critic_context=critic_context,
274
- timeout=timeout,
275
- )
276
- else:
277
- if dispatch_agent is not AgentKind.CORE_AGENT:
278
- raise ValueError(
279
- "`reasoning` is only supported with `kind=AgentKind.CORE_AGENT` "
280
- f"(got kind={dispatch_agent!r})"
281
- )
282
- # The CORE_AGENT-specific overloads of `_dispatch_request` split on
283
- # a narrower `output_schema` discriminator (None vs `type[T]`),
284
- # which the impl's `type[BaseModel] | None` union doesn't cleanly
285
- # narrow into without further branching. The public `run()`
286
- # overloads above already give callers correct return-type
287
- # narrowing, so the internal forward call bypasses overload
288
- # disambiguation on this single dimension.
289
- remote_dispatch_response = await self.environment._dispatch_request( # pyright: ignore[reportCallIssue]
290
- prompt=prompt,
291
- agent=dispatch_agent,
292
- reasoning=reasoning,
293
- clear_chat=clear_chat,
294
- generate_gif=generate_gif,
295
- output_schema=output_schema, # pyright: ignore[reportArgumentType]
296
- previous_request_id=previous_request_id,
297
- chat_history=chat_history,
298
- additional_context=additional_context,
299
- attachment=attachment,
300
- time_zone=time_zone,
301
- user_resource_credentials=user_resource_credentials,
302
- mcp_servers=mcp_servers,
303
- secret_variables=secret_variables,
304
- input_variables=input_variables,
305
- callback_url=callback_url,
306
- callback_secret=callback_secret,
307
- callback_headers=callback_headers,
308
- on_input_required=on_input_required,
309
- critic_context=critic_context,
310
- timeout=timeout,
311
- )
312
- return remote_dispatch_response
253
+ return await self.environment._dispatch_request(
254
+ prompt=prompt,
255
+ agent=dispatch_agent,
256
+ reasoning=reasoning,
257
+ clear_chat=clear_chat,
258
+ generate_gif=generate_gif,
259
+ output_schema=output_schema,
260
+ previous_request_id=previous_request_id,
261
+ chat_history=chat_history,
262
+ additional_context=additional_context,
263
+ attachment=attachment,
264
+ time_zone=time_zone,
265
+ user_resource_credentials=user_resource_credentials,
266
+ mcp_servers=mcp_servers,
267
+ secret_variables=secret_variables,
268
+ input_variables=input_variables,
269
+ callback_url=callback_url,
270
+ callback_secret=callback_secret,
271
+ callback_headers=callback_headers,
272
+ on_input_required=on_input_required,
273
+ critic_context=critic_context,
274
+ timeout=timeout,
275
+ )
313
276
 
314
277
  def _browser_environment(self) -> BaseBrowserEnvironment:
315
278
  if not isinstance(self.environment, BaseBrowserEnvironment):
@@ -840,67 +840,13 @@ class Environment(ABC):
840
840
  "mimeType": mime_type,
841
841
  }
842
842
 
843
- # `reasoning` is only valid with the Core Agent; these two overloads make
844
- # that constraint type-checkable. Generic-agent calls fall through to the
845
- # general overloads below, which do not accept a `reasoning` argument.
846
- @overload
847
- async def _dispatch_request(
848
- self,
849
- *,
850
- prompt: str,
851
- agent: Literal[AgentKind.CORE_AGENT],
852
- reasoning: ReasoningEffort | None = None,
853
- clear_chat: bool | None = None,
854
- generate_gif: bool | None = None,
855
- output_schema: None = None,
856
- previous_request_id: str | None = None,
857
- chat_history: list[RemoteDispatchChatHistoryItem] | None = None,
858
- additional_context: dict[str, str] | None = None,
859
- attachment: File | IO[Any] | None = None,
860
- time_zone: str = "America/Los_Angeles",
861
- user_resource_credentials: UserResourceCredentials | None = None,
862
- mcp_servers: list[McpServer] | None = None,
863
- secret_variables: dict[str, str] | None = None,
864
- input_variables: Mapping[str, Any] | None = None,
865
- callback_url: str | None = None,
866
- callback_secret: str | None = None,
867
- callback_headers: Mapping[str, Any] | None = None,
868
- on_input_required: InputRequiredCallback | None = None,
869
- timeout: int = 1000,
870
- ) -> Response[None]: ...
871
-
872
- @overload
873
- async def _dispatch_request(
874
- self,
875
- *,
876
- prompt: str,
877
- agent: Literal[AgentKind.CORE_AGENT],
878
- reasoning: ReasoningEffort | None = None,
879
- clear_chat: bool | None = None,
880
- generate_gif: bool | None = None,
881
- output_schema: type[_StructuredOutput],
882
- previous_request_id: str | None = None,
883
- chat_history: list[RemoteDispatchChatHistoryItem] | None = None,
884
- additional_context: dict[str, str] | None = None,
885
- attachment: File | IO[Any] | None = None,
886
- time_zone: str = "America/Los_Angeles",
887
- user_resource_credentials: UserResourceCredentials | None = None,
888
- mcp_servers: list[McpServer] | None = None,
889
- secret_variables: dict[str, str] | None = None,
890
- input_variables: Mapping[str, Any] | None = None,
891
- callback_url: str | None = None,
892
- callback_secret: str | None = None,
893
- callback_headers: Mapping[str, Any] | None = None,
894
- on_input_required: InputRequiredCallback | None = None,
895
- timeout: int = 1000,
896
- ) -> Response[_StructuredOutput]: ...
897
-
898
843
  @overload
899
844
  async def _dispatch_request(
900
845
  self,
901
846
  *,
902
847
  prompt: str,
903
848
  agent: AgentKind | str = AgentKind.OPERATOR,
849
+ reasoning: ReasoningEffort | None = None,
904
850
  clear_chat: bool | None = None,
905
851
  generate_gif: bool | None = None,
906
852
  output_schema: None = None,
@@ -927,6 +873,7 @@ class Environment(ABC):
927
873
  *,
928
874
  prompt: str,
929
875
  agent: AgentKind | str = AgentKind.OPERATOR,
876
+ reasoning: ReasoningEffort | None = None,
930
877
  clear_chat: bool | None = None,
931
878
  generate_gif: bool | None = None,
932
879
  output_schema: type[_StructuredOutput],
@@ -978,14 +925,6 @@ class Environment(ABC):
978
925
  """
979
926
  await self._ensure_initialized()
980
927
 
981
- # The overloads enforce this at type-check time when callers use
982
- # ``AgentKind.CORE_AGENT``; the runtime check covers string-form agents
983
- # (``agent="..."``) and callers without a type checker.
984
- if reasoning is not None and agent is not AgentKind.CORE_AGENT:
985
- raise ValueError(
986
- "`reasoning` is only supported with `agent=AgentKind.CORE_AGENT` "
987
- f"(got agent={agent!r})"
988
- )
989
928
  deadline = time.monotonic() + timeout
990
929
 
991
930
  agent_prefix = (
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+ from narada_core.tracing.model import OperatorActionTraceItem, parse_action_trace
5
+ from pydantic import ValidationError
6
+
7
+
8
+ def test_parse_operator_action_trace_exposes_timestamps() -> None:
9
+ trace = parse_action_trace(
10
+ [
11
+ {
12
+ "url": "https://example.com",
13
+ "action": "Clicked Submit",
14
+ "startTs": "2026-07-20T17:00:00.000Z",
15
+ "endTs": "2026-07-20T17:00:01.500Z",
16
+ "durationMs": 1_500,
17
+ }
18
+ ]
19
+ )
20
+
21
+ item = trace[0]
22
+ assert isinstance(item, OperatorActionTraceItem)
23
+ assert item.start_ts == "2026-07-20T17:00:00.000Z"
24
+ assert item.end_ts == "2026-07-20T17:00:01.500Z"
25
+ assert item.duration_ms == 1_500
26
+
27
+
28
+ @pytest.mark.parametrize(
29
+ "timestamps",
30
+ [
31
+ {"startTs": "2026-07-20T17:00:00.000Z", "durationMs": 1_000},
32
+ {"endTs": "2026-07-20T17:00:01.000Z", "durationMs": 1_000},
33
+ {
34
+ "startTs": "2026-07-20T17:00:02.000Z",
35
+ "endTs": "2026-07-20T17:00:01.000Z",
36
+ "durationMs": 0,
37
+ },
38
+ {
39
+ "startTs": "not-a-timestamp",
40
+ "endTs": "2026-07-20T17:00:01.000Z",
41
+ "durationMs": 1_000,
42
+ },
43
+ {
44
+ "startTs": "2026-07-20T17:00:00",
45
+ "endTs": "2026-07-20T17:00:01",
46
+ "durationMs": 1_000,
47
+ },
48
+ {"startTs": 1_000, "endTs": 2_000, "durationMs": 1_000},
49
+ {
50
+ "startTs": "2026-07-20T17:00:00.000Z",
51
+ "endTs": "2026-07-20T17:00:01.000Z",
52
+ },
53
+ {
54
+ "startTs": "2026-07-20T17:00:00.000Z",
55
+ "endTs": "2026-07-20T17:00:01.000Z",
56
+ "durationMs": 999,
57
+ },
58
+ ],
59
+ )
60
+ def test_operator_action_trace_rejects_invalid_timing(
61
+ timestamps: dict[str, str | int],
62
+ ) -> None:
63
+ with pytest.raises(ValidationError):
64
+ OperatorActionTraceItem(
65
+ url="https://example.com",
66
+ action="Clicked Submit",
67
+ **timestamps,
68
+ )
69
+
70
+
71
+ def test_operator_action_trace_accepts_explicit_utc_offset() -> None:
72
+ item = OperatorActionTraceItem(
73
+ url="https://example.com",
74
+ action="Clicked Submit",
75
+ startTs="2026-07-20T17:00:00.000+00:00",
76
+ endTs="2026-07-20T17:00:01.000+00:00",
77
+ durationMs=1_000,
78
+ )
79
+
80
+ assert item.start_ts.endswith("+00:00")
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from typing import Any
4
4
 
5
5
  import pytest
6
- from narada import Agent, Environment
6
+ from narada import Agent, AgentKind, Environment, ReasoningEffort
7
7
 
8
8
 
9
9
  class _FakeResponse:
@@ -100,6 +100,46 @@ async def test_agent_run_reruns_but_environment_initialization_is_cached(
100
100
  "/Operator first",
101
101
  "/Operator second",
102
102
  ]
103
+ assert all("reasoningMode" not in body for body in fake_session.dispatched_bodies)
104
+
105
+
106
+ @pytest.mark.asyncio
107
+ @pytest.mark.parametrize(
108
+ ("kind", "reasoning", "expected_prompt"),
109
+ [
110
+ (AgentKind.PRODUCTIVITY, ReasoningEffort.NONE, "analyze"),
111
+ (AgentKind.OPERATOR, ReasoningEffort.LOW, "/Operator analyze"),
112
+ (AgentKind.CORE_AGENT, ReasoningEffort.MEDIUM, "/coreAgent analyze"),
113
+ ],
114
+ )
115
+ async def test_agent_run_forwards_reasoning_for_every_agent_kind(
116
+ monkeypatch: pytest.MonkeyPatch,
117
+ kind: AgentKind | str,
118
+ reasoning: ReasoningEffort,
119
+ expected_prompt: str,
120
+ ) -> None:
121
+ import narada.environment as environment_module
122
+
123
+ fake_session = _RemoteDispatchFakeClientSession()
124
+ monkeypatch.setattr(
125
+ environment_module.aiohttp, "ClientSession", lambda: fake_session
126
+ )
127
+
128
+ await Agent(environment=_CountingEnvironment(), kind=kind).run(
129
+ "analyze",
130
+ reasoning=reasoning,
131
+ )
132
+
133
+ assert fake_session.dispatched_bodies[0]["prompt"] == expected_prompt
134
+ assert fake_session.dispatched_bodies[0]["reasoningMode"] == reasoning.value
135
+
136
+
137
+ @pytest.mark.asyncio
138
+ async def test_agent_run_rejects_top_level_reasoning_for_named_agent() -> None:
139
+ agent = Agent(environment=_CountingEnvironment(), kind="/owner/custom-agent")
140
+
141
+ with pytest.raises(ValueError, match="named Agent Studio agents"):
142
+ await agent.run("analyze", reasoning=ReasoningEffort.HIGH)
103
143
 
104
144
 
105
145
  @pytest.mark.asyncio
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes