Skip to content

Commit 36132bb

Browse files
author
SentienceDEV
committed
Merge branch 'main' into deepinfra_models
2 parents f96932e + 30ef246 commit 36132bb

23 files changed

+3874
-1982
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -40,61 +40,8 @@ jobs:
4040
# Also clean .pyc files in sentience package specifically
4141
find sentience -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || python -c "import pathlib, shutil; [shutil.rmtree(p) for p in pathlib.Path('sentience').rglob('__pycache__') if p.is_dir()]" || true
4242
find sentience -name "*.pyc" -delete 2>/dev/null || python -c "import pathlib; [p.unlink() for p in pathlib.Path('sentience').rglob('*.pyc')]" || true
43-
# CRITICAL: Fix assertTrue bug if it exists in source (shouldn't happen, but safety check)
44-
python << 'PYEOF'
45-
import re
46-
import os
47-
import sys
48-
49-
# Set UTF-8 encoding for Windows compatibility
50-
if sys.platform == 'win32':
51-
import io
52-
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
53-
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
54-
55-
file_path = 'sentience/agent_runtime.py'
56-
print(f'=== Auto-fix check for {file_path} ===')
57-
try:
58-
if not os.path.exists(file_path):
59-
print(f'ERROR: {file_path} not found!')
60-
sys.exit(1)
61-
62-
with open(file_path, 'r', encoding='utf-8') as f:
63-
content = f.read()
64-
65-
if 'self.assertTrue(' in content:
66-
print('WARNING: Found self.assertTrue( in source file! Auto-fixing...')
67-
# Count occurrences
68-
count = len(re.findall(r'self\.assertTrue\s*\(', content))
69-
print(f'Found {count} occurrence(s) of self.assertTrue(')
70-
71-
# Replace all occurrences
72-
new_content = re.sub(r'self\.assertTrue\s*\(', 'self.assert_(', content)
73-
74-
# Write back
75-
with open(file_path, 'w', encoding='utf-8') as f:
76-
f.write(new_content)
77-
78-
# Verify the fix
79-
with open(file_path, 'r', encoding='utf-8') as f:
80-
verify_content = f.read()
81-
if 'self.assertTrue(' in verify_content:
82-
print('ERROR: Auto-fix failed! File still contains self.assertTrue(')
83-
sys.exit(1)
84-
else:
85-
print('OK: Auto-fixed: Replaced self.assertTrue( with self.assert_(')
86-
print('OK: Verified: File no longer contains self.assertTrue(')
87-
else:
88-
print('OK: Source file is correct (uses self.assert_())')
89-
except Exception as e:
90-
print(f'ERROR in auto-fix: {e}')
91-
import traceback
92-
traceback.print_exc()
93-
sys.exit(1)
94-
PYEOF
95-
# Verify source file is fixed before installation
96-
echo "=== Verifying source file after auto-fix ==="
97-
python -c "import sys; import io; sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') if sys.platform == 'win32' else sys.stdout; content = open('sentience/agent_runtime.py', 'r', encoding='utf-8').read(); assert 'self.assertTrue(' not in content, 'Source file still has self.assertTrue( after auto-fix!'; print('OK: Source file verified: uses self.assert_()')"
43+
# Ensure source uses assert_ (no auto-rewrite).
44+
python -c "import sys; import io; sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') if sys.platform == 'win32' else sys.stdout; content = open('sentience/agent_runtime.py', 'r', encoding='utf-8').read(); assert 'self.assertTrue(' not in content, 'Source file still has self.assertTrue('; print('OK: Source file verified: uses self.assert_()')"
9845
9946
# Force reinstall to ensure latest code
10047
pip install --no-cache-dir --force-reinstall -e ".[dev]"

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,7 @@ Thumbs.db
6060
# Temporary directories from sync workflows
6161
extension-temp/
6262
playground/
63+
64+
# Allow bundled extension assets under sentience/extension/dist
65+
!sentience/extension/dist/
66+
!sentience/extension/dist/**

scripts/sync_extension.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
6+
CHROME_DIR="${REPO_ROOT}/sentience-chrome"
7+
SDK_EXT_DIR="${REPO_ROOT}/sdk-python/sentience/extension"
8+
9+
if [[ ! -d "${CHROME_DIR}" ]]; then
10+
echo "[sync_extension] sentience-chrome not found at ${CHROME_DIR}"
11+
exit 1
12+
fi
13+
14+
if [[ ! -f "${CHROME_DIR}/package.json" ]]; then
15+
echo "[sync_extension] package.json missing in sentience-chrome"
16+
exit 1
17+
fi
18+
19+
echo "[sync_extension] Building sentience-chrome..."
20+
pushd "${CHROME_DIR}" >/dev/null
21+
npm run build
22+
popd >/dev/null
23+
24+
echo "[sync_extension] Syncing dist/ and pkg/ to sdk-python..."
25+
mkdir -p "${SDK_EXT_DIR}/dist" "${SDK_EXT_DIR}/pkg"
26+
cp "${CHROME_DIR}/dist/"* "${SDK_EXT_DIR}/dist/"
27+
cp "${CHROME_DIR}/pkg/"* "${SDK_EXT_DIR}/pkg/"
28+
29+
echo "[sync_extension] Done."

sentience/agent.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,25 @@ def _compute_hash(self, text: str) -> str:
143143
"""Compute SHA256 hash of text."""
144144
return hashlib.sha256(text.encode("utf-8")).hexdigest()
145145

146+
def _best_effort_post_snapshot_digest(self, goal: str) -> str | None:
147+
"""
148+
Best-effort post-action snapshot digest for tracing.
149+
"""
150+
try:
151+
snap_opts = SnapshotOptions(
152+
limit=min(10, self.default_snapshot_limit),
153+
goal=f"{goal} (post)",
154+
)
155+
snap_opts.screenshot = False
156+
snap_opts.show_overlay = self.config.show_overlay if self.config else None
157+
post_snap = snapshot(self.browser, snap_opts)
158+
if post_snap.status != "success":
159+
return None
160+
digest_input = f"{post_snap.url}{post_snap.timestamp}"
161+
return f"sha256:{self._compute_hash(digest_input)}"
162+
except Exception:
163+
return None
164+
146165
def _get_element_bbox(self, element_id: int | None, snap: Snapshot) -> dict[str, float] | None:
147166
"""Get bounding box for an element from snapshot."""
148167
if element_id is None:
@@ -513,6 +532,10 @@ def act( # noqa: C901
513532
snapshot_event_data = TraceEventBuilder.build_snapshot_event(snap_with_diff)
514533
pre_elements = snapshot_event_data.get("elements", [])
515534

535+
post_snapshot_digest = (
536+
self._best_effort_post_snapshot_digest(goal) if self.tracer else None
537+
)
538+
516539
# Build complete step_end event
517540
step_end_data = TraceEventBuilder.build_step_end_event(
518541
step_id=step_id,
@@ -522,6 +545,7 @@ def act( # noqa: C901
522545
pre_url=pre_url,
523546
post_url=post_url,
524547
snapshot_digest=snapshot_digest,
548+
post_snapshot_digest=post_snapshot_digest,
525549
llm_data=llm_data,
526550
exec_data=exec_data,
527551
verify_data=verify_data,
@@ -601,6 +625,7 @@ def act( # noqa: C901
601625
pre_url=_step_pre_url,
602626
post_url=post_url,
603627
snapshot_digest=snapshot_digest,
628+
post_snapshot_digest=None,
604629
llm_data=llm_data,
605630
exec_data=exec_data,
606631
verify_data=None,
@@ -1155,6 +1180,10 @@ async def act( # noqa: C901
11551180
snapshot_event_data = TraceEventBuilder.build_snapshot_event(snap_with_diff)
11561181
pre_elements = snapshot_event_data.get("elements", [])
11571182

1183+
post_snapshot_digest = (
1184+
self._best_effort_post_snapshot_digest(goal) if self.tracer else None
1185+
)
1186+
11581187
# Build complete step_end event
11591188
step_end_data = TraceEventBuilder.build_step_end_event(
11601189
step_id=step_id,
@@ -1164,6 +1193,7 @@ async def act( # noqa: C901
11641193
pre_url=pre_url,
11651194
post_url=post_url,
11661195
snapshot_digest=snapshot_digest,
1196+
post_snapshot_digest=post_snapshot_digest,
11671197
llm_data=llm_data,
11681198
exec_data=exec_data,
11691199
verify_data=verify_data,
@@ -1243,6 +1273,7 @@ async def act( # noqa: C901
12431273
pre_url=_step_pre_url,
12441274
post_url=post_url,
12451275
snapshot_digest=snapshot_digest,
1276+
post_snapshot_digest=None,
12461277
llm_data=llm_data,
12471278
exec_data=exec_data,
12481279
verify_data=None,

sentience/agent_runtime.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,15 @@
6565

6666
import asyncio
6767
import difflib
68+
import hashlib
6869
import time
6970
from dataclasses import dataclass
7071
from typing import TYPE_CHECKING, Any
7172

7273
from .captcha import CaptchaContext, CaptchaHandlingError, CaptchaOptions, CaptchaResolution
7374
from .failure_artifacts import FailureArtifactBuffer, FailureArtifactsOptions
7475
from .models import Snapshot, SnapshotOptions
76+
from .trace_event_builder import TraceEventBuilder
7577
from .verification import AssertContext, AssertOutcome, Predicate
7678

7779
if TYPE_CHECKING:
@@ -138,6 +140,8 @@ def __init__(
138140

139141
# Snapshot state
140142
self.last_snapshot: Snapshot | None = None
143+
self._step_pre_snapshot: Snapshot | None = None
144+
self._step_pre_url: str | None = None
141145

142146
# Failure artifacts (Phase 1)
143147
self._artifact_buffer: FailureArtifactBuffer | None = None
@@ -148,6 +152,12 @@ def __init__(
148152

149153
# Assertions accumulated during current step
150154
self._assertions_this_step: list[dict[str, Any]] = []
155+
self._step_goal: str | None = None
156+
self._last_action: str | None = None
157+
self._last_action_error: str | None = None
158+
self._last_action_outcome: str | None = None
159+
self._last_action_duration_ms: int | None = None
160+
self._last_action_success: bool | None = None
151161

152162
# Task completion tracking
153163
self._task_done: bool = False
@@ -250,6 +260,11 @@ async def snapshot(self, **kwargs: Any) -> Snapshot:
250260
# Check if using legacy browser (backward compat)
251261
if hasattr(self, "_legacy_browser") and hasattr(self, "_legacy_page"):
252262
self.last_snapshot = await self._legacy_browser.snapshot(self._legacy_page, **kwargs)
263+
if self.last_snapshot is not None:
264+
self._cached_url = self.last_snapshot.url
265+
if self._step_pre_snapshot is None:
266+
self._step_pre_snapshot = self.last_snapshot
267+
self._step_pre_url = self.last_snapshot.url
253268
return self.last_snapshot
254269

255270
# Use backend-agnostic snapshot
@@ -262,6 +277,11 @@ async def snapshot(self, **kwargs: Any) -> Snapshot:
262277
options = SnapshotOptions(**options_dict)
263278

264279
self.last_snapshot = await backend_snapshot(self.backend, options=options)
280+
if self.last_snapshot is not None:
281+
self._cached_url = self.last_snapshot.url
282+
if self._step_pre_snapshot is None:
283+
self._step_pre_snapshot = self.last_snapshot
284+
self._step_pre_url = self.last_snapshot.url
265285
if not skip_captcha_handling:
266286
await self._handle_captcha_if_needed(self.last_snapshot, source="gateway")
267287
return self.last_snapshot
@@ -414,6 +434,7 @@ async def record_action(
414434
"""
415435
Record an action in the artifact timeline and capture a frame if enabled.
416436
"""
437+
self._last_action = action
417438
if not self._artifact_buffer:
418439
return
419440
self._artifact_buffer.record_step(
@@ -425,6 +446,107 @@ async def record_action(
425446
if self._artifact_buffer.options.capture_on_action:
426447
await self._capture_artifact_frame()
427448

449+
def _compute_snapshot_digest(self, snap: Snapshot | None) -> str | None:
450+
if snap is None:
451+
return None
452+
try:
453+
return (
454+
"sha256:"
455+
+ hashlib.sha256(f"{snap.url}{snap.timestamp}".encode("utf-8")).hexdigest()
456+
)
457+
except Exception:
458+
return None
459+
460+
async def emit_step_end(
461+
self,
462+
*,
463+
action: str | None = None,
464+
success: bool | None = None,
465+
error: str | None = None,
466+
outcome: str | None = None,
467+
duration_ms: int | None = None,
468+
attempt: int = 0,
469+
verify_passed: bool | None = None,
470+
verify_signals: dict[str, Any] | None = None,
471+
post_url: str | None = None,
472+
post_snapshot_digest: str | None = None,
473+
) -> dict[str, Any]:
474+
"""
475+
Emit a step_end event using TraceEventBuilder.
476+
"""
477+
goal = self._step_goal or ""
478+
pre_snap = self._step_pre_snapshot or self.last_snapshot
479+
pre_url = (
480+
self._step_pre_url
481+
or (pre_snap.url if pre_snap else None)
482+
or self._cached_url
483+
or ""
484+
)
485+
486+
if post_url is None:
487+
try:
488+
post_url = await self.get_url()
489+
except Exception:
490+
post_url = (
491+
(self.last_snapshot.url if self.last_snapshot else None) or self._cached_url
492+
)
493+
post_url = post_url or pre_url
494+
495+
pre_digest = self._compute_snapshot_digest(pre_snap)
496+
post_digest = post_snapshot_digest or self._compute_snapshot_digest(self.last_snapshot)
497+
url_changed = bool(pre_url and post_url and str(pre_url) != str(post_url))
498+
499+
assertions_data = self.get_assertions_for_step_end()
500+
assertions = assertions_data.get("assertions") or []
501+
502+
signals = dict(verify_signals or {})
503+
signals.setdefault("url_changed", url_changed)
504+
if error and "error" not in signals:
505+
signals["error"] = error
506+
507+
passed = (
508+
bool(verify_passed)
509+
if verify_passed is not None
510+
else self.required_assertions_passed()
511+
)
512+
513+
exec_success = bool(success) if success is not None else bool(
514+
self._last_action_success if self._last_action_success is not None else passed
515+
)
516+
517+
exec_data: dict[str, Any] = {
518+
"success": exec_success,
519+
"action": action or self._last_action or "unknown",
520+
"outcome": outcome or self._last_action_outcome or "",
521+
}
522+
if duration_ms is not None:
523+
exec_data["duration_ms"] = int(duration_ms)
524+
if error:
525+
exec_data["error"] = error
526+
527+
verify_data = {
528+
"passed": bool(passed),
529+
"signals": signals,
530+
}
531+
532+
step_end_data = TraceEventBuilder.build_step_end_event(
533+
step_id=self.step_id or "",
534+
step_index=int(self.step_index),
535+
goal=goal,
536+
attempt=int(attempt),
537+
pre_url=str(pre_url or ""),
538+
post_url=str(post_url or ""),
539+
snapshot_digest=pre_digest,
540+
llm_data={},
541+
exec_data=exec_data,
542+
verify_data=verify_data,
543+
pre_elements=None,
544+
assertions=assertions,
545+
post_snapshot_digest=post_digest,
546+
)
547+
self.tracer.emit("step_end", step_end_data, step_id=self.step_id)
548+
return step_end_data
549+
428550
async def _capture_artifact_frame(self) -> None:
429551
if not self._artifact_buffer:
430552
return
@@ -511,6 +633,14 @@ def begin_step(self, goal: str, step_index: int | None = None) -> str:
511633
"""
512634
# Clear previous step state
513635
self._assertions_this_step = []
636+
self._step_pre_snapshot = None
637+
self._step_pre_url = None
638+
self._step_goal = goal
639+
self._last_action = None
640+
self._last_action_error = None
641+
self._last_action_outcome = None
642+
self._last_action_duration_ms = None
643+
self._last_action_success = None
514644

515645
# Update step index
516646
if step_index is not None:

sentience/backends/playwright_backend.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"""
2323

2424
import asyncio
25+
import inspect
2526
import mimetypes
2627
import os
2728
import time
@@ -56,7 +57,14 @@ def __init__(self, page: "AsyncPage") -> None:
5657
# Best-effort download tracking (does not change behavior unless a download occurs).
5758
# pylint: disable=broad-exception-caught
5859
try:
59-
self._page.on("download", lambda d: asyncio.create_task(self._track_download(d)))
60+
result = self._page.on(
61+
"download", lambda d: asyncio.create_task(self._track_download(d))
62+
)
63+
if inspect.isawaitable(result):
64+
try:
65+
asyncio.get_running_loop().create_task(result)
66+
except RuntimeError:
67+
pass
6068
except Exception:
6169
pass
6270

0 commit comments

Comments
 (0)