mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix(backend/copilot): fix tool-result file read failures across turns
Three bugs caused "file not found" errors when the model tried to read SDK tool-result files: 1. Path validation mismatch: is_allowed_local_path() expected tool-results directly under the project dir, but the SDK nests them under a conversation UUID subdirectory. Fixed to match any tool-results/ segment within the project dir. 2. Wrong tool fallback: when the model mistakenly called read_workspace_file (cloud storage) for SDK tool-result paths on local disk, it got "file not found". Added a fallback in ReadWorkspaceFileTool that detects allowed local paths and reads from disk instead. 3. Cross-turn cleanup: _cleanup_sdk_tool_results deleted the entire CLI project directory (including tool-results/) between turns. Subsequent turns referencing those paths via --resume transcript would fail. Removed the project dir cleanup — only the temp cwd is cleaned now. Also added system prompt guidance telling the model to use read_file (not read_workspace_file) for SDK tool-result paths.
This commit is contained in:
@@ -87,7 +87,10 @@ def is_allowed_local_path(path: str, sdk_cwd: str | None = None) -> bool:
|
||||
|
||||
Allowed:
|
||||
- Files under *sdk_cwd* (``/tmp/copilot-<session>/``)
|
||||
- Files under ``~/.claude/projects/<encoded-cwd>/tool-results/`` (SDK tool-results)
|
||||
- Files under any ``tool-results/`` directory within
|
||||
``~/.claude/projects/<encoded-cwd>/``. The SDK may nest tool-results
|
||||
under a conversation UUID, e.g.
|
||||
``<encoded-cwd>/<conversation-uuid>/tool-results/<file>``.
|
||||
"""
|
||||
if not path:
|
||||
return False
|
||||
@@ -106,10 +109,14 @@ def is_allowed_local_path(path: str, sdk_cwd: str | None = None) -> bool:
|
||||
|
||||
encoded = _current_project_dir.get("")
|
||||
if encoded:
|
||||
tool_results_dir = os.path.join(_SDK_PROJECTS_DIR, encoded, "tool-results")
|
||||
if resolved == tool_results_dir or resolved.startswith(
|
||||
tool_results_dir + os.sep
|
||||
):
|
||||
return True
|
||||
project_dir = os.path.join(_SDK_PROJECTS_DIR, encoded)
|
||||
# Allow any tool-results/ directory under the project dir, at any
|
||||
# nesting depth (the SDK may insert a conversation UUID between the
|
||||
# project dir and tool-results/).
|
||||
if resolved.startswith(project_dir + os.sep):
|
||||
relative = resolved[len(project_dir) + 1 :]
|
||||
parts = relative.split(os.sep)
|
||||
if "tool-results" in parts:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -120,6 +120,12 @@ def _build_storage_supplement(
|
||||
|
||||
### File persistence
|
||||
Important files (code, configs, outputs) should be saved to workspace to ensure they persist.
|
||||
|
||||
### SDK tool-result files
|
||||
When tool outputs are large, the SDK truncates them and saves the full output to
|
||||
a local file under `~/.claude/projects/.../tool-results/`. To read these files,
|
||||
always use `read_file` (NOT `read_workspace_file`). `read_workspace_file` reads
|
||||
from cloud workspace storage, where SDK tool-results are NOT stored.
|
||||
{_SHARED_TOOL_NOTES}"""
|
||||
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ from .tool_adapter import (
|
||||
wait_for_stash,
|
||||
)
|
||||
from .transcript import (
|
||||
cleanup_cli_project_dir,
|
||||
download_transcript,
|
||||
upload_transcript,
|
||||
validate_transcript,
|
||||
@@ -283,11 +282,13 @@ def _make_sdk_cwd(session_id: str) -> str:
|
||||
def _cleanup_sdk_tool_results(cwd: str) -> None:
|
||||
"""Remove SDK session artifacts for a specific working directory.
|
||||
|
||||
Cleans up:
|
||||
- ``~/.claude/projects/<encoded-cwd>/`` — CLI session transcripts and
|
||||
tool-result files. Each SDK turn uses a unique cwd, so this directory
|
||||
is safe to remove entirely.
|
||||
- ``/tmp/copilot-<session>/`` — the ephemeral working directory.
|
||||
Cleans up the ephemeral working directory ``/tmp/copilot-<session>/``.
|
||||
|
||||
NOTE: The CLI project directory ``~/.claude/projects/<encoded-cwd>/``
|
||||
is intentionally NOT cleaned up between turns. The SDK stores
|
||||
tool-result files there that the model may reference in subsequent
|
||||
turns via ``--resume``. Deleting them causes "file not found" errors
|
||||
when the model tries to re-read truncated tool outputs.
|
||||
|
||||
Security: *cwd* MUST be created by ``_make_sdk_cwd()`` which sanitizes
|
||||
the session_id.
|
||||
@@ -297,9 +298,6 @@ def _cleanup_sdk_tool_results(cwd: str) -> None:
|
||||
logger.warning(f"[SDK] Rejecting cleanup for path outside workspace: {cwd}")
|
||||
return
|
||||
|
||||
# Clean the CLI's project directory (transcripts + tool-results).
|
||||
cleanup_cli_project_dir(cwd)
|
||||
|
||||
# Clean up the temp cwd directory itself.
|
||||
try:
|
||||
shutil.rmtree(normalized, ignore_errors=True)
|
||||
|
||||
@@ -10,6 +10,8 @@ from pydantic import BaseModel
|
||||
from backend.copilot.context import (
|
||||
E2B_WORKDIR,
|
||||
get_current_sandbox,
|
||||
get_sdk_cwd,
|
||||
is_allowed_local_path,
|
||||
resolve_sandbox_path,
|
||||
)
|
||||
from backend.copilot.model import ChatSession
|
||||
@@ -281,6 +283,47 @@ class WorkspaceFileContentResponse(ToolResponseBase):
|
||||
content_base64: str
|
||||
|
||||
|
||||
def _read_local_tool_result(
|
||||
path: str,
|
||||
char_offset: int,
|
||||
char_length: Optional[int],
|
||||
session_id: str,
|
||||
) -> ToolResponseBase:
|
||||
"""Read an SDK tool-result file from local disk.
|
||||
|
||||
This is a fallback for when the model mistakenly calls
|
||||
``read_workspace_file`` with an SDK tool-result path that only exists on
|
||||
the host filesystem, not in cloud workspace storage.
|
||||
"""
|
||||
expanded = os.path.realpath(os.path.expanduser(path))
|
||||
try:
|
||||
with open(expanded, encoding="utf-8", errors="replace") as fh:
|
||||
text = fh.read()
|
||||
except FileNotFoundError:
|
||||
return ErrorResponse(message=f"File not found: {path}", session_id=session_id)
|
||||
except Exception as exc:
|
||||
return ErrorResponse(
|
||||
message=f"Error reading {path}: {exc}", session_id=session_id
|
||||
)
|
||||
|
||||
total_chars = len(text)
|
||||
end = char_offset + char_length if char_length is not None else total_chars
|
||||
slice_text = text[char_offset:end]
|
||||
|
||||
return WorkspaceFileContentResponse(
|
||||
file_id="local",
|
||||
name=os.path.basename(path),
|
||||
path=path,
|
||||
mime_type="application/json",
|
||||
content_base64=base64.b64encode(slice_text.encode("utf-8")).decode("utf-8"),
|
||||
message=(
|
||||
f"Read chars {char_offset}\u2013{char_offset + len(slice_text)} "
|
||||
f"of {total_chars:,} total from local tool-result {os.path.basename(path)}"
|
||||
),
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
|
||||
class WorkspaceFileMetadataResponse(ToolResponseBase):
|
||||
"""Response containing workspace file metadata and download URL (prevents context bloat)."""
|
||||
|
||||
@@ -539,6 +582,13 @@ class ReadWorkspaceFileTool(BaseTool):
|
||||
manager = await get_manager(user_id, session_id)
|
||||
resolved = await _resolve_file(manager, file_id, path, session_id)
|
||||
if isinstance(resolved, ErrorResponse):
|
||||
# Fallback: if the path is an SDK tool-result on local disk,
|
||||
# read it directly instead of failing. The model sometimes
|
||||
# calls read_workspace_file for these paths by mistake.
|
||||
if path and is_allowed_local_path(path, get_sdk_cwd()):
|
||||
return _read_local_tool_result(
|
||||
path, char_offset, char_length, session_id
|
||||
)
|
||||
return resolved
|
||||
target_file_id, file_info = resolved
|
||||
|
||||
|
||||
Reference in New Issue
Block a user