Skip to content

Commit 41313e5

Browse files
committed
feat(sprites): lazy-warm named-attach sessions
When attaching to an existing sprite (created_by_us=False), SpritesSandboxSession now skips the eager wait-for-running poll and lets the first I/O operation drive the wake-up via a new _ensure_warm() guard. The platform auto-wakes a paused sprite when traffic arrives, so this is essentially free — and avoids paying 1–10s of polling latency just to hand back a session handle. The created-by-us path is unchanged: a fresh sprite still needs a provisioning poll, and we set _warmth_verified=True after. A new _invalidate_warmth() hook lets recovery flows force a re-poll after a transport error. Live timing of named-attach: - create(): 3s → 0.01s - first exec: adds the wake-up roundtrip (formerly paid eagerly) - subsequent I/O: unchanged (cached warm flag) 3 new lazy-warm tests; the _attach test helper now also marks _warmth_verified=True so existing tests that bypass startup keep working.
1 parent 7c4f725 commit 41313e5

2 files changed

Lines changed: 131 additions & 11 deletions

File tree

src/agents/extensions/sandbox/sprites/sandbox.py

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ class SpritesSandboxSession(BaseSandboxSession):
257257
_pty_lock: asyncio.Lock
258258
_pty_processes: dict[int, _SpritePtyProcessEntry]
259259
_reserved_pty_process_ids: set[int]
260+
_warmth_verified: bool
260261

261262
def __init__(
262263
self,
@@ -276,6 +277,12 @@ def __init__(
276277
self._pty_lock = asyncio.Lock()
277278
self._pty_processes = {}
278279
self._reserved_pty_process_ids = set()
280+
# ``_warmth_verified`` tracks whether we've already confirmed the sprite
281+
# is warm/running. Set when a fresh sprite is provisioned (we have to
282+
# poll anyway), or when the first I/O operation drives a successful
283+
# control-plane connect. Resetting via ``_invalidate_warmth`` after a
284+
# transport error forces a re-check on the next operation.
285+
self._warmth_verified = False
279286

280287
@classmethod
281288
def from_state(
@@ -336,19 +343,47 @@ async def _ensure_sprite(self) -> Sprite:
336343
cause=exc,
337344
) from exc
338345
await self._maybe_update_url_settings(sprite)
339-
else:
340-
sprite = await asyncio.to_thread(client.sprite, self.state.sprite_name)
341-
346+
self._sprite = sprite
347+
# Fresh sprite: poll until it reaches a ready status, then refresh
348+
# its info so ``url`` / ``organization_name`` are populated. We
349+
# have to poll regardless because the create call doesn't tell us
350+
# when provisioning finishes.
351+
await self._wait_for_sprite_running()
352+
self._warmth_verified = True
353+
refreshed: Sprite
354+
try:
355+
refreshed = await asyncio.to_thread(client.get_sprite, self.state.sprite_name)
356+
except SpriteError:
357+
refreshed = sprite
358+
self._sprite = refreshed
359+
return refreshed
360+
361+
# Named-attach: skip the readiness poll on attach. The sprite may be
362+
# cold (paused) and we don't want to pay wake-up latency just to hand
363+
# the agent a session handle. The first I/O operation drives the
364+
# wake-up via ``_ensure_warm`` — the platform auto-wakes a paused
365+
# sprite when traffic arrives, so this is essentially free.
366+
sprite = await asyncio.to_thread(client.sprite, self.state.sprite_name)
342367
self._sprite = sprite
368+
return sprite
369+
370+
async def _ensure_warm(self) -> None:
371+
"""Block until the sprite is ready to accept I/O, but only on first use.
372+
373+
``_warmth_verified`` is sticky for the life of the session; cached
374+
until a transport error invalidates it (e.g., the sprite was deleted
375+
out from under us and we have to re-attach in a recovery flow).
376+
"""
377+
378+
if self._warmth_verified:
379+
return
343380
await self._wait_for_sprite_running()
344-
# Refresh sprite info so url / organization_name / status are populated.
345-
refreshed: Sprite
346-
try:
347-
refreshed = await asyncio.to_thread(client.get_sprite, self.state.sprite_name)
348-
except SpriteError:
349-
refreshed = sprite
350-
self._sprite = refreshed
351-
return refreshed
381+
self._warmth_verified = True
382+
383+
def _invalidate_warmth(self) -> None:
384+
"""Force the next I/O operation to re-poll the sprite's status."""
385+
386+
self._warmth_verified = False
352387

353388
def _build_sprite_config(self) -> sprites.SpriteConfig | None:
354389
if (
@@ -586,6 +621,8 @@ async def _exec_with_cwd(
586621
if not normalized:
587622
return ExecResult(stdout=b"", stderr=b"", exit_code=0)
588623

624+
await self._ensure_warm()
625+
589626
control: ControlConnection | None = None
590627
op_conn: OpConn | None = None
591628
try:
@@ -656,6 +693,7 @@ async def pty_exec_start(
656693
) -> PtyExecUpdate:
657694
sanitized_command = self._prepare_exec_command(*command, shell=shell, user=user)
658695
# ``_ensure_control`` will lazily call ``_ensure_sprite``; no extra await here.
696+
await self._ensure_warm()
659697

660698
cc: ControlConnection | None = None
661699
op: OpConn | None = None
@@ -972,6 +1010,7 @@ async def read(self, path: Path, *, user: str | User | None = None) -> io.IOBase
9721010

9731011
normalized_path = await self._validate_path_access(path)
9741012
sprite = await self._ensure_sprite()
1013+
await self._ensure_warm()
9751014
try:
9761015
payload = await asyncio.to_thread(
9771016
lambda: (sprite.filesystem("/") / sandbox_path_str(normalized_path)).read_bytes()
@@ -1003,6 +1042,7 @@ async def write(
10031042
)
10041043

10051044
sprite = await self._ensure_sprite()
1045+
await self._ensure_warm()
10061046
try:
10071047
await asyncio.to_thread(
10081048
lambda: (sprite.filesystem("/") / sandbox_path_str(normalized_path)).write_bytes(

tests/extensions/test_sandbox_sprites.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,20 @@ def _attach(inner: SpritesSandboxSession, *, client: Any, sprite: Any = None) ->
4747
``setattr`` is used to sidestep mypy's invariant attribute typing — the fakes
4848
duck-type the real ``SpritesClient``/``Sprite`` interface only as far as the
4949
tests exercise.
50+
51+
Also marks ``_warmth_verified=True`` so I/O paths skip the lazy
52+
wake-up poll — the test has already set up the fake sprite directly,
53+
so we can trust it's "warm enough" for the assertions under test.
54+
Tests that specifically exercise the wait-for-running poll override
55+
this back to False.
5056
"""
5157

5258
# ``setattr`` (instead of plain assignment) silences mypy's invariant attribute
5359
# check; the fakes only duck-type the parts we exercise.
5460
setattr(inner, "_client", client) # noqa: B010
5561
if sprite is not None:
5662
setattr(inner, "_sprite", sprite) # noqa: B010
63+
setattr(inner, "_warmth_verified", True) # noqa: B010
5764

5865

5966
# ---------- Fakes ----------
@@ -1072,3 +1079,76 @@ def test_sprites_checkpoints_tool_count_depends_on_allow_restore() -> None:
10721079
cap_yes = SpritesCheckpoints(allow_restore=True)
10731080
assert len(cap_no.tools()) == 2 # create + list
10741081
assert len(cap_yes.tools()) == 3 # + restore
1082+
1083+
1084+
# ---------- 17. Lazy wake-up ----------
1085+
1086+
1087+
@pytest.mark.asyncio
1088+
async def test_named_attach_create_does_not_poll_for_running(
1089+
patched_sprites: dict[str, Any],
1090+
) -> None:
1091+
"""create() with sprite_name should NOT call get_sprite during attach.
1092+
1093+
The platform auto-wakes the sprite on first traffic; polling here would
1094+
pay wake-up latency just to hand back a session handle. The first I/O
1095+
operation drives the wake-up via _ensure_warm.
1096+
"""
1097+
1098+
fake_client = patched_sprites["client"]
1099+
fake_client._sprites_by_name["existing"] = _FakeSprite(name="existing")
1100+
client = SpritesSandboxClient(token="tok")
1101+
options = SpritesSandboxClientOptions(sprite_name="existing")
1102+
session = await client.create(options=options)
1103+
inner = session._inner
1104+
assert isinstance(inner, SpritesSandboxSession)
1105+
# No get_sprite calls because we did not poll for warmth.
1106+
assert fake_client.get_sprite_calls == []
1107+
# And the warmth flag stays False, so the next I/O will trigger the poll.
1108+
assert inner._warmth_verified is False
1109+
1110+
1111+
@pytest.mark.asyncio
1112+
async def test_lazy_warm_polls_on_first_exec(patched_sprites: dict[str, Any]) -> None:
1113+
fake_client = patched_sprites["client"]
1114+
fake_client._sprites_by_name["existing"] = _FakeSprite(name="existing")
1115+
fake_control = patched_sprites["control"]
1116+
fake_control.next_ops.append(_FakeOpConn(stdout=b"", exit_code=0))
1117+
1118+
client = SpritesSandboxClient(token="tok")
1119+
session = await client.create(options=SpritesSandboxClientOptions(sprite_name="existing"))
1120+
inner = session._inner
1121+
assert isinstance(inner, SpritesSandboxSession)
1122+
assert inner._warmth_verified is False
1123+
assert fake_client.get_sprite_calls == []
1124+
1125+
# First exec drives the wake-up poll.
1126+
await inner._exec_internal("echo", "hi")
1127+
assert fake_client.get_sprite_calls == ["existing"]
1128+
assert inner._warmth_verified is True
1129+
1130+
# Subsequent exec does NOT re-poll.
1131+
fake_control.next_ops.append(_FakeOpConn(stdout=b"", exit_code=0))
1132+
await inner._exec_internal("echo", "hi2")
1133+
# Still just the one poll from the first call.
1134+
assert fake_client.get_sprite_calls == ["existing"]
1135+
1136+
1137+
@pytest.mark.asyncio
1138+
async def test_lazy_warm_invalidate_forces_repoll(patched_sprites: dict[str, Any]) -> None:
1139+
fake_client = patched_sprites["client"]
1140+
fake_client._sprites_by_name["existing"] = _FakeSprite(name="existing")
1141+
fake_control = patched_sprites["control"]
1142+
fake_control.next_ops.extend([_FakeOpConn(exit_code=0), _FakeOpConn(exit_code=0)])
1143+
1144+
client = SpritesSandboxClient(token="tok")
1145+
session = await client.create(options=SpritesSandboxClientOptions(sprite_name="existing"))
1146+
inner = session._inner
1147+
assert isinstance(inner, SpritesSandboxSession)
1148+
1149+
await inner._exec_internal("echo", "1")
1150+
assert len(fake_client.get_sprite_calls) == 1
1151+
1152+
inner._invalidate_warmth()
1153+
await inner._exec_internal("echo", "2")
1154+
assert len(fake_client.get_sprite_calls) == 2

0 commit comments

Comments
 (0)