page cursor, not after_id
The Managed Agents events endpoint (`GET /v1/sessions/{id}/events`)
does NOT support filtering by event id. It returns an opaque
`next_page` cursor on each response and accepts it back via the
`page` query parameter; an `after_id=` filter returns 400 Bad
Request, which caused every segment resume to fail backfill (visible
as `{"type":"status","kind":"session_error","message":"Backfill
failed: events.list returned 400 Bad Request"}`).
Caught during testing of commit 8e44de5: resuming from a segment
boundary always returned 400 and the brief silently lost events from
the previous segment.
Changes:
- `listAllEvents` now paginates via `page` / `next_page` and pulls
the full session history (limit=1000). The Anthropic API has no
per-id filter, so the caller is responsible for skipping events
already delivered.
- New `pastInitialId` flag at the top of the body loop: on resume,
mute every event up to and including `initialLastEventId`
(still adding them to `seenEventIds` so the live stream doesn't
re-emit them), then start delivering. On a brand-new session the
flag starts true and is a no-op.
- Safety fallback: if backfill completes without ever seeing
`initialLastEventId` (stale cursor / truncated history), flip
the flag to true so we don't get stuck muting forever — the live
stream will start delivering whatever shows up next.
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.