Skip to content

Commit efd65fe

Browse files
author
SentienceDEV
committed
Tabs + JS evaluator + Lifecycle hooks + cli
1 parent 876c007 commit efd65fe

File tree

15 files changed

+815
-89
lines changed

15 files changed

+815
-89
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,24 @@ pip install transformers torch # For local LLMs
2222
pip install -e .
2323
```
2424

25+
## 🧭 Manual driver CLI
26+
27+
Use the interactive CLI to open a page, inspect clickables, and drive actions:
28+
29+
```bash
30+
sentience driver --url https://example.com
31+
```
32+
33+
Commands:
34+
- `open <url>`
35+
- `state [limit]`
36+
- `click <element_id>`
37+
- `type <element_id> <text>`
38+
- `press <key>`
39+
- `screenshot [path]`
40+
- `help`
41+
- `close`
42+
2543
## Jest for AI Web Agent
2644

2745
### Semantic snapshots and assertions that let agents act, verify, and know when they're done.

sentience/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
Snapshot,
8787
SnapshotFilter,
8888
SnapshotOptions,
89+
StepHookContext,
8990
StorageState,
9091
TextContext,
9192
TextMatch,

sentience/agent.py

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
import asyncio
77
import hashlib
8+
import inspect
9+
import logging
810
import time
11+
from collections.abc import Callable
912
from typing import TYPE_CHECKING, Any, Optional, Union
1013

1114
from .action_executor import ActionExecutor
@@ -23,6 +26,7 @@
2326
ScreenshotConfig,
2427
Snapshot,
2528
SnapshotOptions,
29+
StepHookContext,
2630
TokenStats,
2731
)
2832
from .protocols import AsyncBrowserProtocol, BrowserProtocol
@@ -65,6 +69,51 @@ def _safe_tracer_call(
6569
print(f"⚠️ Tracer error (non-fatal): {tracer_error}")
6670

6771

72+
def _safe_hook_call_sync(
73+
hook: Callable[[StepHookContext], Any] | None,
74+
ctx: StepHookContext,
75+
verbose: bool,
76+
) -> None:
77+
if not hook:
78+
return
79+
try:
80+
result = hook(ctx)
81+
if inspect.isawaitable(result):
82+
try:
83+
loop = asyncio.get_running_loop()
84+
except RuntimeError:
85+
asyncio.run(result)
86+
else:
87+
loop.create_task(result)
88+
except Exception as hook_error:
89+
if verbose:
90+
print(f"⚠️ Hook error (non-fatal): {hook_error}")
91+
else:
92+
logging.getLogger(__name__).warning(
93+
"Hook error (non-fatal): %s", hook_error
94+
)
95+
96+
97+
async def _safe_hook_call_async(
98+
hook: Callable[[StepHookContext], Any] | None,
99+
ctx: StepHookContext,
100+
verbose: bool,
101+
) -> None:
102+
if not hook:
103+
return
104+
try:
105+
result = hook(ctx)
106+
if inspect.isawaitable(result):
107+
await result
108+
except Exception as hook_error:
109+
if verbose:
110+
print(f"⚠️ Hook error (non-fatal): {hook_error}")
111+
else:
112+
logging.getLogger(__name__).warning(
113+
"Hook error (non-fatal): %s", hook_error
114+
)
115+
116+
68117
class SentienceAgent(BaseAgent):
69118
"""
70119
High-level agent that combines Sentience SDK with any LLM provider.
@@ -181,6 +230,8 @@ def act( # noqa: C901
181230
goal: str,
182231
max_retries: int = 2,
183232
snapshot_options: SnapshotOptions | None = None,
233+
on_step_start: Callable[[StepHookContext], Any] | None = None,
234+
on_step_end: Callable[[StepHookContext], Any] | None = None,
184235
) -> AgentActionResult:
185236
"""
186237
Execute a high-level goal using observe → think → act loop
@@ -224,6 +275,18 @@ def act( # noqa: C901
224275
pre_url=pre_url,
225276
)
226277

278+
_safe_hook_call_sync(
279+
on_step_start,
280+
StepHookContext(
281+
step_id=step_id,
282+
step_index=self._step_count,
283+
goal=goal,
284+
attempt=0,
285+
url=pre_url,
286+
),
287+
self.verbose,
288+
)
289+
227290
# Track data collected during step execution for step_end emission on failure
228291
_step_snap_with_diff: Snapshot | None = None
229292
_step_pre_url: str | None = None
@@ -396,8 +459,8 @@ def act( # noqa: C901
396459
_step_duration_ms = duration_ms
397460

398461
# Emit action execution trace event if tracer is enabled
462+
post_url = self.browser.page.url if self.browser.page else None
399463
if self.tracer:
400-
post_url = self.browser.page.url if self.browser.page else None
401464

402465
# Include element data for live overlay visualization
403466
elements_data = [
@@ -454,7 +517,6 @@ def act( # noqa: C901
454517
if self.tracer:
455518
# Get pre_url from step_start (stored in tracer or use current)
456519
pre_url = snap.url
457-
post_url = self.browser.page.url if self.browser.page else None
458520

459521
# Compute snapshot digest (simplified - use URL + timestamp)
460522
snapshot_digest = f"sha256:{self._compute_hash(f'{pre_url}{snap.timestamp}')}"
@@ -561,6 +623,20 @@ def act( # noqa: C901
561623
step_id=step_id,
562624
)
563625

626+
_safe_hook_call_sync(
627+
on_step_end,
628+
StepHookContext(
629+
step_id=step_id,
630+
step_index=self._step_count,
631+
goal=goal,
632+
attempt=attempt,
633+
url=post_url,
634+
success=result.success,
635+
outcome=result.outcome,
636+
error=result.error,
637+
),
638+
self.verbose,
639+
)
564640
return result
565641

566642
except Exception as e:
@@ -660,6 +736,20 @@ def act( # noqa: C901
660736
"duration_ms": 0,
661737
}
662738
)
739+
_safe_hook_call_sync(
740+
on_step_end,
741+
StepHookContext(
742+
step_id=step_id,
743+
step_index=self._step_count,
744+
goal=goal,
745+
attempt=attempt,
746+
url=_step_pre_url,
747+
success=False,
748+
outcome="exception",
749+
error=str(e),
750+
),
751+
self.verbose,
752+
)
663753
raise RuntimeError(f"Failed after {max_retries} retries: {e}")
664754

665755
def _track_tokens(self, goal: str, llm_response: LLMResponse):
@@ -833,6 +923,8 @@ async def act( # noqa: C901
833923
goal: str,
834924
max_retries: int = 2,
835925
snapshot_options: SnapshotOptions | None = None,
926+
on_step_start: Callable[[StepHookContext], Any] | None = None,
927+
on_step_end: Callable[[StepHookContext], Any] | None = None,
836928
) -> AgentActionResult:
837929
"""
838930
Execute a high-level goal using observe → think → act loop (async)
@@ -873,6 +965,18 @@ async def act( # noqa: C901
873965
pre_url=pre_url,
874966
)
875967

968+
await _safe_hook_call_async(
969+
on_step_start,
970+
StepHookContext(
971+
step_id=step_id,
972+
step_index=self._step_count,
973+
goal=goal,
974+
attempt=0,
975+
url=pre_url,
976+
),
977+
self.verbose,
978+
)
979+
876980
# Track data collected during step execution for step_end emission on failure
877981
_step_snap_with_diff: Snapshot | None = None
878982
_step_pre_url: str | None = None
@@ -1209,6 +1313,21 @@ async def act( # noqa: C901
12091313
step_id=step_id,
12101314
)
12111315

1316+
post_url = self.browser.page.url if self.browser.page else None
1317+
await _safe_hook_call_async(
1318+
on_step_end,
1319+
StepHookContext(
1320+
step_id=step_id,
1321+
step_index=self._step_count,
1322+
goal=goal,
1323+
attempt=attempt,
1324+
url=post_url,
1325+
success=result.success,
1326+
outcome=result.outcome,
1327+
error=result.error,
1328+
),
1329+
self.verbose,
1330+
)
12121331
return result
12131332

12141333
except Exception as e:
@@ -1308,6 +1427,20 @@ async def act( # noqa: C901
13081427
"duration_ms": 0,
13091428
}
13101429
)
1430+
await _safe_hook_call_async(
1431+
on_step_end,
1432+
StepHookContext(
1433+
step_id=step_id,
1434+
step_index=self._step_count,
1435+
goal=goal,
1436+
attempt=attempt,
1437+
url=_step_pre_url,
1438+
success=False,
1439+
outcome="exception",
1440+
error=str(e),
1441+
),
1442+
self.verbose,
1443+
)
13111444
raise RuntimeError(f"Failed after {max_retries} retries: {e}")
13121445

13131446
def _track_tokens(self, goal: str, llm_response: LLMResponse):

0 commit comments

Comments
 (0)