Two related fixes after re-testing on the deploy preview: 1. SEGMENT_BUDGET_MS: 54_000 -> 40_000. Empirically, Netlify Edge Function responses get cut anywhere between ~49 s and ~60 s wall-clock — significantly more variance than the docs imply. With a 54 s budget, our segment_end write + controller.close() sometimes lands AFTER the platform has already killed the response (curl: 'HTTP/2 stream 1 was not closed cleanly before end of the underlying stream'). 40 s gives ~10 s of headroom on the low end of the observed range while still keeping segment overhead reasonable (~1 segment per 40 s of brief). 2. HEARTBEAT_MS: 10_000 -> 5_000. Tighter keep-alive so the connection stays warm even during long model-thinking gaps where the agent isn't emitting events. 3. Drop the 'await upstream.cancel()' between backfill and tail. The upstream fetch's body is already cleaned up by segmentAbort.abort() (or will be by the runtime once we drop our reference). On Deno Edge, awaiting cancel() on an already-aborted body can hang, which prevents segment_end from being written. Co-Authored-By: alex <alex@semipublic.co>
PMC Funder Discovery Tool
A React + Vite frontend (Tailwind, Noto Serif) that collects public media station details and streams an Anthropic Managed Agent response back to the browser through a Netlify Edge Function proxy.
Setup
npm install
Add your Anthropic credentials (in .env for local dev, in the Netlify UI
for production):
ANTHROPIC_API_KEY=sk-ant-...
# Optional - if you've provisioned a Managed Agent in the Anthropic console:
ANTHROPIC_AGENT_ID=agent_...
# Optional - override the model:
ANTHROPIC_MODEL=claude-sonnet-4-5
Run locally
npm run dev # netlify dev (proxies Vite + functions on :8888)
Open http://localhost:8888.
How streaming works
- The browser POSTs the form to
/api/agent-proxy. - A Netlify Edge Function (Deno runtime) creates an Anthropic Managed
Agent session, opens the upstream SSE event stream, and sends the
user message. It tails the stream and re-opens it (with an
events.listbackfill, deduped by event id) whenever it drops mid session, until the session reports a terminalsession.status_*event or a 20-minute wall-clock budget is hit. - Downstream to the browser the function emits newline-delimited JSON
(
application/x-ndjson) —text,status,heartbeat,segment_end,done,error— one object per line. - The React app reads
response.body.getReader(), splits on\n, and parses each line.textlines append to the brief;statuslines drive a separate “what the agent is doing now” banner. - A
Thinkingflag staystrueuntil the first chunk arrives, then flips to a streaming state with a pulsing cursor.
Why segmented streaming
Netlify Edge Functions empirically cap a single streaming response at ~60 s wall-clock (Netlify's own canonical SSE example cuts at the same mark, even though the docs imply "indefinite" streaming). The Anthropic Managed Agent session itself is fine for several minutes; the limit is per-HTTP-response. So the proxy segments the stream:
- Just before ~54 s the function writes one final NDJSON line —
{"type":"segment_end","sessionId":"sess_…","lastEventId":"evt_…","startedAt":…}— and closes the response. - The React side sees
segment_end, immediately POSTs to/api/agent-proxyagain with{sessionId, lastEventId, startedAt}in the body. - The function recognises this as a resume payload: it does not
create a new session or re-send the user message. It just reopens
the live SSE stream, backfills via
GET /v1/sessions/{id}/events?after_id=lastEventId(deduped byevent.id), and keeps tailing.
The 20-minute overall budget is enforced via Date.now() - startedAt
so it spans across all segments. The previous v2 Node Function (on AWS
Lambda) was killed at ~27 s, well before any reconnect could fire —
moving to Edge Functions extended that to ~60 s per response, and the
segment loop extends it the rest of the way.
Files
- src/App.jsx — form, NDJSON streaming reader, segment-end resume loop, UI states.
- netlify/edge-functions/agent-proxy.ts — Managed Agent proxy with reconnect, backfill, segmentation, and NDJSON wire protocol.