Devin AI 25c4e2442c fix(agent-proxy): conservative segment budget + drop unnecessary upstream.cancel()
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>
2026-05-13 13:16:44 +00:00
2026-05-11 22:27:54 -04:00
2026-05-11 22:27:54 -04:00
2026-05-11 22:27:54 -04:00
2026-05-11 22:27:54 -04:00
2026-05-11 22:27:54 -04:00
2026-05-11 22:27:54 -04:00

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

  1. The browser POSTs the form to /api/agent-proxy.
  2. 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.list backfill, deduped by event id) whenever it drops mid session, until the session reports a terminal session.status_* event or a 20-minute wall-clock budget is hit.
  3. Downstream to the browser the function emits newline-delimited JSON (application/x-ndjson) — text, status, heartbeat, segment_end, done, error — one object per line.
  4. The React app reads response.body.getReader(), splits on \n, and parses each line. text lines append to the brief; status lines drive a separate “what the agent is doing now” banner.
  5. A Thinking flag stays true until 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-proxy again 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 by event.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

S
Description
No description provided
Readme 147 KiB
Languages
JavaScript 48.4%
TypeScript 46.1%
CSS 4.4%
HTML 1.1%