Skip to content

Commit 41cd3f5

Browse files
authored
Merge pull request #204 from SentienceAPI/tighten2
updated debugger doc with reacordAction, autoStop, stepId 0-based
2 parents 81613e1 + 4a5026a commit 41cd3f5

File tree

4 files changed

+89
-11
lines changed

4 files changed

+89
-11
lines changed

sentience/agent_runtime.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ def __init__(
154154

155155
# Step tracking
156156
self.step_id: str | None = None
157-
self.step_index: int = 0
157+
# 0-based step indexing (first auto-generated step_id is "step-0")
158+
self.step_index: int = -1
158159

159160
# Snapshot state
160161
self.last_snapshot: Snapshot | None = None

sentience/debugger.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import asyncio
34
from collections.abc import AsyncIterator
45
from contextlib import asynccontextmanager
56
from typing import TYPE_CHECKING, Any
@@ -26,6 +27,20 @@ def __init__(self, runtime: AgentRuntime, *, auto_step: bool = True) -> None:
2627
self.runtime = runtime
2728
self._step_open = False
2829
self._auto_step = bool(auto_step)
30+
self._auto_opened_step = False
31+
self._auto_opened_step_id: str | None = None
32+
33+
def _schedule_close_auto_step(self) -> None:
34+
"""
35+
Best-effort: close an auto-opened step without forcing callers to await.
36+
"""
37+
if not (self._step_open and self._auto_opened_step):
38+
return
39+
try:
40+
loop = asyncio.get_running_loop()
41+
except RuntimeError:
42+
return
43+
loop.create_task(self.end_step())
2944

3045
@classmethod
3146
def attach(
@@ -46,15 +61,25 @@ def attach(
4661
return cls(runtime=runtime)
4762

4863
def begin_step(self, goal: str, step_index: int | None = None) -> str:
64+
# If we previously auto-opened a verification step, close it before starting a real step.
65+
if self._step_open and self._auto_opened_step:
66+
self._schedule_close_auto_step()
67+
self._auto_opened_step = False
68+
self._auto_opened_step_id = None
4969
self._step_open = True
5070
return self.runtime.begin_step(goal, step_index=step_index)
5171

5272
async def end_step(self, **kwargs: Any) -> dict[str, Any]:
5373
self._step_open = False
74+
self._auto_opened_step = False
75+
self._auto_opened_step_id = None
5476
return await self.runtime.emit_step_end(**kwargs)
5577

5678
@asynccontextmanager
5779
async def step(self, goal: str, step_index: int | None = None) -> AsyncIterator[None]:
80+
# Async form can safely close any auto-opened step before starting.
81+
if self._step_open and self._auto_opened_step:
82+
await self.end_step()
5883
self.begin_step(goal, step_index=step_index)
5984
try:
6085
yield
@@ -64,11 +89,61 @@ async def step(self, goal: str, step_index: int | None = None) -> AsyncIterator[
6489
async def snapshot(self, **kwargs: Any):
6590
return await self.runtime.snapshot(**kwargs)
6691

92+
async def record_action(self, action: str, *, url: str | None = None) -> None:
93+
"""
94+
Sidecar helper: let the host framework report the action it performed.
95+
96+
This improves trace readability and (when artifacts are enabled) enriches the action timeline.
97+
"""
98+
await self.runtime.record_action(action, url=url)
99+
67100
def check(self, predicate, label: str, required: bool = False):
68101
if not self._step_open:
69102
if not self._auto_step:
70103
raise RuntimeError(
71104
f"No active step. Call dbg.begin_step(...) or use 'async with dbg.step(...)' before check(label={label!r})."
72105
)
73106
self.begin_step(f"verify:{label}")
74-
return self.runtime.check(predicate, label, required=required)
107+
self._auto_opened_step = True
108+
self._auto_opened_step_id = getattr(self.runtime, "step_id", None)
109+
110+
base = self.runtime.check(predicate, label, required=required)
111+
112+
# Auto-close auto-opened verification steps after the check completes.
113+
if not self._auto_opened_step:
114+
return base
115+
116+
dbg = self
117+
opened_step_id = self._auto_opened_step_id
118+
119+
class _AutoCloseAssertionHandle:
120+
def __init__(self, inner):
121+
self._inner = inner
122+
123+
def once(self) -> bool:
124+
ok = self._inner.once()
125+
if (
126+
dbg._step_open
127+
and dbg._auto_opened_step
128+
and (
129+
opened_step_id is None
130+
or getattr(dbg.runtime, "step_id", None) == opened_step_id
131+
)
132+
):
133+
dbg._schedule_close_auto_step()
134+
return ok
135+
136+
async def eventually(self, **kwargs: Any) -> bool:
137+
ok = await self._inner.eventually(**kwargs)
138+
if (
139+
dbg._step_open
140+
and dbg._auto_opened_step
141+
and (
142+
opened_step_id is None
143+
or getattr(dbg.runtime, "step_id", None) == opened_step_id
144+
)
145+
):
146+
await dbg.end_step()
147+
return ok
148+
149+
return _AutoCloseAssertionHandle(base)

tests/test_agent_runtime.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ def test_init_with_backend(self) -> None:
9999
assert runtime.backend is backend
100100
assert runtime.tracer is tracer
101101
assert runtime.step_id is None
102-
assert runtime.step_index == 0
102+
# 0-based step ids: first begin_step() will produce "step-0"
103+
assert runtime.step_index == -1
103104
assert runtime.last_snapshot is None
104105
assert runtime.is_task_done is False
105106

@@ -221,7 +222,7 @@ def test_begin_step_generates_step_id(self) -> None:
221222
step_id = runtime.begin_step(goal="Test step")
222223

223224
assert step_id is not None
224-
assert step_id == "step-1" # First step should be step-1
225+
assert step_id == "step-0" # First step should be step-0
225226

226227
def test_begin_step_id_matches_index(self) -> None:
227228
"""Test step_id format matches step_index for Studio compatibility."""
@@ -230,12 +231,12 @@ def test_begin_step_id_matches_index(self) -> None:
230231
runtime = AgentRuntime(backend=backend, tracer=tracer)
231232

232233
step_id_1 = runtime.begin_step(goal="Step 1")
233-
assert step_id_1 == "step-1"
234-
assert runtime.step_index == 1
234+
assert step_id_1 == "step-0"
235+
assert runtime.step_index == 0
235236

236237
step_id_2 = runtime.begin_step(goal="Step 2")
237-
assert step_id_2 == "step-2"
238-
assert runtime.step_index == 2
238+
assert step_id_2 == "step-1"
239+
assert runtime.step_index == 1
239240

240241
# With explicit index
241242
step_id_10 = runtime.begin_step(goal="Step 10", step_index=10)
@@ -249,10 +250,10 @@ def test_begin_step_increments_index(self) -> None:
249250
runtime = AgentRuntime(backend=backend, tracer=tracer)
250251

251252
runtime.begin_step(goal="Step 1")
252-
assert runtime.step_index == 1
253+
assert runtime.step_index == 0
253254

254255
runtime.begin_step(goal="Step 2")
255-
assert runtime.step_index == 2
256+
assert runtime.step_index == 1
256257

257258
def test_begin_step_explicit_index(self) -> None:
258259
"""Test begin_step with explicit step_index."""

tests/test_debugger.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ def test_check_auto_opens_step_when_missing() -> None:
6868

6969
runtime.begin_step.assert_called_once_with("verify:has_cart", step_index=None)
7070
runtime.check.assert_called_once_with(predicate, "has_cart", required=True)
71-
assert handle == "check-handle"
71+
assert hasattr(handle, "once")
72+
assert hasattr(handle, "eventually")
7273

7374

7475
def test_check_strict_mode_requires_explicit_step() -> None:

0 commit comments

Comments
 (0)