Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions src/agents/extensions/sandbox/blaxel/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
from ....sandbox.session.base_sandbox_session import BaseSandboxSession
from ....sandbox.session.dependencies import Dependencies
from ....sandbox.session.manager import Instrumentation
from ....sandbox.session.pty_output import collect_pty_output
from ....sandbox.session.pty_output import collect_pty_output, flush_pty_tail
from ....sandbox.session.pty_types import (
PTY_PROCESSES_MAX,
PTY_PROCESSES_WARNING,
Expand Down Expand Up @@ -856,7 +856,7 @@ async def pty_exec_start(
)

yield_time_ms = 10_000 if yield_time_s is None else int(yield_time_s * 1000)
output, original_token_count = await self._collect_pty_output(
output, original_token_count, source_text = await self._collect_pty_output(
entry=entry,
yield_time_ms=clamp_pty_yield_time_ms(yield_time_ms),
max_output_tokens=max_output_tokens,
Expand All @@ -866,6 +866,8 @@ async def pty_exec_start(
entry=entry,
output=output,
original_token_count=original_token_count,
source_text=source_text,
max_output_tokens=max_output_tokens,
)

async def pty_write_stdin(
Expand All @@ -890,7 +892,7 @@ async def pty_write_stdin(
await asyncio.sleep(0.1)

yield_time_ms = 250 if yield_time_s is None else int(yield_time_s * 1000)
output, original_token_count = await self._collect_pty_output(
output, original_token_count, source_text = await self._collect_pty_output(
entry=entry,
yield_time_ms=resolve_pty_write_yield_time_ms(
yield_time_ms=yield_time_ms, input_empty=chars == ""
Expand All @@ -903,6 +905,8 @@ async def pty_write_stdin(
entry=entry,
output=output,
original_token_count=original_token_count,
source_text=source_text,
max_output_tokens=max_output_tokens,
)

async def pty_terminate_all(self) -> None:
Expand Down Expand Up @@ -964,7 +968,7 @@ async def _collect_pty_output(
entry: _BlaxelPtySessionEntry,
yield_time_ms: int,
max_output_tokens: int | None,
) -> tuple[bytes, int | None]:
) -> tuple[bytes, int | None, str]:
return await collect_pty_output(
output_chunks=entry.output_chunks,
output_lock=entry.output_lock,
Expand All @@ -981,6 +985,8 @@ async def _finalize_pty_update(
entry: _BlaxelPtySessionEntry,
output: bytes,
original_token_count: int | None,
source_text: str = "",
max_output_tokens: int | None = None,
) -> PtyExecUpdate:
exit_code = entry.exit_code if entry.done else None
live_process_id: int | None = process_id
Expand All @@ -989,8 +995,23 @@ async def _finalize_pty_update(
async with self._pty_lock:
removed = self._pty_sessions.pop(process_id, None)
self._reserved_pty_process_ids.discard(process_id)
if removed is not None:
await self._terminate_pty_entry(removed)
# Draining is destructive and the tail lives on the entry, so the removal has to
# commit first. Cancelled the other way round, the session stays registered with
# its last bytes already gone and a later call cannot get them back. Once it is out
# of the map nothing else can reach it either, pty_terminate_all included, so its
# sockets and sessions have to be closed whatever happens to the drain.
try:
output, original_token_count = await flush_pty_tail(
output_chunks=entry.output_chunks,
output_lock=entry.output_lock,
output=output,
source_text=source_text,
original_token_count=original_token_count,
max_output_tokens=max_output_tokens,
)
finally:
if removed is not None:
await self._terminate_pty_entry(removed)
live_process_id = None

return PtyExecUpdate(
Expand Down
68 changes: 35 additions & 33 deletions src/agents/extensions/sandbox/cloudflare/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
_settle_mount_transition,
with_ephemeral_mounts_removed,
)
from ....sandbox.session.pty_output import collect_pty_output, flush_pty_tail
from ....sandbox.session.pty_types import (
PTY_PROCESSES_MAX,
PTY_PROCESSES_WARNING,
Expand All @@ -67,7 +68,6 @@
clamp_pty_yield_time_ms,
process_id_to_prune_from_meta,
resolve_pty_write_yield_time_ms,
truncate_text_by_tokens,
)
from ....sandbox.session.runtime_helpers import RESOLVE_WORKSPACE_PATH_HELPER, RuntimeHelperScript
from ....sandbox.session.sandbox_client import BaseSandboxClient, BaseSandboxClientOptions
Expand Down Expand Up @@ -1033,34 +1033,15 @@ async def _collect_pty_output(
entry: _CloudflarePtyProcessEntry,
yield_time_ms: int,
max_output_tokens: int | None,
) -> tuple[bytes, int | None]:
deadline = time.monotonic() + (yield_time_ms / 1000)
output = bytearray()

while True:
async with entry.output_lock:
while entry.output_chunks:
output.extend(entry.output_chunks.popleft())

if entry.output_closed.is_set():
async with entry.output_lock:
while entry.output_chunks:
output.extend(entry.output_chunks.popleft())
break

remaining_s = deadline - time.monotonic()
if remaining_s <= 0:
break

try:
await asyncio.wait_for(entry.output_notify.wait(), timeout=remaining_s)
except asyncio.TimeoutError:
break
entry.output_notify.clear()

text = output.decode("utf-8", errors="replace")
truncated_text, original_token_count = truncate_text_by_tokens(text, max_output_tokens)
return truncated_text.encode("utf-8", errors="replace"), original_token_count
) -> tuple[bytes, int | None, str]:
return await collect_pty_output(
output_chunks=entry.output_chunks,
output_lock=entry.output_lock,
output_notify=entry.output_notify,
is_done=entry.output_closed.is_set,
yield_time_ms=yield_time_ms,
max_output_tokens=max_output_tokens,
)

async def _finalize_pty_update(
self,
Expand All @@ -1069,15 +1050,32 @@ async def _finalize_pty_update(
entry: _CloudflarePtyProcessEntry,
output: bytes,
original_token_count: int | None,
source_text: str = "",
max_output_tokens: int | None = None,
) -> PtyExecUpdate:
exit_code = entry.exit_code if entry.output_closed.is_set() else None
live_process_id: int | None = process_id
if entry.output_closed.is_set():
async with self._pty_lock:
removed = self._pty_processes.pop(process_id, None)
self._reserved_pty_process_ids.discard(process_id)
if removed is not None:
await self._terminate_pty_entry(removed)
# Draining is destructive and the tail lives on the entry, so the removal has to
# commit first. Cancelled the other way round, the session stays registered with
# its last bytes already gone and a later call cannot get them back. Once it is out
# of the map nothing else can reach it either, pty_terminate_all included, so its
# sockets and sessions have to be closed whatever happens to the drain.
try:
output, original_token_count = await flush_pty_tail(
output_chunks=entry.output_chunks,
output_lock=entry.output_lock,
output=output,
source_text=source_text,
original_token_count=original_token_count,
max_output_tokens=max_output_tokens,
)
finally:
if removed is not None:
await self._terminate_pty_entry(removed)
live_process_id = None

return PtyExecUpdate(
Expand Down Expand Up @@ -1220,7 +1218,7 @@ async def pty_exec_start(
)

yield_time_ms = 10_000 if yield_time_s is None else int(yield_time_s * 1000)
output, original_token_count = await self._collect_pty_output(
output, original_token_count, source_text = await self._collect_pty_output(
entry=entry,
yield_time_ms=clamp_pty_yield_time_ms(yield_time_ms),
max_output_tokens=max_output_tokens,
Expand All @@ -1230,6 +1228,8 @@ async def pty_exec_start(
entry=entry,
output=output,
original_token_count=original_token_count,
source_text=source_text,
max_output_tokens=max_output_tokens,
)

async def pty_write_stdin(
Expand All @@ -1253,7 +1253,7 @@ async def pty_write_stdin(
await asyncio.sleep(0.1)

yield_time_ms = 250 if yield_time_s is None else int(yield_time_s * 1000)
output, original_token_count = await self._collect_pty_output(
output, original_token_count, source_text = await self._collect_pty_output(
entry=entry,
yield_time_ms=resolve_pty_write_yield_time_ms(
yield_time_ms=yield_time_ms,
Expand All @@ -1267,6 +1267,8 @@ async def pty_write_stdin(
entry=entry,
output=output,
original_token_count=original_token_count,
source_text=source_text,
max_output_tokens=max_output_tokens,
)

async def pty_terminate_all(self) -> None:
Expand Down
33 changes: 27 additions & 6 deletions src/agents/extensions/sandbox/daytona/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
from ....sandbox.session.base_sandbox_session import BaseSandboxSession
from ....sandbox.session.dependencies import Dependencies
from ....sandbox.session.manager import Instrumentation
from ....sandbox.session.pty_output import collect_pty_output
from ....sandbox.session.pty_output import collect_pty_output, flush_pty_tail
from ....sandbox.session.pty_types import (
PTY_PROCESSES_MAX,
PTY_PROCESSES_WARNING,
Expand Down Expand Up @@ -755,7 +755,7 @@ async def _on_data(chunk: bytes | str) -> None:
)

yield_time_ms = 10_000 if yield_time_s is None else int(yield_time_s * 1000)
output, original_token_count = await self._collect_pty_output(
output, original_token_count, source_text = await self._collect_pty_output(
entry=entry,
yield_time_ms=clamp_pty_yield_time_ms(yield_time_ms),
max_output_tokens=max_output_tokens,
Expand All @@ -765,6 +765,8 @@ async def _on_data(chunk: bytes | str) -> None:
entry=entry,
output=output,
original_token_count=original_token_count,
source_text=source_text,
max_output_tokens=max_output_tokens,
)

async def _run_pty_waiter(self, entry: _DaytonaPtySessionEntry) -> None:
Expand Down Expand Up @@ -832,7 +834,7 @@ async def pty_write_stdin(
await asyncio.sleep(0.1)

yield_time_ms = 250 if yield_time_s is None else int(yield_time_s * 1000)
output, original_token_count = await self._collect_pty_output(
output, original_token_count, source_text = await self._collect_pty_output(
entry=entry,
yield_time_ms=resolve_pty_write_yield_time_ms(
yield_time_ms=yield_time_ms, input_empty=chars == ""
Expand All @@ -845,6 +847,8 @@ async def pty_write_stdin(
entry=entry,
output=output,
original_token_count=original_token_count,
source_text=source_text,
max_output_tokens=max_output_tokens,
)

async def _finalize_pty_update(
Expand All @@ -854,6 +858,8 @@ async def _finalize_pty_update(
entry: _DaytonaPtySessionEntry,
output: bytes,
original_token_count: int | None,
source_text: str = "",
max_output_tokens: int | None = None,
) -> PtyExecUpdate:
exit_code = entry.exit_code if entry.done else None
live_process_id: int | None = process_id
Expand All @@ -862,8 +868,23 @@ async def _finalize_pty_update(
async with self._pty_lock:
removed = self._pty_sessions.pop(process_id, None)
self._reserved_pty_process_ids.discard(process_id)
if removed is not None:
await self._terminate_pty_entry(removed)
# Draining is destructive and the tail lives on the entry, so the removal has to
# commit first. Cancelled the other way round, the session stays registered with
# its last bytes already gone and a later call cannot get them back. Once it is out
# of the map nothing else can reach it either, pty_terminate_all included, so its
# sockets and sessions have to be closed whatever happens to the drain.
try:
output, original_token_count = await flush_pty_tail(
output_chunks=entry.output_chunks,
output_lock=entry.output_lock,
output=output,
source_text=source_text,
original_token_count=original_token_count,
max_output_tokens=max_output_tokens,
)
finally:
if removed is not None:
await self._terminate_pty_entry(removed)
live_process_id = None

return PtyExecUpdate(
Expand All @@ -887,7 +908,7 @@ async def _collect_pty_output(
entry: _DaytonaPtySessionEntry,
yield_time_ms: int,
max_output_tokens: int | None,
) -> tuple[bytes, int | None]:
) -> tuple[bytes, int | None, str]:
return await collect_pty_output(
output_chunks=entry.output_chunks,
output_lock=entry.output_lock,
Expand Down
Loading