DEV Community

Dinesh_gowtham
Dinesh_gowtham

Posted on

Supercharging AI Pair Programming with Bedrock AgentCore Web Search (Explained Simply)

When your AI coding assistant can’t find the latest library API, it stalls. Bedrock’s new AgentCore Web Search API lets Claude pull fresh docs on the fly, turning autocomplete into real‑time research. Here’s how to wire it into a Lambda‑hosted pair programmer.

Why Live Web Search Changes AI Coding

LLMs (large language models) are amazing at pattern matching, but they only know what they were trained on. Think of a model as a well‑read textbook that stopped being updated in 2023. If a new version of an npm package ships a brand‑new method, the textbook has no page for it. The assistant either guesses (often wrong) or asks you for clarification.

Live web search is like giving the textbook a quick trip to the library every time it hits a question it can’t answer. The model asks the web, gets the most recent documentation, and then continues generating code with that fresh knowledge. The result is:

  • Fewer dead‑ends – the assistant can complete a request without you stepping in.
  • Higher confidence – the model can cite the exact URL it used, so you can verify it.
  • Better productivity – you spend time writing code, not hunting docs.

In plain English: Adding web search turns a static knowledge source into a dynamic research partner.

Tiny demo: fetching a doc page with the AgentCore tool

Below is the smallest possible call that asks Claude to perform a web search. We only ask for the raw HTML; later we’ll trim it down.

import {
  BedrockRuntimeClient,
  InvokeModelCommand,
} from "@aws-sdk/client-bedrock-runtime";

const client = new BedrockRuntimeClient({ region: "us-east-1" });

async function searchDocs(query: string) {
  // The payload follows Bedrock’s tool‑calling format.
  const payload = {
    // The model we want to talk to.
    modelId: "anthropic.claude-v2",
    // The prompt that tells Claude to use the web‑search tool.
    prompt: `Search for the official API reference of "${query}" and return the raw HTML.`,
    // Enable the AgentCore Web Search tool.
    toolConfig: {
      enabledTools: [{ name: "web-search" }],
    },
  };

  const command = new InvokeModelCommand(payload);
  const response = await client.send(command);
  // The model returns a JSON string; we parse it to get the HTML.
  const result = JSON.parse(Buffer.from(response.body).toString());
  return result.output; // raw HTML string
}
Enter fullscreen mode Exit fullscreen mode

Tip: The Bedrock token limit is counted per minute across the whole account, not per request. If many developers run this at once, you may hit a rate limit even though each call looks small.

Enabling Bedrock AgentCore Web Search in Your Model Call

The AgentCore Web Search tool is a tool that the model can invoke during a single invokeModel request. Think of a tool as a side‑kick the model calls when it needs extra data. To make it work you must:

  1. Declare the tool in toolConfig.enabledTools.
  2. Set stream to true if you want to receive partial answers as they arrive (useful for long docs).
  3. Parse the SSE (Server‑Sent Events) stream yourself—Node.js doesn’t do it automatically.

Below is a complete example that streams Claude’s answer, extracts the data: lines, and builds the HTML string.

import {
  BedrockRuntimeClient,
  InvokeModelCommand,
} from "@aws-sdk/client-bedrock-runtime";
import { Readable } from "stream";

const client = new BedrockRuntimeClient({ region: "us-east-1" });

async function invokeWithSearch(query: string): Promise<string> {
  const command = new InvokeModelCommand({
    modelId: "anthropic.claude-v2",
    // Prompt that tells Claude we want it to search and give us the result.
    prompt: `Please use the web‑search tool to find the official documentation for "${query}". Return ONLY the raw HTML of the page.`,
    toolConfig: {
      enabledTools: [{ name: "web-search" }],
    },
    // Request a streaming response so we can start processing early.
    accept: "application/json",
    contentType: "application/json",
    // Bedrock requires the `stream` flag for SSE.
    stream: true,
  });

  const response = await client.send(command);
  const stream = response.body as Readable;

  // Accumulate the HTML pieces as they arrive.
  let html = "";
  for await (const chunk of stream) {
    // Each SSE chunk looks like: "data: {\"output\":\"...\"}"
    const text = chunk.toString();
    const match = text.match(/data:\s*(\{.*\})/);
    if (match) {
      const payload = JSON.parse(match[1]);
      html += payload.output; // concatenate partial HTML
    }
  }

  return html; // full raw HTML from the searched page
}
Enter fullscreen mode Exit fullscreen mode

Key takeaway: Streaming means you have to read each data: line yourself, much like turning the pages of a book one at a time instead of getting the whole book in a single box.

Building a Claude‑Powered Pair Programmer Lambda

Now we put everything together inside an AWS Lambda function. The Lambda receives a JSON payload like:

{
  "package": "axios",
  "signature": "async function getUser(id: string): Promise<User>"
}
Enter fullscreen mode Exit fullscreen mode

The flow is:

  1. Build a search query ("axios latest API reference").
  2. Call invokeWithSearch to fetch the raw HTML of the docs page.
  3. Strip the HTML down to the exact method description using a tiny parser.
  4. Prompt Claude again with the cleaned snippet and the desired function signature.
  5. Return the generated TypeScript code.

Below is the full handler. Every line is commented so a newcomer can follow.

import {
  BedrockRuntimeClient,
  InvokeModelCommand,
} from "@aws-sdk/client-bedrock-runtime";
import {
  LambdaClient,
  InvokeCommand,
} from "@aws-sdk/client-lambda";
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { JSDOM } from "jsdom"; // tiny HTML parser (adds ~30 KB to the bundle)

// ---- 1️⃣  Create Bedrock client (shared across invocations) ----
const bedrock = new BedrockRuntimeClient({ region: "us-east-1" });

// ---- 2️⃣  Helper: ask Claude to search the web and give us raw HTML ----
async function fetchDocHtml(packageName: string): Promise<string> {
  const query = `${packageName} npm API reference`;
  const command = new InvokeModelCommand({
    modelId: "anthropic.claude-v2",
    prompt: `Use the web‑search tool to locate the official documentation for "${query}". Return ONLY the raw HTML.`,
    toolConfig: { enabledTools: [{ name: "web-search" }] },
    accept: "application/json",
    contentType: "application/json",
    stream: true,
  });

  const response = await client.send(command);
  const stream = response.body as Readable;
  let html = "";
  for await (const chunk of stream) {
    const m = chunk.toString().match(/data:\s*(\{.*\})/);
    if (m) html += JSON.parse(m[1]).output;
  }
  return html;
}

// ---- 3️⃣  Helper: extract the relevant API block from HTML ----
function extractApiSection(html: string, signature: string): string {
  const dom = new JSDOM(html);
  const document = dom.window.document;

  // Simple heuristic: find the <code> block that contains the method name.
  const methodName = signature.split("function")[1].trim().split("(")[0];
  const codeBlocks = Array.from(document.querySelectorAll("code"));
  const matching = codeBlocks.find((c) => c.textContent?.includes(methodName));

  // Return the surrounding text so Claude has context.
  return matching?.parentElement?.textContent?.trim() ?? "";
}

// ---- 4️⃣  Helper: ask Claude to generate the implementation ----
async function generateImplementation(
  packageName: string,
  signature: string,
  apiSnippet: string
): Promise<string> {
  const prompt = `
You are a helpful TypeScript developer. Using the "${packageName}" library, implement the following function:

\`\`\`typescript
${signature}
\`\`\`

Here is the relevant part of the official documentation (only include what you need):

""" 
${apiSnippet}
"""

Write only the function body (no extra explanations).`;
  const command = new InvokeModelCommand({
    modelId: "anthropic.claude-v2",
    prompt,
    accept: "application/json",
    contentType: "application/json",
  });

  const resp = await bedrock.send(command);
  const result = JSON.parse(Buffer.from(resp.body).toString());
  return result.output; // generated TypeScript code
}

// ---- 5️⃣  Lambda entry point ----
export const handler = async (
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
  try {
    // Parse incoming JSON.
    const body = JSON.parse(event.body ?? "{}");
    const { package: pkg, signature } = body;

    // Step 1: fetch raw HTML docs.
    const html = await fetchDocHtml(pkg);

    // Step 2: pull out the exact API description.
    const apiSnippet = extractApiSection(html, signature);

    // Guard against empty snippets (common gotcha).
    if (!apiSnippet) {
      throw new Error(
        "Could not locate the requested method in the fetched documentation."
      );
    }

    // Step 3: ask Claude to write the code.
    const code = await generateImplementation(pkg, signature, apiSnippet);

    // Return the generated code as JSON.
    return {
      statusCode: 200,
      body: JSON.stringify({ code }),
      headers: { "Content-Type": "application/json" },
    };
  } catch (err) {
    // Log the error so CloudWatch can capture it (see next section).
    console.error("Pair programmer error:", err);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: (err as Error).message }),
      headers: { "Content-Type": "application/json" },
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

Important gotchas for this Lambda:

  • Node 22 ESM – If you upgrade to the newest runtime, require('esm') stops working inside Lambda layers. Use native import statements instead.
  • SnapStart + VPC – Turning on SnapStart does not help when the function lives inside a VPC; the cold start time is spent attaching the ENI, not initializing the runtime.
  • Provisioned Concurrency – Enables fast starts but charges even when idle; watch the bill.

Tip: Keep the Lambda package under 50 MB (the default limit) by only pulling in jsdom for HTML parsing. Larger bundles cause slower cold starts.

Prompt Strategies for Reliable Search‑Driven Generation

Even with a clean HTML snippet, Claude can still wander if the prompt is vague. Here are three habits that keep the model focused:

  1. State the role clearly – “You are a helpful TypeScript developer.” This sets the tone.
  2. Show the exact signature – Use a fenced code block so Claude sees the exact shape you need.
  3. Limit the context – Provide only the snippet that mentions the method, not the whole page. The earlier gotcha about feeding raw HTML caused hallucinations because the model tried to interpret navigation menus, ads, or unrelated sections.

Putting the strategy into code

function buildPrompt(
  pkg: string,
  signature: string,
  apiSnippet: string
): string {
  // 1️⃣ Declare role
  const role = "You are a helpful TypeScript developer.";

  // 2️⃣ Show signature in a fenced block
  const sigBlock = `\`\`\`typescript\n${signature}\n\`\`\``;

  // 3️⃣ Give a tiny, focused doc excerpt
  const docBlock = `""" \n${apiSnippet}\n"""`;

  // Assemble the final prompt.
  return `${role}
Using the "${pkg}" library, implement the function below.

${sigBlock}

Relevant documentation:

${docBlock}

Write only the implementation (no extra commentary).`;
}
Enter fullscreen mode Exit fullscreen mode

When you replace the earlier prompt string in generateImplementation with a call to buildPrompt, the model’s output becomes shorter and more accurate.

In plain English: Think of the prompt as a recipe. List the ingredients (role, signature, docs) and tell the model exactly what dish (code) you want, and you’ll get a cleaner result.

Testing and Observability with CloudWatch Logs

A pair‑programmer that silently returns the wrong code is more trouble than it’s worth. Use CloudWatch (AWS’s log aggregation service) to capture three things:

What to log Why it matters Example code
Incoming request payload Verify the caller sent the correct package and signature. console.log("Request:", body);
Extracted API snippet Ensure the HTML parsing succeeded before sending to Claude. console.log("API snippet:", apiSnippet.slice(0, 200));
Claude’s raw response Detect if the model returned a “I don’t know” fallback. console.log("Claude output:", code);

Add those logs right after each step in the Lambda handler (already shown in the try block). For more structured monitoring, you can emit custom metrics with the CloudWatch PutMetricData API, for example:

import { CloudWatchClient, PutMetricDataCommand } from "@aws-sdk/client-cloudwatch";

const cw = new CloudWatchClient({ region: "us-east-1" });

async function recordMetric(name: string, value: number) {
  const cmd = new PutMetricDataCommand({
    Namespace: "AI-PairProgrammer",
    MetricData: [{ MetricName: name, Value: value, Unit: "Count" }],
  });
  await cw.send(cmd);
}

// Use it in the handler:
await recordMetric("SuccessfulInvocations", 1);
Enter fullscreen mode Exit fullscreen mode

Gotchas to keep in mind:

  • Rate limits on CloudWatch PutMetricData – The service caps at 150 TPS per account. If you fire many Lambdas at once, batch the calls.
  • Cross‑region latency – If your Lambda runs in eu-west-2 but CloudWatch metrics are sent to us-east-1, you’ll see extra milliseconds. Keep both in the same region for low latency.

Key takeaway: Observability is cheap, but it saves hours of debugging when the web‑search step returns an unexpected page.

The Takeaway

  • Live web search turns a static language model into a research assistant that can fetch up‑to‑date API docs on demand.
  • Enabling the AgentCore Web Search tool requires declaring it in toolConfig and handling the streaming SSE response yourself.
  • A TypeScript Lambda can orchestrate the whole flow: search → HTML extraction → focused prompt → code generation.
  • Keep prompts tight: role, exact signature, and a minimal documentation excerpt to avoid hallucinations.
  • Log the request, the extracted snippet, and Claude’s answer; optionally push custom CloudWatch metrics for health checks.
  • Remember the service‑specific gotchas (Bedrock token‑per‑minute limits, Lambda SnapStart + VPC, streaming SSE parsing) to avoid surprise failures or unexpected costs.

Transparency notice

This article was written with the help of an AI system — Groq (GPT OSS 120B).

Published: 2026-11-10 · Primary focus: Bedrock

All code blocks are intended to be correct and runnable, but please verify them
against the official docs for the tools mentioned before using in production.

Find an error? Drop a comment — corrections are always welcome.

Top comments (0)