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
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "3.18.0"
".": "3.19.0"
}
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 3.19.0 (2026-03-27)

Full Changelog: [v3.18.0...v3.19.0](https://github.com/browserbase/stagehand-python/compare/v3.18.0...v3.19.0)

### Features

* **internal:** implement indices array format for query and form serialization ([b2cccf5](https://github.com/browserbase/stagehand-python/commit/b2cccf56bc7e99d7869b8ed9339956a9f160348a))

## 3.18.0 (2026-03-25)

Full Changelog: [v3.7.0...v3.18.0](https://github.com/browserbase/stagehand-python/compare/v3.7.0...v3.18.0)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "stagehand"
version = "3.18.0"
version = "3.19.0"
description = "The official Python library for the stagehand API"
dynamic = ["readme"]
license = "MIT"
Expand Down
5 changes: 4 additions & 1 deletion src/stagehand/_qs.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ def _stringify_item(
items.extend(self._stringify_item(key, item, opts))
return items
elif array_format == "indices":
raise NotImplementedError("The array indices format is not supported yet")
items = []
for i, item in enumerate(value):
items.extend(self._stringify_item(f"{key}[{i}]", item, opts))
return items
elif array_format == "brackets":
items = []
key = key + "[]"
Expand Down
2 changes: 1 addition & 1 deletion src/stagehand/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

__title__ = "stagehand"
__version__ = "3.18.0" # x-release-please-version
__version__ = "3.19.0" # x-release-please-version
6 changes: 6 additions & 0 deletions src/stagehand/resources/sessions_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ def start(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> Session:
if browser is omit and getattr(self._client, "_server_mode", None) == "local":
browser = {"type": "local"}

start_response = super().start(
model_name=model_name,
act_timeout_ms=act_timeout_ms,
Expand Down Expand Up @@ -136,6 +139,9 @@ async def start(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> AsyncSession:
if browser is omit and getattr(self._client, "_server_mode", None) == "local":
browser = {"type": "local"}

start_response: SessionStartResponse = await super().start(
model_name=model_name,
act_timeout_ms=act_timeout_ms,
Expand Down
41 changes: 41 additions & 0 deletions tests/test_local_server.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import json

import httpx
import pytest
from respx import MockRouter
Expand Down Expand Up @@ -67,6 +69,45 @@ def test_sync_local_mode_starts_before_first_request(respx_mock: MockRouter, mon
assert dummy.closed == 1


@pytest.mark.respx(base_url="http://127.0.0.1:43127")
def test_sync_local_mode_defaults_browser_type_local_when_omitted(
respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
_set_required_env(monkeypatch)

dummy = _DummySeaServer("http://127.0.0.1:43127")
request_body: dict[str, object] = {}

def _capture_start_request(request: httpx.Request) -> httpx.Response:
request_body["json"] = json.loads(request.content.decode("utf-8"))
return httpx.Response(
200,
json={
"success": True,
"data": {
"available": True,
"connectUrl": "ws://example",
"sessionId": "00000000-0000-0000-0000-000000000001",
},
},
)

respx_mock.post("/v1/sessions/start").mock(side_effect=_capture_start_request)

client = Stagehand(server="local", _local_stagehand_binary_path="/does/not/matter/in/test")
client._sea_server = dummy # type: ignore[attr-defined]

resp = client.sessions.start(model_name="openai/gpt-5-nano")
assert resp.success is True
assert request_body["json"] == {
"modelName": "openai/gpt-5-nano",
"browser": {"type": "local"},
}

client.close()
assert dummy.closed == 1


@pytest.mark.respx(base_url="http://127.0.0.1:43124")
@pytest.mark.asyncio
async def test_async_local_mode_starts_before_first_request(
Expand Down
Loading