From 916a17c808bd63cba07f28114f4dde704b5970fa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 07:28:26 +0000 Subject: [PATCH 1/5] Add client implementation guide for frontend HTTP tools and reorder reference nav Add a step-by-step "Implementing Frontend Tools in Your Client" section to the HTTP API docs covering tool definition, SSE stream parsing, pause-mode resume flow, and noop-mode fire-and-forget handling with JavaScript examples. Move HTTP API to first position in reference nav. https://claude.ai/code/session_013fd6r27uVfY2RijXoJhJs2 Signed-off-by: Claude --- docs/reference/.nav.yml | 2 +- docs/reference/http-api.md | 182 +++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 1 deletion(-) diff --git a/docs/reference/.nav.yml b/docs/reference/.nav.yml index 3ee0db2e5b..ceb55cbfaf 100644 --- a/docs/reference/.nav.yml +++ b/docs/reference/.nav.yml @@ -1,10 +1,10 @@ nav: + - HTTP API: http-api.md - Python SDK: python-sdk.md - Environment Variables: environment-variables.md - Helm Configuration: helm-configuration.md - Kubernetes Permissions: kubernetes-permissions.md - Microsoft Teams Bot Permissions: teams-permissions.md - - HTTP API: http-api.md - Runbooks: runbooks.md - Slash Commands: slash-commands.md - Context Management: context-management.md diff --git a/docs/reference/http-api.md b/docs/reference/http-api.md index 0e4cd78c45..fe81289a67 100644 --- a/docs/reference/http-api.md +++ b/docs/reference/http-api.md @@ -396,6 +396,188 @@ Noop tools execute instantly on the server with a canned response. The client se - `frontend_tool_results.result` must be a string (JSON-encode objects) - Both `pending_approvals` and `pending_frontend_tool_calls` can appear in the same `approval_required` event if the LLM calls both types in one iteration +#### Implementing Frontend Tools in Your Client + +This section walks through how to build client-side support for frontend tools, step by step. + +**1. Define your tools in the request** + +Pass tool definitions in the `frontend_tools` array. Each tool needs a `name`, `description`, and optionally `parameters` (JSON Schema) and `mode`. + +```javascript +const frontendTools = [ + { + name: "render_chart", + description: "Render a chart in the UI with the given metric and time range.", + mode: "pause", + parameters: { + type: "object", + properties: { + chart_type: { type: "string", enum: ["line", "bar", "pie"] }, + metric: { type: "string" }, + time_range: { type: "string" } + }, + required: ["chart_type", "metric"] + } + }, + { + name: "navigate_to_page", + description: "Navigate the user to a page in the application.", + mode: "noop", + noop_response: "Navigation triggered." + } +]; +``` + +**2. Send the streaming request** + +```javascript +const response = await fetch("http:///api/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ask: "Show me CPU usage for the last hour", + stream: true, + frontend_tools: frontendTools, + // Include these when resuming a paused stream: + // conversation_history: [...], + // frontend_tool_results: [...] + }) +}); +``` + +**3. Parse the SSE stream** + +Read the stream and dispatch on `event` type. The key events for frontend tools are `start_tool_calling`, `tool_calling_result`, and `approval_required`. + +```javascript +const reader = response.body.getReader(); +const decoder = new TextDecoder(); +let buffer = ""; + +while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + + // Split on double newline (SSE event boundary) + const parts = buffer.split("\n\n"); + buffer = parts.pop(); // keep incomplete chunk + + for (const part of parts) { + const eventMatch = part.match(/^event:\s*(.+)$/m); + const dataMatch = part.match(/^data:\s*(.+)$/m); + if (!eventMatch || !dataMatch) continue; + + const eventType = eventMatch[1]; + const payload = JSON.parse(dataMatch[1]); + handleEvent(eventType, payload); + } +} +``` + +**4. Handle pause-mode tool calls** + +When the LLM calls a pause-mode frontend tool, the stream emits an `approval_required` event with `pending_frontend_tool_calls`. Execute the tool locally, then resume. + +```javascript +function handleEvent(eventType, payload) { + switch (eventType) { + case "approval_required": + // Handle frontend tool calls + if (payload.pending_frontend_tool_calls?.length > 0) { + handleFrontendToolCalls( + payload.pending_frontend_tool_calls, + payload.conversation_history + ); + } + break; + + case "start_tool_calling": + console.log(`Tool started: ${payload.tool_name}`); + break; + + case "tool_calling_result": + console.log(`Tool result: ${payload.name}`, payload.result); + break; + + case "ai_message": + renderMarkdown(payload.content); + break; + + case "ai_answer_end": + // Store conversation_history for follow-up messages + saveConversationHistory(payload.conversation_history); + break; + } +} +``` + +**5. Execute tools and resume the stream** + +For each pending frontend tool call, run your local implementation and send results back. + +```javascript +async function handleFrontendToolCalls(pendingCalls, conversationHistory) { + const results = []; + + for (const call of pendingCalls) { + // Execute tool locally based on name + let result; + switch (call.tool_name) { + case "render_chart": + result = await renderChartInUI(call.arguments); + break; + default: + result = { error: `Unknown tool: ${call.tool_name}` }; + } + + results.push({ + tool_call_id: call.tool_call_id, + tool_name: call.tool_name, + result: JSON.stringify(result) // Must be a string + }); + } + + // Resume the stream with results + const response = await fetch("http:///api/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ask: originalQuestion, + stream: true, + conversation_history: conversationHistory, + frontend_tools: frontendTools, // Re-send tool definitions + frontend_tool_results: results + }) + }); + + // Continue reading this new stream (same parsing logic as step 3) + readSSEStream(response); +} +``` + +**6. Handle noop-mode tools (fire-and-forget)** + +Noop tools don't pause the stream. Watch for `start_tool_calling` events and execute side effects. + +```javascript +case "start_tool_calling": + if (payload.tool_name === "navigate_to_page") { + // Fire-and-forget: execute without blocking the stream + navigateToPage(payload); + } + break; +``` + +**Key implementation notes:** + +- **Re-send `frontend_tools`** on every request, including resume requests — tool definitions are not persisted server-side +- **`result` must be a string** — JSON-encode objects before sending +- **`conversation_history`** from the `approval_required` event must be passed back when resuming +- **Mixed pauses**: If both `pending_approvals` and `pending_frontend_tool_calls` appear in the same event, send both `tool_decisions` and `frontend_tool_results` in the resume request +- **Error handling**: If your tool fails, return a JSON error string as the `result` — the LLM will see it and can adapt + --- ### `/api/model` (GET) From b4431d87c4715e00ba4dfa7eb03d6281a942c896 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 08:24:06 +0000 Subject: [PATCH 2/5] Use fetch-event-source library instead of manual SSE parsing in client guide Replace hand-rolled ReadableStream + regex parsing with @microsoft/fetch-event-source, which is what real projects use for POST-based SSE endpoints. Consolidate steps 2+3 into one and update the resume flow to reuse the streamChat helper. https://claude.ai/code/session_013fd6r27uVfY2RijXoJhJs2 Signed-off-by: Claude --- docs/reference/http-api.md | 87 ++++++++++++-------------------------- 1 file changed, 28 insertions(+), 59 deletions(-) diff --git a/docs/reference/http-api.md b/docs/reference/http-api.md index fe81289a67..39ef9ff3d7 100644 --- a/docs/reference/http-api.md +++ b/docs/reference/http-api.md @@ -429,54 +429,33 @@ const frontendTools = [ ]; ``` -**2. Send the streaming request** +**2. Send the streaming request and parse SSE events** -```javascript -const response = await fetch("http:///api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - ask: "Show me CPU usage for the last hour", - stream: true, - frontend_tools: frontendTools, - // Include these when resuming a paused stream: - // conversation_history: [...], - // frontend_tool_results: [...] - }) -}); -``` - -**3. Parse the SSE stream** - -Read the stream and dispatch on `event` type. The key events for frontend tools are `start_tool_calling`, `tool_calling_result`, and `approval_required`. +Since `/api/chat` is a POST endpoint, the browser-native `EventSource` API (GET-only) doesn't work directly. Use a library like [sse.js](https://github.com/mpetazzoni/sse.js), [fetch-event-source](https://github.com/Azure/fetch-event-source), or your framework's built-in SSE support. ```javascript -const reader = response.body.getReader(); -const decoder = new TextDecoder(); -let buffer = ""; - -while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - - // Split on double newline (SSE event boundary) - const parts = buffer.split("\n\n"); - buffer = parts.pop(); // keep incomplete chunk - - for (const part of parts) { - const eventMatch = part.match(/^event:\s*(.+)$/m); - const dataMatch = part.match(/^data:\s*(.+)$/m); - if (!eventMatch || !dataMatch) continue; - - const eventType = eventMatch[1]; - const payload = JSON.parse(dataMatch[1]); - handleEvent(eventType, payload); - } +import { fetchEventSource } from "@microsoft/fetch-event-source"; + +function streamChat({ ask, conversationHistory, frontendToolResults }) { + fetchEventSource("http:///api/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + ask, + stream: true, + frontend_tools: frontendTools, + conversation_history: conversationHistory, + frontend_tool_results: frontendToolResults, + }), + onmessage(event) { + const payload = JSON.parse(event.data); + handleEvent(event.event, payload); + }, + }); } ``` -**4. Handle pause-mode tool calls** +**3. Handle pause-mode tool calls** When the LLM calls a pause-mode frontend tool, the stream emits an `approval_required` event with `pending_frontend_tool_calls`. Execute the tool locally, then resume. @@ -513,7 +492,7 @@ function handleEvent(eventType, payload) { } ``` -**5. Execute tools and resume the stream** +**4. Execute tools and resume the stream** For each pending frontend tool call, run your local implementation and send results back. @@ -522,7 +501,6 @@ async function handleFrontendToolCalls(pendingCalls, conversationHistory) { const results = []; for (const call of pendingCalls) { - // Execute tool locally based on name let result; switch (call.tool_name) { case "render_chart": @@ -539,25 +517,16 @@ async function handleFrontendToolCalls(pendingCalls, conversationHistory) { }); } - // Resume the stream with results - const response = await fetch("http:///api/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - ask: originalQuestion, - stream: true, - conversation_history: conversationHistory, - frontend_tools: frontendTools, // Re-send tool definitions - frontend_tool_results: results - }) + // Resume by opening a new stream with the results + streamChat({ + ask: originalQuestion, + conversationHistory, + frontendToolResults: results, }); - - // Continue reading this new stream (same parsing logic as step 3) - readSSEStream(response); } ``` -**6. Handle noop-mode tools (fire-and-forget)** +**5. Handle noop-mode tools (fire-and-forget)** Noop tools don't pause the stream. Watch for `start_tool_calling` events and execute side effects. From 2f1738e43a350cb0b357398723b82acf87b06762 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 08:25:57 +0000 Subject: [PATCH 3/5] Clarify that pause-mode frontend tools span two HTTP requests Add a numbered request-pause-resume explanation to the Frontend Tools section showing that a single LLM turn is split across request 1 (stream until pause) and request 2 (resume with tool results). Update the implementation guide headings and code comments to label which request is which. https://claude.ai/code/session_013fd6r27uVfY2RijXoJhJs2 Signed-off-by: Claude --- docs/reference/http-api.md | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/reference/http-api.md b/docs/reference/http-api.md index 39ef9ff3d7..735c61788f 100644 --- a/docs/reference/http-api.md +++ b/docs/reference/http-api.md @@ -314,6 +314,16 @@ Frontend tools have two modes: - **`pause`** (default): The stream pauses when the LLM calls the tool. The client executes the tool and resumes by sending results back. The LLM receives real results and continues reasoning with that data. - **`noop`**: The server returns a canned response immediately and the LLM continues without pausing. The client sees the tool call in SSE events (`start_tool_calling` + `tool_calling_result`) and can execute it as a fire-and-forget side effect. +**Pause mode spans two HTTP requests.** A single logical conversation turn is split across a request–pause–resume cycle: + +1. **Request 1** — the client sends `ask` + `frontend_tools`. The server streams SSE events until the LLM calls a pause-mode tool, then emits an `approval_required` event containing `pending_frontend_tool_calls` and `conversation_history`. The stream ends here. +2. The client executes the tool locally (render a chart, query a local DB, etc.). +3. **Request 2** — the client sends a new POST to `/api/chat` with the `conversation_history` from request 1, plus `frontend_tool_results` containing the tool output. The server feeds the results back to the LLM, which continues reasoning and streams the rest of its answer. + +If the LLM calls multiple pause-mode tools in one turn, they all appear in a single `approval_required` event — the client executes all of them and sends all results together in request 2. If the LLM calls another pause-mode tool later in the same conversation turn, the cycle repeats (request 3, 4, etc.). + +Noop-mode tools do **not** pause — the entire turn completes in a single request. + **Declaring frontend tools:** Each tool in the `frontend_tools` array has: @@ -398,7 +408,7 @@ Noop tools execute instantly on the server with a canned response. The client se #### Implementing Frontend Tools in Your Client -This section walks through how to build client-side support for frontend tools, step by step. +This section walks through building client-side support for pause-mode frontend tools. The key thing to understand is that a single LLM turn spans **two HTTP requests**: the first streams until the LLM calls your tool, and the second resumes the LLM after you return results. **1. Define your tools in the request** @@ -433,6 +443,8 @@ const frontendTools = [ Since `/api/chat` is a POST endpoint, the browser-native `EventSource` API (GET-only) doesn't work directly. Use a library like [sse.js](https://github.com/mpetazzoni/sse.js), [fetch-event-source](https://github.com/Azure/fetch-event-source), or your framework's built-in SSE support. +This helper is called for both request 1 (initial) and request 2 (resume with tool results): + ```javascript import { fetchEventSource } from "@microsoft/fetch-event-source"; @@ -444,8 +456,8 @@ function streamChat({ ask, conversationHistory, frontendToolResults }) { ask, stream: true, frontend_tools: frontendTools, - conversation_history: conversationHistory, - frontend_tool_results: frontendToolResults, + conversation_history: conversationHistory, // undefined on first request + frontend_tool_results: frontendToolResults, // undefined on first request }), onmessage(event) { const payload = JSON.parse(event.data); @@ -455,9 +467,9 @@ function streamChat({ ask, conversationHistory, frontendToolResults }) { } ``` -**3. Handle pause-mode tool calls** +**3. Handle the stream pause (end of request 1)** -When the LLM calls a pause-mode frontend tool, the stream emits an `approval_required` event with `pending_frontend_tool_calls`. Execute the tool locally, then resume. +When the LLM calls a pause-mode frontend tool, the stream emits an `approval_required` event with `pending_frontend_tool_calls` and then ends. Execute the tool locally, then start request 2 to resume. ```javascript function handleEvent(eventType, payload) { @@ -492,9 +504,9 @@ function handleEvent(eventType, payload) { } ``` -**4. Execute tools and resume the stream** +**4. Execute tools and send request 2 to resume** -For each pending frontend tool call, run your local implementation and send results back. +For each pending frontend tool call, run your local implementation, then open a new stream (request 2) with the results. The server feeds the results back to the LLM, which continues its answer. ```javascript async function handleFrontendToolCalls(pendingCalls, conversationHistory) { @@ -517,7 +529,7 @@ async function handleFrontendToolCalls(pendingCalls, conversationHistory) { }); } - // Resume by opening a new stream with the results + // Request 2: resume the LLM with tool results streamChat({ ask: originalQuestion, conversationHistory, From 59678269cc9602667a0157dab860182ac9770d02 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 08:32:56 +0000 Subject: [PATCH 4/5] Remove ambiguous use of 'turn' in frontend tools docs 'Turn' could mean a single LLM iteration or the full multi-iteration request. Replaced with 'iteration' when referring to one LLM tool-call cycle and 'request' when referring to the full HTTP request. https://claude.ai/code/session_013fd6r27uVfY2RijXoJhJs2 Signed-off-by: Claude --- docs/reference/http-api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/http-api.md b/docs/reference/http-api.md index 735c61788f..85ce5a695f 100644 --- a/docs/reference/http-api.md +++ b/docs/reference/http-api.md @@ -314,15 +314,15 @@ Frontend tools have two modes: - **`pause`** (default): The stream pauses when the LLM calls the tool. The client executes the tool and resumes by sending results back. The LLM receives real results and continues reasoning with that data. - **`noop`**: The server returns a canned response immediately and the LLM continues without pausing. The client sees the tool call in SSE events (`start_tool_calling` + `tool_calling_result`) and can execute it as a fire-and-forget side effect. -**Pause mode spans two HTTP requests.** A single logical conversation turn is split across a request–pause–resume cycle: +**Pause mode spans two HTTP requests.** What would normally be a single request is split across a request–pause–resume cycle: 1. **Request 1** — the client sends `ask` + `frontend_tools`. The server streams SSE events until the LLM calls a pause-mode tool, then emits an `approval_required` event containing `pending_frontend_tool_calls` and `conversation_history`. The stream ends here. 2. The client executes the tool locally (render a chart, query a local DB, etc.). 3. **Request 2** — the client sends a new POST to `/api/chat` with the `conversation_history` from request 1, plus `frontend_tool_results` containing the tool output. The server feeds the results back to the LLM, which continues reasoning and streams the rest of its answer. -If the LLM calls multiple pause-mode tools in one turn, they all appear in a single `approval_required` event — the client executes all of them and sends all results together in request 2. If the LLM calls another pause-mode tool later in the same conversation turn, the cycle repeats (request 3, 4, etc.). +If the LLM calls multiple pause-mode tools in one iteration, they all appear in a single `approval_required` event — the client executes all of them and sends all results together in request 2. If the LLM calls another pause-mode tool in a later iteration, the cycle repeats (request 3, 4, etc.). -Noop-mode tools do **not** pause — the entire turn completes in a single request. +Noop-mode tools do **not** pause — the entire request completes without interruption, regardless of how many LLM iterations it takes. **Declaring frontend tools:** @@ -408,7 +408,7 @@ Noop tools execute instantly on the server with a canned response. The client se #### Implementing Frontend Tools in Your Client -This section walks through building client-side support for pause-mode frontend tools. The key thing to understand is that a single LLM turn spans **two HTTP requests**: the first streams until the LLM calls your tool, and the second resumes the LLM after you return results. +This section walks through building client-side support for pause-mode frontend tools. The key thing to understand is that what would normally be a single request is split across **two HTTP requests**: the first streams until the LLM calls your tool, and the second resumes the LLM after you return results. **1. Define your tools in the request** From 45eb14ab892b77fca0b6bfc803ef3f4efe94bca3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 08:37:41 +0000 Subject: [PATCH 5/5] Simplify SSE library recommendation to focus on what works https://claude.ai/code/session_013fd6r27uVfY2RijXoJhJs2 Signed-off-by: Claude --- docs/reference/http-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/http-api.md b/docs/reference/http-api.md index 85ce5a695f..266367e195 100644 --- a/docs/reference/http-api.md +++ b/docs/reference/http-api.md @@ -441,7 +441,7 @@ const frontendTools = [ **2. Send the streaming request and parse SSE events** -Since `/api/chat` is a POST endpoint, the browser-native `EventSource` API (GET-only) doesn't work directly. Use a library like [sse.js](https://github.com/mpetazzoni/sse.js), [fetch-event-source](https://github.com/Azure/fetch-event-source), or your framework's built-in SSE support. +Since `/api/chat` is a POST endpoint, use an SSE library that supports POST requests, such as [fetch-event-source](https://github.com/Azure/fetch-event-source) or [sse.js](https://github.com/mpetazzoni/sse.js). This helper is called for both request 1 (initial) and request 2 (resume with tool results):