mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix(copilot): inject working directory into SDK prompt + workspace download links (#12215)
## Summary - Replaces the static `_SDK_TOOL_SUPPLEMENT` placeholder path with `_build_sdk_tool_supplement(cwd: str)` that injects the session-specific working directory - `sdk_cwd` is computed once via `_make_sdk_cwd(session_id)`, `os.makedirs` is called after lock acquisition (inside the protected `try/finally`), and the same variable is used everywhere — no drift between prompt and execution directory - Added `ValueError`/`OSError` error handling for cwd preparation with proper `StreamError` emission - Teaches the SDK agent how to share workspace files with the user via `workspace://` Markdown links (images render inline, videos render with player controls, other files as download links) - `WorkspaceWriteResponse` now includes `download_url` (pre-formatted `workspace://file_id#mime` string) and a normalised `mime_type` field (MIME parameters stripped, lowercased) - Frontend: workspace `workspace://` regular links now resolve to absolute URLs so Streamdown's "Copy link" copies the full URL - Frontend: Streamdown's "Open link" button colour overridden to match the design system (violet accent) — previously near-invisible in dark mode due to `--primary` resolving to near-white ## Motivation The SDK agent was seeing a hardcoded placeholder path in the system prompt instead of the real working directory, causing it to reference wrong paths in tool calls. Additionally, there was no guidance for the agent on how to share files it writes to the workspace with the user in chat. ## Test plan - [ ] CI green (test 3.11 / 3.12 / 3.13) - [ ] Start a copilot session with `CHAT_USE_CLAUDE_AGENT_SDK=true` and verify the agent references the correct `sdk_cwd` path in its tool calls - [ ] Ask the agent to write a file and confirm it responds with a clickable download link / inline image using the `workspace://` syntax - [ ] Verify the "Open link" button in the Streamdown external-link dialog is visible in both light and dark mode - [ ] Click "Copy link" on a workspace file link and confirm it copies the full URL (including host)
This commit is contained in:
@@ -83,10 +83,13 @@ COPILOT_SYSTEM_PREFIX = "[COPILOT_SYSTEM]" # Renders as system info message
|
||||
# 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
|
||||
|
||||
@@ -94,9 +97,16 @@ _SDK_TOOL_SUPPLEMENT = """
|
||||
- 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.
|
||||
@@ -122,6 +132,21 @@ When you create or modify important files (code, configs, outputs), you MUST:
|
||||
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:
|
||||
``
|
||||
- **Video** — renders inline in chat with player controls:
|
||||
``
|
||||
|
||||
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
|
||||
@@ -132,6 +157,7 @@ is delivered to the user via a background stream.
|
||||
All tasks must run in the foreground.
|
||||
"""
|
||||
|
||||
|
||||
STREAM_LOCK_PREFIX = "copilot:stream:lock:"
|
||||
|
||||
|
||||
@@ -460,14 +486,13 @@ 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())
|
||||
stream_completed = False
|
||||
use_resume = False
|
||||
resume_file: str | None = None
|
||||
captured_transcript = CapturedTranscript()
|
||||
sdk_cwd = ""
|
||||
|
||||
# Acquire stream lock to prevent concurrent streams to the same session
|
||||
lock = AsyncClusterLock(
|
||||
@@ -490,21 +515,30 @@ async def stream_chat_completion_sdk(
|
||||
)
|
||||
return
|
||||
|
||||
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()
|
||||
|
||||
# Make sure there is no more code between the lock acquitition and try-block.
|
||||
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)
|
||||
# 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
|
||||
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)
|
||||
|
||||
yield StreamStart(messageId=message_id, sessionId=session_id)
|
||||
|
||||
set_execution_context(user_id, session)
|
||||
try:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -680,11 +684,21 @@ class WriteWorkspaceFileTool(BaseTool):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Strip MIME parameters (e.g. "text/html; charset=utf-8" → "text/html")
|
||||
# and normalise to lowercase so the fragment is URL-safe.
|
||||
normalized_mime = (rec.mime_type or "").split(";", 1)[0].strip().lower()
|
||||
download_url = (
|
||||
f"workspace://{rec.id}#{normalized_mime}"
|
||||
if normalized_mime
|
||||
else f"workspace://{rec.id}"
|
||||
)
|
||||
return WorkspaceWriteResponse(
|
||||
file_id=rec.id,
|
||||
name=rec.name,
|
||||
path=rec.path,
|
||||
mime_type=normalized_mime,
|
||||
size_bytes=rec.size_bytes,
|
||||
download_url=download_url,
|
||||
source=source,
|
||||
content_preview=preview,
|
||||
message=msg,
|
||||
|
||||
@@ -104,11 +104,15 @@ function resolveWorkspaceUrls(text: string): string {
|
||||
// These are blocked by Streamdown's rehype-harden sanitizer because
|
||||
// "workspace://" is not in the allowed URL-scheme whitelist, which causes
|
||||
// "[blocked]" to appear next to the link text.
|
||||
// Use an absolute URL so Streamdown's "Copy link" button copies the full
|
||||
// URL (including host) rather than just the path.
|
||||
resolved = resolved.replace(
|
||||
/(?<!!)\[([^\]]*)\]\(workspace:\/\/([^)#\s]+)(?:#[^)#\s]*)?\)/g,
|
||||
(_match, linkText: string, fileId: string) => {
|
||||
const apiPath = getGetWorkspaceDownloadFileByIdUrl(fileId);
|
||||
const url = `/api/proxy${apiPath}`;
|
||||
const origin =
|
||||
typeof window !== "undefined" ? window.location.origin : "";
|
||||
const url = `${origin}/api/proxy${apiPath}`;
|
||||
return `[${linkText || "Download file"}](${url})`;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -181,6 +181,11 @@ body[data-google-picker-open="true"] [data-dialog-content] {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/* Streamdown external link dialog: "Open link" button */
|
||||
[data-streamdown="link-safety-modal"] button:last-of-type {
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* CoPilot chat table styling — remove left/right borders, increase padding */
|
||||
[data-streamdown="table-wrapper"] table {
|
||||
border-left: none;
|
||||
|
||||
Reference in New Issue
Block a user