sentienceapi 0.92.2__py3-none-any.whl → 0.98.0__py3-none-any.whl

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.

Potentially problematic release.


This version of sentienceapi might be problematic. Click here for more details.

Files changed (64) hide show
  1. sentience/__init__.py +107 -2
  2. sentience/_extension_loader.py +156 -1
  3. sentience/action_executor.py +2 -0
  4. sentience/actions.py +354 -9
  5. sentience/agent.py +4 -0
  6. sentience/agent_runtime.py +840 -0
  7. sentience/asserts/__init__.py +70 -0
  8. sentience/asserts/expect.py +621 -0
  9. sentience/asserts/query.py +383 -0
  10. sentience/async_api.py +8 -1
  11. sentience/backends/__init__.py +137 -0
  12. sentience/backends/actions.py +372 -0
  13. sentience/backends/browser_use_adapter.py +241 -0
  14. sentience/backends/cdp_backend.py +393 -0
  15. sentience/backends/exceptions.py +211 -0
  16. sentience/backends/playwright_backend.py +194 -0
  17. sentience/backends/protocol.py +216 -0
  18. sentience/backends/sentience_context.py +469 -0
  19. sentience/backends/snapshot.py +483 -0
  20. sentience/browser.py +230 -74
  21. sentience/canonicalization.py +207 -0
  22. sentience/cloud_tracing.py +65 -24
  23. sentience/constants.py +6 -0
  24. sentience/cursor_policy.py +142 -0
  25. sentience/extension/content.js +35 -0
  26. sentience/extension/injected_api.js +310 -15
  27. sentience/extension/manifest.json +1 -1
  28. sentience/extension/pkg/sentience_core.d.ts +22 -22
  29. sentience/extension/pkg/sentience_core.js +192 -144
  30. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  31. sentience/extension/release.json +29 -29
  32. sentience/failure_artifacts.py +241 -0
  33. sentience/integrations/__init__.py +6 -0
  34. sentience/integrations/langchain/__init__.py +12 -0
  35. sentience/integrations/langchain/context.py +18 -0
  36. sentience/integrations/langchain/core.py +326 -0
  37. sentience/integrations/langchain/tools.py +180 -0
  38. sentience/integrations/models.py +46 -0
  39. sentience/integrations/pydanticai/__init__.py +15 -0
  40. sentience/integrations/pydanticai/deps.py +20 -0
  41. sentience/integrations/pydanticai/toolset.py +468 -0
  42. sentience/llm_provider.py +695 -18
  43. sentience/models.py +536 -3
  44. sentience/ordinal.py +280 -0
  45. sentience/query.py +66 -4
  46. sentience/schemas/trace_v1.json +27 -1
  47. sentience/snapshot.py +384 -93
  48. sentience/snapshot_diff.py +39 -54
  49. sentience/text_search.py +1 -0
  50. sentience/trace_event_builder.py +20 -1
  51. sentience/trace_indexing/indexer.py +3 -49
  52. sentience/tracer_factory.py +1 -3
  53. sentience/verification.py +618 -0
  54. sentience/visual_agent.py +3 -1
  55. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +198 -40
  56. sentienceapi-0.98.0.dist-info/RECORD +92 -0
  57. sentience/utils.py +0 -296
  58. sentienceapi-0.92.2.dist-info/RECORD +0 -65
  59. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
  60. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
  61. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
  62. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
  63. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
  64. {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,372 @@
1
+ """
2
+ Backend-agnostic actions for browser-use integration.
3
+
4
+ These actions work with any BrowserBackend implementation,
5
+ enabling Sentience grounding with browser-use or other frameworks.
6
+
7
+ Usage with browser-use:
8
+ from sentience.backends import BrowserUseAdapter
9
+ from sentience.backends.actions import click, type_text, scroll
10
+
11
+ adapter = BrowserUseAdapter(session)
12
+ backend = await adapter.create_backend()
13
+
14
+ # Take snapshot and click element
15
+ snap = await snapshot_from_backend(backend)
16
+ element = find(snap, 'role=button[name="Submit"]')
17
+ await click(backend, element.bbox)
18
+ """
19
+
20
+ import asyncio
21
+ import time
22
+ from typing import TYPE_CHECKING, Any, Literal
23
+
24
+ from ..cursor_policy import CursorPolicy, build_human_cursor_path
25
+ from ..models import ActionResult, BBox, Snapshot
26
+
27
+ if TYPE_CHECKING:
28
+ from .protocol import BrowserBackend
29
+
30
+
31
+ async def click(
32
+ backend: "BrowserBackend",
33
+ target: BBox | dict[str, float] | tuple[float, float],
34
+ button: Literal["left", "right", "middle"] = "left",
35
+ click_count: int = 1,
36
+ move_first: bool = True,
37
+ cursor_policy: CursorPolicy | None = None,
38
+ ) -> ActionResult:
39
+ """
40
+ Click at coordinates using the backend.
41
+
42
+ Args:
43
+ backend: BrowserBackend implementation
44
+ target: Click target - BBox (clicks center), dict with x/y, or (x, y) tuple
45
+ button: Mouse button to click
46
+ click_count: Number of clicks (1=single, 2=double)
47
+ move_first: Whether to move mouse to position before clicking
48
+
49
+ Returns:
50
+ ActionResult with success status
51
+
52
+ Example:
53
+ # Click at coordinates
54
+ await click(backend, (100, 200))
55
+
56
+ # Click element bbox center
57
+ await click(backend, element.bbox)
58
+
59
+ # Double-click
60
+ await click(backend, element.bbox, click_count=2)
61
+ """
62
+ start_time = time.time()
63
+
64
+ # Resolve coordinates
65
+ x, y = _resolve_coordinates(target)
66
+ cursor_meta: dict | None = None
67
+
68
+ try:
69
+ # Optional mouse move for hover effects
70
+ if move_first:
71
+ if cursor_policy is not None and cursor_policy.mode == "human":
72
+ pos = getattr(backend, "_sentience_cursor_pos", None)
73
+ if not isinstance(pos, tuple) or len(pos) != 2:
74
+ pos = (float(x), float(y))
75
+
76
+ cursor_meta = build_human_cursor_path(
77
+ start=(float(pos[0]), float(pos[1])),
78
+ target=(float(x), float(y)),
79
+ policy=cursor_policy,
80
+ )
81
+ pts = cursor_meta.get("path", [])
82
+ duration_ms_move = int(cursor_meta.get("duration_ms") or 0)
83
+ per_step_s = (
84
+ (duration_ms_move / max(1, len(pts))) / 1000.0 if duration_ms_move > 0 else 0.0
85
+ )
86
+ for p in pts:
87
+ await backend.mouse_move(float(p["x"]), float(p["y"]))
88
+ if per_step_s > 0:
89
+ await asyncio.sleep(per_step_s)
90
+ pause_ms = int(cursor_meta.get("pause_before_click_ms") or 0)
91
+ if pause_ms > 0:
92
+ await asyncio.sleep(pause_ms / 1000.0)
93
+ else:
94
+ await backend.mouse_move(x, y)
95
+ await asyncio.sleep(0.02) # Brief pause for hover
96
+
97
+ # Perform click
98
+ await backend.mouse_click(x, y, button=button, click_count=click_count)
99
+ setattr(backend, "_sentience_cursor_pos", (float(x), float(y)))
100
+
101
+ duration_ms = int((time.time() - start_time) * 1000)
102
+ return ActionResult(
103
+ success=True,
104
+ duration_ms=duration_ms,
105
+ outcome="dom_updated",
106
+ cursor=cursor_meta,
107
+ )
108
+ except Exception as e:
109
+ duration_ms = int((time.time() - start_time) * 1000)
110
+ return ActionResult(
111
+ success=False,
112
+ duration_ms=duration_ms,
113
+ outcome="error",
114
+ error={"code": "click_failed", "reason": str(e)},
115
+ cursor=cursor_meta,
116
+ )
117
+
118
+
119
+ async def type_text(
120
+ backend: "BrowserBackend",
121
+ text: str,
122
+ target: BBox | dict[str, float] | tuple[float, float] | None = None,
123
+ clear_first: bool = False,
124
+ ) -> ActionResult:
125
+ """
126
+ Type text, optionally clicking a target first.
127
+
128
+ Args:
129
+ backend: BrowserBackend implementation
130
+ text: Text to type
131
+ target: Optional click target before typing (BBox, dict, or tuple)
132
+ clear_first: If True, select all and delete before typing
133
+
134
+ Returns:
135
+ ActionResult with success status
136
+
137
+ Example:
138
+ # Type into focused element
139
+ await type_text(backend, "Hello World")
140
+
141
+ # Click input then type
142
+ await type_text(backend, "search query", target=search_box.bbox)
143
+
144
+ # Clear and type
145
+ await type_text(backend, "new value", target=input.bbox, clear_first=True)
146
+ """
147
+ start_time = time.time()
148
+
149
+ try:
150
+ # Click target if provided
151
+ if target is not None:
152
+ x, y = _resolve_coordinates(target)
153
+ await backend.mouse_click(x, y)
154
+ await asyncio.sleep(0.05) # Wait for focus
155
+
156
+ # Clear existing content if requested
157
+ if clear_first:
158
+ # Select all (Ctrl+A / Cmd+A) and delete
159
+ await backend.eval("document.execCommand('selectAll')")
160
+ await asyncio.sleep(0.02)
161
+
162
+ # Type the text
163
+ await backend.type_text(text)
164
+
165
+ duration_ms = int((time.time() - start_time) * 1000)
166
+ return ActionResult(
167
+ success=True,
168
+ duration_ms=duration_ms,
169
+ outcome="dom_updated",
170
+ )
171
+ except Exception as e:
172
+ duration_ms = int((time.time() - start_time) * 1000)
173
+ return ActionResult(
174
+ success=False,
175
+ duration_ms=duration_ms,
176
+ outcome="error",
177
+ error={"code": "type_failed", "reason": str(e)},
178
+ )
179
+
180
+
181
+ async def scroll(
182
+ backend: "BrowserBackend",
183
+ delta_y: float = 300,
184
+ target: BBox | dict[str, float] | tuple[float, float] | None = None,
185
+ ) -> ActionResult:
186
+ """
187
+ Scroll the page or element.
188
+
189
+ Args:
190
+ backend: BrowserBackend implementation
191
+ delta_y: Scroll amount (positive=down, negative=up)
192
+ target: Optional position for scroll (defaults to viewport center)
193
+
194
+ Returns:
195
+ ActionResult with success status
196
+
197
+ Example:
198
+ # Scroll down 300px
199
+ await scroll(backend, 300)
200
+
201
+ # Scroll up 500px
202
+ await scroll(backend, -500)
203
+
204
+ # Scroll at specific position
205
+ await scroll(backend, 200, target=(500, 300))
206
+ """
207
+ start_time = time.time()
208
+
209
+ try:
210
+ x: float | None = None
211
+ y: float | None = None
212
+
213
+ if target is not None:
214
+ x, y = _resolve_coordinates(target)
215
+
216
+ await backend.wheel(delta_y=delta_y, x=x, y=y)
217
+
218
+ # Wait for scroll to settle
219
+ await asyncio.sleep(0.1)
220
+
221
+ duration_ms = int((time.time() - start_time) * 1000)
222
+ return ActionResult(
223
+ success=True,
224
+ duration_ms=duration_ms,
225
+ outcome="dom_updated",
226
+ )
227
+ except Exception as e:
228
+ duration_ms = int((time.time() - start_time) * 1000)
229
+ return ActionResult(
230
+ success=False,
231
+ duration_ms=duration_ms,
232
+ outcome="error",
233
+ error={"code": "scroll_failed", "reason": str(e)},
234
+ )
235
+
236
+
237
+ async def scroll_to_element(
238
+ backend: "BrowserBackend",
239
+ element_id: int,
240
+ behavior: Literal["smooth", "instant", "auto"] = "instant",
241
+ block: Literal["start", "center", "end", "nearest"] = "center",
242
+ ) -> ActionResult:
243
+ """
244
+ Scroll element into view using JavaScript scrollIntoView.
245
+
246
+ Args:
247
+ backend: BrowserBackend implementation
248
+ element_id: Element ID from snapshot (requires sentience_registry)
249
+ behavior: Scroll behavior
250
+ block: Vertical alignment
251
+
252
+ Returns:
253
+ ActionResult with success status
254
+ """
255
+ start_time = time.time()
256
+
257
+ try:
258
+ scrolled = await backend.eval(
259
+ f"""
260
+ (() => {{
261
+ const el = window.sentience_registry && window.sentience_registry[{element_id}];
262
+ if (el && el.scrollIntoView) {{
263
+ el.scrollIntoView({{
264
+ behavior: '{behavior}',
265
+ block: '{block}',
266
+ inline: 'nearest'
267
+ }});
268
+ return true;
269
+ }}
270
+ return false;
271
+ }})()
272
+ """
273
+ )
274
+
275
+ # Wait for scroll animation
276
+ wait_time = 0.3 if behavior == "smooth" else 0.05
277
+ await asyncio.sleep(wait_time)
278
+
279
+ duration_ms = int((time.time() - start_time) * 1000)
280
+
281
+ if scrolled:
282
+ return ActionResult(
283
+ success=True,
284
+ duration_ms=duration_ms,
285
+ outcome="dom_updated",
286
+ )
287
+ else:
288
+ return ActionResult(
289
+ success=False,
290
+ duration_ms=duration_ms,
291
+ outcome="error",
292
+ error={"code": "scroll_failed", "reason": "Element not found in registry"},
293
+ )
294
+ except Exception as e:
295
+ duration_ms = int((time.time() - start_time) * 1000)
296
+ return ActionResult(
297
+ success=False,
298
+ duration_ms=duration_ms,
299
+ outcome="error",
300
+ error={"code": "scroll_failed", "reason": str(e)},
301
+ )
302
+
303
+
304
+ async def wait_for_stable(
305
+ backend: "BrowserBackend",
306
+ state: Literal["interactive", "complete"] = "complete",
307
+ timeout_ms: int = 10000,
308
+ ) -> ActionResult:
309
+ """
310
+ Wait for page to reach stable state.
311
+
312
+ Args:
313
+ backend: BrowserBackend implementation
314
+ state: Target document.readyState
315
+ timeout_ms: Maximum wait time
316
+
317
+ Returns:
318
+ ActionResult with success status
319
+ """
320
+ start_time = time.time()
321
+
322
+ try:
323
+ await backend.wait_ready_state(state=state, timeout_ms=timeout_ms)
324
+
325
+ duration_ms = int((time.time() - start_time) * 1000)
326
+ return ActionResult(
327
+ success=True,
328
+ duration_ms=duration_ms,
329
+ outcome="dom_updated",
330
+ )
331
+ except TimeoutError as e:
332
+ duration_ms = int((time.time() - start_time) * 1000)
333
+ return ActionResult(
334
+ success=False,
335
+ duration_ms=duration_ms,
336
+ outcome="error",
337
+ error={"code": "timeout", "reason": str(e)},
338
+ )
339
+ except Exception as e:
340
+ duration_ms = int((time.time() - start_time) * 1000)
341
+ return ActionResult(
342
+ success=False,
343
+ duration_ms=duration_ms,
344
+ outcome="error",
345
+ error={"code": "wait_failed", "reason": str(e)},
346
+ )
347
+
348
+
349
+ def _resolve_coordinates(
350
+ target: BBox | dict[str, float] | tuple[float, float],
351
+ ) -> tuple[float, float]:
352
+ """
353
+ Resolve target to (x, y) coordinates.
354
+
355
+ - BBox: Returns center point
356
+ - dict: Returns x, y keys (or center if width/height present)
357
+ - tuple: Returns as-is
358
+ """
359
+ if isinstance(target, BBox):
360
+ return (target.x + target.width / 2, target.y + target.height / 2)
361
+ elif isinstance(target, tuple):
362
+ return target
363
+ elif isinstance(target, dict):
364
+ # If has width/height, compute center
365
+ if "width" in target and "height" in target:
366
+ x = target.get("x", 0) + target["width"] / 2
367
+ y = target.get("y", 0) + target["height"] / 2
368
+ return (x, y)
369
+ # Otherwise use x/y directly
370
+ return (target.get("x", 0), target.get("y", 0))
371
+ else:
372
+ raise ValueError(f"Invalid target type: {type(target)}")
@@ -0,0 +1,241 @@
1
+ """
2
+ Browser-use adapter for Sentience SDK.
3
+
4
+ This module provides BrowserUseAdapter which wraps browser-use's BrowserSession
5
+ and provides a CDPBackendV0 for Sentience operations.
6
+
7
+ Usage:
8
+ from browser_use import BrowserSession, BrowserProfile
9
+ from sentience import get_extension_dir
10
+ from sentience.backends import BrowserUseAdapter
11
+
12
+ # Create browser-use session with Sentience extension
13
+ profile = BrowserProfile(args=[f"--load-extension={get_extension_dir()}"])
14
+ session = BrowserSession(browser_profile=profile)
15
+ await session.start()
16
+
17
+ # Create Sentience adapter
18
+ adapter = BrowserUseAdapter(session)
19
+ backend = await adapter.create_backend()
20
+
21
+ # Use backend for Sentience operations
22
+ viewport = await backend.refresh_page_info()
23
+ await backend.mouse_click(100, 200)
24
+ """
25
+
26
+ from typing import TYPE_CHECKING, Any
27
+
28
+ from .cdp_backend import CDPBackendV0, CDPTransport
29
+
30
+ if TYPE_CHECKING:
31
+ # Import browser-use types only for type checking
32
+ # This avoids requiring browser-use as a hard dependency
33
+ pass
34
+
35
+
36
+ class BrowserUseCDPTransport(CDPTransport):
37
+ """
38
+ CDP transport implementation for browser-use.
39
+
40
+ Wraps browser-use's CDP client to provide the CDPTransport interface.
41
+ Uses cdp-use library pattern: cdp_client.send.Domain.method(params={}, session_id=)
42
+ """
43
+
44
+ def __init__(self, cdp_client: Any, session_id: str) -> None:
45
+ """
46
+ Initialize transport with browser-use CDP client.
47
+
48
+ Args:
49
+ cdp_client: browser-use's CDP client (from cdp_session.cdp_client)
50
+ session_id: CDP session ID (from cdp_session.session_id)
51
+ """
52
+ self._client = cdp_client
53
+ self._session_id = session_id
54
+
55
+ async def send(self, method: str, params: dict | None = None) -> dict:
56
+ """
57
+ Send CDP command using browser-use's cdp-use client.
58
+
59
+ Translates method name like "Runtime.evaluate" to
60
+ cdp_client.send.Runtime.evaluate(params={...}, session_id=...).
61
+
62
+ Args:
63
+ method: CDP method name, e.g., "Runtime.evaluate"
64
+ params: Method parameters
65
+
66
+ Returns:
67
+ CDP response dict
68
+ """
69
+ # Split method into domain and method name
70
+ # e.g., "Runtime.evaluate" -> ("Runtime", "evaluate")
71
+ parts = method.split(".", 1)
72
+ if len(parts) != 2:
73
+ raise ValueError(f"Invalid CDP method format: {method}")
74
+
75
+ domain_name, method_name = parts
76
+
77
+ # Get the domain object from cdp_client.send
78
+ domain = getattr(self._client.send, domain_name, None)
79
+ if domain is None:
80
+ raise ValueError(f"Unknown CDP domain: {domain_name}")
81
+
82
+ # Get the method from the domain
83
+ method_func = getattr(domain, method_name, None)
84
+ if method_func is None:
85
+ raise ValueError(f"Unknown CDP method: {method}")
86
+
87
+ # Call the method with params and session_id
88
+ result = await method_func(
89
+ params=params or {},
90
+ session_id=self._session_id,
91
+ )
92
+
93
+ # cdp-use returns the result directly or None
94
+ return result if result is not None else {}
95
+
96
+
97
+ class BrowserUseAdapter:
98
+ """
99
+ Adapter to use Sentience with browser-use's BrowserSession.
100
+
101
+ This adapter:
102
+ 1. Wraps browser-use's CDP client with BrowserUseCDPTransport
103
+ 2. Creates CDPBackendV0 for Sentience operations
104
+ 3. Provides access to the underlying page for extension calls
105
+
106
+ Example:
107
+ from browser_use import BrowserSession, BrowserProfile
108
+ from sentience import get_extension_dir, snapshot_async, SnapshotOptions
109
+ from sentience.backends import BrowserUseAdapter
110
+
111
+ # Setup browser-use with Sentience extension
112
+ profile = BrowserProfile(args=[f"--load-extension={get_extension_dir()}"])
113
+ session = BrowserSession(browser_profile=profile)
114
+ await session.start()
115
+
116
+ # Create adapter and backend
117
+ adapter = BrowserUseAdapter(session)
118
+ backend = await adapter.create_backend()
119
+
120
+ # Navigate (using browser-use)
121
+ page = await session.get_current_page()
122
+ await page.goto("https://example.com")
123
+
124
+ # Take Sentience snapshot (uses extension)
125
+ snap = await snapshot_async(adapter, SnapshotOptions())
126
+
127
+ # Use backend for precise clicking
128
+ await backend.mouse_click(snap.elements[0].bbox.x, snap.elements[0].bbox.y)
129
+ """
130
+
131
+ def __init__(self, session: Any) -> None:
132
+ """
133
+ Initialize adapter with browser-use BrowserSession.
134
+
135
+ Args:
136
+ session: browser-use BrowserSession instance
137
+ """
138
+ self._session = session
139
+ self._backend: CDPBackendV0 | None = None
140
+ self._transport: BrowserUseCDPTransport | None = None
141
+
142
+ @property
143
+ def page(self) -> Any:
144
+ """
145
+ Get the current Playwright page from browser-use.
146
+
147
+ This is needed for Sentience snapshot() which calls window.sentience.snapshot().
148
+
149
+ Returns:
150
+ Playwright Page object
151
+ """
152
+ # browser-use stores page in session
153
+ # Access pattern may vary by browser-use version
154
+ if hasattr(self._session, "page"):
155
+ return self._session.page
156
+ if hasattr(self._session, "_page"):
157
+ return self._session._page
158
+ if hasattr(self._session, "get_current_page"):
159
+ # This is async, but we need sync access for property
160
+ # Caller should use get_page_async() instead
161
+ raise RuntimeError("Use await adapter.get_page_async() to get the page")
162
+ raise RuntimeError("Could not find page in browser-use session")
163
+
164
+ async def get_page_async(self) -> Any:
165
+ """
166
+ Get the current Playwright page (async).
167
+
168
+ Returns:
169
+ Playwright Page object
170
+ """
171
+ if hasattr(self._session, "get_current_page"):
172
+ return await self._session.get_current_page()
173
+ return self.page
174
+
175
+ @property
176
+ def api_key(self) -> str | None:
177
+ """
178
+ API key for Sentience API (for snapshot compatibility).
179
+
180
+ Returns None since browser-use users pass api_key via SnapshotOptions.
181
+ """
182
+ return None
183
+
184
+ @property
185
+ def api_url(self) -> str | None:
186
+ """
187
+ API URL for Sentience API (for snapshot compatibility).
188
+
189
+ Returns None to use default.
190
+ """
191
+ return None
192
+
193
+ async def create_backend(self) -> CDPBackendV0:
194
+ """
195
+ Create CDP backend for Sentience operations.
196
+
197
+ This method:
198
+ 1. Gets or creates a CDP session from browser-use
199
+ 2. Creates BrowserUseCDPTransport to wrap the CDP client
200
+ 3. Creates CDPBackendV0 with the transport
201
+
202
+ Returns:
203
+ CDPBackendV0 instance ready for use
204
+
205
+ Raises:
206
+ RuntimeError: If CDP session cannot be created
207
+ """
208
+ if self._backend is not None:
209
+ return self._backend
210
+
211
+ # Get CDP session from browser-use
212
+ # browser-use uses: cdp_session = await session.get_or_create_cdp_session()
213
+ if not hasattr(self._session, "get_or_create_cdp_session"):
214
+ raise RuntimeError(
215
+ "browser-use session does not have get_or_create_cdp_session method. "
216
+ "Make sure you're using a compatible version of browser-use."
217
+ )
218
+
219
+ cdp_session = await self._session.get_or_create_cdp_session()
220
+
221
+ # Extract CDP client and session ID
222
+ cdp_client = cdp_session.cdp_client
223
+ session_id = cdp_session.session_id
224
+
225
+ # Create transport and backend
226
+ self._transport = BrowserUseCDPTransport(cdp_client, session_id)
227
+ self._backend = CDPBackendV0(self._transport)
228
+
229
+ return self._backend
230
+
231
+ async def get_transport(self) -> BrowserUseCDPTransport:
232
+ """
233
+ Get the CDP transport (creates backend if needed).
234
+
235
+ Returns:
236
+ BrowserUseCDPTransport instance
237
+ """
238
+ if self._transport is None:
239
+ await self.create_backend()
240
+ assert self._transport is not None
241
+ return self._transport