Devin AI df3c8e7d1c fix(agent-proxy): events.list uses opaque 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>
2026-05-13 13:08:54 +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%