From 0219be0e8957924164fbbecfebecd54c29030fae Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Sat, 20 Jun 2026 10:23:49 -0400 Subject: [PATCH 01/13] offload large execute output at the source --- .../deepagents/backends/protocol.py | 11 ++ .../deepagents/deepagents/backends/sandbox.py | 82 +++++++++++++ .../deepagents/middleware/filesystem.py | 115 ++++++++++++++---- 3 files changed, 186 insertions(+), 22 deletions(-) diff --git a/libs/deepagents/deepagents/backends/protocol.py b/libs/deepagents/deepagents/backends/protocol.py index 295c739dc7..5cb6e25e13 100644 --- a/libs/deepagents/deepagents/backends/protocol.py +++ b/libs/deepagents/deepagents/backends/protocol.py @@ -799,6 +799,17 @@ class ExecuteResponse: truncated: bool = False """Whether the output was truncated due to backend limitations.""" + # Supports the execute offload-without-roundtrip path (capture-at-source): for + # large output, the command result is written to a file in the sandbox and only + # a preview is returned, avoiding a full-payload round-trip back through the + # agent process. Unset for ordinary execution. + offloaded: bool = False + """Whether the full output was captured to a file in the sandbox. + + When `True`, `output` holds only a head/tail preview; the full content lives + at the capture path on the sandbox filesystem. + """ + class SandboxBackendProtocol(BackendProtocol): """Extension of `BackendProtocol` that adds shell command execution. diff --git a/libs/deepagents/deepagents/backends/sandbox.py b/libs/deepagents/deepagents/backends/sandbox.py index afc27b5af0..1cae7b8873 100644 --- a/libs/deepagents/deepagents/backends/sandbox.py +++ b/libs/deepagents/deepagents/backends/sandbox.py @@ -583,6 +583,88 @@ def _build_edit_tmpfile_cmd(file_path: str, old_tmp: str, new_tmp: str, *, repla ) +_EXECUTE_CAPTURE_SENTINEL: Final = "__DEEPAGENTS_EXEC_META__" +"""First-line marker identifying capture-wrapper output: ` `.""" + +_EXECUTE_CAPTURE_TRUNC: Final = "__DEEPAGENTS_EXEC_TRUNC__" +"""In-preview marker between the head and tail of an offloaded result.""" + +_EXECUTE_CAPTURE_HEAD_BYTES: Final = 2000 +_EXECUTE_CAPTURE_TAIL_BYTES: Final = 2000 + +_EXECUTE_CAPTURE_CMD_TEMPLATE = """__da_f=__PATH_Q__ +mkdir -p "$(dirname "$__da_f")" 2>/dev/null +__da_cmd=$(cat <<'__DELIM__' +__COMMAND__ +__DELIM__ +) +( eval "$__da_cmd" ) > "$__da_f" 2>&1 +__da_ec=$? +__da_bytes=$(wc -c < "$__da_f" 2>/dev/null | tr -d ' ') +: "${__da_bytes:=0}" +if [ "$__da_bytes" -le __BUDGET__ ]; then + printf '%s %s %s\\n' '__SENTINEL__' "$__da_ec" 0 + cat "$__da_f" + rm -f "$__da_f" +else + printf '%s %s %s\\n' '__SENTINEL__' "$__da_ec" 1 + head -c __HEAD__ "$__da_f" + printf '\\n%s\\n' '__TRUNC__' + tail -c __TAIL__ "$__da_f" +fi +""" +"""Pure POSIX sh wrapper for capture-at-source `execute`. + +Runs the command in a subshell — so a command `exit` cannot abort the +measurement step, and the backend's own shell/env is preserved via `eval` — with +combined output redirected to the capture file. A byte size (`wc -c`) then drives +the inline-vs-offload branch. The command is embedded via a quoted heredoc with a +random delimiter to avoid shell-quoting issues; the (internal, sanitized) path is +shell-quoted. +""" + + +def _build_capture_execute_cmd(command: str, capture_path: str, *, inline_budget: int) -> str: + """Build the capture-at-source wrapper command for `execute`. + + `inline_budget` is the byte threshold at or below which output is returned + inline; above it the output is left at `capture_path` and only a head/tail + preview is returned. + """ + delim = "__DEEPAGENTS_CMD_" + base64.b32encode(os.urandom(10)).decode("ascii").rstrip("=") + "__" + # __COMMAND__ is substituted last so command content can never collide with a + # remaining placeholder token. + return ( + _EXECUTE_CAPTURE_CMD_TEMPLATE.replace("__PATH_Q__", shlex.quote(capture_path)) + .replace("__DELIM__", delim) + .replace("__BUDGET__", str(inline_budget)) + .replace("__SENTINEL__", _EXECUTE_CAPTURE_SENTINEL) + .replace("__HEAD__", str(_EXECUTE_CAPTURE_HEAD_BYTES)) + .replace("__TAIL__", str(_EXECUTE_CAPTURE_TAIL_BYTES)) + .replace("__TRUNC__", _EXECUTE_CAPTURE_TRUNC) + .replace("__COMMAND__", command) + ) + + +def _parse_capture_execute_output(output: str, *, backend_truncated: bool = False) -> ExecuteResponse: + """Parse capture-wrapper stdout into an `ExecuteResponse`. + + Returns the raw output verbatim (no exit code, not offloaded) when the meta + line is absent — e.g. if the backend truncated transport. The caller must + not re-run the command on this fallback. `backend_truncated` carries the + transport-truncation flag from the underlying `execute` through to the result. + """ + first, _, body = output.partition("\n") + parts = first.split(" ") + if len(parts) != 3 or parts[0] != _EXECUTE_CAPTURE_SENTINEL: # noqa: PLR2004 + return ExecuteResponse(output=output, truncated=backend_truncated) + try: + exit_code = int(parts[1]) + except ValueError: + return ExecuteResponse(output=output, truncated=backend_truncated) + return ExecuteResponse(output=body, exit_code=exit_code, offloaded=parts[2] == "1", truncated=backend_truncated) + + class BaseSandbox(SandboxBackendProtocol, ABC): """Base sandbox implementation with `execute()` as the core abstract method. diff --git a/libs/deepagents/deepagents/middleware/filesystem.py b/libs/deepagents/deepagents/middleware/filesystem.py index 05ba7e9ed2..f27b157824 100644 --- a/libs/deepagents/deepagents/middleware/filesystem.py +++ b/libs/deepagents/deepagents/middleware/filesystem.py @@ -39,10 +39,12 @@ from deepagents._api.deprecation import warn_deprecated from deepagents.backends import CompositeBackend, FilesystemBackend, LocalShellBackend, StateBackend +from deepagents.backends.composite import _route_for_path from deepagents.backends.protocol import ( BACKEND_TYPES as BACKEND_TYPES, # Re-export type here for backwards compatibility BackendProtocol, EditResult, + ExecuteResponse, FileData as FileData, # Re-export for backwards compatibility FileInfo, GlobResult, @@ -54,6 +56,12 @@ _resolve_backend, execute_accepts_timeout, ) +from deepagents.backends.sandbox import ( + _EXECUTE_CAPTURE_TRUNC, + BaseSandbox, + _build_capture_execute_cmd, + _parse_capture_execute_output, +) from deepagents.backends.utils import ( _get_file_type, check_empty_content, @@ -1671,6 +1679,61 @@ async def async_grep( args_schema=GrepSchema, ) + def _capture_path_if_local(self, resolved_backend: BackendProtocol, tool_call_id: str | None) -> str | None: + """Return the sandbox-local offload path for capture-at-source, or `None` to skip. + + Capture-at-source writes output to a literal path via the sandbox shell + and later reads it back through the backend, so it is only sound when + `execute()` and `read_file` share one filesystem at that path. Only + `BaseSandbox` guarantees this, so the optimization is gated on it (a + stub or host-shell backend falls back to inline execute plus generic + eviction). Also returns `None` when eviction is disabled, the tool call + has no id, or the offload path would route to a different backend. + """ + if not self._tool_token_limit_before_evict or not tool_call_id: + return None + capture_path = f"{self._large_tool_results_prefix}/{sanitize_tool_call_id(tool_call_id)}" + if isinstance(resolved_backend, CompositeBackend): + if not isinstance(resolved_backend.default, BaseSandbox): + return None + backend, _backend_path, route_prefix = _route_for_path( + default=resolved_backend.default, + sorted_routes=resolved_backend.sorted_routes, + path=capture_path, + ) + # Safe only when the path falls through to the default backend + # unchanged, since execute() also runs on the default. + if route_prefix is None and backend is resolved_backend.default: + return capture_path + return None + if isinstance(resolved_backend, BaseSandbox): + return capture_path + return None + + @staticmethod + def _format_execute_output(output: str, exit_code: int | None, *, truncated: bool) -> str: + """Format raw command output with status and truncation notes for the model.""" + parts = [output] + if exit_code is not None: + cmd_status = "succeeded" if exit_code == 0 else "failed" + parts.append(f"\n[Command {cmd_status} with exit code {exit_code}]") + if truncated: + parts.append("\n[Output was truncated due to size limits]") + return "".join(parts) + + def _interpret_capture_output(self, result: ExecuteResponse, capture_path: str, tool_call_id: str) -> str: + """Build `ToolMessage` content from a capture-at-source `execute` result.""" + if not result.offloaded: + return self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) + cmd_status = "succeeded" if result.exit_code == 0 else "failed" + preview = result.output.replace(_EXECUTE_CAPTURE_TRUNC, "... [middle truncated] ...") + content_sample = f"[Command {cmd_status} with exit code {result.exit_code}]\n{preview}" + return TOO_LARGE_TOOL_MSG.format( + tool_call_id=tool_call_id, + file_path=capture_path, + content_sample=content_sample, + ) + def _create_execute_tool(self) -> BaseTool: # noqa: C901 """Create the execute tool for sandbox command execution.""" tool_description = self._custom_tool_descriptions.get("execute") or EXECUTE_TOOL_DESCRIPTION @@ -1729,8 +1792,16 @@ def sync_execute( # noqa: PLR0911 - early returns for distinct error conditions tool_call_id=runtime.tool_call_id, status="error", ) + capture_path = self._capture_path_if_local(resolved_backend, runtime.tool_call_id) + exec_command = ( + _build_capture_execute_cmd( + command, capture_path, inline_budget=NUM_CHARS_PER_TOKEN * cast("int", self._tool_token_limit_before_evict) + ) + if capture_path is not None + else command + ) try: - result = executable.execute(command, timeout=timeout) if timeout is not None else executable.execute(command) + result = executable.execute(exec_command, timeout=timeout) if timeout is not None else executable.execute(exec_command) except NotImplementedError as e: return ToolMessage( content=f"Error: Execution not available. {e}", @@ -1746,18 +1817,14 @@ def sync_execute( # noqa: PLR0911 - early returns for distinct error conditions status="error", ) - # Format output for LLM consumption - parts = [result.output] - - if result.exit_code is not None: - cmd_status = "succeeded" if result.exit_code == 0 else "failed" - parts.append(f"\n[Command {cmd_status} with exit code {result.exit_code}]") - - if result.truncated: - parts.append("\n[Output was truncated due to size limits]") + if capture_path is not None: + parsed = _parse_capture_execute_output(result.output, backend_truncated=result.truncated) + content = self._interpret_capture_output(parsed, capture_path, cast("str", runtime.tool_call_id)) + else: + content = self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) return ToolMessage( - content="".join(parts), + content=content, name="execute", tool_call_id=runtime.tool_call_id, status="success", @@ -1818,8 +1885,16 @@ async def async_execute( # noqa: PLR0911 - early returns for distinct error con tool_call_id=runtime.tool_call_id, status="error", ) + capture_path = self._capture_path_if_local(resolved_backend, runtime.tool_call_id) + exec_command = ( + _build_capture_execute_cmd( + command, capture_path, inline_budget=NUM_CHARS_PER_TOKEN * cast("int", self._tool_token_limit_before_evict) + ) + if capture_path is not None + else command + ) try: - result = await executable.aexecute(command, timeout=timeout) if timeout is not None else await executable.aexecute(command) + result = await executable.aexecute(exec_command, timeout=timeout) if timeout is not None else await executable.aexecute(exec_command) except NotImplementedError as e: return ToolMessage( content=f"Error: Execution not available. {e}", @@ -1835,18 +1910,14 @@ async def async_execute( # noqa: PLR0911 - early returns for distinct error con status="error", ) - # Format output for LLM consumption - parts = [result.output] - - if result.exit_code is not None: - cmd_status = "succeeded" if result.exit_code == 0 else "failed" - parts.append(f"\n[Command {cmd_status} with exit code {result.exit_code}]") - - if result.truncated: - parts.append("\n[Output was truncated due to size limits]") + if capture_path is not None: + parsed = _parse_capture_execute_output(result.output, backend_truncated=result.truncated) + content = self._interpret_capture_output(parsed, capture_path, cast("str", runtime.tool_call_id)) + else: + content = self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) return ToolMessage( - content="".join(parts), + content=content, name="execute", tool_call_id=runtime.tool_call_id, status="success", From 243077b67330b6e9b1afdfe44bf1c1f16df932aa Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Sat, 20 Jun 2026 10:33:20 -0400 Subject: [PATCH 02/13] bound capture size and signal truncation --- .../deepagents/deepagents/backends/sandbox.py | 60 +++++++++++++------ .../deepagents/middleware/filesystem.py | 5 +- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/libs/deepagents/deepagents/backends/sandbox.py b/libs/deepagents/deepagents/backends/sandbox.py index 1cae7b8873..283c5b6926 100644 --- a/libs/deepagents/deepagents/backends/sandbox.py +++ b/libs/deepagents/deepagents/backends/sandbox.py @@ -584,7 +584,7 @@ def _build_edit_tmpfile_cmd(file_path: str, old_tmp: str, new_tmp: str, *, repla _EXECUTE_CAPTURE_SENTINEL: Final = "__DEEPAGENTS_EXEC_META__" -"""First-line marker identifying capture-wrapper output: ` `.""" +"""First-line marker identifying capture-wrapper output: ` `.""" _EXECUTE_CAPTURE_TRUNC: Final = "__DEEPAGENTS_EXEC_TRUNC__" """In-preview marker between the head and tail of an offloaded result.""" @@ -592,36 +592,50 @@ def _build_edit_tmpfile_cmd(file_path: str, old_tmp: str, new_tmp: str, *, repla _EXECUTE_CAPTURE_HEAD_BYTES: Final = 2000 _EXECUTE_CAPTURE_TAIL_BYTES: Final = 2000 +_EXECUTE_CAPTURE_MAX_BYTES: Final = 10 * 1024 * 1024 +"""Hard cap on captured stdout/stderr persisted to the sandbox. + +Bounds sandbox disk use for runaway output: the captured stream is piped through +`head -c`, so when the cap is hit the writer receives `SIGPIPE` and nothing +further reaches disk even if the command ignores the signal. Set well above the +inline budget so legitimately large output is still preserved in full; output +beyond the cap is truncated and flagged. +""" + +# The captured stream is piped through `head -c` so the on-disk file can never +# exceed the cap regardless of how the command behaves. Because that puts the +# command in a pipeline, its real exit code is recovered from a sidecar file +# rather than `$?` (which would be `head`'s). The command runs in a subshell so a +# command `exit` cannot abort the wrapper, and `eval` preserves the backend's own +# shell/env. The command is embedded via a quoted heredoc with a random delimiter +# to avoid shell-quoting issues; the (internal, sanitized) path is shell-quoted. _EXECUTE_CAPTURE_CMD_TEMPLATE = """__da_f=__PATH_Q__ +__da_ecf="$__da_f.ec" mkdir -p "$(dirname "$__da_f")" 2>/dev/null __da_cmd=$(cat <<'__DELIM__' __COMMAND__ __DELIM__ ) -( eval "$__da_cmd" ) > "$__da_f" 2>&1 -__da_ec=$? +{ ( eval "$__da_cmd" ); echo "$?" > "$__da_ecf"; } 2>&1 | head -c __MAXBYTES__ > "$__da_f" +__da_ec=$(cat "$__da_ecf" 2>/dev/null) +: "${__da_ec:=1}" +rm -f "$__da_ecf" __da_bytes=$(wc -c < "$__da_f" 2>/dev/null | tr -d ' ') : "${__da_bytes:=0}" +__da_capped=0 +[ "$__da_bytes" -ge __MAXBYTES__ ] && __da_capped=1 if [ "$__da_bytes" -le __BUDGET__ ]; then - printf '%s %s %s\\n' '__SENTINEL__' "$__da_ec" 0 + printf '%s %s %s %s\\n' '__SENTINEL__' "$__da_ec" 0 0 cat "$__da_f" rm -f "$__da_f" else - printf '%s %s %s\\n' '__SENTINEL__' "$__da_ec" 1 + printf '%s %s %s %s\\n' '__SENTINEL__' "$__da_ec" 1 "$__da_capped" head -c __HEAD__ "$__da_f" printf '\\n%s\\n' '__TRUNC__' tail -c __TAIL__ "$__da_f" fi """ -"""Pure POSIX sh wrapper for capture-at-source `execute`. - -Runs the command in a subshell — so a command `exit` cannot abort the -measurement step, and the backend's own shell/env is preserved via `eval` — with -combined output redirected to the capture file. A byte size (`wc -c`) then drives -the inline-vs-offload branch. The command is embedded via a quoted heredoc with a -random delimiter to avoid shell-quoting issues; the (internal, sanitized) path is -shell-quoted. -""" +"""Pure POSIX sh wrapper for capture-at-source `execute`. See the comment above.""" def _build_capture_execute_cmd(command: str, capture_path: str, *, inline_budget: int) -> str: @@ -629,7 +643,8 @@ def _build_capture_execute_cmd(command: str, capture_path: str, *, inline_budget `inline_budget` is the byte threshold at or below which output is returned inline; above it the output is left at `capture_path` and only a head/tail - preview is returned. + preview is returned. Captured output is hard-capped at + `_EXECUTE_CAPTURE_MAX_BYTES`; beyond that it is truncated and flagged. """ delim = "__DEEPAGENTS_CMD_" + base64.b32encode(os.urandom(10)).decode("ascii").rstrip("=") + "__" # __COMMAND__ is substituted last so command content can never collide with a @@ -637,6 +652,7 @@ def _build_capture_execute_cmd(command: str, capture_path: str, *, inline_budget return ( _EXECUTE_CAPTURE_CMD_TEMPLATE.replace("__PATH_Q__", shlex.quote(capture_path)) .replace("__DELIM__", delim) + .replace("__MAXBYTES__", str(_EXECUTE_CAPTURE_MAX_BYTES)) .replace("__BUDGET__", str(inline_budget)) .replace("__SENTINEL__", _EXECUTE_CAPTURE_SENTINEL) .replace("__HEAD__", str(_EXECUTE_CAPTURE_HEAD_BYTES)) @@ -651,18 +667,24 @@ def _parse_capture_execute_output(output: str, *, backend_truncated: bool = Fals Returns the raw output verbatim (no exit code, not offloaded) when the meta line is absent — e.g. if the backend truncated transport. The caller must - not re-run the command on this fallback. `backend_truncated` carries the - transport-truncation flag from the underlying `execute` through to the result. + not re-run the command on this fallback. `truncated` is set when the captured + output hit the size cap (the saved file is incomplete) or `backend_truncated` + is passed through from the underlying `execute`. """ first, _, body = output.partition("\n") parts = first.split(" ") - if len(parts) != 3 or parts[0] != _EXECUTE_CAPTURE_SENTINEL: # noqa: PLR2004 + if len(parts) != 4 or parts[0] != _EXECUTE_CAPTURE_SENTINEL: # noqa: PLR2004 return ExecuteResponse(output=output, truncated=backend_truncated) try: exit_code = int(parts[1]) except ValueError: return ExecuteResponse(output=output, truncated=backend_truncated) - return ExecuteResponse(output=body, exit_code=exit_code, offloaded=parts[2] == "1", truncated=backend_truncated) + return ExecuteResponse( + output=body, + exit_code=exit_code, + offloaded=parts[2] == "1", + truncated=parts[3] == "1" or backend_truncated, + ) class BaseSandbox(SandboxBackendProtocol, ABC): diff --git a/libs/deepagents/deepagents/middleware/filesystem.py b/libs/deepagents/deepagents/middleware/filesystem.py index f27b157824..0718f9df9e 100644 --- a/libs/deepagents/deepagents/middleware/filesystem.py +++ b/libs/deepagents/deepagents/middleware/filesystem.py @@ -1726,8 +1726,11 @@ def _interpret_capture_output(self, result: ExecuteResponse, capture_path: str, if not result.offloaded: return self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) cmd_status = "succeeded" if result.exit_code == 0 else "failed" + status_line = f"[Command {cmd_status} with exit code {result.exit_code}]" + if result.truncated: + status_line += "\n[Output exceeded the capture size limit and was truncated; the saved file is incomplete]" preview = result.output.replace(_EXECUTE_CAPTURE_TRUNC, "... [middle truncated] ...") - content_sample = f"[Command {cmd_status} with exit code {result.exit_code}]\n{preview}" + content_sample = f"{status_line}\n{preview}" return TOO_LARGE_TOOL_MSG.format( tool_call_id=tool_call_id, file_path=capture_path, From 20d1ad221fc76e00f0d2099d98a066428bb0124b Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Sat, 20 Jun 2026 10:49:02 -0400 Subject: [PATCH 03/13] cosmetic changes to content preview --- .../deepagents/deepagents/backends/sandbox.py | 21 ++++++++++++------- .../deepagents/middleware/filesystem.py | 4 +--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/libs/deepagents/deepagents/backends/sandbox.py b/libs/deepagents/deepagents/backends/sandbox.py index 283c5b6926..5ed1df64aa 100644 --- a/libs/deepagents/deepagents/backends/sandbox.py +++ b/libs/deepagents/deepagents/backends/sandbox.py @@ -586,9 +586,8 @@ def _build_edit_tmpfile_cmd(file_path: str, old_tmp: str, new_tmp: str, *, repla _EXECUTE_CAPTURE_SENTINEL: Final = "__DEEPAGENTS_EXEC_META__" """First-line marker identifying capture-wrapper output: ` `.""" -_EXECUTE_CAPTURE_TRUNC: Final = "__DEEPAGENTS_EXEC_TRUNC__" -"""In-preview marker between the head and tail of an offloaded result.""" - +_EXECUTE_CAPTURE_HEAD_LINES: Final = 5 +_EXECUTE_CAPTURE_TAIL_LINES: Final = 5 _EXECUTE_CAPTURE_HEAD_BYTES: Final = 2000 _EXECUTE_CAPTURE_TAIL_BYTES: Final = 2000 @@ -629,10 +628,17 @@ def _build_edit_tmpfile_cmd(file_path: str, old_tmp: str, new_tmp: str, *, repla cat "$__da_f" rm -f "$__da_f" else + __da_lines=$(wc -l < "$__da_f" 2>/dev/null | tr -d ' ') + : "${__da_lines:=0}" + __da_omitted=$((__da_lines - __HEADLINES__ - __TAILLINES__)) printf '%s %s %s %s\\n' '__SENTINEL__' "$__da_ec" 1 "$__da_capped" - head -c __HEAD__ "$__da_f" - printf '\\n%s\\n' '__TRUNC__' - tail -c __TAIL__ "$__da_f" + if [ "$__da_omitted" -gt 0 ]; then + head -c __HEAD__ "$__da_f" | head -n __HEADLINES__ + printf '... [%s lines truncated] ...\\n' "$__da_omitted" + tail -c __TAIL__ "$__da_f" | tail -n __TAILLINES__ + else + head -c $((__HEAD__ + __TAIL__)) "$__da_f" + fi fi """ """Pure POSIX sh wrapper for capture-at-source `execute`. See the comment above.""" @@ -655,9 +661,10 @@ def _build_capture_execute_cmd(command: str, capture_path: str, *, inline_budget .replace("__MAXBYTES__", str(_EXECUTE_CAPTURE_MAX_BYTES)) .replace("__BUDGET__", str(inline_budget)) .replace("__SENTINEL__", _EXECUTE_CAPTURE_SENTINEL) + .replace("__HEADLINES__", str(_EXECUTE_CAPTURE_HEAD_LINES)) + .replace("__TAILLINES__", str(_EXECUTE_CAPTURE_TAIL_LINES)) .replace("__HEAD__", str(_EXECUTE_CAPTURE_HEAD_BYTES)) .replace("__TAIL__", str(_EXECUTE_CAPTURE_TAIL_BYTES)) - .replace("__TRUNC__", _EXECUTE_CAPTURE_TRUNC) .replace("__COMMAND__", command) ) diff --git a/libs/deepagents/deepagents/middleware/filesystem.py b/libs/deepagents/deepagents/middleware/filesystem.py index 0718f9df9e..a79fbc3b72 100644 --- a/libs/deepagents/deepagents/middleware/filesystem.py +++ b/libs/deepagents/deepagents/middleware/filesystem.py @@ -57,7 +57,6 @@ execute_accepts_timeout, ) from deepagents.backends.sandbox import ( - _EXECUTE_CAPTURE_TRUNC, BaseSandbox, _build_capture_execute_cmd, _parse_capture_execute_output, @@ -1729,8 +1728,7 @@ def _interpret_capture_output(self, result: ExecuteResponse, capture_path: str, status_line = f"[Command {cmd_status} with exit code {result.exit_code}]" if result.truncated: status_line += "\n[Output exceeded the capture size limit and was truncated; the saved file is incomplete]" - preview = result.output.replace(_EXECUTE_CAPTURE_TRUNC, "... [middle truncated] ...") - content_sample = f"{status_line}\n{preview}" + content_sample = f"{status_line}\n{result.output}" return TOO_LARGE_TOOL_MSG.format( tool_call_id=tool_call_id, file_path=capture_path, From c18bde837bcf3fe4a298b801c2c8321189d3ab13 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Sat, 20 Jun 2026 10:59:31 -0400 Subject: [PATCH 04/13] refactor offloaded bool --- .../deepagents/backends/protocol.py | 11 -------- .../deepagents/deepagents/backends/sandbox.py | 28 +++++++++++-------- .../deepagents/middleware/filesystem.py | 16 +++++++---- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/libs/deepagents/deepagents/backends/protocol.py b/libs/deepagents/deepagents/backends/protocol.py index 5cb6e25e13..295c739dc7 100644 --- a/libs/deepagents/deepagents/backends/protocol.py +++ b/libs/deepagents/deepagents/backends/protocol.py @@ -799,17 +799,6 @@ class ExecuteResponse: truncated: bool = False """Whether the output was truncated due to backend limitations.""" - # Supports the execute offload-without-roundtrip path (capture-at-source): for - # large output, the command result is written to a file in the sandbox and only - # a preview is returned, avoiding a full-payload round-trip back through the - # agent process. Unset for ordinary execution. - offloaded: bool = False - """Whether the full output was captured to a file in the sandbox. - - When `True`, `output` holds only a head/tail preview; the full content lives - at the capture path on the sandbox filesystem. - """ - class SandboxBackendProtocol(BackendProtocol): """Extension of `BackendProtocol` that adds shell command execution. diff --git a/libs/deepagents/deepagents/backends/sandbox.py b/libs/deepagents/deepagents/backends/sandbox.py index 5ed1df64aa..7db2d3bfad 100644 --- a/libs/deepagents/deepagents/backends/sandbox.py +++ b/libs/deepagents/deepagents/backends/sandbox.py @@ -669,27 +669,31 @@ def _build_capture_execute_cmd(command: str, capture_path: str, *, inline_budget ) -def _parse_capture_execute_output(output: str, *, backend_truncated: bool = False) -> ExecuteResponse: - """Parse capture-wrapper stdout into an `ExecuteResponse`. - - Returns the raw output verbatim (no exit code, not offloaded) when the meta - line is absent — e.g. if the backend truncated transport. The caller must - not re-run the command on this fallback. `truncated` is set when the captured - output hit the size cap (the saved file is incomplete) or `backend_truncated` - is passed through from the underlying `execute`. +def _parse_capture_execute_output(output: str, *, backend_truncated: bool = False) -> tuple[bool, ExecuteResponse]: + """Parse capture-wrapper stdout into `(offloaded, ExecuteResponse)`. + + `offloaded` is kept separate from `ExecuteResponse` because it describes the + capture mechanism, not the command result (an ordinary `execute` never sets + it). Falls back to `(False, raw output)` when the meta line is absent — e.g. + if the backend truncated transport; the caller must not re-run the command in + that case. `ExecuteResponse.truncated` is set when the captured output hit the + size cap (the saved file is incomplete) or `backend_truncated` is passed + through from the underlying `execute`. """ first, _, body = output.partition("\n") parts = first.split(" ") if len(parts) != 4 or parts[0] != _EXECUTE_CAPTURE_SENTINEL: # noqa: PLR2004 - return ExecuteResponse(output=output, truncated=backend_truncated) + offloaded = False + return offloaded, ExecuteResponse(output=output, truncated=backend_truncated) try: exit_code = int(parts[1]) except ValueError: - return ExecuteResponse(output=output, truncated=backend_truncated) - return ExecuteResponse( + offloaded = False + return offloaded, ExecuteResponse(output=output, truncated=backend_truncated) + offloaded = parts[2] == "1" + return offloaded, ExecuteResponse( output=body, exit_code=exit_code, - offloaded=parts[2] == "1", truncated=parts[3] == "1" or backend_truncated, ) diff --git a/libs/deepagents/deepagents/middleware/filesystem.py b/libs/deepagents/deepagents/middleware/filesystem.py index a79fbc3b72..e3295b37ea 100644 --- a/libs/deepagents/deepagents/middleware/filesystem.py +++ b/libs/deepagents/deepagents/middleware/filesystem.py @@ -1720,9 +1720,9 @@ def _format_execute_output(output: str, exit_code: int | None, *, truncated: boo parts.append("\n[Output was truncated due to size limits]") return "".join(parts) - def _interpret_capture_output(self, result: ExecuteResponse, capture_path: str, tool_call_id: str) -> str: + def _interpret_capture_output(self, *, offloaded: bool, result: ExecuteResponse, capture_path: str, tool_call_id: str) -> str: """Build `ToolMessage` content from a capture-at-source `execute` result.""" - if not result.offloaded: + if not offloaded: return self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) cmd_status = "succeeded" if result.exit_code == 0 else "failed" status_line = f"[Command {cmd_status} with exit code {result.exit_code}]" @@ -1819,8 +1819,10 @@ def sync_execute( # noqa: PLR0911 - early returns for distinct error conditions ) if capture_path is not None: - parsed = _parse_capture_execute_output(result.output, backend_truncated=result.truncated) - content = self._interpret_capture_output(parsed, capture_path, cast("str", runtime.tool_call_id)) + offloaded, parsed = _parse_capture_execute_output(result.output, backend_truncated=result.truncated) + content = self._interpret_capture_output( + offloaded=offloaded, result=parsed, capture_path=capture_path, tool_call_id=cast("str", runtime.tool_call_id) + ) else: content = self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) @@ -1912,8 +1914,10 @@ async def async_execute( # noqa: PLR0911 - early returns for distinct error con ) if capture_path is not None: - parsed = _parse_capture_execute_output(result.output, backend_truncated=result.truncated) - content = self._interpret_capture_output(parsed, capture_path, cast("str", runtime.tool_call_id)) + offloaded, parsed = _parse_capture_execute_output(result.output, backend_truncated=result.truncated) + content = self._interpret_capture_output( + offloaded=offloaded, result=parsed, capture_path=capture_path, tool_call_id=cast("str", runtime.tool_call_id) + ) else: content = self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) From 156246622115cc8acd64494b1134c1c12c4955f6 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Sat, 20 Jun 2026 11:23:31 -0400 Subject: [PATCH 05/13] add tests --- .../test_local_sandbox_operations.py | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py b/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py index dc951f0f5d..00aeca609c 100644 --- a/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py +++ b/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py @@ -26,7 +26,9 @@ from pathlib import Path import pytest +from langchain.tools import ToolRuntime +from deepagents.backends import CompositeBackend from deepagents.backends.filesystem import _map_exception_to_standard_error from deepagents.backends.protocol import ( EditResult, @@ -40,6 +42,7 @@ WriteResult, ) from deepagents.backends.sandbox import _EDIT_INLINE_MAX_BYTES, BaseSandbox +from deepagents.middleware.filesystem import FilesystemMiddleware # Skip all tests in this module unless RUN_SANDBOX_TESTS=true pytestmark = pytest.mark.skipif( @@ -1600,3 +1603,103 @@ def test_complex_directory_operations(self, sandbox: LocalSubprocessSandbox) -> grep_result = sandbox.grep("file", path=base_dir).matches assert grep_result is not None assert len(grep_result) >= 3 # At least 3 matches + + +# 5000 of these lines (~250 KB) clear the default eviction budget (~80 KB). +_BIG_OUTPUT_CMD = 'for i in $(seq 1 5000); do echo "line $i: padding text to make the output long enough to offload"; done' + + +class TestExecuteCaptureOffload: + """End-to-end capture-at-source offload via the execute tool on a real shell. + + Drives the `execute` and `read_file` tools through a `CompositeBackend` whose + default is a `LocalSubprocessSandbox` and whose `artifacts_root` lives under + the translated virtual root, so the wrapper's capture file lands in the test + directory rather than the host filesystem root. + """ + + @pytest.fixture(scope="class") + def sandbox(self) -> Iterator[LocalSubprocessSandbox]: + return LocalSubprocessSandbox() + + @pytest.fixture(autouse=True) + def setup_test_dir(self, sandbox: LocalSubprocessSandbox, tmp_path: Path) -> None: + sandbox.set_real_root(str(tmp_path / "sandbox_ops")) + sandbox.execute("rm -rf /tmp/test_sandbox_ops && mkdir -p /tmp/test_sandbox_ops") + + @pytest.fixture + def tools(self, sandbox: LocalSubprocessSandbox) -> tuple: + backend = CompositeBackend(default=sandbox, routes={}, artifacts_root=VIRTUAL_SANDBOX_ROOT) + middleware = FilesystemMiddleware(backend=backend) + execute_tool = next(t for t in middleware.tools if t.name == "execute") + read_tool = next(t for t in middleware.tools if t.name == "read_file") + return execute_tool, read_tool + + @staticmethod + def _runtime(tool_call_id: str) -> ToolRuntime: + return ToolRuntime( + state={}, + context=None, + tool_call_id=tool_call_id, + store=None, + stream_writer=lambda _: None, + config={}, + ) + + @staticmethod + def _capture_path(tool_call_id: str) -> str: + return f"{VIRTUAL_SANDBOX_ROOT}/large_tool_results/{tool_call_id}" + + def test_small_output_returned_inline_and_leaves_no_file(self, tools: tuple, sandbox: LocalSubprocessSandbox) -> None: + execute_tool, _ = tools + result = execute_tool.invoke({"command": "echo hello", "runtime": self._runtime("c_small")}) + + assert "hello" in result.content + assert "exit code 0" in result.content + # Small results are not offloaded -- no pointer, and the capture file is removed. + assert "large_tool_results" not in result.content + listing = sandbox.execute(f"ls {VIRTUAL_SANDBOX_ROOT}/large_tool_results/ 2>/dev/null | wc -l") + assert listing.output.strip() == "0" + + def test_large_output_offloads_and_full_content_roundtrips(self, tools: tuple) -> None: + execute_tool, read_tool = tools + rt = self._runtime("c_large") + result = execute_tool.invoke({"command": _BIG_OUTPUT_CMD, "runtime": rt}) + + capture_path = self._capture_path("c_large") + # Preview + pointer, not the full output inline. + assert capture_path in result.content + assert "read_file" in result.content + assert "line 1:" in result.content # head shown + assert "line 5000:" in result.content # tail shown + assert "lines truncated" in result.content + # A middle line is absent from the preview... + assert "line 2500:" not in result.content + + # ...but recoverable in full via read_file on the offload path: a middle + # slice the preview never showed is present on disk. + read = read_tool.invoke({"file_path": capture_path, "offset": 2499, "limit": 3, "runtime": rt}) + assert "line 2500:" in read.content + + def test_nonzero_exit_code_preserved(self, tools: tuple) -> None: + execute_tool, _ = tools + result = execute_tool.invoke({"command": "echo oops; exit 3", "runtime": self._runtime("c_ec")}) + + assert "oops" in result.content + assert "exit code 3" in result.content + + def test_runaway_output_is_capped_and_flagged(self, tools: tuple, sandbox: LocalSubprocessSandbox, monkeypatch: pytest.MonkeyPatch) -> None: + # Cap must exceed the eviction budget so a capped result still offloads + # (rather than fitting inline). Default budget is ~80 KB. + cap = 100_000 + monkeypatch.setattr("deepagents.backends.sandbox._EXECUTE_CAPTURE_MAX_BYTES", cap) + + execute_tool, _ = tools + rt = self._runtime("c_cap") + # ~250 KB of output, well over the cap. + result = execute_tool.invoke({"command": _BIG_OUTPUT_CMD, "runtime": rt}) + + assert "exceeded the capture size limit" in result.content + # The on-disk capture file is bounded at the cap regardless of total output. + size = sandbox.execute(f"wc -c < {self._capture_path('c_cap')}").output.strip() + assert size == str(cap) From 34de2402ccba8cdc1697d5d146e31affd681b560 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Sat, 20 Jun 2026 12:52:23 -0400 Subject: [PATCH 06/13] add escape hatch --- libs/deepagents/deepagents/backends/sandbox.py | 10 ++++++++++ .../deepagents/deepagents/middleware/filesystem.py | 14 ++++++++------ .../unit_tests/test_local_sandbox_operations.py | 13 +++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/libs/deepagents/deepagents/backends/sandbox.py b/libs/deepagents/deepagents/backends/sandbox.py index 7db2d3bfad..19471f93f5 100644 --- a/libs/deepagents/deepagents/backends/sandbox.py +++ b/libs/deepagents/deepagents/backends/sandbox.py @@ -720,6 +720,16 @@ class BaseSandbox(SandboxBackendProtocol, ABC): and the `id` property. """ + enable_capture_offload: bool = True + """Whether `FilesystemMiddleware` may use capture-at-source offload for `execute`. + + When `True` (default), large `execute` output is captured to a file in the + sandbox and only a preview is returned, avoiding a round-trip back through the + agent process. Set to `False` (on the subclass or an instance) to fall back to + inline execution plus the middleware's generic eviction -- an escape hatch for + environments where the capture wrapper's shell assumptions do not hold. + """ + @abstractmethod def execute( self, diff --git a/libs/deepagents/deepagents/middleware/filesystem.py b/libs/deepagents/deepagents/middleware/filesystem.py index e3295b37ea..4332edef30 100644 --- a/libs/deepagents/deepagents/middleware/filesystem.py +++ b/libs/deepagents/deepagents/middleware/filesystem.py @@ -1686,27 +1686,29 @@ def _capture_path_if_local(self, resolved_backend: BackendProtocol, tool_call_id `execute()` and `read_file` share one filesystem at that path. Only `BaseSandbox` guarantees this, so the optimization is gated on it (a stub or host-shell backend falls back to inline execute plus generic - eviction). Also returns `None` when eviction is disabled, the tool call - has no id, or the offload path would route to a different backend. + eviction). Also returns `None` when the sandbox opts out via + `enable_capture_offload`, eviction is disabled, the tool call has no id, + or the offload path would route to a different backend. """ if not self._tool_token_limit_before_evict or not tool_call_id: return None capture_path = f"{self._large_tool_results_prefix}/{sanitize_tool_call_id(tool_call_id)}" if isinstance(resolved_backend, CompositeBackend): - if not isinstance(resolved_backend.default, BaseSandbox): + default = resolved_backend.default + if not isinstance(default, BaseSandbox) or not default.enable_capture_offload: return None backend, _backend_path, route_prefix = _route_for_path( - default=resolved_backend.default, + default=default, sorted_routes=resolved_backend.sorted_routes, path=capture_path, ) # Safe only when the path falls through to the default backend # unchanged, since execute() also runs on the default. - if route_prefix is None and backend is resolved_backend.default: + if route_prefix is None and backend is default: return capture_path return None if isinstance(resolved_backend, BaseSandbox): - return capture_path + return capture_path if resolved_backend.enable_capture_offload else None return None @staticmethod diff --git a/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py b/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py index 00aeca609c..b632c2eda8 100644 --- a/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py +++ b/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py @@ -1703,3 +1703,16 @@ def test_runaway_output_is_capped_and_flagged(self, tools: tuple, sandbox: Local # The on-disk capture file is bounded at the cap regardless of total output. size = sandbox.execute(f"wc -c < {self._capture_path('c_cap')}").output.strip() assert size == str(cap) + + def test_enable_capture_offload_flag_gates_capture(self) -> None: + # Fresh sandbox so toggling the flag does not leak into the shared fixture. + sandbox = LocalSubprocessSandbox() + backend = CompositeBackend(default=sandbox, routes={}, artifacts_root=VIRTUAL_SANDBOX_ROOT) + middleware = FilesystemMiddleware(backend=backend) + + # Enabled by default -> a sandbox-local capture path is chosen. + assert middleware._capture_path_if_local(backend, "c1") is not None + + # Opting out -> capture is skipped (caller falls back to generic eviction). + sandbox.enable_capture_offload = False + assert middleware._capture_path_if_local(backend, "c1") is None From 332902aba5a0527f11696562e451d17b0796b496 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Sat, 20 Jun 2026 13:12:57 -0400 Subject: [PATCH 07/13] add comments in capture wrapper --- libs/deepagents/deepagents/backends/sandbox.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libs/deepagents/deepagents/backends/sandbox.py b/libs/deepagents/deepagents/backends/sandbox.py index 19471f93f5..a27dfe8102 100644 --- a/libs/deepagents/deepagents/backends/sandbox.py +++ b/libs/deepagents/deepagents/backends/sandbox.py @@ -608,13 +608,20 @@ def _build_edit_tmpfile_cmd(file_path: str, old_tmp: str, new_tmp: str, *, repla # command `exit` cannot abort the wrapper, and `eval` preserves the backend's own # shell/env. The command is embedded via a quoted heredoc with a random delimiter # to avoid shell-quoting issues; the (internal, sanitized) path is shell-quoted. -_EXECUTE_CAPTURE_CMD_TEMPLATE = """__da_f=__PATH_Q__ +_EXECUTE_CAPTURE_CMD_TEMPLATE = """# ===== deepagents capture-at-source offload (auto-generated wrapper) ===== +# Runs the requested command below, capturing its combined output to a file in +# the sandbox: returned inline when small, or as a head/tail preview when large +# (the full result stays at the path for read_file). Disable this wrapping with +# BaseSandbox.enable_capture_offload = False. +__da_f=__PATH_Q__ __da_ecf="$__da_f.ec" mkdir -p "$(dirname "$__da_f")" 2>/dev/null +# ----- requested command (verbatim, between the heredoc markers) ----- __da_cmd=$(cat <<'__DELIM__' __COMMAND__ __DELIM__ ) +# ----- end requested command; everything below is offload machinery ----- { ( eval "$__da_cmd" ); echo "$?" > "$__da_ecf"; } 2>&1 | head -c __MAXBYTES__ > "$__da_f" __da_ec=$(cat "$__da_ecf" 2>/dev/null) : "${__da_ec:=1}" From df565b63d88af8cb7e1333f4dab6666aa1032fb4 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Thu, 25 Jun 2026 15:53:55 -0400 Subject: [PATCH 08/13] preserve exit code when capture output hits the cap --- libs/deepagents/deepagents/backends/sandbox.py | 18 ++++++++++-------- .../test_local_sandbox_operations.py | 8 ++++++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/libs/deepagents/deepagents/backends/sandbox.py b/libs/deepagents/deepagents/backends/sandbox.py index a27dfe8102..5238e273d7 100644 --- a/libs/deepagents/deepagents/backends/sandbox.py +++ b/libs/deepagents/deepagents/backends/sandbox.py @@ -601,13 +601,15 @@ def _build_edit_tmpfile_cmd(file_path: str, old_tmp: str, new_tmp: str, *, repla beyond the cap is truncated and flagged. """ -# The captured stream is piped through `head -c` so the on-disk file can never -# exceed the cap regardless of how the command behaves. Because that puts the -# command in a pipeline, its real exit code is recovered from a sidecar file -# rather than `$?` (which would be `head`'s). The command runs in a subshell so a -# command `exit` cannot abort the wrapper, and `eval` preserves the backend's own -# shell/env. The command is embedded via a quoted heredoc with a random delimiter -# to avoid shell-quoting issues; the (internal, sanitized) path is shell-quoted. +# The captured stream is piped into `head -c` (caps the on-disk file) followed by +# `cat > /dev/null` (drains the rest), so the file can never exceed the cap yet the +# command still reaches EOF and exits normally -- closing the pipe early would +# SIGPIPE-kill it and corrupt its exit code. Because the command is in a pipeline, +# its real exit code is recovered from a sidecar file rather than `$?` (which would +# be the pipeline's). The command runs in a subshell so a command `exit` cannot +# abort the wrapper, and `eval` preserves the backend's own shell/env. The command +# is embedded via a quoted heredoc with a random delimiter to avoid shell-quoting +# issues; the (internal, sanitized) path is shell-quoted. _EXECUTE_CAPTURE_CMD_TEMPLATE = """# ===== deepagents capture-at-source offload (auto-generated wrapper) ===== # Runs the requested command below, capturing its combined output to a file in # the sandbox: returned inline when small, or as a head/tail preview when large @@ -622,7 +624,7 @@ def _build_edit_tmpfile_cmd(file_path: str, old_tmp: str, new_tmp: str, *, repla __DELIM__ ) # ----- end requested command; everything below is offload machinery ----- -{ ( eval "$__da_cmd" ); echo "$?" > "$__da_ecf"; } 2>&1 | head -c __MAXBYTES__ > "$__da_f" +{ ( eval "$__da_cmd" ); echo "$?" > "$__da_ecf"; } 2>&1 | { head -c __MAXBYTES__ > "$__da_f"; cat > /dev/null; } __da_ec=$(cat "$__da_ecf" 2>/dev/null) : "${__da_ec:=1}" rm -f "$__da_ecf" diff --git a/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py b/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py index b632c2eda8..d1cc600508 100644 --- a/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py +++ b/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py @@ -1696,10 +1696,14 @@ def test_runaway_output_is_capped_and_flagged(self, tools: tuple, sandbox: Local execute_tool, _ = tools rt = self._runtime("c_cap") - # ~250 KB of output, well over the cap. - result = execute_tool.invoke({"command": _BIG_OUTPUT_CMD, "runtime": rt}) + # ~250 KB of output over the cap, but the command exits 0. The cap drains + # the excess instead of SIGPIPE-killing the producer, so the command's real + # exit code survives -- a regression guard: closing the pipe early would + # report this successful command as failed. + result = execute_tool.invoke({"command": f"{_BIG_OUTPUT_CMD}; exit 0", "runtime": rt}) assert "exceeded the capture size limit" in result.content + assert "succeeded with exit code 0" in result.content # The on-disk capture file is bounded at the cap regardless of total output. size = sandbox.execute(f"wc -c < {self._capture_path('c_cap')}").output.strip() assert size == str(cap) From 4de7ad19b1e6be18de5bd3078dae57e2c15dfb92 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Fri, 26 Jun 2026 13:10:29 -0400 Subject: [PATCH 09/13] move offloading to execute_with_offload on sandbox --- .../deepagents/deepagents/backends/sandbox.py | 61 +++++++++++- .../deepagents/middleware/filesystem.py | 93 +++++++++---------- .../test_local_sandbox_operations.py | 26 ++++-- 3 files changed, 117 insertions(+), 63 deletions(-) diff --git a/libs/deepagents/deepagents/backends/sandbox.py b/libs/deepagents/deepagents/backends/sandbox.py index 52b3bc0a15..d16749bb13 100644 --- a/libs/deepagents/deepagents/backends/sandbox.py +++ b/libs/deepagents/deepagents/backends/sandbox.py @@ -39,6 +39,7 @@ ReadResult, SandboxBackendProtocol, WriteResult, + execute_accepts_timeout, ) from deepagents.backends.utils import _get_file_type @@ -650,21 +651,23 @@ def _build_edit_tmpfile_cmd(file_path: str, old_tmp: str, new_tmp: str, *, repla """Pure POSIX sh wrapper for capture-at-source `execute`. See the comment above.""" -def _build_capture_execute_cmd(command: str, capture_path: str, *, inline_budget: int) -> str: +def _build_capture_execute_cmd(command: str, capture_path: str, *, inline_budget: int, max_capture_bytes: int | None = None) -> str: """Build the capture-at-source wrapper command for `execute`. `inline_budget` is the byte threshold at or below which output is returned inline; above it the output is left at `capture_path` and only a head/tail - preview is returned. Captured output is hard-capped at - `_EXECUTE_CAPTURE_MAX_BYTES`; beyond that it is truncated and flagged. + preview is returned. Captured output is hard-capped at `max_capture_bytes` + (defaulting to `_EXECUTE_CAPTURE_MAX_BYTES`, resolved here so it stays + overridable/patchable); beyond that it is truncated and flagged. """ + cap = max_capture_bytes if max_capture_bytes is not None else _EXECUTE_CAPTURE_MAX_BYTES delim = "__DEEPAGENTS_CMD_" + base64.b32encode(os.urandom(10)).decode("ascii").rstrip("=") + "__" # __COMMAND__ is substituted last so command content can never collide with a # remaining placeholder token. return ( _EXECUTE_CAPTURE_CMD_TEMPLATE.replace("__PATH_Q__", shlex.quote(capture_path)) .replace("__DELIM__", delim) - .replace("__MAXBYTES__", str(_EXECUTE_CAPTURE_MAX_BYTES)) + .replace("__MAXBYTES__", str(cap)) .replace("__BUDGET__", str(inline_budget)) .replace("__SENTINEL__", _EXECUTE_CAPTURE_SENTINEL) .replace("__HEADLINES__", str(_EXECUTE_CAPTURE_HEAD_LINES)) @@ -755,6 +758,56 @@ def execute( `ExecuteResponse` with combined output, exit code, and truncation flag. """ + def execute_with_offload( + self, + command: str, + capture_path: str, + *, + max_inline_bytes: int, + max_capture_bytes: int | None = None, + timeout: int | None = None, + ) -> tuple[bool, ExecuteResponse]: + """Run `command`, offloading large output to a file in the sandbox. + + Captures the command's combined output: returned inline when it is at or + below `max_inline_bytes`, otherwise left at `capture_path` (so the caller + can surface a `read_file` pointer) with only a head/tail preview returned. + Captured output is hard-capped at `max_capture_bytes` (default + `_EXECUTE_CAPTURE_MAX_BYTES`) without killing the command, so the exit + code is preserved. + + Returns `(offloaded, ExecuteResponse)`, where `offloaded` is `True` when + the result was left at `capture_path` and `output` holds only the preview. + When `enable_capture_offload` is `False`, runs the command unwrapped and + returns `(False, …)` with the full output — letting callers fall back to + their own handling (e.g. generic eviction). + """ + use_timeout = timeout is not None and execute_accepts_timeout(type(self)) + if not self.enable_capture_offload: + result = self.execute(command, timeout=timeout) if use_timeout else self.execute(command) + return False, result + wrapper = _build_capture_execute_cmd(command, capture_path, inline_budget=max_inline_bytes, max_capture_bytes=max_capture_bytes) + result = self.execute(wrapper, timeout=timeout) if use_timeout else self.execute(wrapper) + return _parse_capture_execute_output(result.output, backend_truncated=result.truncated) + + async def aexecute_with_offload( + self, + command: str, + capture_path: str, + *, + max_inline_bytes: int, + max_capture_bytes: int | None = None, + timeout: int | None = None, # noqa: ASYNC109 # forwarded to the backend, not an asyncio timeout + ) -> tuple[bool, ExecuteResponse]: + """Async version of `execute_with_offload`, delegating to `aexecute`.""" + use_timeout = timeout is not None and execute_accepts_timeout(type(self)) + if not self.enable_capture_offload: + result = await self.aexecute(command, timeout=timeout) if use_timeout else await self.aexecute(command) + return False, result + wrapper = _build_capture_execute_cmd(command, capture_path, inline_budget=max_inline_bytes, max_capture_bytes=max_capture_bytes) + result = await self.aexecute(wrapper, timeout=timeout) if use_timeout else await self.aexecute(wrapper) + return _parse_capture_execute_output(result.output, backend_truncated=result.truncated) + def ls(self, path: str) -> LsResult: """Structured listing with file metadata using os.scandir.""" result = self.execute(_build_ls_cmd(path)) diff --git a/libs/deepagents/deepagents/middleware/filesystem.py b/libs/deepagents/deepagents/middleware/filesystem.py index 14164195fc..539ff1f895 100644 --- a/libs/deepagents/deepagents/middleware/filesystem.py +++ b/libs/deepagents/deepagents/middleware/filesystem.py @@ -58,11 +58,7 @@ _supports_delete, execute_accepts_timeout, ) -from deepagents.backends.sandbox import ( - BaseSandbox, - _build_capture_execute_cmd, - _parse_capture_execute_output, -) +from deepagents.backends.sandbox import BaseSandbox from deepagents.backends.utils import ( _get_file_type, _glob_anchor, @@ -1825,24 +1821,27 @@ async def async_grep( args_schema=GrepSchema, ) - def _capture_path_if_local(self, resolved_backend: BackendProtocol, tool_call_id: str | None) -> str | None: - """Return the sandbox-local offload path for capture-at-source, or `None` to skip. + def _resolve_capture(self, resolved_backend: BackendProtocol, tool_call_id: str | None) -> tuple[BaseSandbox, str] | None: + """Resolve `(executor, capture_path)` for capture-at-source, or `None` to skip. Capture-at-source writes output to a literal path via the sandbox shell and later reads it back through the backend, so it is only sound when `execute()` and `read_file` share one filesystem at that path. Only - `BaseSandbox` guarantees this, so the optimization is gated on it (a - stub or host-shell backend falls back to inline execute plus generic - eviction). Also returns `None` when the sandbox opts out via - `enable_capture_offload`, eviction is disabled, the tool call has no id, - or the offload path would route to a different backend. + `BaseSandbox` guarantees this, so it is gated on it (a stub or host-shell + backend falls back to plain execute plus generic eviction). Also returns + `None` when eviction is disabled, the tool call has no id, or the offload + path would route to a different backend. + + Whether capture is actually applied is left to the executor's + `execute_with_offload` (which honors `enable_capture_offload`); this only + decides whether the offload path is sound to attempt. """ if not self._tool_token_limit_before_evict or not tool_call_id: return None capture_path = f"{self._large_tool_results_prefix}/{sanitize_tool_call_id(tool_call_id)}" if isinstance(resolved_backend, CompositeBackend): default = resolved_backend.default - if not isinstance(default, BaseSandbox) or not default.enable_capture_offload: + if not isinstance(default, BaseSandbox): return None backend, _backend_path, route_prefix = _route_for_path( default=default, @@ -1852,10 +1851,10 @@ def _capture_path_if_local(self, resolved_backend: BackendProtocol, tool_call_id # Safe only when the path falls through to the default backend # unchanged, since execute() also runs on the default. if route_prefix is None and backend is default: - return capture_path + return default, capture_path return None if isinstance(resolved_backend, BaseSandbox): - return capture_path if resolved_backend.enable_capture_offload else None + return resolved_backend, capture_path return None @staticmethod @@ -1942,16 +1941,22 @@ def sync_execute( # noqa: PLR0911 - early returns for distinct error conditions tool_call_id=runtime.tool_call_id, status="error", ) - capture_path = self._capture_path_if_local(resolved_backend, runtime.tool_call_id) - exec_command = ( - _build_capture_execute_cmd( - command, capture_path, inline_budget=NUM_CHARS_PER_TOKEN * cast("int", self._tool_token_limit_before_evict) - ) - if capture_path is not None - else command - ) + capture = self._resolve_capture(resolved_backend, runtime.tool_call_id) try: - result = executable.execute(exec_command, timeout=timeout) if timeout is not None else executable.execute(exec_command) + if capture is not None: + executor, capture_path = capture + offloaded, result = executor.execute_with_offload( + command, + capture_path, + max_inline_bytes=NUM_CHARS_PER_TOKEN * cast("int", self._tool_token_limit_before_evict), + timeout=timeout, + ) + content = self._interpret_capture_output( + offloaded=offloaded, result=result, capture_path=capture_path, tool_call_id=cast("str", runtime.tool_call_id) + ) + else: + result = executable.execute(command, timeout=timeout) if timeout is not None else executable.execute(command) + content = self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) except NotImplementedError as e: return ToolMessage( content=f"Error: Execution not available. {e}", @@ -1967,14 +1972,6 @@ def sync_execute( # noqa: PLR0911 - early returns for distinct error conditions status="error", ) - if capture_path is not None: - offloaded, parsed = _parse_capture_execute_output(result.output, backend_truncated=result.truncated) - content = self._interpret_capture_output( - offloaded=offloaded, result=parsed, capture_path=capture_path, tool_call_id=cast("str", runtime.tool_call_id) - ) - else: - content = self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) - return ToolMessage( content=content, name="execute", @@ -2037,16 +2034,22 @@ async def async_execute( # noqa: PLR0911 - early returns for distinct error con tool_call_id=runtime.tool_call_id, status="error", ) - capture_path = self._capture_path_if_local(resolved_backend, runtime.tool_call_id) - exec_command = ( - _build_capture_execute_cmd( - command, capture_path, inline_budget=NUM_CHARS_PER_TOKEN * cast("int", self._tool_token_limit_before_evict) - ) - if capture_path is not None - else command - ) + capture = self._resolve_capture(resolved_backend, runtime.tool_call_id) try: - result = await executable.aexecute(exec_command, timeout=timeout) if timeout is not None else await executable.aexecute(exec_command) + if capture is not None: + executor, capture_path = capture + offloaded, result = await executor.aexecute_with_offload( + command, + capture_path, + max_inline_bytes=NUM_CHARS_PER_TOKEN * cast("int", self._tool_token_limit_before_evict), + timeout=timeout, + ) + content = self._interpret_capture_output( + offloaded=offloaded, result=result, capture_path=capture_path, tool_call_id=cast("str", runtime.tool_call_id) + ) + else: + result = await executable.aexecute(command, timeout=timeout) if timeout is not None else await executable.aexecute(command) + content = self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) except NotImplementedError as e: return ToolMessage( content=f"Error: Execution not available. {e}", @@ -2062,14 +2065,6 @@ async def async_execute( # noqa: PLR0911 - early returns for distinct error con status="error", ) - if capture_path is not None: - offloaded, parsed = _parse_capture_execute_output(result.output, backend_truncated=result.truncated) - content = self._interpret_capture_output( - offloaded=offloaded, result=parsed, capture_path=capture_path, tool_call_id=cast("str", runtime.tool_call_id) - ) - else: - content = self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) - return ToolMessage( content=content, name="execute", diff --git a/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py b/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py index 0c459af8ae..2e2cb2bb61 100644 --- a/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py +++ b/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py @@ -1707,15 +1707,21 @@ def test_runaway_output_is_capped_and_flagged(self, tools: tuple, sandbox: Local size = sandbox.execute(f"wc -c < {self._capture_path('c_cap')}").output.strip() assert size == str(cap) - def test_enable_capture_offload_flag_gates_capture(self) -> None: - # Fresh sandbox so toggling the flag does not leak into the shared fixture. - sandbox = LocalSubprocessSandbox() - backend = CompositeBackend(default=sandbox, routes={}, artifacts_root=VIRTUAL_SANDBOX_ROOT) - middleware = FilesystemMiddleware(backend=backend) - - # Enabled by default -> a sandbox-local capture path is chosen. - assert middleware._capture_path_if_local(backend, "c1") is not None + def test_enable_capture_offload_flag_controls_offload(self, sandbox: LocalSubprocessSandbox) -> None: + budget = 100 # small, so _BIG_OUTPUT_CMD would offload when capture is enabled - # Opting out -> capture is skipped (caller falls back to generic eviction). + # Disabled -> command runs unwrapped: full output inline, not offloaded, no file. sandbox.enable_capture_offload = False - assert middleware._capture_path_if_local(backend, "c1") is None + off_path = self._capture_path("flag_off") + offloaded, result = sandbox.execute_with_offload(_BIG_OUTPUT_CMD, off_path, max_inline_bytes=budget) + assert offloaded is False + assert "line 5000:" in result.output # full output returned, not a preview + assert "line 2500:" in result.output + assert sandbox.execute(f"test -e {off_path} && echo Y || echo N").output.strip() == "N" + + # Enabled -> offloaded to a file; only a head/tail preview is returned. + sandbox.enable_capture_offload = True + on_path = self._capture_path("flag_on") + offloaded, result = sandbox.execute_with_offload(_BIG_OUTPUT_CMD, on_path, max_inline_bytes=budget) + assert offloaded is True + assert "line 2500:" not in result.output # middle omitted -> it's a preview From 1e9d3d69f01ce0029cad4a2a21d1896ca06a764e Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Fri, 26 Jun 2026 15:19:25 -0400 Subject: [PATCH 10/13] make opt-in (on for langsmith) --- libs/deepagents/deepagents/backends/langsmith.py | 4 ++++ libs/deepagents/deepagents/backends/sandbox.py | 14 ++++++++------ .../unit_tests/test_local_sandbox_operations.py | 2 ++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/libs/deepagents/deepagents/backends/langsmith.py b/libs/deepagents/deepagents/backends/langsmith.py index 92570ecfe3..db9ae58756 100644 --- a/libs/deepagents/deepagents/backends/langsmith.py +++ b/libs/deepagents/deepagents/backends/langsmith.py @@ -48,6 +48,10 @@ def _binary_read_result(file_path: str, raw: bytes) -> ReadResult: class LangSmithSandbox(BaseSandbox): """LangSmith sandbox implementation conforming to [`SandboxBackendProtocol`][deepagents.backends.protocol.SandboxBackendProtocol].""" + # LangSmith sandbox images ship a POSIX shell + coreutils compatible with the + # capture wrapper, so opt in to capture-at-source offload for `execute`. + enable_capture_offload = True + def __init__(self, sandbox: Sandbox) -> None: """Create a backend wrapping an existing LangSmith sandbox. diff --git a/libs/deepagents/deepagents/backends/sandbox.py b/libs/deepagents/deepagents/backends/sandbox.py index d16749bb13..796bd6ac9c 100644 --- a/libs/deepagents/deepagents/backends/sandbox.py +++ b/libs/deepagents/deepagents/backends/sandbox.py @@ -729,14 +729,16 @@ class BaseSandbox(SandboxBackendProtocol, ABC): and the `id` property. """ - enable_capture_offload: bool = True + enable_capture_offload: bool = False """Whether `FilesystemMiddleware` may use capture-at-source offload for `execute`. - When `True` (default), large `execute` output is captured to a file in the - sandbox and only a preview is returned, avoiding a round-trip back through the - agent process. Set to `False` (on the subclass or an instance) to fall back to - inline execution plus the middleware's generic eviction -- an escape hatch for - environments where the capture wrapper's shell assumptions do not hold. + When `True`, large `execute` output is captured to a file in the sandbox and + only a preview is returned, avoiding a round-trip back through the agent + process. Defaults to `False` (opt-in) because the capture wrapper's shell and + coreutils assumptions are not guaranteed on every sandbox image; subclasses + known to be compatible set it to `True`. When `False`, `execute_with_offload` + runs the command unwrapped and the middleware falls back to inline execution + plus generic eviction. """ @abstractmethod diff --git a/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py b/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py index 2e2cb2bb61..5c259a70cc 100644 --- a/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py +++ b/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py @@ -61,6 +61,8 @@ def __init__(self) -> None: self._id = "local-subprocess-sandbox" self._virtual_root = VIRTUAL_SANDBOX_ROOT self._real_root = self._virtual_root + # Real host shell, so capture-at-source (opt-in, default off) is supported. + self.enable_capture_offload = True def set_real_root(self, real_root: str) -> None: """Set the on-disk directory used for test file operations.""" From 67f06058ba406b94ea83256f1d83b5163633441a Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Fri, 26 Jun 2026 15:35:13 -0400 Subject: [PATCH 11/13] cr --- .../deepagents/deepagents/backends/sandbox.py | 38 +++++++++++++++---- .../deepagents/middleware/filesystem.py | 21 ++++++---- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/libs/deepagents/deepagents/backends/sandbox.py b/libs/deepagents/deepagents/backends/sandbox.py index 796bd6ac9c..a498f73ed8 100644 --- a/libs/deepagents/deepagents/backends/sandbox.py +++ b/libs/deepagents/deepagents/backends/sandbox.py @@ -648,7 +648,12 @@ def _build_edit_tmpfile_cmd(file_path: str, old_tmp: str, new_tmp: str, *, repla fi fi """ -"""Pure POSIX sh wrapper for capture-at-source `execute`. See the comment above.""" +# Pure POSIX sh wrapper for capture-at-source `execute`; see the comment above the template. + + +def _new_heredoc_delim() -> str: + """Return a random heredoc delimiter, e.g. `__DEEPAGENTS_CMD_<80 random bits>__`.""" + return "__DEEPAGENTS_CMD_" + base64.b32encode(os.urandom(10)).decode("ascii").rstrip("=") + "__" def _build_capture_execute_cmd(command: str, capture_path: str, *, inline_budget: int, max_capture_bytes: int | None = None) -> str: @@ -661,7 +666,12 @@ def _build_capture_execute_cmd(command: str, capture_path: str, *, inline_budget overridable/patchable); beyond that it is truncated and flagged. """ cap = max_capture_bytes if max_capture_bytes is not None else _EXECUTE_CAPTURE_MAX_BYTES - delim = "__DEEPAGENTS_CMD_" + base64.b32encode(os.urandom(10)).decode("ascii").rstrip("=") + "__" + # The command is embedded in a quoted heredoc; guarantee the delimiter cannot + # appear in it so the command can never terminate the heredoc early. The + # delimiter is 80 random bits, so this regenerates only astronomically rarely. + delim = _new_heredoc_delim() + while delim in command: + delim = _new_heredoc_delim() # __COMMAND__ is substituted last so command content can never collide with a # remaining placeholder token. return ( @@ -679,18 +689,30 @@ def _build_capture_execute_cmd(command: str, capture_path: str, *, inline_budget def _parse_capture_execute_output(output: str, *, backend_truncated: bool = False) -> tuple[bool, ExecuteResponse]: - """Parse capture-wrapper stdout into `(offloaded, ExecuteResponse)`. + r"""Parse capture-wrapper stdout into `(offloaded, ExecuteResponse)`. + + The wrapper emits a meta line followed by the body: + + \n + + i.e. four space-separated fields on the first line — the sentinel, the + command's exit code, `1`/`0` for whether output was offloaded to the capture + file, and `1`/`0` for whether it hit the size cap — then everything after the + first newline is the body (full output when inline, head/tail preview when + offloaded). `offloaded` is kept separate from `ExecuteResponse` because it describes the capture mechanism, not the command result (an ordinary `execute` never sets - it). Falls back to `(False, raw output)` when the meta line is absent — e.g. - if the backend truncated transport; the caller must not re-run the command in - that case. `ExecuteResponse.truncated` is set when the captured output hit the - size cap (the saved file is incomplete) or `backend_truncated` is passed - through from the underlying `execute`. + it). Falls back to `(False, raw output)` when the meta line is absent or + malformed — e.g. if the backend truncated transport; the caller must not + re-run the command in that case. `ExecuteResponse.truncated` is set when the + captured output hit the size cap (the saved file is incomplete) or + `backend_truncated` is passed through from the underlying `execute`. """ first, _, body = output.partition("\n") parts = first.split(" ") + # Expect exactly the four meta fields described above; anything else is not + # our wrapper's output, so fall back to returning it verbatim. if len(parts) != 4 or parts[0] != _EXECUTE_CAPTURE_SENTINEL: # noqa: PLR2004 offloaded = False return offloaded, ExecuteResponse(output=output, truncated=backend_truncated) diff --git a/libs/deepagents/deepagents/middleware/filesystem.py b/libs/deepagents/deepagents/middleware/filesystem.py index 539ff1f895..ba6b6e6b0a 100644 --- a/libs/deepagents/deepagents/middleware/filesystem.py +++ b/libs/deepagents/deepagents/middleware/filesystem.py @@ -1822,19 +1822,24 @@ async def async_grep( ) def _resolve_capture(self, resolved_backend: BackendProtocol, tool_call_id: str | None) -> tuple[BaseSandbox, str] | None: - """Resolve `(executor, capture_path)` for capture-at-source, or `None` to skip. + """Resolve the executing sandbox and offload path for capture-at-source. Capture-at-source writes output to a literal path via the sandbox shell - and later reads it back through the backend, so it is only sound when - `execute()` and `read_file` share one filesystem at that path. Only - `BaseSandbox` guarantees this, so it is gated on it (a stub or host-shell - backend falls back to plain execute plus generic eviction). Also returns - `None` when eviction is disabled, the tool call has no id, or the offload - path would route to a different backend. + and later reads it back through the backend, which requires `execute()` + and `read_file` to resolve to the same filesystem at that path. Only + `BaseSandbox` provides that guarantee, so it is gated on it; the offload + path must also route to the executing backend rather than a different + composite route. Whether capture is actually applied is left to the executor's `execute_with_offload` (which honors `enable_capture_offload`); this only - decides whether the offload path is sound to attempt. + decides whether the offload path is valid to attempt. + + Returns: + `(executor, capture_path)` when capture-at-source can be attempted, or + `None` to skip it (eviction disabled, no tool-call id, the backend is + not a `BaseSandbox`, or the offload path routes elsewhere) — in which + case the caller uses plain execute plus generic eviction. """ if not self._tool_token_limit_before_evict or not tool_call_id: return None From cdc87ee0674877f6e56dd853b1b9d6bf13e56044 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Fri, 26 Jun 2026 16:10:22 -0400 Subject: [PATCH 12/13] cr --- .../deepagents/backends/protocol.py | 20 +++++++++ .../deepagents/deepagents/backends/sandbox.py | 43 ++++++++----------- .../deepagents/middleware/filesystem.py | 31 ++++++------- .../test_local_sandbox_operations.py | 14 +++--- 4 files changed, 60 insertions(+), 48 deletions(-) diff --git a/libs/deepagents/deepagents/backends/protocol.py b/libs/deepagents/deepagents/backends/protocol.py index ec8fb45374..7d248541b6 100644 --- a/libs/deepagents/deepagents/backends/protocol.py +++ b/libs/deepagents/deepagents/backends/protocol.py @@ -849,6 +849,26 @@ class ExecuteResponse: """Whether the output was truncated due to backend limitations.""" +@dataclass(frozen=True, slots=True) +class ExecuteOffloadResult: + """Result of [`BaseSandbox.execute_with_offload`][deepagents.backends.sandbox.BaseSandbox.execute_with_offload]. + + `offloaded` describes the capture mechanism and is kept off `ExecuteResponse` + (which an ordinary `execute` never sets). + """ + + offloaded: bool + """Whether the output was left at the capture path. + + When `True`, `response.output` holds only a head/tail preview and the full + output lives at the capture path on the sandbox filesystem. When `False`, + `response.output` is the complete output. + """ + + response: ExecuteResponse + """The command result. `response.truncated` indicates the output hit the size cap.""" + + class SandboxBackendProtocol(BackendProtocol): """Extension of `BackendProtocol` that adds shell command execution. diff --git a/libs/deepagents/deepagents/backends/sandbox.py b/libs/deepagents/deepagents/backends/sandbox.py index a498f73ed8..f21793037a 100644 --- a/libs/deepagents/deepagents/backends/sandbox.py +++ b/libs/deepagents/deepagents/backends/sandbox.py @@ -27,6 +27,7 @@ ASYNC_GREP_TIMEOUT, DeleteResult, EditResult, + ExecuteOffloadResult, ExecuteResponse, FileData, FileDownloadResponse, @@ -688,8 +689,8 @@ def _build_capture_execute_cmd(command: str, capture_path: str, *, inline_budget ) -def _parse_capture_execute_output(output: str, *, backend_truncated: bool = False) -> tuple[bool, ExecuteResponse]: - r"""Parse capture-wrapper stdout into `(offloaded, ExecuteResponse)`. +def _parse_capture_execute_output(output: str, *, backend_truncated: bool = False) -> ExecuteOffloadResult: + r"""Parse capture-wrapper stdout into an `ExecuteOffloadResult`. The wrapper emits a meta line followed by the body: @@ -701,11 +702,9 @@ def _parse_capture_execute_output(output: str, *, backend_truncated: bool = Fals first newline is the body (full output when inline, head/tail preview when offloaded). - `offloaded` is kept separate from `ExecuteResponse` because it describes the - capture mechanism, not the command result (an ordinary `execute` never sets - it). Falls back to `(False, raw output)` when the meta line is absent or - malformed — e.g. if the backend truncated transport; the caller must not - re-run the command in that case. `ExecuteResponse.truncated` is set when the + Falls back to `offloaded=False` with the raw output when the meta line is + absent or malformed — e.g. if the backend truncated transport; the caller + must not re-run the command in that case. `response.truncated` is set when the captured output hit the size cap (the saved file is incomplete) or `backend_truncated` is passed through from the underlying `execute`. """ @@ -714,18 +713,14 @@ def _parse_capture_execute_output(output: str, *, backend_truncated: bool = Fals # Expect exactly the four meta fields described above; anything else is not # our wrapper's output, so fall back to returning it verbatim. if len(parts) != 4 or parts[0] != _EXECUTE_CAPTURE_SENTINEL: # noqa: PLR2004 - offloaded = False - return offloaded, ExecuteResponse(output=output, truncated=backend_truncated) + return ExecuteOffloadResult(offloaded=False, response=ExecuteResponse(output=output, truncated=backend_truncated)) try: exit_code = int(parts[1]) except ValueError: - offloaded = False - return offloaded, ExecuteResponse(output=output, truncated=backend_truncated) - offloaded = parts[2] == "1" - return offloaded, ExecuteResponse( - output=body, - exit_code=exit_code, - truncated=parts[3] == "1" or backend_truncated, + return ExecuteOffloadResult(offloaded=False, response=ExecuteResponse(output=output, truncated=backend_truncated)) + return ExecuteOffloadResult( + offloaded=parts[2] == "1", + response=ExecuteResponse(output=body, exit_code=exit_code, truncated=parts[3] == "1" or backend_truncated), ) @@ -790,7 +785,7 @@ def execute_with_offload( max_inline_bytes: int, max_capture_bytes: int | None = None, timeout: int | None = None, - ) -> tuple[bool, ExecuteResponse]: + ) -> ExecuteOffloadResult: """Run `command`, offloading large output to a file in the sandbox. Captures the command's combined output: returned inline when it is at or @@ -800,16 +795,16 @@ def execute_with_offload( `_EXECUTE_CAPTURE_MAX_BYTES`) without killing the command, so the exit code is preserved. - Returns `(offloaded, ExecuteResponse)`, where `offloaded` is `True` when - the result was left at `capture_path` and `output` holds only the preview. + Returns an `ExecuteOffloadResult`; `offloaded` is `True` when the result + was left at `capture_path` and `response.output` holds only the preview. When `enable_capture_offload` is `False`, runs the command unwrapped and - returns `(False, …)` with the full output — letting callers fall back to - their own handling (e.g. generic eviction). + returns `offloaded=False` with the full output — letting callers fall back + to their own handling (e.g. generic eviction). """ use_timeout = timeout is not None and execute_accepts_timeout(type(self)) if not self.enable_capture_offload: result = self.execute(command, timeout=timeout) if use_timeout else self.execute(command) - return False, result + return ExecuteOffloadResult(offloaded=False, response=result) wrapper = _build_capture_execute_cmd(command, capture_path, inline_budget=max_inline_bytes, max_capture_bytes=max_capture_bytes) result = self.execute(wrapper, timeout=timeout) if use_timeout else self.execute(wrapper) return _parse_capture_execute_output(result.output, backend_truncated=result.truncated) @@ -822,12 +817,12 @@ async def aexecute_with_offload( max_inline_bytes: int, max_capture_bytes: int | None = None, timeout: int | None = None, # noqa: ASYNC109 # forwarded to the backend, not an asyncio timeout - ) -> tuple[bool, ExecuteResponse]: + ) -> ExecuteOffloadResult: """Async version of `execute_with_offload`, delegating to `aexecute`.""" use_timeout = timeout is not None and execute_accepts_timeout(type(self)) if not self.enable_capture_offload: result = await self.aexecute(command, timeout=timeout) if use_timeout else await self.aexecute(command) - return False, result + return ExecuteOffloadResult(offloaded=False, response=result) wrapper = _build_capture_execute_cmd(command, capture_path, inline_budget=max_inline_bytes, max_capture_bytes=max_capture_bytes) result = await self.aexecute(wrapper, timeout=timeout) if use_timeout else await self.aexecute(wrapper) return _parse_capture_execute_output(result.output, backend_truncated=result.truncated) diff --git a/libs/deepagents/deepagents/middleware/filesystem.py b/libs/deepagents/deepagents/middleware/filesystem.py index ba6b6e6b0a..49be34916d 100644 --- a/libs/deepagents/deepagents/middleware/filesystem.py +++ b/libs/deepagents/deepagents/middleware/filesystem.py @@ -45,7 +45,7 @@ BackendProtocol, DeleteResult, EditResult, - ExecuteResponse, + ExecuteOffloadResult, FileData as FileData, # Re-export for backwards compatibility FileInfo, GlobResult, @@ -1873,15 +1873,16 @@ def _format_execute_output(output: str, exit_code: int | None, *, truncated: boo parts.append("\n[Output was truncated due to size limits]") return "".join(parts) - def _interpret_capture_output(self, *, offloaded: bool, result: ExecuteResponse, capture_path: str, tool_call_id: str) -> str: - """Build `ToolMessage` content from a capture-at-source `execute` result.""" - if not offloaded: - return self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) - cmd_status = "succeeded" if result.exit_code == 0 else "failed" - status_line = f"[Command {cmd_status} with exit code {result.exit_code}]" - if result.truncated: + def _interpret_capture_output(self, offload: ExecuteOffloadResult, capture_path: str, tool_call_id: str) -> str: + """Build `ToolMessage` content from an `execute_with_offload` result.""" + response = offload.response + if not offload.offloaded: + return self._format_execute_output(response.output, response.exit_code, truncated=response.truncated) + cmd_status = "succeeded" if response.exit_code == 0 else "failed" + status_line = f"[Command {cmd_status} with exit code {response.exit_code}]" + if response.truncated: status_line += "\n[Output exceeded the capture size limit and was truncated; the saved file is incomplete]" - content_sample = f"{status_line}\n{result.output}" + content_sample = f"{status_line}\n{response.output}" return TOO_LARGE_TOOL_MSG.format( tool_call_id=tool_call_id, file_path=capture_path, @@ -1950,15 +1951,13 @@ def sync_execute( # noqa: PLR0911 - early returns for distinct error conditions try: if capture is not None: executor, capture_path = capture - offloaded, result = executor.execute_with_offload( + offload = executor.execute_with_offload( command, capture_path, max_inline_bytes=NUM_CHARS_PER_TOKEN * cast("int", self._tool_token_limit_before_evict), timeout=timeout, ) - content = self._interpret_capture_output( - offloaded=offloaded, result=result, capture_path=capture_path, tool_call_id=cast("str", runtime.tool_call_id) - ) + content = self._interpret_capture_output(offload, capture_path, cast("str", runtime.tool_call_id)) else: result = executable.execute(command, timeout=timeout) if timeout is not None else executable.execute(command) content = self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) @@ -2043,15 +2042,13 @@ async def async_execute( # noqa: PLR0911 - early returns for distinct error con try: if capture is not None: executor, capture_path = capture - offloaded, result = await executor.aexecute_with_offload( + offload = await executor.aexecute_with_offload( command, capture_path, max_inline_bytes=NUM_CHARS_PER_TOKEN * cast("int", self._tool_token_limit_before_evict), timeout=timeout, ) - content = self._interpret_capture_output( - offloaded=offloaded, result=result, capture_path=capture_path, tool_call_id=cast("str", runtime.tool_call_id) - ) + content = self._interpret_capture_output(offload, capture_path, cast("str", runtime.tool_call_id)) else: result = await executable.aexecute(command, timeout=timeout) if timeout is not None else await executable.aexecute(command) content = self._format_execute_output(result.output, result.exit_code, truncated=result.truncated) diff --git a/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py b/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py index 5c259a70cc..3379efe4a6 100644 --- a/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py +++ b/libs/deepagents/tests/unit_tests/test_local_sandbox_operations.py @@ -1715,15 +1715,15 @@ def test_enable_capture_offload_flag_controls_offload(self, sandbox: LocalSubpro # Disabled -> command runs unwrapped: full output inline, not offloaded, no file. sandbox.enable_capture_offload = False off_path = self._capture_path("flag_off") - offloaded, result = sandbox.execute_with_offload(_BIG_OUTPUT_CMD, off_path, max_inline_bytes=budget) - assert offloaded is False - assert "line 5000:" in result.output # full output returned, not a preview - assert "line 2500:" in result.output + offload = sandbox.execute_with_offload(_BIG_OUTPUT_CMD, off_path, max_inline_bytes=budget) + assert offload.offloaded is False + assert "line 5000:" in offload.response.output # full output returned, not a preview + assert "line 2500:" in offload.response.output assert sandbox.execute(f"test -e {off_path} && echo Y || echo N").output.strip() == "N" # Enabled -> offloaded to a file; only a head/tail preview is returned. sandbox.enable_capture_offload = True on_path = self._capture_path("flag_on") - offloaded, result = sandbox.execute_with_offload(_BIG_OUTPUT_CMD, on_path, max_inline_bytes=budget) - assert offloaded is True - assert "line 2500:" not in result.output # middle omitted -> it's a preview + offload = sandbox.execute_with_offload(_BIG_OUTPUT_CMD, on_path, max_inline_bytes=budget) + assert offload.offloaded is True + assert "line 2500:" not in offload.response.output # middle omitted -> it's a preview From 162cba5595ac6c0c34d643bc506bc4043a0413b5 Mon Sep 17 00:00:00 2001 From: Chester Curme Date: Fri, 26 Jun 2026 17:47:39 -0400 Subject: [PATCH 13/13] doctring --- libs/deepagents/deepagents/backends/sandbox.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/libs/deepagents/deepagents/backends/sandbox.py b/libs/deepagents/deepagents/backends/sandbox.py index f21793037a..b080a7c7f4 100644 --- a/libs/deepagents/deepagents/backends/sandbox.py +++ b/libs/deepagents/deepagents/backends/sandbox.py @@ -793,13 +793,14 @@ def execute_with_offload( can surface a `read_file` pointer) with only a head/tail preview returned. Captured output is hard-capped at `max_capture_bytes` (default `_EXECUTE_CAPTURE_MAX_BYTES`) without killing the command, so the exit - code is preserved. + code is preserved. When `enable_capture_offload` is `False`, the command + runs unwrapped and the full output is returned (`offloaded=False`), so + callers can fall back to their own handling (e.g. generic eviction). - Returns an `ExecuteOffloadResult`; `offloaded` is `True` when the result - was left at `capture_path` and `response.output` holds only the preview. - When `enable_capture_offload` is `False`, runs the command unwrapped and - returns `offloaded=False` with the full output — letting callers fall back - to their own handling (e.g. generic eviction). + Returns: + An `ExecuteOffloadResult`. `offloaded=True` when the result was left + at `capture_path` and `response.output` holds only the preview; + `offloaded=False` when `response.output` is the complete output. """ use_timeout = timeout is not None and execute_accepts_timeout(type(self)) if not self.enable_capture_offload: