sentienceapi 0.90.16__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 (90) hide show
  1. sentience/__init__.py +120 -6
  2. sentience/_extension_loader.py +156 -1
  3. sentience/action_executor.py +217 -0
  4. sentience/actions.py +758 -30
  5. sentience/agent.py +806 -293
  6. sentience/agent_config.py +3 -0
  7. sentience/agent_runtime.py +840 -0
  8. sentience/asserts/__init__.py +70 -0
  9. sentience/asserts/expect.py +621 -0
  10. sentience/asserts/query.py +383 -0
  11. sentience/async_api.py +89 -1141
  12. sentience/backends/__init__.py +137 -0
  13. sentience/backends/actions.py +372 -0
  14. sentience/backends/browser_use_adapter.py +241 -0
  15. sentience/backends/cdp_backend.py +393 -0
  16. sentience/backends/exceptions.py +211 -0
  17. sentience/backends/playwright_backend.py +194 -0
  18. sentience/backends/protocol.py +216 -0
  19. sentience/backends/sentience_context.py +469 -0
  20. sentience/backends/snapshot.py +483 -0
  21. sentience/base_agent.py +95 -0
  22. sentience/browser.py +678 -39
  23. sentience/browser_evaluator.py +299 -0
  24. sentience/canonicalization.py +207 -0
  25. sentience/cloud_tracing.py +507 -42
  26. sentience/constants.py +6 -0
  27. sentience/conversational_agent.py +77 -43
  28. sentience/cursor_policy.py +142 -0
  29. sentience/element_filter.py +136 -0
  30. sentience/expect.py +98 -2
  31. sentience/extension/background.js +56 -185
  32. sentience/extension/content.js +150 -287
  33. sentience/extension/injected_api.js +1088 -1368
  34. sentience/extension/manifest.json +1 -1
  35. sentience/extension/pkg/sentience_core.d.ts +22 -22
  36. sentience/extension/pkg/sentience_core.js +275 -433
  37. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  38. sentience/extension/release.json +47 -47
  39. sentience/failure_artifacts.py +241 -0
  40. sentience/formatting.py +9 -53
  41. sentience/inspector.py +183 -1
  42. sentience/integrations/__init__.py +6 -0
  43. sentience/integrations/langchain/__init__.py +12 -0
  44. sentience/integrations/langchain/context.py +18 -0
  45. sentience/integrations/langchain/core.py +326 -0
  46. sentience/integrations/langchain/tools.py +180 -0
  47. sentience/integrations/models.py +46 -0
  48. sentience/integrations/pydanticai/__init__.py +15 -0
  49. sentience/integrations/pydanticai/deps.py +20 -0
  50. sentience/integrations/pydanticai/toolset.py +468 -0
  51. sentience/llm_interaction_handler.py +191 -0
  52. sentience/llm_provider.py +765 -66
  53. sentience/llm_provider_utils.py +120 -0
  54. sentience/llm_response_builder.py +153 -0
  55. sentience/models.py +595 -3
  56. sentience/ordinal.py +280 -0
  57. sentience/overlay.py +109 -2
  58. sentience/protocols.py +228 -0
  59. sentience/query.py +67 -5
  60. sentience/read.py +95 -3
  61. sentience/recorder.py +223 -3
  62. sentience/schemas/trace_v1.json +128 -9
  63. sentience/screenshot.py +48 -2
  64. sentience/sentience_methods.py +86 -0
  65. sentience/snapshot.py +599 -55
  66. sentience/snapshot_diff.py +126 -0
  67. sentience/text_search.py +120 -5
  68. sentience/trace_event_builder.py +148 -0
  69. sentience/trace_file_manager.py +197 -0
  70. sentience/trace_indexing/index_schema.py +95 -7
  71. sentience/trace_indexing/indexer.py +105 -48
  72. sentience/tracer_factory.py +120 -9
  73. sentience/tracing.py +172 -8
  74. sentience/utils/__init__.py +40 -0
  75. sentience/utils/browser.py +46 -0
  76. sentience/{utils.py → utils/element.py} +3 -42
  77. sentience/utils/formatting.py +59 -0
  78. sentience/verification.py +618 -0
  79. sentience/visual_agent.py +2058 -0
  80. sentience/wait.py +68 -2
  81. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +199 -40
  82. sentienceapi-0.98.0.dist-info/RECORD +92 -0
  83. sentience/extension/test-content.js +0 -4
  84. sentienceapi-0.90.16.dist-info/RECORD +0 -50
  85. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
  86. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
  87. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
  88. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
  89. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
  90. {sentienceapi-0.90.16.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,483 @@
1
+ """
2
+ Backend-agnostic snapshot for browser-use integration.
3
+
4
+ Takes Sentience snapshots using BrowserBackend protocol,
5
+ enabling element grounding with browser-use or other frameworks.
6
+
7
+ Usage with browser-use:
8
+ from sentience.backends import BrowserUseAdapter, snapshot, CachedSnapshot
9
+
10
+ adapter = BrowserUseAdapter(session)
11
+ backend = await adapter.create_backend()
12
+
13
+ # Take snapshot
14
+ snap = await snapshot(backend)
15
+ print(f"Found {len(snap.elements)} elements")
16
+
17
+ # With caching (reuse if fresh)
18
+ cache = CachedSnapshot(backend, max_age_ms=2000)
19
+ snap1 = await cache.get() # Fresh snapshot
20
+ snap2 = await cache.get() # Returns cached if < 2s old
21
+ cache.invalidate() # Force refresh on next get()
22
+ """
23
+
24
+ import asyncio
25
+ import time
26
+ from typing import TYPE_CHECKING, Any
27
+
28
+ from ..constants import SENTIENCE_API_URL
29
+ from ..models import Snapshot, SnapshotOptions
30
+ from ..snapshot import (
31
+ _build_snapshot_payload,
32
+ _merge_api_result_with_local,
33
+ _post_snapshot_to_gateway_async,
34
+ )
35
+ from .exceptions import ExtensionDiagnostics, ExtensionNotLoadedError, SnapshotError
36
+
37
+ if TYPE_CHECKING:
38
+ from .protocol import BrowserBackend
39
+
40
+
41
+ def _is_execution_context_destroyed_error(e: Exception) -> bool:
42
+ """
43
+ Playwright (and other browser backends) can throw while a navigation is in-flight.
44
+
45
+ Common symptoms:
46
+ - "Execution context was destroyed, most likely because of a navigation"
47
+ - "Cannot find context with specified id"
48
+ """
49
+ msg = str(e).lower()
50
+ return (
51
+ "execution context was destroyed" in msg
52
+ or "most likely because of a navigation" in msg
53
+ or "cannot find context with specified id" in msg
54
+ )
55
+
56
+
57
+ async def _eval_with_navigation_retry(
58
+ backend: "BrowserBackend",
59
+ expression: str,
60
+ *,
61
+ retries: int = 10,
62
+ settle_state: str = "interactive",
63
+ settle_timeout_ms: int = 10000,
64
+ ) -> Any:
65
+ """
66
+ Evaluate JS, retrying once/ twice if the page is mid-navigation.
67
+
68
+ This makes snapshots resilient to cases like:
69
+ - press Enter (navigation) → snapshot immediately → context destroyed
70
+ """
71
+ last_err: Exception | None = None
72
+ for attempt in range(retries + 1):
73
+ try:
74
+ return await backend.eval(expression)
75
+ except Exception as e:
76
+ last_err = e
77
+ if not _is_execution_context_destroyed_error(e) or attempt >= retries:
78
+ raise
79
+ # Navigation is in-flight; wait for new document context then retry.
80
+ try:
81
+ await backend.wait_ready_state(state=settle_state, timeout_ms=settle_timeout_ms) # type: ignore[arg-type]
82
+ except Exception:
83
+ # If readyState polling also fails mid-nav, still retry after a short backoff.
84
+ pass
85
+ # Exponential-ish backoff (caps quickly), tuned for real navigations.
86
+ await asyncio.sleep(min(0.25 * (attempt + 1), 1.5))
87
+
88
+ # Unreachable in practice, but keeps type-checkers happy.
89
+ raise last_err if last_err else RuntimeError("eval failed")
90
+
91
+
92
+ class CachedSnapshot:
93
+ """
94
+ Snapshot cache with staleness detection.
95
+
96
+ Caches snapshots and returns cached version if still fresh.
97
+ Useful for reducing redundant snapshot calls in action loops.
98
+
99
+ Usage:
100
+ cache = CachedSnapshot(backend, max_age_ms=2000)
101
+
102
+ # First call takes fresh snapshot
103
+ snap1 = await cache.get()
104
+
105
+ # Second call returns cached if < 2s old
106
+ snap2 = await cache.get()
107
+
108
+ # Invalidate after actions that change DOM
109
+ await click(backend, element.bbox)
110
+ cache.invalidate()
111
+
112
+ # Next get() will take fresh snapshot
113
+ snap3 = await cache.get()
114
+ """
115
+
116
+ def __init__(
117
+ self,
118
+ backend: "BrowserBackend",
119
+ max_age_ms: int = 2000,
120
+ options: SnapshotOptions | None = None,
121
+ ) -> None:
122
+ """
123
+ Initialize cached snapshot.
124
+
125
+ Args:
126
+ backend: BrowserBackend implementation
127
+ max_age_ms: Maximum cache age in milliseconds (default: 2000)
128
+ options: Default snapshot options
129
+ """
130
+ self._backend = backend
131
+ self._max_age_ms = max_age_ms
132
+ self._options = options
133
+ self._cached: Snapshot | None = None
134
+ self._cached_at: float = 0 # timestamp in seconds
135
+ self._cached_url: str | None = None
136
+
137
+ async def get(
138
+ self,
139
+ options: SnapshotOptions | None = None,
140
+ force_refresh: bool = False,
141
+ ) -> Snapshot:
142
+ """
143
+ Get snapshot, using cache if fresh.
144
+
145
+ Args:
146
+ options: Override default options for this call
147
+ force_refresh: If True, always take fresh snapshot
148
+
149
+ Returns:
150
+ Snapshot (cached or fresh)
151
+ """
152
+ # Check if we need to refresh
153
+ if force_refresh or self._is_stale():
154
+ self._cached = await snapshot(
155
+ self._backend,
156
+ options or self._options,
157
+ )
158
+ self._cached_at = time.time()
159
+ self._cached_url = self._cached.url
160
+
161
+ assert self._cached is not None
162
+ return self._cached
163
+
164
+ def invalidate(self) -> None:
165
+ """
166
+ Invalidate cache, forcing refresh on next get().
167
+
168
+ Call this after actions that modify the DOM.
169
+ """
170
+ self._cached = None
171
+ self._cached_at = 0
172
+ self._cached_url = None
173
+
174
+ def _is_stale(self) -> bool:
175
+ """Check if cache is stale and needs refresh."""
176
+ if self._cached is None:
177
+ return True
178
+
179
+ # Check age
180
+ age_ms = (time.time() - self._cached_at) * 1000
181
+ if age_ms > self._max_age_ms:
182
+ return True
183
+
184
+ return False
185
+
186
+ @property
187
+ def is_cached(self) -> bool:
188
+ """Check if a cached snapshot exists."""
189
+ return self._cached is not None
190
+
191
+ @property
192
+ def age_ms(self) -> float:
193
+ """Get age of cached snapshot in milliseconds."""
194
+ if self._cached is None:
195
+ return float("inf")
196
+ return (time.time() - self._cached_at) * 1000
197
+
198
+
199
+ async def snapshot(
200
+ backend: "BrowserBackend",
201
+ options: SnapshotOptions | None = None,
202
+ ) -> Snapshot:
203
+ """
204
+ Take a Sentience snapshot using the backend protocol.
205
+
206
+ This function respects the `use_api` option and can call either:
207
+ - Server-side API (Pro/Enterprise tier) when `use_api=True` and API key is provided
208
+ - Local extension (Free tier) when `use_api=False` or no API key
209
+
210
+ Requires:
211
+ - Sentience extension loaded in browser (via --load-extension)
212
+ - Extension injected window.sentience API
213
+
214
+ Args:
215
+ backend: BrowserBackend implementation (CDPBackendV0, PlaywrightBackend, etc.)
216
+ options: Snapshot options (limit, filter, screenshot, use_api, sentience_api_key, etc.)
217
+
218
+ Returns:
219
+ Snapshot with elements, viewport, and optional screenshot
220
+
221
+ Example:
222
+ from sentience.backends import BrowserUseAdapter
223
+ from sentience.backends.snapshot import snapshot
224
+ from sentience.models import SnapshotOptions
225
+
226
+ adapter = BrowserUseAdapter(session)
227
+ backend = await adapter.create_backend()
228
+
229
+ # Basic snapshot (uses local extension)
230
+ snap = await snapshot(backend)
231
+
232
+ # With server-side API (Pro/Enterprise tier)
233
+ snap = await snapshot(backend, SnapshotOptions(
234
+ use_api=True,
235
+ sentience_api_key="sk_pro_xxxxx",
236
+ limit=100,
237
+ screenshot=True
238
+ ))
239
+
240
+ # Force local extension (Free tier)
241
+ snap = await snapshot(backend, SnapshotOptions(
242
+ use_api=False
243
+ ))
244
+ """
245
+ if options is None:
246
+ options = SnapshotOptions()
247
+
248
+ # Determine if we should use server-side API
249
+ # Same logic as main snapshot() function in sentience/snapshot.py
250
+ should_use_api = (
251
+ options.use_api if options.use_api is not None else (options.sentience_api_key is not None)
252
+ )
253
+
254
+ if should_use_api and options.sentience_api_key:
255
+ # Use server-side API (Pro/Enterprise tier)
256
+ return await _snapshot_via_api(backend, options)
257
+ else:
258
+ # Use local extension (Free tier)
259
+ return await _snapshot_via_extension(backend, options)
260
+
261
+
262
+ async def _wait_for_extension(
263
+ backend: "BrowserBackend",
264
+ timeout_ms: int = 5000,
265
+ ) -> None:
266
+ """
267
+ Wait for Sentience extension to inject window.sentience API.
268
+
269
+ Args:
270
+ backend: BrowserBackend implementation
271
+ timeout_ms: Maximum wait time
272
+
273
+ Raises:
274
+ RuntimeError: If extension not injected within timeout
275
+ """
276
+ import asyncio
277
+ import logging
278
+
279
+ logger = logging.getLogger("sentience.backends.snapshot")
280
+
281
+ start = time.monotonic()
282
+ timeout_sec = timeout_ms / 1000.0
283
+ poll_count = 0
284
+
285
+ logger.debug(f"Waiting for extension injection (timeout={timeout_ms}ms)...")
286
+
287
+ while True:
288
+ elapsed = time.monotonic() - start
289
+ poll_count += 1
290
+
291
+ if poll_count % 10 == 0: # Log every 10 polls (~1 second)
292
+ logger.debug(f"Extension poll #{poll_count}, elapsed={elapsed*1000:.0f}ms")
293
+
294
+ if elapsed >= timeout_sec:
295
+ # Gather diagnostics
296
+ try:
297
+ diag_dict = await backend.eval(
298
+ """
299
+ (() => ({
300
+ sentience_defined: typeof window.sentience !== 'undefined',
301
+ sentience_snapshot: typeof window.sentience?.snapshot === 'function',
302
+ url: window.location.href,
303
+ extension_id: document.documentElement.dataset.sentienceExtensionId || null,
304
+ has_content_script: !!document.documentElement.dataset.sentienceExtensionId
305
+ }))()
306
+ """
307
+ )
308
+ diagnostics = ExtensionDiagnostics.from_dict(diag_dict)
309
+ logger.debug(f"Extension diagnostics: {diag_dict}")
310
+ except Exception as e:
311
+ diagnostics = ExtensionDiagnostics(error=f"Could not gather diagnostics: {e}")
312
+
313
+ raise ExtensionNotLoadedError.from_timeout(
314
+ timeout_ms=timeout_ms,
315
+ diagnostics=diagnostics,
316
+ )
317
+
318
+ # Check if extension is ready
319
+ try:
320
+ ready = await backend.eval(
321
+ "typeof window.sentience !== 'undefined' && "
322
+ "typeof window.sentience.snapshot === 'function'"
323
+ )
324
+ if ready:
325
+ return
326
+ except Exception:
327
+ pass # Keep polling
328
+
329
+ await asyncio.sleep(0.1)
330
+
331
+
332
+ async def _snapshot_via_extension(
333
+ backend: "BrowserBackend",
334
+ options: SnapshotOptions,
335
+ ) -> Snapshot:
336
+ """Take snapshot using local extension (Free tier)"""
337
+ # Wait for extension injection
338
+ await _wait_for_extension(backend, timeout_ms=5000)
339
+
340
+ # Build options dict for extension API
341
+ ext_options = _build_extension_options(options)
342
+
343
+ # Call extension's snapshot function
344
+ result = await _eval_with_navigation_retry(
345
+ backend,
346
+ f"""
347
+ (() => {{
348
+ const options = {_json_serialize(ext_options)};
349
+ return window.sentience.snapshot(options);
350
+ }})()
351
+ """,
352
+ )
353
+
354
+ if result is None:
355
+ # Try to get URL for better error message
356
+ try:
357
+ url = await backend.eval("window.location.href")
358
+ except Exception:
359
+ url = None
360
+ raise SnapshotError.from_null_result(url=url)
361
+
362
+ # Show overlay if requested
363
+ if options.show_overlay:
364
+ raw_elements = result.get("raw_elements", [])
365
+ if raw_elements:
366
+ await _eval_with_navigation_retry(
367
+ backend,
368
+ f"""
369
+ (() => {{
370
+ if (window.sentience && window.sentience.showOverlay) {{
371
+ window.sentience.showOverlay({_json_serialize(raw_elements)}, null);
372
+ }}
373
+ }})()
374
+ """,
375
+ )
376
+
377
+ # Build and return Snapshot
378
+ return Snapshot(**result)
379
+
380
+
381
+ async def _snapshot_via_api(
382
+ backend: "BrowserBackend",
383
+ options: SnapshotOptions,
384
+ ) -> Snapshot:
385
+ """Take snapshot using server-side API (Pro/Enterprise tier)"""
386
+ # Default API URL (same as main snapshot function)
387
+ api_url = SENTIENCE_API_URL
388
+
389
+ # Wait for extension injection (needed even for API mode to collect raw data)
390
+ await _wait_for_extension(backend, timeout_ms=5000)
391
+
392
+ # Step 1: Get raw data from local extension (always happens locally)
393
+ raw_options: dict[str, Any] = {}
394
+ if options.screenshot is not False:
395
+ raw_options["screenshot"] = options.screenshot
396
+
397
+ # Call extension to get raw elements
398
+ raw_result = await _eval_with_navigation_retry(
399
+ backend,
400
+ f"""
401
+ (() => {{
402
+ const options = {_json_serialize(raw_options)};
403
+ return window.sentience.snapshot(options);
404
+ }})()
405
+ """,
406
+ )
407
+
408
+ if raw_result is None:
409
+ try:
410
+ url = await backend.eval("window.location.href")
411
+ except Exception:
412
+ url = None
413
+ raise SnapshotError.from_null_result(url=url)
414
+
415
+ # Step 2: Send to server for smart ranking/filtering
416
+ payload = _build_snapshot_payload(raw_result, options)
417
+
418
+ try:
419
+ api_result = await _post_snapshot_to_gateway_async(
420
+ payload, options.sentience_api_key, api_url
421
+ )
422
+
423
+ # Merge API result with local data (screenshot, etc.)
424
+ snapshot_data = _merge_api_result_with_local(api_result, raw_result)
425
+
426
+ # Show visual overlay if requested (use API-ranked elements)
427
+ if options.show_overlay:
428
+ elements = api_result.get("elements", [])
429
+ if elements:
430
+ await _eval_with_navigation_retry(
431
+ backend,
432
+ f"""
433
+ (() => {{
434
+ if (window.sentience && window.sentience.showOverlay) {{
435
+ window.sentience.showOverlay({_json_serialize(elements)}, null);
436
+ }}
437
+ }})()
438
+ """,
439
+ )
440
+
441
+ return Snapshot(**snapshot_data)
442
+ except (RuntimeError, ValueError):
443
+ # Re-raise validation errors as-is
444
+ raise
445
+ except Exception as e:
446
+ # Fallback to local extension on API error
447
+ # This matches the behavior of the main snapshot function
448
+ raise RuntimeError(
449
+ f"Server-side snapshot API failed: {e}. "
450
+ "Try using use_api=False to use local extension instead."
451
+ ) from e
452
+
453
+
454
+ def _build_extension_options(options: SnapshotOptions) -> dict[str, Any]:
455
+ """Build options dict for extension API call."""
456
+ ext_options: dict[str, Any] = {}
457
+
458
+ # Screenshot config
459
+ if options.screenshot is not False:
460
+ if hasattr(options.screenshot, "model_dump"):
461
+ ext_options["screenshot"] = options.screenshot.model_dump()
462
+ else:
463
+ ext_options["screenshot"] = options.screenshot
464
+
465
+ # Limit (only if not default)
466
+ if options.limit != 50:
467
+ ext_options["limit"] = options.limit
468
+
469
+ # Filter
470
+ if options.filter is not None:
471
+ if hasattr(options.filter, "model_dump"):
472
+ ext_options["filter"] = options.filter.model_dump()
473
+ else:
474
+ ext_options["filter"] = options.filter
475
+
476
+ return ext_options
477
+
478
+
479
+ def _json_serialize(obj: Any) -> str:
480
+ """Serialize object to JSON string for embedding in JS."""
481
+ import json
482
+
483
+ return json.dumps(obj)
sentience/base_agent.py CHANGED
@@ -1,3 +1,5 @@
1
+ from typing import Optional
2
+
1
3
  """
2
4
  BaseAgent: Abstract base class for all Sentience agents
3
5
  Defines the interface that all agent implementations must follow
@@ -99,3 +101,96 @@ class BaseAgent(ABC):
99
101
  >>> # filtered now contains only relevant elements
100
102
  """
101
103
  return snapshot.elements
104
+
105
+
106
+ class BaseAgentAsync(ABC):
107
+ """
108
+ Abstract base class for all async Sentience agents.
109
+
110
+ Provides a standard interface for:
111
+ - Executing natural language goals (act)
112
+ - Tracking execution history
113
+ - Monitoring token usage
114
+ - Filtering elements based on goals
115
+
116
+ Subclasses must implement:
117
+ - act(): Execute a natural language goal (async)
118
+ - get_history(): Return execution history
119
+ - get_token_stats(): Return token usage statistics
120
+ - clear_history(): Reset history and token counters
121
+
122
+ Subclasses can override:
123
+ - filter_elements(): Customize element filtering logic
124
+ """
125
+
126
+ @abstractmethod
127
+ async def act(self, goal: str, **kwargs) -> AgentActionResult:
128
+ """
129
+ Execute a natural language goal using the agent (async).
130
+
131
+ Args:
132
+ goal: Natural language instruction (e.g., "Click the login button")
133
+ **kwargs: Additional parameters (implementation-specific)
134
+
135
+ Returns:
136
+ AgentActionResult with execution details
137
+
138
+ Raises:
139
+ RuntimeError: If execution fails after retries
140
+ """
141
+ pass
142
+
143
+ @abstractmethod
144
+ def get_history(self) -> list[ActionHistory]:
145
+ """
146
+ Get the execution history of all actions taken.
147
+
148
+ Returns:
149
+ List of ActionHistory entries
150
+ """
151
+ pass
152
+
153
+ @abstractmethod
154
+ def get_token_stats(self) -> TokenStats:
155
+ """
156
+ Get token usage statistics for the agent session.
157
+
158
+ Returns:
159
+ TokenStats with cumulative token counts
160
+ """
161
+ pass
162
+
163
+ @abstractmethod
164
+ def clear_history(self) -> None:
165
+ """
166
+ Clear execution history and reset token counters.
167
+
168
+ This resets the agent to a clean state.
169
+ """
170
+ pass
171
+
172
+ def filter_elements(self, snapshot: Snapshot, goal: str | None = None) -> list[Element]:
173
+ """
174
+ Filter elements from a snapshot based on goal context.
175
+
176
+ Default implementation returns all elements unchanged.
177
+ Subclasses can override to implement custom filtering logic
178
+ such as:
179
+ - Removing irrelevant elements based on goal keywords
180
+ - Boosting importance of matching elements
181
+ - Filtering by role, size, or visual properties
182
+
183
+ Args:
184
+ snapshot: Current page snapshot
185
+ goal: User's goal (can inform filtering strategy)
186
+
187
+ Returns:
188
+ Filtered list of elements (default: all elements)
189
+
190
+ Example:
191
+ >>> agent = SentienceAgentAsync(browser, llm)
192
+ >>> snap = await snapshot_async(browser)
193
+ >>> filtered = agent.filter_elements(snap, goal="Click login")
194
+ >>> # filtered now contains only relevant elements
195
+ """
196
+ return snapshot.elements