diff --git a/docs/src/features/sessions/browser-controls.mdx b/docs/src/features/sessions/browser-controls.mdx
index 1c0a3f05e..fc54f309d 100644
--- a/docs/src/features/sessions/browser-controls.mdx
+++ b/docs/src/features/sessions/browser-controls.mdx
@@ -194,7 +194,7 @@ Press a keyboard key.
### EvaluateJs
-Evaluate JavaScript code on the current page and return the result.
+Evaluate JavaScript code on the current page and return the result. `session.evaluate_js(code)` returns the evaluated string directly (objects and arrays as JSON); a failing script raises with the actual JavaScript error.
diff --git a/docs/src/sdk-reference/misc/evaluatejsaction.mdx b/docs/src/sdk-reference/misc/evaluatejsaction.mdx
index 32b419cae..cf67fb76f 100644
--- a/docs/src/sdk-reference/misc/evaluatejsaction.mdx
+++ b/docs/src/sdk-reference/misc/evaluatejsaction.mdx
@@ -16,11 +16,17 @@ You will not get any output from console.log(), so simply use the return value i
**Example:**
```python
-session.execute(type="evaluate_js", code="document.title")
-session.execute(type="evaluate_js", code="Array.from(document.querySelectorAll('a')).map(a => a.href)")
-session.execute(type="evaluate_js", code="(() => { const els = document.querySelectorAll('a'); return els.length; })()")
+title = session.evaluate_js("document.title")
+links = session.evaluate_js("Array.from(document.querySelectorAll('a')).map(a => a.href)")
+count = session.evaluate_js("(() => { const els = document.querySelectorAll('a'); return els.length; })()")
```
+`session.evaluate_js(code)` returns the evaluated string directly (objects and
+arrays as JSON, a JS `null` as the string `"null"`) and raises on failure with
+the actual JavaScript error. The action form
+`session.execute(type="evaluate_js", code=...)` returns the `ExecutionResult`
+envelope instead
+
## Fields
diff --git a/docs/src/sdk-reference/misc/remotesession.mdx b/docs/src/sdk-reference/misc/remotesession.mdx
index db4aa6ece..c0126d86c 100644
--- a/docs/src/sdk-reference/misc/remotesession.mdx
+++ b/docs/src/sdk-reference/misc/remotesession.mdx
@@ -52,6 +52,20 @@ Debug information for the session.
---
+### evaluate_js
+
+```python
+evaluate_js(code: , raise_on_failure: = True) -> str | notte_core.browser.observation.ExecutionResult
+```
+
+Evaluate JavaScript on the current page and return its result as a string
+
+**Returns:**
+
+`UnionType`[`str`, [`ExecutionResult`](/sdk-reference/misc/executionresult)[`ExecutionResult`](/sdk-reference/misc/executionresult.md)]
+
+---
+
### execute
```python
diff --git a/docs/src/sdk-reference/remotesession/evaluate_js.mdx b/docs/src/sdk-reference/remotesession/evaluate_js.mdx
new file mode 100644
index 000000000..2bf66dd60
--- /dev/null
+++ b/docs/src/sdk-reference/remotesession/evaluate_js.mdx
@@ -0,0 +1,28 @@
+---
+title: "evaluate_js"
+description: "Evaluate JavaScript on the current page and return its result as a string"
+---
+
+
+
+The result is stringified the way `evaluate_js` records it: objects and
+arrays as JSON, a JS `null` as the string `"null"`. On failure the typed
+exception is raised with the actual JavaScript error; pass
+`raise_on_failure=False` to get the `ExecutionResult` envelope instead.
+
+```python
+payload = json.loads(session.evaluate_js("(async () => JSON.stringify(await res.json()))()"))
+```
+
+
+## Parameters
+
+
+
+
+
+
+
+## Returns
+
+`UnionType`[`str`, [`ExecutionResult`](/sdk-reference/misc/executionresult)[`ExecutionResult`](/sdk-reference/misc/executionresult.md)]
diff --git a/docs/src/sdk-reference/remotesession/index.mdx b/docs/src/sdk-reference/remotesession/index.mdx
index a15c7d70f..3d92d99ea 100644
--- a/docs/src/sdk-reference/remotesession/index.mdx
+++ b/docs/src/sdk-reference/remotesession/index.mdx
@@ -73,6 +73,24 @@ Attributes:
Get detailed debug information for the session
+
+
+ Evaluate JavaScript on the current page and return its result as a string
+
+
+
+
+ Evaluate JavaScript on the current page and return its result as a string
+
+
a.href)"))
```
diff --git a/docs/src/testers/browser-controls/eval_js.py b/docs/src/testers/browser-controls/eval_js.py
index 98e66c65d..6445b7776 100644
--- a/docs/src/testers/browser-controls/eval_js.py
+++ b/docs/src/testers/browser-controls/eval_js.py
@@ -1,8 +1,13 @@
-# @sniptest filename=goto_new_tab.py
+# @sniptest filename=eval_js.py
+import json
+
from notte_sdk import NotteClient
client = NotteClient()
with client.Session() as session:
session.execute(type="goto", url="https://notte.cc/")
- session.execute(type="evaluate_js", code="document.title")
+ # evaluate_js returns the evaluated string; failures raise with the JS error
+ title = session.evaluate_js("document.title")
+ # objects and arrays come back as JSON
+ links = json.loads(session.evaluate_js("Array.from(document.querySelectorAll('a')).map(a => a.href)"))
diff --git a/packages/notte-browser/src/notte_browser/session.py b/packages/notte-browser/src/notte_browser/session.py
index 9407e18dd..c6f86afce 100644
--- a/packages/notte-browser/src/notte_browser/session.py
+++ b/packages/notte-browser/src/notte_browser/session.py
@@ -64,7 +64,7 @@
from notte_core.common.telemetry import track_usage
from notte_core.credentials.base import BaseVault, LocatorAttributes
from notte_core.data.space import DataSpace, ImageData, StructuredData, TBaseModel
-from notte_core.errors.actions import ActionExecutionError, InvalidActionError
+from notte_core.errors.actions import ActionExecutionError, EvaluateJsNoDataError, InvalidActionError
from notte_core.errors.base import NotteBaseError
from notte_core.errors.provider import RateLimitError
from notte_core.profiling import profiler
@@ -1047,6 +1047,44 @@ def execute(
self.aexecute(action=action, raise_on_failure=raise_on_failure, **kwargs) # pyright: ignore [reportArgumentType]
)
+ # raise_on_failure=True (default) -> the evaluated string; failures raise
+ @overload
+ async def aevaluate_js(self, code: str, *, raise_on_failure: Literal[True] = ...) -> str: ...
+
+ # raise_on_failure=False -> the ExecutionResult envelope, like execute()
+ @overload
+ async def aevaluate_js(self, code: str, *, raise_on_failure: Literal[False]) -> ExecutionResult: ...
+
+ async def aevaluate_js(self, code: str, *, raise_on_failure: bool = True) -> str | ExecutionResult:
+ """
+ Evaluate JavaScript on the current page and return its result as a string.
+
+ The result is stringified the way `evaluate_js` records it: objects and
+ arrays as JSON, a JS `null` as the string `"null"`. On failure the typed
+ exception is raised with the actual JavaScript error; pass
+ `raise_on_failure=False` to get the `ExecutionResult` envelope instead.
+ """
+ result = await self.aexecute(type="evaluate_js", code=code, raise_on_failure=raise_on_failure)
+ if not raise_on_failure:
+ return result
+ if result.data is None:
+ # cannot happen with this package's execute path, which always sets
+ # data on a successful eval; a typed error beats a stripped assert
+ raise EvaluateJsNoDataError()
+ return result.data.markdown
+
+ @overload
+ def evaluate_js(self, code: str, *, raise_on_failure: Literal[True] = ...) -> str: ...
+
+ @overload
+ def evaluate_js(self, code: str, *, raise_on_failure: Literal[False]) -> ExecutionResult: ...
+
+ def evaluate_js(self, code: str, *, raise_on_failure: bool = True) -> str | ExecutionResult:
+ """
+ Synchronous version of aevaluate_js.
+ """
+ return asyncio.run(self.aevaluate_js(code, raise_on_failure=raise_on_failure))
+
@overload
async def ascrape(self, /, *, only_images: Literal[True], raise_on_failure: bool = True) -> list[ImageData]: ...
diff --git a/packages/notte-core/src/notte_core/actions/actions.py b/packages/notte-core/src/notte_core/actions/actions.py
index e5c45472e..3148c1e1a 100644
--- a/packages/notte-core/src/notte_core/actions/actions.py
+++ b/packages/notte-core/src/notte_core/actions/actions.py
@@ -974,10 +974,16 @@ class EvaluateJsAction(ToolAction):
**Example:**
```python
- session.execute(type="evaluate_js", code="document.title")
- session.execute(type="evaluate_js", code="Array.from(document.querySelectorAll('a')).map(a => a.href)")
- session.execute(type="evaluate_js", code="(() => { const els = document.querySelectorAll('a'); return els.length; })()")
+ title = session.evaluate_js("document.title")
+ links = session.evaluate_js("Array.from(document.querySelectorAll('a')).map(a => a.href)")
+ count = session.evaluate_js("(() => { const els = document.querySelectorAll('a'); return els.length; })()")
```
+
+ `session.evaluate_js(code)` returns the evaluated string directly (objects and
+ arrays as JSON, a JS `null` as the string `"null"`) and raises on failure with
+ the actual JavaScript error. The action form
+ `session.execute(type="evaluate_js", code=...)` returns the `ExecutionResult`
+ envelope instead.
"""
type: Literal["evaluate_js"] = "evaluate_js" # pyright: ignore [reportIncompatibleVariableOverride]
diff --git a/packages/notte-core/src/notte_core/errors/actions.py b/packages/notte-core/src/notte_core/errors/actions.py
index e8a1d2838..60d7f81ad 100644
--- a/packages/notte-core/src/notte_core/errors/actions.py
+++ b/packages/notte-core/src/notte_core/errors/actions.py
@@ -21,6 +21,17 @@ def __init__(self, action_id: str, url: str, reason: str | None = None) -> None:
)
+class EvaluateJsNoDataError(ActionError):
+ def __init__(self) -> None:
+ message = "evaluate_js reported success but returned no data"
+ super().__init__(
+ dev_message=message,
+ user_message=f"{message}.",
+ agent_message=message,
+ should_notify_team=True,
+ )
+
+
class NotEnoughActionsListedError(ActionError):
def __init__(self, n_trials: int, n_actions: int, threshold: float) -> None:
super().__init__(
diff --git a/packages/notte-sdk/src/notte_sdk/endpoints/sessions.py b/packages/notte-sdk/src/notte_sdk/endpoints/sessions.py
index efe3c9536..8a61e0823 100644
--- a/packages/notte-sdk/src/notte_sdk/endpoints/sessions.py
+++ b/packages/notte-sdk/src/notte_sdk/endpoints/sessions.py
@@ -44,6 +44,7 @@
from notte_core.common.resource import SyncResource
from notte_core.common.telemetry import track_usage
from notte_core.data.space import ImageData, StructuredData, TBaseModel
+from notte_core.errors.actions import EvaluateJsNoDataError
from notte_core.errors.base import NotteBaseError
from notte_core.utils.files import create_or_append_cookies_to_file
from pydantic import BaseModel
@@ -1596,3 +1597,32 @@ def execute(
agent_message=fallback_message,
)
return result
+
+ # raise_on_failure=True (default) -> the evaluated string; failures raise
+ @overload
+ def evaluate_js(self, code: str, *, raise_on_failure: Literal[True] = ...) -> str: ...
+
+ # raise_on_failure=False -> the ExecutionResult envelope, like execute()
+ @overload
+ def evaluate_js(self, code: str, *, raise_on_failure: Literal[False]) -> ExecutionResult: ...
+
+ def evaluate_js(self, code: str, *, raise_on_failure: bool = True) -> str | ExecutionResult:
+ """
+ Evaluate JavaScript on the current page and return its result as a string.
+
+ The result is stringified the way `evaluate_js` records it: objects and
+ arrays as JSON, a JS `null` as the string `"null"`. On failure the typed
+ exception is raised with the actual JavaScript error; pass
+ `raise_on_failure=False` to get the `ExecutionResult` envelope instead.
+
+ ```python
+ payload = json.loads(session.evaluate_js("(async () => JSON.stringify(await res.json()))()"))
+ ```
+ """
+ result = self.execute(type="evaluate_js", code=code, raise_on_failure=raise_on_failure)
+ if not raise_on_failure:
+ return result
+ if result.data is None:
+ # an API build that predates the eval-js fix reports success without data
+ raise EvaluateJsNoDataError()
+ return result.data.markdown
diff --git a/tests/sdk/test_evaluate_js_helper.py b/tests/sdk/test_evaluate_js_helper.py
new file mode 100644
index 000000000..236698ad9
--- /dev/null
+++ b/tests/sdk/test_evaluate_js_helper.py
@@ -0,0 +1,59 @@
+"""Remote `evaluate_js()`: the string on success, the typed raise on failure."""
+
+import datetime as dt
+
+import pytest
+from notte_core.actions import EvaluateJsAction
+from notte_core.browser.observation import ExecutionResult
+from notte_core.data.space import DataSpace
+from notte_core.errors.actions import ActionExecutionError, EvaluateJsNoDataError
+from notte_core.errors.base import ErrorConfig
+
+from tests.sdk.test_execute_raise_on_failure import over_the_wire, remote_session
+
+CODE = "1 + 1"
+
+
+def eval_result(*, success: bool, markdown: str | None = None, exception: Exception | None = None) -> ExecutionResult:
+ now = dt.datetime.now(dt.timezone.utc)
+ return ExecutionResult(
+ action=EvaluateJsAction(code=CODE),
+ success=success,
+ message="ok" if success else "JavaScript evaluation failed: boom",
+ data=DataSpace(markdown=markdown) if markdown is not None else None,
+ exception=exception,
+ started_at=now,
+ ended_at=now,
+ )
+
+
+def test_evaluate_js_returns_the_string() -> None:
+ session = remote_session(over_the_wire(eval_result(success=True, markdown="2")))
+
+ assert session.evaluate_js(CODE) == "2"
+
+
+def test_evaluate_js_failure_raises_the_typed_error() -> None:
+ with ErrorConfig.message_mode("user"):
+ exception = ActionExecutionError(action_id="evaluate_js", url="https://example.com", reason="boom")
+ session = remote_session(over_the_wire(eval_result(success=False, exception=exception)))
+
+ with pytest.raises(ActionExecutionError):
+ _ = session.evaluate_js(CODE)
+
+
+def test_evaluate_js_returns_the_envelope_when_not_raising() -> None:
+ session = remote_session(over_the_wire(eval_result(success=False)))
+
+ result = session.evaluate_js(CODE, raise_on_failure=False)
+
+ assert isinstance(result, ExecutionResult)
+ assert result.success is False
+
+
+def test_evaluate_js_success_without_data_raises_instead_of_returning_none() -> None:
+ """An API build that predates the eval-js fix can report success with no data."""
+ session = remote_session(over_the_wire(eval_result(success=True)))
+
+ with pytest.raises(EvaluateJsNoDataError, match="returned no data"):
+ _ = session.evaluate_js(CODE)
diff --git a/tests/test_evaluate_js_helper.py b/tests/test_evaluate_js_helper.py
new file mode 100644
index 000000000..f6c03dc94
--- /dev/null
+++ b/tests/test_evaluate_js_helper.py
@@ -0,0 +1,39 @@
+"""`evaluate_js()` returns the evaluated string; the envelope stays on the `False` overload."""
+
+import pytest
+from notte_browser.session import NotteSession
+from notte_core.browser.observation import ExecutionResult
+from notte_core.errors.actions import ActionExecutionError
+
+
+@pytest.mark.asyncio
+async def test_aevaluate_js_returns_the_string() -> None:
+ async with NotteSession(headless=True) as session:
+ assert await session.aevaluate_js("1 + 1") == "2"
+ # a JS `null` is a successful evaluation and arrives as the string "null"
+ assert await session.aevaluate_js("null") == "null"
+ assert await session.aevaluate_js("[1, 2]") == "[\n 1,\n 2\n]"
+
+
+@pytest.mark.asyncio
+async def test_aevaluate_js_failure_raises_the_js_error() -> None:
+ async with NotteSession(headless=True) as session:
+ with pytest.raises(ActionExecutionError, match="JavaScript evaluation failed"):
+ _ = await session.aevaluate_js("notAFunction()")
+
+
+@pytest.mark.asyncio
+async def test_aevaluate_js_returns_the_envelope_when_not_raising() -> None:
+ async with NotteSession(headless=True) as session:
+ result = await session.aevaluate_js("notAFunction()", raise_on_failure=False)
+
+ assert isinstance(result, ExecutionResult)
+ assert result.success is False
+ assert result.message.startswith("JavaScript evaluation failed:")
+
+
+# NOTE: no sync-variant test here on purpose. A sync NotteSession (asyncio.run
+# under nest_asyncio) breaks the next async browser launch in the same pytest
+# process, so mixing the two in one file flakes under random test ordering.
+# The sync wrapper is a one-line delegation to aevaluate_js and its overload
+# typing is pinned by typing_cases/evaluate_js_overloads.py.
diff --git a/typing_cases/evaluate_js_overloads.py b/typing_cases/evaluate_js_overloads.py
new file mode 100644
index 000000000..24b61cb73
--- /dev/null
+++ b/typing_cases/evaluate_js_overloads.py
@@ -0,0 +1,36 @@
+"""Reveal-type cases for evaluate_js overload resolution (checked by basedpyright and ty).
+
+Keep this file free of runtime side effects; checkers only need the annotations.
+"""
+
+from __future__ import annotations
+
+from typing import reveal_type
+
+from notte_browser.session import NotteSession
+from notte_sdk.endpoints.sessions import RemoteSession
+
+
+def _check_remote_session(session: RemoteSession) -> None:
+ text = session.evaluate_js("1 + 1")
+ reveal_type(text) # str
+ envelope = session.evaluate_js("1 + 1", raise_on_failure=False)
+ reveal_type(envelope) # ExecutionResult
+
+
+def _check_computed_bool(session: RemoteSession, flag: bool) -> None:
+ # a non-literal bool matches via argument expansion on both checkers;
+ # no plain-bool overload is needed
+ either = session.evaluate_js("1 + 1", raise_on_failure=flag)
+ reveal_type(either) # str | ExecutionResult
+
+
+async def _check_local_session(session: NotteSession) -> None:
+ text = await session.aevaluate_js("1 + 1")
+ reveal_type(text) # str
+ envelope = await session.aevaluate_js("1 + 1", raise_on_failure=False)
+ reveal_type(envelope) # ExecutionResult
+ sync_text = session.evaluate_js("1 + 1")
+ reveal_type(sync_text) # str
+ sync_envelope = session.evaluate_js("1 + 1", raise_on_failure=False)
+ reveal_type(sync_envelope) # ExecutionResult