npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bedrock-ui-stream

v0.1.1

Published

Translate an Amazon Bedrock Agent InvokeAgent response stream into an AI SDK UI message stream, for use with useChat.

Downloads

272

Readme

bedrock-ui-stream

Translate an Amazon Bedrock Agent InvokeAgent response stream into an AI SDK UI message stream, so useChat works unchanged.

npm version npm downloads minzipped size CI license: MIT

The problem

InvokeAgent answers with a 14-member AWS union: text chunks, traces, knowledge-base citations, code-interpreter files, return-control payloads and ten typed exceptions. useChat reads the AI SDK's UI message stream protocol. Neither SDK converts between them, and vercel/ai#2081 has been open for 25 months, with the AI SDK maintainer's position being that this adapter layer belongs in application code. This is that layer — and the single most useful thing in it is a one-line request default you would have no reason to guess at, described below.

Install

npm install bedrock-ui-stream

Zero runtime dependencies. ai@^7 and @aws-sdk/client-bedrock-agent-runtime@^3.700.0 are peer dependencies — the two packages your app already has. Neither is bundled.

The working example

// app/api/chat/route.ts
import { BedrockAgentRuntimeClient, InvokeAgentCommand } from '@aws-sdk/client-bedrock-agent-runtime';
import { toInvokeAgentInput, toUIMessageStream } from 'bedrock-ui-stream';
import { createUIMessageStreamResponse } from 'ai';

const client = new BedrockAgentRuntimeClient({ region: process.env.AWS_REGION });

export async function POST(req: Request) {
  const { id, messages } = await req.json();

  const response = await client.send(new InvokeAgentCommand({
    agentId: process.env.BEDROCK_AGENT_ID!,
    agentAliasId: process.env.BEDROCK_AGENT_ALIAS_ID!,
    sessionId: id,                    // one useChat chat === one Bedrock session
    enableTrace: true,                // the only way to learn the agent itself failed
    ...toInvokeAgentInput(messages),  // -> { inputText, sessionState?, streamingConfigurations }
  }));

  return createUIMessageStreamResponse({ stream: toUIMessageStream(response) });
}

The client is plain useChat, unchanged:

'use client';
import { useChat } from '@ai-sdk/react';

export function Chat({ chatId }: { chatId: string }) {
  const { messages, sendMessage } = useChat({ id: chatId });
  // ...
}

Full runnable versions of both halves, plus tool calling and citation rendering, are in examples/.

The mental model that trips everyone up

Bedrock keeps conversation history server-side, keyed on sessionId. You send one inputText per turn, not the message array. toInvokeAgentInput exists to enforce exactly that: it reads the newest user turn and ignores the rest of the history.

sessionId is conversation identity. This library never generates one for you — a per-request sessionId makes the agent silently amnesiac, which looks like a model quality problem and costs hours. useChat already sends a stable chat id; use it, and persist it with the conversation rather than letting a remount regenerate it.


Why this instead of your own adapter

The translation itself is a morning's work. These are the things that cost the rest of the week.

streamFinalResponse is false service-side, and that is why your stream looks broken

toInvokeAgentInput returns streamingConfigurations: { streamFinalResponse: true }.

From the AWS API reference, on streamFinalResponse:

Specifies whether to enable streaming for the final response. This is set to false by default.

With it off — the service default — Bedrock buffers the entire answer and delivers it as a single chunk at the very end. AWS's own InvokeAgent documentation says as much: "The InvokeAgent returns one chunk for the entire interaction." A perfectly wired useChat then shows nothing for ten seconds and then everything at once, and the reasonable conclusion is that useChat streaming is broken.

That is almost certainly what people are hitting in issue #2081. Nothing in the request looks wrong, nothing errors, and the fix is a field most people never find. If you take one thing from this package, take this line.

It is overridable:

const input = toInvokeAgentInput(messages, {
  streamingConfigurations: { streamFinalResponse: true, applyGuardrailInterval: 200 },
});

applyGuardrailInterval defaults to 50 characters service-side. Pass streamingConfigurations: false to omit the key entirely and inherit the service defaults.

[!IMPORTANT] Streaming needs an extra IAM permission, and this package turns streaming on by default. Your agent's execution role needs bedrock:InvokeModelWithResponseStream in addition to bedrock:InvokeModel — AWS documents this on the streamingConfigurations field.

The failure mode is confusing: with the permission missing, streamFinalResponse: true throws AccessDeniedException while false keeps working, so it looks like the flag itself is broken. See aws-sdk-js-v3#6712.

If your agent worked before you installed this package and now returns 403, this is why. Grant the permission, or pass streamingConfigurations: false to opt out. The AccessDeniedException message names this cause first.

What the flag does and does not fix

It reliably turns one chunk into many. It does not guarantee tokens arrive as they are generated:

  • Chunk counts vary enormously by model on the same agent and prompt. Reported in aws-sdk-js-v3#6712: Sonnet 3 → 374 chunks, Haiku 3 → 107, Sonnet 3.5 v2 → 50, Nova Lite → 27, Haiku 3.5 → 4, Claude Instant → 2.
  • Delivery can still arrive as a burst. boto3#4744 — open, reproduced by AWS — has trace at 1.33 s and the first chunk at 7.18 s, with every remaining chunk inside 10 ms.

Expect the response shape to change. Measure before you promise anyone lower time-to-first-token.

Agents created before 2025-03-31 have a further wrinkle: AWS briefly documented that streaming required orchestration-only prompts, and users with knowledge-base-equipped agents in that window reported collapsing back to a single chunk. That precondition is no longer in the service model.

chunk.bytes boundaries are not UTF-8 codepoint boundaries

A multi-byte character can straddle two events. This library holds one TextDecoder({ stream: true }) across the whole stream. A per-chunk new TextDecoder().decode() returns U+FFFD for any split character — invisible in English-language testing, and reported later as "the library corrupts emoji". There is a test for it.

failureTrace is the only signal that the agent failed

When the agent fails but the HTTP stream stays healthy, Bedrock sends a failureTrace and ends the stream. No exception, no answer. This library turns it into an error chunk unconditionally, so it cannot be silenced by the trace options — but it only reaches you if the request set enableTrace: true.

Return control is a wire format, not a callback

Action groups configured with RETURN_CONTROL pause the agent mid-turn. Resuming requires sending back invocationId and a result shaped per invocation kind, on a request with no inputText — Bedrock takes the results instead of a new user turn and rejects a request carrying both. That state has to survive a browser reload, so this library persists it in the message part itself and reads it back. See Return control.

AWS's error text reaches your console

ai masks stream errors as 'An error occurred.' by default. A masked Bedrock error is undebuggable, so this library builds its ReadableStream directly rather than wrapping createUIMessageStream({ execute }), specifically so masking cannot happen. You get AWS's message verbatim plus a remediation clause, and an onError hook to redact it.

When not to use this

  • Bedrock AgentCore (InvokeAgentRuntime). Out of scope, and no general adapter can exist — InvokeAgentRuntime returns opaque bytes, not a modeled union. The reasoning is in AgentCore is out of scope.

  • An AgentCore agent that already emits AI SDK chunks. There is nothing to translate. Pass your stream straight to createUIMessageStreamResponse and skip this package:

    return createUIMessageStreamResponse({ stream: yourAgentCoreStream });
  • A non-useChat frontend. The output is the AI SDK UI message stream protocol specifically. If your client reads raw text or your own JSON envelope, iterate response.completion yourself — you do not need an adapter to a protocol you are not using.

  • Bedrock Runtime rather than Bedrock Agents. For Converse / InvokeModel use @ai-sdk/amazon-bedrock, which is a real AI SDK provider. This package only translates InvokeAgent.


API reference

Five values and eight types, all from the package root. Everything else is internal, and deep imports are blocked at module resolution — exports declares no wildcard subpaths.

toUIMessageStream(response, options?)

Takes the whole InvokeAgentCommandOutput (or anything with a completion) and returns a ReadableStream of UI message chunks. It takes the wrapper rather than the bare iterable because completion is typed AsyncIterable<ResponseStream> | undefined, so passing response.completion would fail tsc --strict and every app would open with response.completion!.

| Option | Default | Notes | | --- | --- | --- | | sendReasoning | true | orchestrationTrace.rationalereasoning-* parts. Needs enableTrace. | | sendSources | true | chunk.attributionsource-url / source-document parts. | | sendFiles | true | files events → file parts as data: URLs. See Files. | | sendStart | true | Emit the leading start chunk. Set false when merging into another stream. | | sendFinish | true | Emit the trailing finish chunk. Set false when merging into another stream. | | sendTrace | false | Emit transient data-bedrock-trace parts. | | onError | built-in | Map a failure to errorText. | | onEvent | — | Observe every raw ResponseStream event before translation. | | generateId | crypto.randomUUID | IDs for text parts, sources and tool calls. |

toInvokeAgentInput(messages, options?)

Derives { inputText?, sessionState?, streamingConfigurations? } from useChat history. Spread it into your own command input. It does two things: sends one user turn rather than the array, and rebuilds the return-control round trip when history contains answered dynamic-tool parts.

Its one option is streamingConfigurations, covered above.

createAgentRouteHandler(config)

A Request -> Response handler, for when you have no credential chain, region strategy or sessionAttributes to place:

// app/api/chat/route.ts
import { BedrockAgentRuntimeClient } from '@aws-sdk/client-bedrock-agent-runtime';
import { createAgentRouteHandler } from 'bedrock-ui-stream';

export const POST = createAgentRouteHandler({
  client: new BedrockAgentRuntimeClient({ region: process.env.AWS_REGION }),
  agentId: process.env.BEDROCK_AGENT_ID!,
  agentAliasId: process.env.BEDROCK_AGENT_ALIAS_ID!,
});

| Option | Default | Notes | | --- | --- | --- | | client | — | Required. Yours, so your credentials, region and retry strategy all apply. | | agentId / agentAliasId | — | Required. | | enableTrace | true | See below. | | sessionId | body.id | (body, request) => string. | | prepareInput | — | Last look at the assembled command input. | | streamOptions | — | Passed through to toUIMessageStream. | | headers | — | Merged over the UI-message-stream response defaults. |

prepareInput is where sessionState.sessionAttributes, memoryId, endSession and bedrockModelConfigurations go:

export const POST = createAgentRouteHandler({
  client,
  agentId: process.env.BEDROCK_AGENT_ID!,
  agentAliasId: process.env.BEDROCK_AGENT_ALIAS_ID!,
  prepareInput: ({ input }) => ({
    ...input,
    memoryId: 'mem-1',
    sessionState: { ...input.sessionState, sessionAttributes: { tier: 'gold' } },
  }),
});

enableTrace defaults to true here

failureTrace is the only signal that the agent failed while the HTTP stream stayed healthy, and it arrives only with tracing on. Without it, that failure mode is invisible: the stream just ends with no answer and no exception.

The cost is bandwidth on the AWS → your-server hop. Trace payloads are large — modelInvocationInput carries the whole rendered prompt — and they cross that hop even when sendTrace: false keeps them out of the browser. Set enableTrace: false if you have measured that cost and would rather have silent agent failures.

Errors

The rule: construction throws, streaming emits.

toUIMessageStream throws BedrockUIStreamError only for caller mistakes detectable before any byte reaches the client. Once it has returned a stream it never throws — every failure becomes an { type: 'error', errorText } chunk, the only error channel the wire protocol has.

BedrockUIStreamError codes: no_completion_stream, no_user_message, invalid_tool_output. Use the static BedrockUIStreamError.isInstance(error) rather than instanceof; it survives two copies of this package in one dependency tree.

The default errorText is Bedrock <ExceptionName>: <AWS message> <remediation> — AWS's message verbatim, plus one library-added remediation clause per exception type. To redact before anything reaches the browser:

const stream = toUIMessageStream(response, {
  onError: (error) => {
    console.error(error);           // full detail stays server-side
    return 'The assistant could not complete that request.';
  },
});

In createAgentRouteHandler, pre-stream failures become real HTTP status codes so useChat populates its error state properly: $fault: 'client' → 400, 'server' → 502, throttling → 429 with retry-after, access denied → 403, not found → 404.

Two deliberate cases:

  • failureTrace → an error chunk. Never suppressible, and not gated behind sendTrace. Requires enableTrace: true on the request to arrive at all.
  • $unknown → ignored, never thrown. AWS adds ResponseStream members without a major version bump; throwing would break working apps on a routine SDK upgrade.

Exports

| Export | Kind | | --- | --- | | toUIMessageStream | function | | toInvokeAgentInput | function | | createAgentRouteHandler | function | | BedrockUIStreamError | class, with isInstance | | BedrockAgentFailureError | class — what failureTrace is wrapped in before onError sees it | | ToUIMessageStreamOptions, InvokeAgentInput, ToInvokeAgentInputOptions | types | | AgentRouteHandlerConfig, AgentRouteHandlerRequestBody | types | | BedrockReturnControlMetadata, BedrockTraceData | types | | BedrockUIStreamErrorCode | type |


Advanced

Four features that need more than an option name to use well.

Return control (action groups you run yourself)

Action groups are configured in AWS, not declared in TypeScript, so their names are unknowable at compile time. returnControl events therefore become dynamic-tool parts, the AI SDK's construct for runtime-discovered tools, carrying a toolMetadata: BedrockReturnControlMetadata that drives the round trip.

Bedrock's FunctionParameter is a name/type/value list where value is always a string regardless of the declared type. This library pivots it into an object and coerces per Bedrock's ParameterType vocabulary (string, number, integer, boolean, array), falling back to the raw string for anything unrecognised rather than mangling it.

toInvokeAgentInput reads that metadata back off persisted parts:

const history = [
  { id: 'u1', role: 'user', parts: [{ type: 'text', text: 'Where is order A-1001?' }] },
  {
    id: 'a1',
    role: 'assistant',
    parts: [{
      type: 'dynamic-tool',
      toolName: call.toolName,
      toolCallId: call.toolCallId,
      state: 'output-available',
      input: call.input,
      output: { status: 'shipped' },
      toolMetadata: call.toolMetadata,
    }],
  },
];

const input = toInvokeAgentInput(history);
// input.inputText     -> undefined
// input.sessionState  -> {
//   invocationId: 'inv-abc',
//   returnControlInvocationResults: [{
//     functionResult: {
//       actionGroup: 'OrderActions',
//       function: 'lookupOrder',
//       responseBody: { TEXT: { body: '{"status":"shipped"}' } },
//     },
//   }],
// }

On the client, useChat's onToolCall plus addToolOutput and sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls close the loop with no extra plumbing — see examples/return-control.

Two things worth knowing:

  • Tool output goes back as ContentBody.body, which is a string — objects are JSON-stringified, conventionally under the key "TEXT".
  • No inputText is sent on a resuming turn. Only results belonging to the newest invocationId are returned; older ones were already consumed.

A part still in input-available state (output not yet supplied) is not treated as an answer, so the request falls back to normal inputText behaviour. A part in output-error state is sent back as a FAILURE result, so the agent can apologise instead of waiting for a result that never comes.

Citations

chunk.attribution is not a standalone event — it is metadata about a byte range of text already emitted. Each retrievedReference becomes one source part:

  • Locations with a URL (webLocation, confluenceLocation, salesforceLocation, sharePointLocation, oneDriveLocation, googleDriveLocation, and HTTP Kendra URIs) → source-url.
  • s3Location, non-HTTP Kendra URIs, customDocumentLocation, sqlLocation and anything unrecognised → source-document, with a synthesised title and a guessed mediaType (both are required, non-optional fields on that chunk).

There is no de-duplication: a reference cited twice yields two source parts, each with its own span. Group on providerMetadata.bedrock.location if you want them merged — examples/traces-and-citations does.

providerMetadata.bedrock carries span (raw, exactly as AWS sent it), resolvedSpan, chunkTextOffset, citedText, location, content and metadata. See Known assumptions on spans.

Traces

const stream = toUIMessageStream(response, { sendTrace: true });

Trace parts are data-bedrock-trace and carry transient: true, so they reach the client for live rendering but stay out of persisted history. Trace payloads are large and re-POSTing them every turn would be wasteful. The part name is semver-protected; the payload is BedrockTraceData.

Declare the part in your own UIMessage type and the client gets it typed rather than unknown:

import type { UIMessage } from 'ai';
import type { BedrockTraceData } from 'bedrock-ui-stream';

export type BedrockUIMessage = UIMessage<never, { 'bedrock-trace': BedrockTraceData }>;

Then toUIMessageStream<BedrockUIMessage>(response, { sendTrace: true }) on the server and useChat<BedrockUIMessage>() on the client.

Files, and why you may not want them

file chunks require a URL and Bedrock hands over raw bytes, so bytes become data: URLs. Those persist into message history and get re-POSTed on every subsequent turn. If your agent's code interpreter emits megabyte-scale output, turn it off and upload yourself:

const stream = toUIMessageStream(response, {
  sendFiles: false,
  onEvent: async (event) => {
    for (const file of event.files?.files ?? []) {
      if (file.bytes === undefined) continue;
      await uploadToS3(file.name ?? 'untitled', file.bytes);
    }
  },
});

onEvent is the general escape hatch for anything this library does not map: metering token usage from modelInvocationOutput, structured logging, custom trace handling. A thrown error or rejected promise there is swallowed — an observer must not be able to corrupt the answer stream.

AgentCore is out of scope, and no general adapter can exist

InvokeAgent returns a modeled union: 14 typed members, a generated visitor, and typed error classes. That is a schema, and it can be faithfully translated. This package does exactly that.

InvokeAgentRuntime returns response?: StreamingBlobPayloadOutputTypesopaque bytes, whose documented contract is that the format of this data depends on the specific agent configuration and the requested accept type. No union, no members, no error taxonomy. The string text/event-stream does not appear anywhere in the package. You send arbitrary bytes to a container you wrote and get arbitrary bytes back.

There is no schema to adapt. The two APIs cannot share an adapter because there is no common type to write a function against. An AgentCore entry point would be a thin wrapper around a decode callback you have to write anyway — a public surface whose correctness depends on a convention this library cannot see, test or version.

If your AgentCore container already emits AI SDK UI message chunks, there is nothing to translate:

return createUIMessageStreamResponse({ stream: yourAgentCoreStream });

bedrock-ui-stream/agentcore is reserved in the exports map and deliberately unimplemented. If demand appears it will ship with a required decode option under a different name — createAgentCoreStream, because "translate a known event type" is not what it would do.

Known assumptions

Flagged rather than buried, because they are unverified against production traffic.

Citation span offsets are assumed cumulative. AWS documents Span.start/end only as "where the text with a citation starts/ends in the generated output". This library reads that as offsets into the full generated response and tracks a running character offset accordingly. Both readings are recoverable from providerMetadata.bedrock: span is the raw value untouched, resolvedSpan is the cumulative interpretation, and chunkTextOffset is where the carrying chunk's text began — add it to span.start for the chunk-relative reading instead.

chunk.attribution may or may not accompany bytes. Both shapes are handled: a chunk with both emits text then sources; an attribution-only chunk emits sources against the current offset. Nothing requires one shape.

If you can confirm or refute either from production traffic, an issue saying so is one of the most useful contributions available.

Stability

Semver-protected: the five exported values and the public types; option names and defaults; BedrockUIStreamError codes; the BedrockReturnControlMetadata shape (it round-trips through persisted history, so it is a wire format); the data-bedrock-trace part name; and the guarantee that toUIMessageStream never throws after returning.

Explicitly internal: anything not exported from the root; deep import paths (blocked at module resolution — no wildcard subpaths in exports); the exact errorText prose (the taxonomy is stable, the wording is not — do not regex it); the source-url vs source-document heuristics, mediaType guessing and FunctionParameter.type coercion; and the contents of providerMetadata.bedrock.

Peers, not dependencies: ai@^7 and @aws-sdk/client-bedrock-agent-runtime. Two copies of either in one build would be a real bug. A future ai@8 ships as a new major here, not a widened range.

Note for CommonJS consumers. ai@7 is ESM-only. Both peers are loaded through a lazy dynamic import(), so require('bedrock-ui-stream') works everywhere and toUIMessageStream / toInvokeAgentInput need no ESM interop at all. Only createAgentRouteHandler touches the peers, and it is already async.

Examples

| Example | What it shows | | --- | --- | | nextjs-route-handler | The smallest complete endpoint, plus the useChat client. | | return-control | A full tool round trip: server, action group implementation, and the useChat half. | | traces-and-citations | Reasoning, live traces and knowledge-base citations, with a typed trace data part. |

Every example is executed by the test suite, so it cannot drift from the library.

Contributing

Issues and pull requests are welcome, including from first-time contributors — see CONTRIBUTING.md for setup, layout and what a good PR looks like. Production reports about the two known assumptions are especially valuable, because they cannot be resolved from the AWS documentation alone.

License

MIT © Rizwan Saleem. See LICENSE.