feat(dev): agent-proxy routes for the Agent Inspector - #2085
Conversation
fcff328 to
013ffe2
Compare
013ffe2 to
5e98a35
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## refactor #2085 +/- ##
============================================
+ Coverage 97.38% 97.39% +0.01%
============================================
Files 440 449 +9
Lines 26626 27474 +848
============================================
+ Hits 25929 26759 +830
- Misses 697 715 +18 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
5e98a35 to
5cfa15f
Compare
5cfa15f to
d6abf97
Compare
6f5b3a3 to
0a6bec4
Compare
Add the Inspector routes that talk to a running agent or read the project spec, extending the C1 route table: - POST /invocations proxies HTTP, A2A, and AGUI agents, normalizing each into the SPA's data:<json> SSE contract. MCP agents get a clear error directing them to /api/mcp rather than being mis-proxied as HTTP. - POST /api/mcp forwards a JSON-RPC body to the agent's /mcp endpoint and buffers the reply under a 10MB cap. - GET /api/a2a/agent-card fetches the running agent's A2A card. - GET /api/resources flattens the project spec into the resource graph. Every upstream fetch carries the client's abort signal, and io/httpServer streams async-iterable bodies with backpressure so a disconnect tears the upstream request down on Node. A single session id threads through the request header, agent body, and echoed x-session-id. SSE parsing follows the framing rules (optional leading space, multi-line data fields, blank-line event boundary) instead of a hardcoded slice.
- collapse invokeHttpAgent/invokeAguiAgent into a shared forwardInvocation - single-return parseAgentEvent normalizing empty payloads to null - hoist the SSE TextEncoder to module scope - readCapped iterates over the shared iterateBody helper - drop the a2aId counter; A2A message ids use randomUUID - add the AGUI missing-prompt test
Delete JSDoc and inline notes that narrate what the code already shows. Keep only one-line notes for non-obvious reasoning (security guards, cross-runtime disconnect behavior, wire-contract field names, SSE framing).
…atch - Inline the single-caller invokeHttpAgent wrapper into handleInvocations. - Make invokeAguiAgent async so its guard returns apiError directly. - Return kind from extractSseEventText instead of re-deriving it in a separate isStatusUpdateEvent pass. - Collapse the A2A part/artifact accumulator loops into filter/map/join.
d66062a to
0c0f90f
Compare
- Populate the resources fixture with one of every resource type so each wire-shaping map callback is exercised (was 55% covered). - Add invocation unhappy paths: upstream 502s, parseAgentEvent null frames, A2A artifact/task extraction, non-streaming and non-JSON fallbacks. - Cover httpServer backpressure drain and the post-headers stream error.
Hweinstock
left a comment
There was a problem hiding this comment.
LGTM! some small suggestions/questions but no blockers.
| return json(200, status); | ||
| } | ||
|
|
||
| /** POST /api/start — start an agent on demand; concurrent starts share one attempt. */ |
There was a problem hiding this comment.
q: were these removed on purpose?
There was a problem hiding this comment.
Yes, on purpose. Those route JSDocs just restated the method/path already visible in the handler, so the comment-cleanup pass dropped them per the no-restating-the-code guideline. The security and wire-contract comments stayed.
| body, | ||
| signal, | ||
| }); | ||
| } catch (error) { |
There was a problem hiding this comment.
would there be a benefit to wire the logger here?
There was a problem hiding this comment.
There's no logger in this layer today (the dev handler surfaces status via renderStatus and lets errors propagate). The upstream failure already reaches the user as the 502 body in the Inspector, so I left it. Happy to add structured logging if we introduce a logger dep for the dev command more broadly.
| // Handles bedrock {text}, {error}, ConverseStream contentBlockDelta, bare JSON string, and non-JSON tokens. | ||
| export function parseAgentEvent(data: string): string | { error: string } | null { | ||
| try { | ||
| const parsed: unknown = JSON.parse(data); |
There was a problem hiding this comment.
would it simplify this code to use a zod schema that we parse with?
There was a problem hiding this comment.
I looked at it — zod does not buy much here. parseAgentEvent takes loosely-typed passthrough tokens from arbitrary agent runtimes, handles several shapes ({text}, {error}, ConverseStream delta, bare string) AND a non-JSON fallback that returns the raw token. A zod union would still need the try/catch + raw fallback + empty-to-null glue, so the imperative form stays clearer. Left as is.
| } | ||
|
|
||
| // When streamedFromStatus is set, artifact-update text is skipped because status-update already streamed it. | ||
| function extractSseEventText( |
There was a problem hiding this comment.
should this function signature / name mention that its A2A specific?
| expect(await response.text()).toBe(`data: ${JSON.stringify({ error: "boom" })}\n\n`); | ||
| }); | ||
|
|
||
| test("passes a non-SSE response body through untouched", async () => { |
There was a problem hiding this comment.
i like how readable these tests are.
| ): Promise<void> { | ||
| for await (const chunk of body) { | ||
| if (signal.aborted) break; | ||
| if (!response.write(chunk)) await drain(response, signal); |
Rename extractSseEventText to extractA2aEventText; it only handles A2A artifact/status/task event kinds, so the generic SSE name misled. Addresses review feedback on #2085.
Rename extractSseEventText to extractA2aEventText; it only handles A2A artifact/status/task event kinds, so the generic SSE name misled. Addresses review feedback on #2085.
|
Claude Security Review: no high-confidence findings. (run) |
Rename extractSseEventText to extractA2aEventText; it only handles A2A artifact/status/task event kinds, so the generic SSE name misled. Addresses review feedback on #2085.
What
Adds the Agent Inspector routes that talk to a running agent or read the project spec. This layer is still a pure request-to-response handler and is not reachable from the CLI yet. The
project devwiring lands in #2086.Routes
POST /invocations— protocol-aware proxy. HTTP, A2A, and AGUI agents are each normalized into the SPA'sdata: <json>SSE contract. MCP returns a clear error pointing at/api/mcprather than being proxied as HTTP.POST /api/mcp— forwards a JSON-RPC body to the agent's/mcpendpoint, buffering the reply under a 10MB cap.GET /api/a2a/agent-card— fetches the running agent's A2A card.GET /api/resources— flattensproject.specinto the resource graph the SPA renders.Tests
Every route is unit tested behind fake deps: invocation routing and SSE normalization (including A2A dedup and the non-streaming fallback), the MCP forward and its size cap, the agent-card paths, the resource graph, and
httpServer's streaming and abort handling.bun test,typecheck,lint:check,format:checkall green.