DEV Community

Dinesh_gowtham
Dinesh_gowtham

Posted on

CodeWhisperer Contextual Suggestions Explained: How the AI Knows Your Codebase and How to Customize It

Ever wondered why AWS CodeWhisperer can suggest a perfect one‑liner before you finish typing? The secret lies in how it builds a lightweight context window from your repository and caches prompts. This post pulls back the curtain and shows you how to make those suggestions even smarter.


What CodeWhisperer’s Context Window Really Is

When CodeWhisperer offers a completion, it isn’t guessing in the dark. It first builds a context window – a small collection of source files that it thinks are relevant to the line you’re typing. Think of the context window like a librarian who, before answering a question, pulls the most useful books from the shelves and puts them on a table for quick reference. The librarian doesn’t bring the whole library, only the handful of titles that match the query.

In CodeWhisperer’s world, the “books” are the files that live in your repository, and the “table” is a JSON payload that is sent to the model. The model reads that payload, learns the names, shapes, and patterns of the code you have, then writes the next line.

In plain English: The context window is a tiny snapshot of your code that the AI uses to make its suggestion.

How the window is built

  1. File discovery – Walk the directory tree.
  2. Ignore rules – Skip anything listed in .gitignore.
  3. Size limit – Stop when the accumulated size reaches the service limit (currently ~ 10 KB of text).

Below is a minimal Node.js helper that implements those steps. It uses only the built‑in fs module and a tiny .gitignore parser so you can see the logic clearly.

// src/contextBuilder.ts
import { createReadStream, readdirSync, statSync, readFileSync } from "fs";
import { join, extname } from "path";
import ignore from "ignore";

/**
 * Reads .gitignore (if present) and returns a function that tells
 * you whether a path should be skipped.
 */
function getIgnoreFilter(repoRoot: string) {
  const gitignorePath = join(repoRoot, ".gitignore");
  const ig = ignore();
  try {
    const content = readFileSync(gitignorePath, "utf-8");
    ig.add(content);
  } catch {
    // No .gitignore – nothing to ignore
  }
  return (filePath: string) => ig.ignores(filePath.replace(repoRoot + "/", ""));
}

/**
 * Recursively walks a directory and collects file paths
 * that match the ignore filter and are under the size limit.
 */
export function buildContextWindow(
  repoRoot: string,
  maxBytes = 10 * 1024 // 10 KB
): { files: Record<string, string>; totalBytes: number } {
  const ignoreFilter = getIgnoreFilter(repoRoot);
  const files: Record<string, string> = {};
  let totalBytes = 0;

  function walk(dir: string) {
    for (const entry of readdirSync(dir)) {
      const fullPath = join(dir, entry);
      const relPath = fullPath.replace(repoRoot + "/", "");
      const stats = statSync(fullPath);

      // Skip directories
      if (stats.isDirectory()) continue;

      // Respect .gitignore
      if (ignoreFilter(fullPath)) continue;

      // Only consider source files (you can expand this list)
      if (![".js", ".ts", ".java", ".py"].includes(extname(entry))) continue;

      const content = readFileSync(fullPath, "utf-8");
      const newSize = totalBytes + Buffer.byteLength(content, "utf-8");

      // Stop adding files once we hit the limit
      if (newSize > maxBytes) return;

      files[relPath] = content;
      totalBytes = newSize;
    }
  }

  walk(repoRoot);
  return { files, totalBytes };
}
Enter fullscreen mode Exit fullscreen mode

What the code does

  • Reads .gitignore (if it exists) and builds a filter.
  • Walks the repository, ignoring directories, non‑source files, and anything the filter says to skip.
  • Stops once the accumulated byte count exceeds the limit, ensuring the payload stays small enough for the API.

Now you have a concrete “snapshot” that you can hand to CodeWhisperer.


How Prompt Caching Boosts Latency and Accuracy

Sending the same set of files to the model for every keystroke would be wasteful. Imagine a teacher who re‑writes the same lecture notes for every student question—slow and redundant. CodeWhisperer solves this with prompt caching: it stores the JSON representation of the context window in a durable location (usually Amazon S3) and re‑uses it for many suggestion requests.

Why does this help?

  • Faster round‑trips – The service can fetch the cached prompt with a single, cheap S3 GET instead of rebuilding the whole snapshot.
  • Consistent suggestions – As long as the cache reflects the latest code, the model sees the same context each time, reducing jitter in completions.

Tip: Keep the cache key (the S3 object name) tied to a git commit SHA or a timestamp. That way you know when the cache is stale and needs refreshing.

Uploading a prompt to S3

The following snippet shows how to turn the context window into a JSON string and store it in an S3 bucket. It uses the official AWS SDK for JavaScript v3 (@aws-sdk/client-s3).

// src/promptUploader.ts
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { buildContextWindow } from "./contextBuilder";

const s3 = new S3Client({ region: "us-east-1" });

/**
 * Packages the context window as JSON and uploads it.
 *
 * @param repoRoot  Root folder of the local repository.
 * @param bucket    Destination S3 bucket.
 * @param key       Object key (e.g., `codewhisperer/prompts/commit-abc123.json`).
 */
export async function cachePromptInS3(
  repoRoot: string,
  bucket: string,
  key: string
): Promise<string> {
  const { files, totalBytes } = buildContextWindow(repoRoot);
  const payload = JSON.stringify({ files, totalBytes });

  const command = new PutObjectCommand({
    Bucket: bucket,
    Key: key,
    Body: payload,
    ContentType: "application/json",
  });

  await s3.send(command);
  // Return a URI that CodeWhisperer can understand
  return `s3://${bucket}/${key}`;
}
Enter fullscreen mode Exit fullscreen mode

Key points

  • The PutObjectCommand writes the JSON payload to S3.
  • The function returns an S3 URI (s3://bucket/key) which you will pass to CodeWhisperer later.
  • Because we only upload once per commit, subsequent suggestion requests can reuse the same URI, dramatically cutting latency.

Extending the Context with Custom Files on S3

Sometimes the code you need for good suggestions lives outside the local repo. Generated client SDKs, shared protobuf files, or a team‑wide utilities library might be stored centrally in an S3 bucket. You can enrich the context window by pulling those extra files into the same JSON payload.

Why blend local and remote files?

  • Coverage – The model sees the definitions it would otherwise miss, preventing “unknown type” suggestions.
  • Control – You decide exactly which shared artifacts are visible, avoiding accidental exposure of internal secrets.

Merging remote snippets

Below is a helper that reads a list of S3 object keys, downloads their contents, and merges them into the existing context before uploading the final prompt.

// src/remoteMerger.ts
import {
  S3Client,
  GetObjectCommand,
  GetObjectCommandOutput,
} from "@aws-sdk/client-s3";
import { Readable } from "stream";
import { cachePromptInS3 } from "./promptUploader";
import { buildContextWindow } from "./contextBuilder";

const s3 = new S3Client({ region: "us-east-1" });

/**
 * Turns a streaming body into a string.
 */
function streamToString(stream: Readable): Promise<string> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
    stream.on("error", reject);
    stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
  });
}

/**
 * Downloads extra files from S3 and adds them to the local context.
 *
 * @param repoRoot   Local repository root.
 * @param bucket     S3 bucket holding the extra files.
 * @param extraKeys  Array of object keys to download (e.g., `shared/types.proto`).
 * @param promptKey  Destination key for the merged prompt JSON.
 */
export async function buildExtendedPrompt(
  repoRoot: string,
  bucket: string,
  extraKeys: string[],
  promptKey: string
): Promise<string> {
  // Start with the local snapshot
  const localContext = buildContextWindow(repoRoot);
  const mergedFiles = { ...localContext.files };

  // Pull each remote file and add it under a virtual path
  for (const key of extraKeys) {
    const getCmd = new GetObjectCommand({ Bucket: bucket, Key: key });
    const response: GetObjectCommandOutput = await s3.send(getCmd);
    const body = response.Body as Readable;
    const content = await streamToString(body);
    mergedFiles[`s3://${bucket}/${key}`] = content;
  }

  // Upload the combined payload
  const payload = JSON.stringify({
    files: mergedFiles,
    totalBytes: Buffer.byteLength(JSON.stringify(mergedFiles), "utf-8"),
  });

  const putCmd = new PutObjectCommand({
    Bucket: bucket,
    Key: promptKey,
    Body: payload,
    ContentType: "application/json",
  });
  await s3.send(putCmd);

  return `s3://${bucket}/${promptKey}`;
}
Enter fullscreen mode Exit fullscreen mode

Explanation

  • streamToString converts the streamed S3 object into a string (the SDK returns a stream for large objects).
  • Remote files are stored under a virtual path that starts with s3:// so you can later identify their origin.
  • The final JSON is uploaded back to the same bucket, ready for CodeWhisperer.

Analogy: Think of the merged prompt as a scrapbook where you glue together pages from your own diary (local code) and postcards from a friend (shared S3 files). The AI reads the whole scrapbook at once.


Practical Example: Building a Tailored Suggestion Service in Node.js

Now let’s put everything together. The script below does the following:

  1. Streams source files from a local repo, respecting .gitignore.
  2. Downloads a couple of shared utilities from S3.
  3. Packs everything into a prompt JSON and uploads it to S3 (caching).
  4. Calls CodeWhisperer’s GenerateCodeSuggestions operation, passing the S3 URI as the contextFileUri.
  5. Prints the first suggestion to the console.

You need two SDK clients:

  • @aws-sdk/client-codewhisperer – talks to the CodeWhisperer service.
  • @aws-sdk/client-s3 – reads/writes the prompt JSON and any extra files.
// src/suggestionService.ts
import {
  CodeWhispererClient,
  GenerateCodeSuggestionsCommand,
  GenerateCodeSuggestionsCommandInput,
} from "@aws-sdk/client-codewhisperer";
import { cachePromptInS3 } from "./promptUploader";
import { buildExtendedPrompt } from "./remoteMerger";

const codeWhisperer = new CodeWhispererClient({ region: "us-east-1" });

/**
 * Orchestrates the end‑to‑end flow.
 *
 * @param repoRoot   Path to the local repository you want to assist.
 * @param bucket     S3 bucket used for prompt caching.
 * @param extraKeys  Remote S3 keys to include (e.g., shared schemas).
 * @param filePath   The file currently being edited (relative to repoRoot).
 * @param line       Zero‑based line number where the cursor sits.
 */
export async function getSuggestion(
  repoRoot: string,
  bucket: string,
  extraKeys: string[],
  filePath: string,
  line: number
) {
  // 1️⃣ Build and upload an extended prompt (cached)
  const promptKey = `codewhisperer/prompts/${Date.now()}.json`;
  const contextUri = await buildExtendedPrompt(repoRoot, bucket, extraKeys, promptKey);

  // 2️⃣ Prepare the request for CodeWhisperer
  const input: GenerateCodeSuggestionsCommandInput = {
    // The name of the model (currently "codewhisperer-2023-07-10")
    modelId: "codewhisperer-2023-07-10",
    // The S3 URI that points at the cached context JSON
    contextFileUri: contextUri,
    // The snippet of code you have typed so far (we read it from the local file)
    // In a real editor integration you would send the in‑memory buffer.
    fileContent: await Deno.readTextFile(`${repoRoot}/${filePath}`),
    // The language identifier, e.g., "typescript"
    programmingLanguage: { languageName: "typescript" },
    // The line where the cursor sits – the model will generate after this line.
    cursorPosition: { lineNumber: line + 1, columnNumber: 1 },
  };

  // 3️⃣ Call the service
  const command = new GenerateCodeSuggestionsCommand(input);
  const response = await codeWhisperer.send(command);

  // 4️⃣ Pull out the first suggestion (if any)
  const suggestion = response.suggestions?.[0]?.content?.text;
  console.log("💡 Suggested code:\n", suggestion ?? "No suggestion returned");
}

// Example invocation – adjust paths and bucket as needed
await getSuggestion(
  "/Users/alex/projects/my-app",
  "my-codewhisperer-bucket",
  ["shared/types.proto", "shared/utils.ts"],
  "src/controllers/userController.ts",
  42 // line number where the cursor is
);
Enter fullscreen mode Exit fullscreen mode

Step‑by‑step walk‑through

  • Step 1 calls buildExtendedPrompt, which itself uses the context‑builder and remote‑merger we wrote earlier. The returned contextUri points at a JSON object that lives in S3.
  • Step 2 builds the GenerateCodeSuggestionsCommandInput. The modelId tells the service which version of the underlying model to use (the date‑stamp format is part of the API). fileContent contains the full current file; CodeWhisperer will focus on the cursorPosition when generating.
  • Step 3 sends the request. Because the context lives on S3, the service fetches it directly, saving you a round‑trip.
  • Step 4 extracts the first suggestion and prints it. In a UI you would display it inline.

Key takeaway: By caching the prompt once per commit and reusing the S3 URI, you keep the latency low and the suggestions stable across many keystrokes.


Debugging Common Gotchas and Unexpected Suggestions

Even with a clean pipeline, you may run into puzzling behavior. Below are the most frequent surprises and how to fix them.

1️⃣ .gitignore hides files you actually need

CodeWhisperer’s context builder respects .gitignore. If your project generates TypeScript definitions into a dist/ folder that is ignored, the model will never see those types, leading to “unknown type” completions.

Fix:

  • Add the generated folder to a secondary ignore file (e.g., .codewhispererignore) and tell the builder to read that instead, or
  • Remove the pattern from .gitignore only for the purpose of building the context (you can pass a custom ignore list to ignore()).

2️⃣ Stale caches after a new commit

If you push a new commit but keep using the old S3 prompt key, the model will still read the outdated snapshot.

Fix:

  • Encode the git SHA into the prompt key, e.g., prompts/${sha}.json.
  • In CI/CD pipelines, delete the previous prompt object after a successful deployment.

3️⃣ S3 Express One Zone operation mismatch

When the bucket lives in the S3 Express One Zone storage class, some SDK operations (like GetObject) have different signatures in v2 versus v3. Using the wrong client version throws “InvalidOperation” errors.

Fix:

  • Stick to the v3 SDK (@aws-sdk/client-s3) for all operations; it fully supports Express One Zone.
  • Verify the bucket’s storage class with the console before writing production code.

4️⃣ Presigned URLs expiring mid‑workflow

If you generate a presigned URL for the prompt JSON and hand it to CodeWhisperer, the URL may expire before the service fetches it, resulting in a 403 (Forbidden) error.

Fix:

  • Keep the URL’s Expires value at least 15 minutes to cover network latency and retries.
  • Alternatively, give the service the plain s3:// URI and let AWS handle the fetch with your IAM role.

5️⃣ Object Lock in Compliance mode

Some teams enable Object Lock to meet regulatory requirements. In Compliance mode, even the root user cannot delete or overwrite the object. If you try to update a cached prompt that is locked, the upload fails silently.

Fix:

  • Store cached prompts in a dedicated bucket without Object Lock.
  • If you must use a locked bucket, write each prompt to a new key (e.g., timestamped) instead of overwriting.

6️⃣ Eventual consistency on list operations

After uploading a new prompt JSON, a subsequent ListObjectsV2 call may not immediately see it. If your workflow relies on listing to pick the “latest” prompt, you might pick an older version.

Fix:

  • Use GetObject directly with the known key rather than listing.
  • If you must list, add a short exponential back‑off (e.g., retry up to 3 times with 200 ms intervals).

7️⃣ Transfer acceleration costs surprise

Enabling S3 Transfer Acceleration can speed up uploads from distant regions, but the cost per GB is higher. If you upload many prompt files during a CI run, the bill can grow quickly.

Fix:

  • Keep the prompt size small (under the 10 KB limit).
  • Disable acceleration for the bucket used exclusively for CodeWhisperer prompts.

Below is a defensive wrapper around the suggestion call that handles 403 errors and retries on eventual consistency.

// src/safeSuggest.ts
import {
  CodeWhispererClient,
  GenerateCodeSuggestionsCommand,
  GenerateCodeSuggestionsCommandInput,
} from "@aws-sdk/client-codewhisperer";

const cwClient = new CodeWhispererClient({ region: "us-east-1" });

export async function safeGenerateSuggestion(
  input: GenerateCodeSuggestionsCommandInput,
  maxAttempts = 3
) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      const cmd = new GenerateCodeSuggestionsCommand(input);
      const resp = await cwClient.send(cmd);
      return resp;
    } catch (err: any) {
      // 403 usually means the S3 URI couldn't be accessed yet
      if (err.name === "AccessDeniedException" && attempt < maxAttempts) {
        console.warn(`🔒 Attempt ${attempt} failed – retrying after back‑off`);
        await new Promise((r) => setTimeout(r, 200 * attempt));
        continue;
      }
      throw err; // re‑throw if it's a different error or max attempts exceeded
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Tip: Centralising error handling like this keeps your main suggestion logic tidy and makes it easier to add new retries later.


The Takeaway

  • CodeWhisperer reads a context window, a tiny, carefully selected set of source files, before generating a suggestion.
  • Prompt caching stores that window as JSON in S3, letting the service fetch it quickly and keep suggestions stable.
  • You can extend the window with additional files stored on S3, such as generated SDKs or shared schemas.
  • A complete Node.js 22 script can (a) build the window, (b) merge remote snippets, (c) upload the prompt, and (d) call GenerateCodeSuggestions with the resulting S3 URI.
  • Common pitfalls include .gitignore hiding needed files, stale cache keys, S3 storage‑class quirks, presigned‑URL expiration, Object Lock restrictions, eventual consistency, and hidden transfer‑acceleration costs.
  • Guarding against those gotchas with explicit key naming, retries, and careful bucket configuration makes the experience reliable and pleasant.

Armed with these insights, you can turn CodeWhisperer from a mysterious autocomplete wizard into a predictable, tunable teammate that truly understands the shape of your codebase. Happy coding!


Transparency notice

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

Published: 2026-09-11 · Primary focus: CodeWhisperer

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)