From bcbd6a85c392ee0b51089d78df3ec10a8e05d5fc Mon Sep 17 00:00:00 2001 From: Javier Martinez Date: Mon, 6 Jul 2026 14:59:42 +0200 Subject: [PATCH] docs: add present server --- fern/docs/pages/tools/code-execution.mdx | 27 +++++++- ui/docs/SOURCE_OF_TRUTH.md | 3 +- ui/index.html | 79 +++++++++++++++++++++++- 3 files changed, 102 insertions(+), 7 deletions(-) diff --git a/fern/docs/pages/tools/code-execution.mdx b/fern/docs/pages/tools/code-execution.mdx index b192699b..5f48769a 100644 --- a/fern/docs/pages/tools/code-execution.mdx +++ b/fern/docs/pages/tools/code-execution.mdx @@ -15,7 +15,7 @@ The code execution subsystem provides: | Type identifier | Role | |---|---| -| `code_execution_v1` | Top-level wrapper — expands into `bash_v1`, `text_editor_v1`, and optionally `present_files_v1` | +| `code_execution_v1` | Top-level wrapper — expands into `bash_v1`, `text_editor_v1`, and optionally `present_files_v1` and `present_server_v1` | | `bash_v1` | Run arbitrary bash commands in a sandboxed subprocess | | `text_editor_v1` | View, create, and edit files in the workspace | | `view_v1` | View file or directory contents | @@ -23,11 +23,12 @@ The code execution subsystem provides: | `create_v1` | Create a new file | | `insert_v1` | Insert text after a given line | | `present_files_v1` | Expose generated files to the caller | +| `present_server_v1` | Expose an HTTP service running in the sandbox to the caller | Standalone endpoints are not provided for code execution tools. Use them through `/v1/messages`. - `present_files_v1` is a server-only metadata tool that surfaces created files to API consumers. It is enabled automatically when `code_execution_v1` is used and does not require explicit configuration. + `present_files_v1` and `present_server_v1` are server-only metadata tools that surface generated artifacts and running services to API consumers. They are enabled automatically when `code_execution_v1` is used and do not require explicit configuration. --- @@ -36,7 +37,7 @@ Standalone endpoints are not provided for code execution tools. Use them through When the model submits a `code_execution_v1` tool call, the server expands it into its constituent tools, creates or reuses a per-session sandbox, and executes the requested operations: -1. **Tool expansion** — `code_execution_v1` expands into `bash_v1`, `text_editor_v1`, and optionally `present_files_v1` +1. **Tool expansion** — `code_execution_v1` expands into `bash_v1`, `text_editor_v1`, and optionally `present_files_v1` and `present_server_v1` 2. **Session management** — each chat session gets an isolated workspace on the host filesystem 3. **Path translation** — canonical LLM-visible paths (`/home/agent/workspace/`) are transparently mapped to real host paths 4. **Subprocess isolation** — bash commands run as child processes with OS-level resource limits (`setrlimit`) @@ -189,3 +190,23 @@ Code execution sessions are managed automatically: | Windows | None | None | Subprocesses run without resource limits | All platforms support the same canonical path translation, output scrubbing, and session lifecycle management. + +--- + +## `present_server` tool + +`present_server_v1` exposes an HTTP service running inside the sandbox to the caller. Call it after starting a server (for example a web app, Jupyter, or Streamlit) so the user can open or interact with it. + +The model invokes it with: + +| Field | Type | Required | Description | +|---|---|---|---| +| `port` | `int` | Yes | Port the service is listening on inside the sandbox. | +| `service_name` | `str` | No | Human-readable name for the service (defaults to `"App"`). | +| `initial_path` | `str \| null` | No | Optional path appended to the URL to deep-link to a specific route. | + +The tool result contains a `resource_link` block whose `uri` is the publicly reachable URL of the tunneled service, plus a `text` block describing the service. If the sandbox backend does not support HTTP ingress, the tool returns a `text` block explaining that no endpoint is available. + + + HTTP ingress support depends on the configured `code_execution.provider`. Only sandbox backends that support per-session endpoint tunneling can fulfill `present_server_v1` calls. + diff --git a/ui/docs/SOURCE_OF_TRUTH.md b/ui/docs/SOURCE_OF_TRUTH.md index 107380ff..4bd7f36e 100644 --- a/ui/docs/SOURCE_OF_TRUTH.md +++ b/ui/docs/SOURCE_OF_TRUTH.md @@ -55,7 +55,8 @@ These are things not obvious from reading `index.html` that future agents should - **Scroll fades** — `.chat-list-wrap` and `.messages` both use `mask-image` with `--fade-top-stop`/`--fade-bot-stop` custom properties updated on scroll by `updateChatListFade()` and `updateMessagesFade()`. - **Toggle switches** — all `input[type="checkbox"]` elements are styled as custom CSS pill toggles with no native appearance. - **Floating panel frost** — `.modal-card`, `.menu-panel`, and `.model-dropdown` override the shared glass group with a near-solid dark background (`rgba(10,12,22,0.82–0.94)`), `blur(72px) saturate(1.4)`, and a `to bottom` gradient (lighter at top, denser at bottom) for readability and visual grounding. -- **Code Execution tools** — the Code Execution toggle in the Tools menu sends `{ name: "code_execution", type: "code_execution_v1" }` in the tools array. The backend expands this into `bash`, `text_editor` (view/str_replace/create/insert), and `present_files` (Zylon only). Tool use blocks for these tools render as styled `.code-exec-block` details elements with terminal output, line-numbered file views, diff highlighting, and exit-code badges. Adjacent tool_use + tool_result blocks are combined into a single block via blocks pairing in `blocksToHtml`. The toggle is stored per-chat in `chat.settings.enabledCodeExecution`. +- **Code Execution tools** — the Code Execution toggle in the Tools menu sends `{ name: "code_execution", type: "code_execution_v1" }` in the tools array. The backend expands this into `bash`, `text_editor` (view/str_replace/create/insert), `present_files`, and `present_server`. Tool use blocks for these tools render as styled `.code-exec-block` details elements with terminal output, line-numbered file views, diff highlighting, and exit-code badges. Adjacent tool_use + tool_result blocks are combined into a single block via blocks pairing in `blocksToHtml`. The `isCodeExecTool` allowlist (`bash`, `view`, `str_replace`, `create`, `insert`, `present_files`, `present_server`) drives pairing and per-tool rendering. The toggle is stored per-chat in `chat.settings.enabledCodeExecution`. - **Code Execution session continuity** — when code execution is enabled, `chat.id` is sent as `container` in `ChatBody` so the backend reuses the same sandbox session across all messages in a chat. The `container` field must be set whenever code execution tools are active. - **Code Execution file upload** — when Code Execution is enabled, a **Files** button appears in the composer toolbar. It opens a file picker that uploads directly to `POST /v1/files?scope_id={chat.id}` (multipart/form-data). Uploaded files land in the session workspace and are accessible to the model's bash/file tools. - **Code Execution file downloads** — `present_files` tool results that contain `local_resource` blocks are rendered as `.code-exec-download` anchor elements linking to `GET /v1/files/{file_id}/content?scope_id={chat.id}`. The `file_id` and `mime_type` come from the `local_resource` block schema. +- **Code Execution server links** — `present_server` tool results that contain `resource_link` blocks are rendered as `.code-exec-server-link` anchor elements (globe icon + service name + tunneled URL) opening in a new tab. The `uri`, `name`, and `description` come from the `resource_link` block schema. The tool_use summary shows `service_name:port` (and an optional `initial_path` deep-link). diff --git a/ui/index.html b/ui/index.html index 04e5e4eb..1444cb65 100644 --- a/ui/index.html +++ b/ui/index.html @@ -2407,6 +2407,11 @@ color: rgba(200, 170, 255, 0.92); } + .code-exec-block .code-exec-tool-label.server { + background: rgba(112, 183, 255, 0.14); + color: rgba(140, 200, 255, 0.92); + } + /* ── Code execution file downloads ──────────────────────────────────── */ .code-exec-download { display: inline-flex; @@ -2447,6 +2452,46 @@ flex: 0 0 auto; } + /* ── Code execution server links (present_server) ──────────────────── */ + .code-exec-server-link { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border: 1px solid rgba(112, 183, 255, 0.22); + border-radius: 10px; + background: rgba(112, 183, 255, 0.08); + color: rgba(140, 200, 255, 0.92); + font-size: 13px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + text-decoration: none; + transition: background 150ms ease, border-color 150ms ease; + cursor: pointer; + width: 100%; + box-sizing: border-box; + } + .code-exec-server-link:hover { + background: rgba(112, 183, 255, 0.16); + border-color: rgba(112, 183, 255, 0.36); + } + .code-exec-server-link svg { + flex: 0 0 auto; + width: 14px; + height: 14px; + } + .code-exec-server-link .code-exec-server-name { + font-weight: 600; + flex: 0 0 auto; + } + .code-exec-server-link .code-exec-server-url { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + opacity: 0.85; + } + /* ── Modal backdrop blur ─────────────────────────────────────────────── */ .modal-backdrop { backdrop-filter: blur(14px); @@ -5191,11 +5236,11 @@ } function isCodeExecTool(name) { - return ["bash", "view", "str_replace", "create", "insert", "present_files"].includes(name); + return ["bash", "view", "str_replace", "create", "insert", "present_files", "present_server"].includes(name); } function codeExecToolLabel(name) { - const map = { bash: "Bash", view: "Read", str_replace: "Edit", create: "Create", insert: "Insert", present_files: "Present" }; + const map = { bash: "Bash", view: "Read", str_replace: "Edit", create: "Create", insert: "Insert", present_files: "Present", present_server: "Server" }; return map[name] || name; } @@ -5205,6 +5250,7 @@ if (name === "create") return "create"; if (name === "str_replace") return "edit"; if (name === "present_files") return "present"; + if (name === "present_server") return "server"; return "file"; } @@ -5231,6 +5277,11 @@ } else if (name === "present_files") { const files = Array.isArray(input.files) ? input.files : []; detail = `
${files.map(f => `
${icons.file}${escapeHtml(typeof f === "string" ? f : f.path || f.name || "")}
`).join("")}
`; + } else if (name === "present_server") { + const port = input.port != null ? `:${input.port}` : ""; + const service = input.service_name || "App"; + const initialPath = input.initial_path ? ` → ${input.initial_path}` : ""; + detail = `
${icons.globe}${escapeHtml(service)}${escapeHtml(port)}${escapeHtml(initialPath)}
`; } // If we have a result block, render the result content in the same body @@ -5279,6 +5330,28 @@ } else { resultHtml = rawText ? `
${escapeHtml(rawText)}
` : ""; } + } else if (name === "present_server") { + const content = resultBlock.content; + if (Array.isArray(content)) { + const linkItems = content.filter(b => b?.type === "resource_link"); + const textItems = content.filter(b => b?.type === "text" || typeof b === "string"); + const textHtml = textItems.map(b => escapeHtml(typeof b === "string" ? b : (b.text || ""))).join(" "); + if (linkItems.length > 0) { + resultHtml = `
${linkItems.map(l => { + const url = l.uri || ""; + const svc = l.name || "App"; + const desc = l.description ? escapeHtml(l.description) : ""; + if (url) { + return `${icons.globe}${escapeHtml(svc)}${escapeHtml(url)}${desc ? `
${desc}
` : ""}`; + } + return `
${icons.globe}${escapeHtml(svc)}
`; + }).join("")}
${textHtml ? `
${textHtml}
` : ""}`; + } else { + resultHtml = textHtml ? `
${textHtml}
` : ""; + } + } else { + resultHtml = rawText ? `
${escapeHtml(rawText)}
` : ""; + } } else { resultHtml = renderFileView(rawText); } @@ -5287,7 +5360,7 @@ } } - const summaryLabel = name === "bash" ? (input.command ? input.command.split("\n")[0].slice(0, 60) + (input.command.length > 60 ? "…" : "") : "") : (input.path || ""); + const summaryLabel = name === "bash" ? (input.command ? input.command.split("\n")[0].slice(0, 60) + (input.command.length > 60 ? "…" : "") : "") : name === "present_server" ? `${input.service_name || "App"}${input.port != null ? `:${input.port}` : ""}` : (input.path || ""); return `
${label}${escapeHtml(summaryLabel)}
${detail}
`; }