Skip to content
66 changes: 50 additions & 16 deletions autogpt_platform/backend/backend/copilot/sdk/service.py

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed — sdk_cwd creation is now inside the outer try: block (line 518), with _cleanup_sdk_tool_results(sdk_cwd) and lock.release() both in the corresponding finally: block (lines 1116–1120). The stale diff showed it outside the try block, but the current code has it properly scoped.

Original file line number Diff line number Diff line change
Expand Up @@ -83,20 +83,30 @@ def available(self) -> bool:
# IMPORTANT: Must be less than frontend timeout (12s in useCopilotPage.ts)
_HEARTBEAT_INTERVAL = 10.0 # seconds


# Appended to the system prompt to inform the agent about available tools.
# The SDK built-in Bash is NOT available — use mcp__copilot__bash_exec instead,
# which has kernel-level network isolation (unshare --net).
_SDK_TOOL_SUPPLEMENT = """
def _build_sdk_tool_supplement(cwd: str) -> str:
"""Build the SDK tool supplement with the actual working directory injected."""
return f"""

## Tool notes

### Shell commands
- The SDK built-in Bash tool is NOT available. Use the `bash_exec` MCP tool
for shell commands — it runs in a network-isolated sandbox.

### Working directory
- Your working directory is: `{cwd}`
- All SDK Read/Write/Edit/Glob/Grep tools AND `bash_exec` operate inside this
directory. This is the ONLY writable path — do not attempt to read or write
anywhere else on the filesystem.
- Use relative paths or absolute paths under `{cwd}` for all file operations.

### Two storage systems — CRITICAL to understand

1. **Ephemeral working directory** (`/tmp/copilot-<session>/`):
1. **Ephemeral working directory** (`{cwd}`):
- Shared by SDK Read/Write/Edit/Glob/Grep tools AND `bash_exec`
- Files here are **lost between turns** — do NOT rely on them persisting
- Use for temporary work: running scripts, processing data, etc.
Expand All @@ -122,6 +132,21 @@ def available(self) -> bool:
2. At the start of a new turn, call `list_workspace_files` to see what files
are available from previous turns

### Sharing files with the user
After saving a file to the persistent workspace with `write_workspace_file`,
share it with the user by embedding the `download_url` from the response in
your message as a Markdown link or image:

- **Any file** — shows as a clickable download link:
`[report.csv](workspace://file_id#text/csv)`
- **Image** — renders inline in chat:
`![chart](workspace://file_id#image/png)`
- **Video** — renders inline in chat with player controls:
`![recording](workspace://file_id#video/mp4)`

The `download_url` field in the `write_workspace_file` response is already
in the correct format — paste it directly after the `(` in the Markdown.

### Long-running tools
Long-running tools (create_agent, edit_agent, etc.) are handled
asynchronously. You will receive an immediate response; the actual result
Expand All @@ -132,6 +157,7 @@ def available(self) -> bool:
All tasks must run in the foreground.
"""


STREAM_LOCK_PREFIX = "copilot:stream:lock:"


Expand Down Expand Up @@ -460,12 +486,6 @@ async def stream_chat_completion_sdk(
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)

# Build system prompt (reuses non-SDK path with Langfuse support)
has_history = len(session.messages) > 1
system_prompt, _ = await _build_system_prompt(
user_id, has_conversation_history=has_history
)
system_prompt += _SDK_TOOL_SUPPLEMENT
message_id = str(uuid.uuid4())
stream_id = str(uuid.uuid4())

Expand All @@ -490,22 +510,36 @@ async def stream_chat_completion_sdk(
)
return

# Build system prompt (reuses non-SDK path with Langfuse support).
# Pre-compute the cwd here so the exact working directory path can be
# injected into the supplement instead of the generic placeholder.
# Catch ValueError early so the failure yields a clean StreamError rather
# than propagating outside the stream error-handling path.
has_history = len(session.messages) > 1
sdk_cwd = ""
try:
sdk_cwd = _make_sdk_cwd(session_id)
os.makedirs(sdk_cwd, exist_ok=True)
except (ValueError, OSError) as e:
logger.error("[SDK] [%s] Invalid SDK cwd: %s", session_id[:12], e)
yield StreamError(
errorText="Unable to initialize working directory.",
code="sdk_cwd_error",
)
return
system_prompt, _ = await _build_system_prompt(
user_id, has_conversation_history=has_history
)
system_prompt += _build_sdk_tool_supplement(sdk_cwd)

Comment thread
majdyz marked this conversation as resolved.
Outdated
yield StreamStart(messageId=message_id, sessionId=session_id)

stream_completed = False
# Initialise variables before the try so the finally block can
# always attempt transcript upload regardless of errors.
sdk_cwd = ""
use_resume = False
resume_file: str | None = None
captured_transcript = CapturedTranscript()

try:
# Use a session-specific temp dir to avoid cleanup race conditions
# between concurrent sessions.
sdk_cwd = _make_sdk_cwd(session_id)
os.makedirs(sdk_cwd, exist_ok=True)

set_execution_context(user_id, session)
try:
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,11 @@ class WorkspaceWriteResponse(ToolResponseBase):
file_id: str
name: str
path: str
mime_type: str
size_bytes: int
# workspace:// URL the agent can embed directly in chat to give the user a link.
# Format: workspace://<file_id>#<mime_type> (frontend resolves to download URL)
download_url: str
source: str | None = None # "content", "base64", or "copied from <path>"
content_preview: str | None = None # First 200 chars for text files

Expand Down Expand Up @@ -680,11 +684,18 @@ async def _execute(
except Exception:
pass

download_url = (
f"workspace://{rec.id}#{rec.mime_type}"
if rec.mime_type
else f"workspace://{rec.id}"
)
return WorkspaceWriteResponse(
file_id=rec.id,
name=rec.name,
path=rec.path,
mime_type=rec.mime_type or "",
size_bytes=rec.size_bytes,
download_url=download_url,
Comment thread
majdyz marked this conversation as resolved.
source=source,
content_preview=preview,
message=msg,
Expand Down
Loading