redpanda-data / redpanda-data/console

A2A adapter follow-ups post #2389 (arch hygiene + testability)

Open
#2,391 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

frontend
Dominant language
TypeScript
Stars
4.3k
Forks
432
Avg merge
3d 6h
Merged PRs (30d)
40

Description

Context

Follow-up to #2389 (A2A + AI SDK v6 + streamdown v2 + AI Elements re-sync). The architectural analysis done on that PR surfaced several improvements that are out of scope for the bump itself but should be addressed in a dedicated pass. Grouped by effort; each item has file:line references.

Critical-path hygiene (target first)

1. Extract pure stream-mapper from doStream

Files: frontend/src/components/pages/agents/details/a2a/a2a-chat-language-model.tsx

doStream (≈120 lines around L260–378) mixes transport setup, capability detection, non-streaming fallback, first-chunk metadata, and event-to-v2-stream-part mapping in one TransformStream. Extract a pure a2aEventToV2StreamParts(event, state): { parts: LanguageModelV2StreamPart[]; state } into a2a-stream-mapper.ts. Zero behavior change; unlocks unit tests on every TaskStatusUpdateEvent.status.state branch.

2. Finish or remove simulatedStream non-streaming branch

Files: a2a-chat-language-model.tsx:291-306

When clientCard.capabilities.streaming === false the adapter builds a simulatedStream but the error branch is a bare // FIXME: error and the subsequent sendMessageStream(streamParams) re-sends the request unconditionally. Decide: either complete the non-streaming path (error + skip real stream) or delete the simulatedStream block.

3. Split StreamingState and narrow activeTextBlock type

Files: frontend/src/components/pages/agents/details/a2a/chat/hooks/streaming-types.ts:75-122, event-handlers.ts:375-394

StreamingState has six active fields + three @deprecated legacy fields; activeTextBlock is typed ContentBlock | null but actually stores artifact blocks too, requiring a type guard in every consumer. Delete the three deprecated fields and narrow activeTextBlock to Extract<ContentBlock, { type: 'artifact' }> | null (+ rename to activeStreamingBlock).

Testability / isolation

4. Extract parseA2AError into its own module

Files: frontend/src/components/pages/agents/details/a2a/chat/hooks/use-message-streaming.ts:49-97

parseA2AError reverse-engineers JSON-RPC errors out of Error.message strings using 5 regexes because @a2a-js/sdk drops structured code/message/data into text. Move to chat/utils/parse-a2a-error.ts with table-driven tests. Also file an upstream @a2a-js/sdk issue asking for structured errors.

5. Hoist createAuthenticatedFetch

Files: a2a-chat-language-model.tsx:202-212 and :265-273, chat/utils/a2a-client.ts:21-30

Three copies of the same fetch-with-auth-header closure; the streaming path adds X-Redpanda-Stream-Tokens: true. Hoist to createAuthenticatedFetch(jwt, extraHeaders?) in a2a-client.ts.

Type-safety and runtime robustness

6. Replace Buffer with Uint8Array in browser code

Files: a2a-chat-language-model.tsx:414, 483

Buffer.from(...) relies on a Node shim polyfill. LanguageModelV2File.data accepts Uint8Array directly — no polyfill needed. A bundler regression would currently produce a runtime Buffer is not defined.

7. Add assertNever on stream-chunk switch

Files: use-message-streaming.ts, a2a-chat-language-model.tsx TransformStream.transform

v6 added source, reasoning-delta, tool-input-start/delta/end stream parts; our outer hook's switch doesn't handle them and silently falls through. Add default: assertNever(chunk) so new v6 chunk kinds fail loudly.

8. Throw UnsupportedFunctionalityError on data parts

Files: a2a-chat-language-model.tsx:426-428 (convertProviderPartToContent)

Currently silently drops part.kind === 'data' in doGenerate. Either throw UnsupportedFunctionalityError to make the gap explicit or delete the whole doGenerate method (unused; streamText drives everything).

Provider-shape refactor (biggest lever, separate PR)

9. Move A2A adapter to transport + pure mapper + thin glue

Per interface-design analysis on #2389: separate A2aTransport (SSE/JSON-RPC plumbing, fake-able in tests) from a2aEventToV2StreamParts (pure reducer, trivially testable) and wire them through a ~8-line LanguageModelV2 object. Cheapest path to V3 forward-compat; no call-site churn beyond a2a-provider.ts:40. Depends on #1 landing first.

Out of scope / won't do here

  • @modelcontextprotocol/sdk bump — its own PR, not mixed with this work.
  • prompt-input.tsx re-sync — heavy divergence, needs its own focused pass.
  • Adopting upstream ai-elements Attachments, ChainOfThought, Reasoning, PromptInputActionAddScreenshot — do when the AI Agents feature actually wires them.

Suggested order

  1. Merge #2389 first.
  2. Tackle #1, #3 together (both in the adapter / streaming-state area) with a new test file covering the extracted mapper.
  3. Then #2, #7, #8 as a single "adapter correctness" PR.
  4. Then #4, #5, #6 as "adapter hygiene".
  5. Finally #9 once the safety net from #1 is in place.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with frontend/src/components/pages/agents/details/a2a/a2a-chat-language-model.tsx and the related streaming types and handlers. Read the suggested order, beginning with the stream mapper and StreamingState work, then inspect existing tests and the A2A client utilities. Done means the selected refactors preserve behavior, add isolated coverage for stream-event branches, and remove the identified unsafe or silent paths.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
developer-experience, frontend, testing
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.