From 1d70eff68d1cc63934aa074e8fd89503845865d6 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 28 Oct 2026 16:15:05 +0000
Subject: [PATCH 1/2] feat: Add mid-session browser profile saves
Stainless-Generated-From: 4fbf9561f04097a8d4949d901b8ca0db4f778769
---
.stats.yml | 2 +-
api.md | 2 +
src/kernel/resources/browsers/browsers.py | 174 ++++++++++++++++++
src/kernel/types/__init__.py | 1 +
.../types/browser_save_profile_params.py | 15 ++
tests/api_resources/test_browsers.py | 105 +++++++++++
6 files changed, 298 insertions(+), 1 deletion(-)
create mode 100644 src/kernel/types/browser_save_profile_params.py
diff --git a/.stats.yml b/.stats.yml
index caa7c062..ea0d5273 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1 @@
-configured_endpoints: 175
+configured_endpoints: 176
diff --git a/api.md b/api.md
index a2995e3a..734e8fbb 100644
--- a/api.md
+++ b/api.md
@@ -138,6 +138,7 @@ from kernel.types import (
BrowserUsage,
BrowserVideoMemory,
Profile,
+ SaveProfileRequest,
Tags,
VaultReference,
BrowserCreateResponse,
@@ -158,6 +159,7 @@ Methods:
- client.browsers.delete_by_id(id_or_name) -> None
- client.browsers.load_extensions(id_or_name, \*\*params) -> None
- client.browsers.repl(id_or_name, \*\*params) -> BrowserReplResult
+- client.browsers.save_profile(id_or_name, \*\*params) -> Profile
## Telemetry
diff --git a/src/kernel/resources/browsers/browsers.py b/src/kernel/resources/browsers/browsers.py
index ec9b5176..00010a7e 100644
--- a/src/kernel/resources/browsers/browsers.py
+++ b/src/kernel/resources/browsers/browsers.py
@@ -33,6 +33,7 @@
browser_create_params,
browser_update_params,
browser_retrieve_params,
+ browser_save_profile_params,
browser_load_extensions_params,
)
from .process import (
@@ -88,6 +89,7 @@
AsyncWebmcpResourceWithStreamingResponse,
)
from ..._base_client import AsyncPaginator, make_request_options
+from ...types.profile import Profile
from ...types.tags_param import TagsParam
from .playwright.playwright import (
PlaywrightResource,
@@ -897,6 +899,86 @@ def repl(
cast_to=BrowserReplResult,
)
+ def save_profile(
+ self,
+ id_or_name: str,
+ *,
+ id: str | Omit = omit,
+ name: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ idempotency_key: str | None = None,
+ ) -> Profile:
+ """
+ Saves the running browser's current user-data state to an existing or new
+ profile without ending the browser session.
+
+ Saving restarts the browser. Active CDP and WebDriver connections are closed and
+ must reconnect, and WebDriver clients must create a new session. Open tabs are
+ restored, but pages reload and in-memory page state is lost. The browser is
+ stopped only while its user data is copied; this request returns once the
+ restarted browser accepts CDP connections again and the profile has been
+ uploaded. Caches, Service Worker storage and Google Drive IndexedDB data are
+ left out of the saved profile but kept in the running browser.
+
+ This does not attach the destination profile to the browser or change its
+ save_changes setting. Active browsers that already loaded the destination
+ profile keep their in-memory state. Pools with refresh_on_profile_update enabled
+ flush idle sessions and replenish them from the new snapshot; other browsers
+ receive it the next time they load the profile. When saves made with this
+ endpoint to the same profile overlap, the save that started most recently wins,
+ even if an earlier one finishes later. The automatic save at the end of a
+ session whose profile has save_changes enabled is ordered against these saves by
+ when each finishes.
+
+ A successful save restarts the browser's inactivity timeout (timeout_seconds). A
+ failed save leaves it unchanged.
+
+ Returns 409 when the browser is no longer active, when a pooled browser was
+ released or reacquired after the request was made (browser_lease_changed), or
+ when the browser is briefly unable to accept a save (browser_busy), in which
+ case the request can be retried.
+
+ Args:
+ id: Existing profile ID to overwrite.
+
+ name: Profile name to overwrite or create.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+
+ idempotency_key: Specify a custom idempotency key for this request
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return self._post(
+ path_template("/browsers/{id_or_name}/profiles", id_or_name=id_or_name),
+ body=maybe_transform(
+ {
+ "id": id,
+ "name": name,
+ },
+ browser_save_profile_params.BrowserSaveProfileParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=Profile,
+ )
+
class AsyncBrowsersResource(AsyncAPIResource):
@cached_property
@@ -1671,6 +1753,86 @@ async def repl(
cast_to=BrowserReplResult,
)
+ async def save_profile(
+ self,
+ id_or_name: str,
+ *,
+ id: str | Omit = omit,
+ name: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ idempotency_key: str | None = None,
+ ) -> Profile:
+ """
+ Saves the running browser's current user-data state to an existing or new
+ profile without ending the browser session.
+
+ Saving restarts the browser. Active CDP and WebDriver connections are closed and
+ must reconnect, and WebDriver clients must create a new session. Open tabs are
+ restored, but pages reload and in-memory page state is lost. The browser is
+ stopped only while its user data is copied; this request returns once the
+ restarted browser accepts CDP connections again and the profile has been
+ uploaded. Caches, Service Worker storage and Google Drive IndexedDB data are
+ left out of the saved profile but kept in the running browser.
+
+ This does not attach the destination profile to the browser or change its
+ save_changes setting. Active browsers that already loaded the destination
+ profile keep their in-memory state. Pools with refresh_on_profile_update enabled
+ flush idle sessions and replenish them from the new snapshot; other browsers
+ receive it the next time they load the profile. When saves made with this
+ endpoint to the same profile overlap, the save that started most recently wins,
+ even if an earlier one finishes later. The automatic save at the end of a
+ session whose profile has save_changes enabled is ordered against these saves by
+ when each finishes.
+
+ A successful save restarts the browser's inactivity timeout (timeout_seconds). A
+ failed save leaves it unchanged.
+
+ Returns 409 when the browser is no longer active, when a pooled browser was
+ released or reacquired after the request was made (browser_lease_changed), or
+ when the browser is briefly unable to accept a save (browser_busy), in which
+ case the request can be retried.
+
+ Args:
+ id: Existing profile ID to overwrite.
+
+ name: Profile name to overwrite or create.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+
+ idempotency_key: Specify a custom idempotency key for this request
+ """
+ if not id_or_name:
+ raise ValueError(f"Expected a non-empty value for `id_or_name` but received {id_or_name!r}")
+ return await self._post(
+ path_template("/browsers/{id_or_name}/profiles", id_or_name=id_or_name),
+ body=await async_maybe_transform(
+ {
+ "id": id,
+ "name": name,
+ },
+ browser_save_profile_params.BrowserSaveProfileParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ idempotency_key=idempotency_key,
+ ),
+ cast_to=Profile,
+ )
+
class BrowsersResourceWithRawResponse:
def __init__(self, browsers: BrowsersResource) -> None:
@@ -1700,6 +1862,9 @@ def __init__(self, browsers: BrowsersResource) -> None:
self.repl = to_raw_response_wrapper(
browsers.repl,
)
+ self.save_profile = to_raw_response_wrapper(
+ browsers.save_profile,
+ )
@cached_property
def telemetry(self) -> TelemetryResourceWithRawResponse:
@@ -1774,6 +1939,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
self.repl = async_to_raw_response_wrapper(
browsers.repl,
)
+ self.save_profile = async_to_raw_response_wrapper(
+ browsers.save_profile,
+ )
@cached_property
def telemetry(self) -> AsyncTelemetryResourceWithRawResponse:
@@ -1848,6 +2016,9 @@ def __init__(self, browsers: BrowsersResource) -> None:
self.repl = to_streamed_response_wrapper(
browsers.repl,
)
+ self.save_profile = to_streamed_response_wrapper(
+ browsers.save_profile,
+ )
@cached_property
def telemetry(self) -> TelemetryResourceWithStreamingResponse:
@@ -1922,6 +2093,9 @@ def __init__(self, browsers: AsyncBrowsersResource) -> None:
self.repl = async_to_streamed_response_wrapper(
browsers.repl,
)
+ self.save_profile = async_to_streamed_response_wrapper(
+ browsers.save_profile,
+ )
@cached_property
def telemetry(self) -> AsyncTelemetryResourceWithStreamingResponse:
diff --git a/src/kernel/types/__init__.py b/src/kernel/types/__init__.py
index a7735052..10bb66a1 100644
--- a/src/kernel/types/__init__.py
+++ b/src/kernel/types/__init__.py
@@ -135,6 +135,7 @@
from .invocation_update_response import InvocationUpdateResponse as InvocationUpdateResponse
from .browser_pool_acquire_params import BrowserPoolAcquireParams as BrowserPoolAcquireParams
from .browser_pool_release_params import BrowserPoolReleaseParams as BrowserPoolReleaseParams
+from .browser_save_profile_params import BrowserSaveProfileParams as BrowserSaveProfileParams
from .config_registry_list_params import ConfigRegistryListParams as ConfigRegistryListParams
from .browser_network_config_param import BrowserNetworkConfigParam as BrowserNetworkConfigParam
from .browser_network_update_param import BrowserNetworkUpdateParam as BrowserNetworkUpdateParam
diff --git a/src/kernel/types/browser_save_profile_params.py b/src/kernel/types/browser_save_profile_params.py
new file mode 100644
index 00000000..86942b81
--- /dev/null
+++ b/src/kernel/types/browser_save_profile_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import TypedDict
+
+__all__ = ["BrowserSaveProfileParams"]
+
+
+class BrowserSaveProfileParams(TypedDict, total=False):
+ id: str
+ """Existing profile ID to overwrite."""
+
+ name: str
+ """Profile name to overwrite or create."""
diff --git a/tests/api_resources/test_browsers.py b/tests/api_resources/test_browsers.py
index 6c5e9696..9c6d8a79 100644
--- a/tests/api_resources/test_browsers.py
+++ b/tests/api_resources/test_browsers.py
@@ -10,6 +10,7 @@
from kernel import Kernel, AsyncKernel
from tests.utils import assert_matches_type
from kernel.types import (
+ Profile,
BrowserReplResult,
BrowserCurlResponse,
BrowserListResponse,
@@ -578,6 +579,58 @@ def test_path_params_repl(self, client: Kernel) -> None:
code="code",
)
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_save_profile(self, client: Kernel) -> None:
+ browser = client.browsers.save_profile(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
+ assert_matches_type(Profile, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_save_profile_with_all_params(self, client: Kernel) -> None:
+ browser = client.browsers.save_profile(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ id="x",
+ name="name",
+ )
+ assert_matches_type(Profile, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_save_profile(self, client: Kernel) -> None:
+ response = client.browsers.with_raw_response.save_profile(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ browser = response.parse()
+ assert_matches_type(Profile, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_save_profile(self, client: Kernel) -> None:
+ with client.browsers.with_streaming_response.save_profile(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ browser = response.parse()
+ assert_matches_type(Profile, browser, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_save_profile(self, client: Kernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ client.browsers.with_raw_response.save_profile(
+ id_or_name="",
+ )
+
class TestAsyncBrowsers:
parametrize = pytest.mark.parametrize(
@@ -1136,3 +1189,55 @@ async def test_path_params_repl(self, async_client: AsyncKernel) -> None:
id_or_name="",
code="code",
)
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_save_profile(self, async_client: AsyncKernel) -> None:
+ browser = await async_client.browsers.save_profile(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
+ assert_matches_type(Profile, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_save_profile_with_all_params(self, async_client: AsyncKernel) -> None:
+ browser = await async_client.browsers.save_profile(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ id="x",
+ name="name",
+ )
+ assert_matches_type(Profile, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_save_profile(self, async_client: AsyncKernel) -> None:
+ response = await async_client.browsers.with_raw_response.save_profile(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ browser = await response.parse()
+ assert_matches_type(Profile, browser, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_save_profile(self, async_client: AsyncKernel) -> None:
+ async with async_client.browsers.with_streaming_response.save_profile(
+ id_or_name="htzv5orfit78e1m2biiifpbv",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ browser = await response.parse()
+ assert_matches_type(Profile, browser, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_save_profile(self, async_client: AsyncKernel) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `id_or_name` but received ''"):
+ await async_client.browsers.with_raw_response.save_profile(
+ id_or_name="",
+ )
From 953a51aa591eaf6a5f548cfa3c234119d09ce274 Mon Sep 17 00:00:00 2001
From: "kernel-internal[bot]"
<260533166+kernel-internal[bot]@users.noreply.github.com>
Date: Wed, 28 Oct 2026 16:19:30 +0000
Subject: [PATCH 2/2] release: 0.134.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 7 +++++++
pyproject.toml | 2 +-
src/kernel/_version.py | 2 +-
4 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 4a1242df..774500bf 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.133.0"
+ ".": "0.134.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 471f5980..57441236 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.134.0](https://github.com/kernel/kernel-python-sdk/compare/v0.133.0...v0.134.0) (2026-10-28)
+
+
+### Features
+
+* Add mid-session browser profile saves ([1d70eff](https://github.com/kernel/kernel-python-sdk/commit/1d70eff68d1cc63934aa074e8fd89503845865d6))
+
## [0.133.0](https://github.com/kernel/kernel-python-sdk/compare/v0.132.0...v0.133.0) (2026-10-28)
diff --git a/pyproject.toml b/pyproject.toml
index bba5860d..1cd696b3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "kernel"
-version = "0.133.0"
+version = "0.134.0"
description = "The official Python library for the kernel API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/kernel/_version.py b/src/kernel/_version.py
index 720e7ab3..11d1945f 100644
--- a/src/kernel/_version.py
+++ b/src/kernel/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "kernel"
-__version__ = "0.133.0" # x-release-please-version
+__version__ = "0.134.0" # x-release-please-version