hashbrown

Amazon Bedrock

Install the Bedrock adapter, the AWS Bedrock Runtime SDK, and the AG-UI SSE encoder:

npm install @hashbrownai/bedrock @aws-sdk/client-bedrock-runtime @ag-ui/core @ag-ui/encoder

Streaming Text Responses

HashbrownBedrock.stream.text(options) accepts an AG-UI RunAgentInput and returns an AsyncIterable<AGUIEvent>. The adapter uses the AWS SDK's ConverseStream API. Encode its events as AG-UI SSE at your HTTP boundary.

The model or inference profile is server configuration and is not read from the client run input.

API Reference

NameTypeDescription
modelstringServer-selected Bedrock model or inference profile ID.
inputBedrockHashbrownRunAgentInputAG-UI run input, including messages and tools.
clientOptionsBedrockRuntimeClientConfig(Optional) AWS SDK client configuration. Mutually exclusive with client.
clientBedrockRuntimeClient(Optional) Reusable AWS SDK client. Mutually exclusive with clientOptions.
signalAbortSignal(Optional) Cancels the Bedrock request when the HTTP client disconnects.
activitiesActivityCallback(Optional) Sends activities alongside the model's output. See Sending Activities.
transformRequestOptions(params) => params | Promise<params>(Optional) Transforms the final Bedrock ConverseStreamCommandInput.

The adapter maps system and developer instructions, message history, tools, tool results, and native structured output. Bedrock reasoning text, signatures, and redacted reasoning become AG-UI reasoning records that can be sent back on continuation. Citations, images, response metadata, and unsupported native blocks are preserved as AG-UI RAW events. Provider and mapping failures are emitted as RUN_ERROR events.

Set input.forwardedProps.hashbrown.responseSchema to use Bedrock native structured output on a model that supports it:

const input = {
  ...runInput,
  forwardedProps: {
    hashbrown: {
      responseSchema: {
        type: 'object',
        properties: {
          answer: { type: 'string' },
        },
        required: ['answer'],
        additionalProperties: false,
      },
    },
  },
};

Node.js Server Integration

import type { RunAgentInput } from '@ag-ui/core';
import { EventEncoder } from '@ag-ui/encoder';
import { HashbrownBedrock } from '@hashbrownai/bedrock';
import express from 'express';

const app = express();
app.use(express.json());

app.post('/run', async (req, res) => {
  const abortController = new AbortController();
  req.once('aborted', () => abortController.abort());
  res.once('close', () => abortController.abort());
  const stream = HashbrownBedrock.stream.text({
    clientOptions: { region: process.env.AWS_REGION ?? 'us-east-1' },
    model: process.env.BEDROCK_MODEL_ID!,
    input: req.body as RunAgentInput,
    signal: abortController.signal,
  });
  const encoder = new EventEncoder();

  res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
  res.header('Content-Type', encoder.getContentType());
  res.header('Connection', 'keep-alive');
  res.flushHeaders();

  for await (const event of stream) {
    res.write(encoder.encodeSSE(event));
  }

  if (!res.writableEnded) {
    res.end();
  }
});

app.listen(3000);
import type { RunAgentInput } from '@ag-ui/core';
import { EventEncoder } from '@ag-ui/encoder';
import { HashbrownBedrock } from '@hashbrownai/bedrock';
import Fastify from 'fastify';

const fastify = Fastify();

fastify.post('/run', async (request, reply) => {
  const abortController = new AbortController();
  request.raw.once('aborted', () => abortController.abort());
  reply.raw.once('close', () => abortController.abort());
  const stream = HashbrownBedrock.stream.text({
    clientOptions: { region: process.env.AWS_REGION ?? 'us-east-1' },
    model: process.env.BEDROCK_MODEL_ID!,
    input: request.body as RunAgentInput,
    signal: abortController.signal,
  });
  const encoder = new EventEncoder();

  reply.header('Cache-Control', 'no-cache, no-store, must-revalidate');
  reply.header('Content-Type', encoder.getContentType());
  reply.header('Connection', 'keep-alive');

  for await (const event of stream) {
    reply.raw.write(encoder.encodeSSE(event));
  }

  if (!reply.raw.writableEnded) {
    reply.raw.end();
  }
});

fastify.listen({ port: 3000 });
import type { RunAgentInput } from '@ag-ui/core';
import { EventEncoder } from '@ag-ui/encoder';
import { Body, Controller, Post, Req, Res } from '@nestjs/common';
import { HashbrownBedrock } from '@hashbrownai/bedrock';
import type { Request, Response } from 'express';

@Controller()
export class RunController {
  @Post('run')
  async run(
    @Body() input: RunAgentInput,
    @Req() req: Request,
    @Res() res: Response,
  ) {
    const abortController = new AbortController();
    req.once('aborted', () => abortController.abort());
    res.once('close', () => abortController.abort());
    const stream = HashbrownBedrock.stream.text({
      clientOptions: { region: process.env.AWS_REGION ?? 'us-east-1' },
      model: process.env.BEDROCK_MODEL_ID!,
      input,
      signal: abortController.signal,
    });
    const encoder = new EventEncoder();

    res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
    res.header('Content-Type', encoder.getContentType());
    res.header('Connection', 'keep-alive');
    res.flushHeaders();

    for await (const event of stream) {
      res.write(encoder.encodeSSE(event));
    }

    if (!res.writableEnded) {
      res.end();
    }
  }
}
import type { RunAgentInput } from '@ag-ui/core';
import { EventEncoder } from '@ag-ui/encoder';
import { HashbrownBedrock } from '@hashbrownai/bedrock';
import { Hono } from 'hono';

const app = new Hono();

app.post('/run', async (c) => {
  const input = (await c.req.json()) as RunAgentInput;
  const stream = HashbrownBedrock.stream.text({
    clientOptions: { region: process.env.AWS_REGION ?? 'us-east-1' },
    model: process.env.BEDROCK_MODEL_ID!,
    input,
    signal: c.req.raw.signal,
  });
  const encoder = new EventEncoder();
  const textEncoder = new TextEncoder();

  return new Response(
    new ReadableStream({
      async start(controller) {
        for await (const event of stream) {
          controller.enqueue(textEncoder.encode(encoder.encodeSSE(event)));
        }
        controller.close();
      },
    }),
    {
      headers: {
        'Cache-Control': 'no-cache, no-store, must-revalidate',
        'Content-Type': encoder.getContentType(),
      },
    },
  );
});

export default app;

AWS Credentials and Client Reuse

clientOptions uses the AWS SDK default credential provider chain, including environment variables, shared AWS config, ECS task roles, EC2 instance roles, and web identity credentials.

const stream = HashbrownBedrock.stream.text({
  clientOptions: {
    region: 'us-east-1',
  },
  model: process.env.BEDROCK_MODEL_ID!,
  input,
});

Pass a configured BedrockRuntimeClient through client when you need to reuse connections, retries, or middleware. Hashbrown destroys clients it creates from clientOptions; it never destroys a client supplied through client.

Sending Activities

Pass activities to send AG-UI activities alongside the model's output. Activities are display-only progress, such as a plan or a progress report, and the model never sees them. See Activities for how the client renders them.

import { HashbrownBedrock } from '@hashbrownai/bedrock';

const stream = HashbrownBedrock.stream.text({
  clientOptions: { region: process.env.AWS_REGION ?? 'us-east-1' },
  model: process.env.BEDROCK_MODEL_ID!,
  input,
  signal,
  activities: async ({ activity, model }) => {
    const plan = activity.snapshot(`plan-${input.runId}`, 'plan', {
      steps: ['Read the question', 'Answer'],
      done: 0,
    });

    await model;

    plan.patch([{ op: 'replace', path: '/done', value: 2 }]);
  },
});

The callback receives an activity writer, a model promise, and the run's signal:

  • activity.snapshot(id, activityType, content) sends an ACTIVITY_SNAPSHOT and returns a handle. The handle's patch(operations) sends an ACTIVITY_DELTA with RFC 6902 JSON Patch operations, and replace(content) sends a new snapshot.
  • Activity ids must be unique within the thread. Reusing an id from an earlier turn updates that turn's activity in place, so the example builds the id from input.runId to give each run its own plan.
  • model resolves when the model stream ends, before the run finishes, and rejects if the model call fails.
  • Activity events and model events are sent in the order they happen. The callback starts after RUN_STARTED, and the terminal event waits until the callback settles. Model text streams as it arrives and is never held back.
  • If the callback throws while the model is streaming, Hashbrown aborts the model and ends the run with one RUN_ERROR. If it throws after the model has finished, RUN_ERROR replaces RUN_FINISHED. If the model fails too, the model's error wins.
  • When the client disconnects, signal aborts. No terminal event is sent, and writes become no-ops.
  • A write after the run has closed throws, as does a second snapshot that reuses an id with a different activityType.

Transform Request Options

Use transformRequestOptions for provider-owned inference settings, guardrails, request metadata, performance configuration, or a forced tool choice.

const stream = HashbrownBedrock.stream.text({
  clientOptions: { region: 'us-east-1' },
  model: process.env.BEDROCK_MODEL_ID!,
  input,
  transformRequestOptions: (options) => ({
    ...options,
    inferenceConfig: {
      maxTokens: 2048,
      temperature: 0.2,
    },
    requestMetadata: {
      application: 'my-app',
    },
  }),
});

};


### Node.js Server Integration

<hb-backend-code-example>

<div backend="express">

```ts
import type { RunAgentInput } from '@ag-ui/core';
import { EventEncoder } from '@ag-ui/encoder';
import { HashbrownBedrock } from '@hashbrownai/bedrock';
import express from 'express';

const app = express();
app.use(express.json());

app.post('/run', async (req, res) => {
  const abortController = new AbortController();
  req.once('aborted', () => abortController.abort());
  res.once('close', () => abortController.abort());
  const stream = HashbrownBedrock.stream.text({
    clientOptions: { region: process.env.AWS_REGION ?? 'us-east-1' },
    model: process.env.BEDROCK_MODEL_ID!,
    input: req.body as RunAgentInput,
    signal: abortController.signal,
  });
  const encoder = new EventEncoder();

  res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
  res.header('Content-Type', encoder.getContentType());
  res.header('Connection', 'keep-alive');
  res.flushHeaders();

  for await (const event of stream) {
    res.write(encoder.encodeSSE(event));
  }

  if (!res.writableEnded) {
    res.end();
  }
});

app.listen(3000);
import type { RunAgentInput } from '@ag-ui/core';
import { EventEncoder } from '@ag-ui/encoder';
import { HashbrownBedrock } from '@hashbrownai/bedrock';
import Fastify from 'fastify';

const fastify = Fastify();

fastify.post('/run', async (request, reply) => {
  const abortController = new AbortController();
  request.raw.once('aborted', () => abortController.abort());
  reply.raw.once('close', () => abortController.abort());
  const stream = HashbrownBedrock.stream.text({
    clientOptions: { region: process.env.AWS_REGION ?? 'us-east-1' },
    model: process.env.BEDROCK_MODEL_ID!,
    input: request.body as RunAgentInput,
    signal: abortController.signal,
  });
  const encoder = new EventEncoder();

  reply.header('Cache-Control', 'no-cache, no-store, must-revalidate');
  reply.header('Content-Type', encoder.getContentType());
  reply.header('Connection', 'keep-alive');

  for await (const event of stream) {
    reply.raw.write(encoder.encodeSSE(event));
  }

  if (!reply.raw.writableEnded) {
    reply.raw.end();
  }
});

fastify.listen({ port: 3000 });
import type { RunAgentInput } from '@ag-ui/core';
import { EventEncoder } from '@ag-ui/encoder';
import { Body, Controller, Post, Req, Res } from '@nestjs/common';
import { HashbrownBedrock } from '@hashbrownai/bedrock';
import type { Request, Response } from 'express';

@Controller()
export class RunController {
  @Post('run')
  async run(
    @Body() input: RunAgentInput,
    @Req() req: Request,
    @Res() res: Response,
  ) {
    const abortController = new AbortController();
    req.once('aborted', () => abortController.abort());
    res.once('close', () => abortController.abort());
    const stream = HashbrownBedrock.stream.text({
      clientOptions: { region: process.env.AWS_REGION ?? 'us-east-1' },
      model: process.env.BEDROCK_MODEL_ID!,
      input,
      signal: abortController.signal,
    });
    const encoder = new EventEncoder();

    res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
    res.header('Content-Type', encoder.getContentType());
    res.header('Connection', 'keep-alive');
    res.flushHeaders();

    for await (const event of stream) {
      res.write(encoder.encodeSSE(event));
    }

    if (!res.writableEnded) {
      res.end();
    }
  }
}
import type { RunAgentInput } from '@ag-ui/core';
import { EventEncoder } from '@ag-ui/encoder';
import { HashbrownBedrock } from '@hashbrownai/bedrock';
import { Hono } from 'hono';

const app = new Hono();

app.post('/run', async (c) => {
  const input = (await c.req.json()) as RunAgentInput;
  const stream = HashbrownBedrock.stream.text({
    clientOptions: { region: process.env.AWS_REGION ?? 'us-east-1' },
    model: process.env.BEDROCK_MODEL_ID!,
    input,
    signal: c.req.raw.signal,
  });
  const encoder = new EventEncoder();
  const textEncoder = new TextEncoder();

  return new Response(
    new ReadableStream({
      async start(controller) {
        for await (const event of stream) {
          controller.enqueue(textEncoder.encode(encoder.encodeSSE(event)));
        }
        controller.close();
      },
    }),
    {
      headers: {
        'Cache-Control': 'no-cache, no-store, must-revalidate',
        'Content-Type': encoder.getContentType(),
      },
    },
  );
});

export default app;

AWS Credentials and Client Reuse

clientOptions uses the AWS SDK default credential provider chain, including environment variables, shared AWS config, ECS task roles, EC2 instance roles, and web identity credentials.

const stream = HashbrownBedrock.stream.text({
  clientOptions: {
    region: 'us-east-1',
  },
  model: process.env.BEDROCK_MODEL_ID!,
  input,
});

Pass a configured BedrockRuntimeClient through client when you need to reuse connections, retries, or middleware. Hashbrown destroys clients it creates from clientOptions; it never destroys a client supplied through client.

Transform Request Options

Use transformRequestOptions for provider-owned inference settings, guardrails, request metadata, performance configuration, or a forced tool choice.

const stream = HashbrownBedrock.stream.text({
  clientOptions: { region: 'us-east-1' },
  model: process.env.BEDROCK_MODEL_ID!,
  input,
  transformRequestOptions: (options) => ({
    ...options,
    inferenceConfig: {
      maxTokens: 2048,
      temperature: 0.2,
    },
    requestMetadata: {
      application: 'my-app',
    },
  }),
});
Amazon BedrockStreaming Text ResponsesAPI ReferenceNode.js Server IntegrationAWS Credentials and Client ReuseSending ActivitiesTransform Request OptionsAWS Credentials and Client ReuseTransform Request Options