Back to home

03 — BEHAVIOR CONTROL

Context injection (todos, file structure, design system)

9 min read

Your agent's effectiveness depends entirely on what it knows. An agent without context will recreate components that already exist, ignore your design system, and lose track of what it was supposed to be doing. Context injection solves this by feeding your agent relevant information at the right moments.

But here's the challenge: you can't just dump everything into every request. That wastes tokens, slows down responses, and dilutes the signal with noise. Smart context injection is about giving the agent exactly what it needs, when it needs it, and caching what you can.

The Context Problem

Imagine your agent is implementing a new feature. Without context:

  • It doesn't know what files already exist
  • It has no idea what your design system looks like
  • It can't see what tasks are in progress
  • It doesn't know if you have a database connected
  • It's unaware of existing integrations

The agent will make mistakes. It'll create duplicate files, use random colors instead of your design tokens, forget about the three other tasks you asked it to do, and write code that doesn't match your existing patterns.

Context injection fixes this by making relevant information available to the agent at decision time.

Types of Context

There are several categories of context your agent needs:

Static Context - Rarely changes, can be cached

  • Codebase file structure (directories and root files)
  • Design system (CSS variables, color palettes, component patterns)
  • Project metadata (template type, framework version)

Semi-Static Context - Changes occasionally, cache and refresh

  • Database connection info (Supabase project details)
  • Integration status (Stripe webhooks, auth providers)
  • Environment configuration

Dynamic Context - Changes frequently, inject per-request

  • Current todos and their status
  • Files the user is currently viewing
  • Runtime errors that just occurred
  • Attachments in this specific message
  • Conversation history

On-Demand Context - Only inject when explicitly needed

  • Full file contents for specific paths
  • Git history or blame information
  • Test results or logs
  • Third-party API documentation

The key is matching context type to injection strategy.

Injecting File Structure

Your agent needs to know what files exist without reading the entire codebase. File structure context gives it a map:

<current_project_files_structure>
DIRECTORIES:
  src/
  src/app/
  src/components/
  src/components/ui/
  src/lib/
  src/hooks/

ROOT FILES:
  package.json
  tsconfig.json
  next.config.ts
  README.md
</current_project_files_structure>

This is lightweight (just paths, not contents) and tells the agent:

  • What directories exist for organizing new files
  • What root configuration files are present
  • Where components and utilities live
  • What the project structure looks like

Generate this once during initialization:

async getCodebaseStructure(): Promise<string> {
  const excludedDirs = ['node_modules', '.next', 'dist', 'build', '.git'];
 
  // Use find command for efficiency in sandboxes
  const findCommand = `find . \\( ${excludedDirs.map(d => `-name ${d}`).join(' -o ')} \\) -prune -o -print`;
  const output = await sandboxClient.runCommand(findCommand);
 
  // Parse into directories and root files
  // Format for readability
  return formattedStructure;
}

Cache this in your agent's state and inject it into each request. It's small enough that the cost is minimal compared to the value.

Design System Context

Your agent should respect your design system. Inject CSS variables, color palettes, and component patterns:

async gatherDesignSystemContext(): Promise<DesignContext> {
  const context: DesignContext = {};
 
  // Read globals.css for CSS variables and theme
  const globalsCss = await sandboxClient.readFile('src/app/globals.css');
  if (globalsCss) {
    context.globals_css = globalsCss;
  }
 
  // Check for design documentation
  const designDoc = await sandboxClient.readFile('website_design.md');
  if (designDoc) {
    context.website_design = designDoc;
  }
 
  return context;
}

This context prevents agents from using random colors like #3B82F6 when you have a defined color system with --primary and --accent variables.

You can inject this in the system prompt or as part of the request context. Since design systems change rarely, fetch it once during initialization and cache it.

Todo Context - The Smart Approach

Here's a pattern that works remarkably well: append current todos to every tool output.

Instead of adding todos to the request (which costs tokens up front), add them to tool results:

export const appendTodoContext = (result: string, todos?: TodoItem[]): string => {
  if (!todos || todos.length === 0) return result;
 
  const todoLines = ['\n\n=== Current Task List ==='];
 
  for (const todo of todos) {
    todoLines.push(`[${todo.id}] ${todo.content} - ${todo.status}`);
  }
 
  todoLines.push('========================');
  return result + todoLines.join('\n');
};

Every tool (read, grep, bash, etc.) appends this context to its output. The agent sees todos after every action, keeping them top of mind without bloating the initial request.

This is surprisingly effective. The agent naturally maintains awareness of the task list because it's reminded after each operation.

Runtime Context - Only When Relevant

Some context only matters in specific situations. Inject it conditionally:

Current Page Context - When the user is viewing a specific route:

if (currentPage) {
  agentUserMessageContent.push({
    type: 'input_text',
    text: `User is currently viewing: ${currentPage.route}\n\nFile: ${currentPage.filePath}\n\n${currentPage.content}`
  });
}

Detected Errors - When runtime errors occurred:

if (detectedErrors) {
  agentUserMessageContent.push({
    type: 'input_text',
    text: `Runtime errors detected:\n\n${detectedErrors}`
  });
}

Integration Context - When the project has specific integrations:

// Add Supabase context if project is connected
if (this.supabaseContext) {
  agentUserMessageContent.push({
    type: 'input_text',
    text: this.supabaseContext
  });
}
 
// Add Stripe context if webhooks configured
if (this.stripeContext) {
  agentUserMessageContent.push({
    type: 'input_text',
    text: this.stripeContext
  });
}

These are fetched during initialization and cached, then injected only in requests where they're relevant.

On-Demand File Embedding

Sometimes the agent needs specific files that aren't in the conversation yet. Support explicit embedding:

if (extraPathsToEmbed && extraPathsToEmbed.length > 0) {
  for (const path of extraPathsToEmbed) {
    const content = await this.readSandboxFile(path);
    if (content) {
      agentUserMessageContent.push({
        type: 'input_text',
        text: `<${path}>\n${content}\n</${path}>`
      });
    }
  }
}

This is useful when you know certain files are relevant (like a config file or a base component the agent should reference) but don't want to include them in every request.

Context Through Agent Context Object

Beyond message content, you can pass context through the agent's execution context:

const agentContext: AgentContext = {
  projectId: this.projectId,
  sandboxClient: this.sandboxClient,
  mux: this.mux,
  template: this.template,
  filesInSession: filesInSession,
  todos: this.todos,
  attachmentImageUrls: this.attachmentImageUrls,
  hasFileOperations: this.hasFileOperations,
  markFileOperationDone: () => this.markFileOperationDone(),
};

This context is available to all tools during execution. Tools can check flags like hasFileOperations or access the current todos list to make better decisions.

It's metadata about the session rather than information for the agent's reasoning.

Caching Strategy

Here's the caching pattern that works:

Fetch Once (During Initialization):

  • File structure
  • Design system context
  • Supabase/Stripe integration info

Store in Agent State:

class CodingAgent {
  private fileStructure: string = '';
  private designSystemContext: DesignSystemContext = {};
  private supabaseContext: string | null = null;
  private stripeContext: string | null = null;
 
  async initialize() {
    await Promise.all([
      (async () => {
        this.fileStructure = await this.getCodebaseStructure();
      })(),
      (async () => {
        this.designSystemContext = await this.gatherDesignSystemContext();
      })(),
      (async () => {
        this.supabaseContext = await getSupabaseContext(this.projectId);
      })(),
      (async () => {
        this.stripeContext = await getStripeContext(this.projectId);
      })()
    ]);
  }
}

Refresh When Needed:

  • File structure: After significant file operations
  • Supabase context: When user connects/disconnects a project
  • Stripe context: When webhooks are added/removed

Never Cache:

  • Current todos (change every request)
  • Runtime errors (dynamic)
  • User's current page (changes as they navigate)
  • Attachments (unique per message)

Context Ordering Matters

When building your message content, order matters:

  1. User request first - What the user actually asked for
  2. Current state - What page they're on, any errors
  3. Attachments - Screenshots, designs, reference images
  4. Codebase structure - File organization
  5. Extra embedded files - Specific files to reference
  6. Integration context - Database, payments, auth

This order keeps the most important information (the user's actual request) at the top and progressively adds supporting context.

When Context Bloats Requests

Watch for these signs that you're over-injecting context:

Every request is 20K+ tokens before the LLM even responds - You're including too much static context. Cache more aggressively.

Agent performance degrades - Too much context dilutes the signal. The agent focuses on irrelevant details.

Responses slow down significantly - Large context windows take longer to process. Trim what's not essential.

Costs spike - You're paying for context tokens on every request even when they're not used.

If you see these patterns, audit what you're injecting and move things from request-level to on-demand or tool-level context.

Testing Context Effectiveness

How do you know if your context injection is working?

Without file structure: Agent creates files in wrong directories or recreates existing files

With file structure: Agent correctly places new files and checks for existing ones

Without design system: Agent uses arbitrary colors, mismatched styles, inconsistent spacing

With design system: Agent respects your color variables, matches existing component patterns

Without todos: Agent completes one task and stops, forgetting the other three you mentioned

With todos: Agent systematically works through all tasks in order

Track these behaviors. If your agent keeps making mistakes that proper context would prevent, adjust your injection strategy.

The Balance

Good context injection is a balancing act:

  • Enough context that the agent makes informed decisions
  • Not so much that you waste tokens on irrelevant information
  • Cached where possible to avoid repeated fetching
  • Dynamic where necessary to stay current
  • Structured so the agent can easily parse and use it

Get this right and your agent becomes dramatically more reliable. It stops making rookie mistakes, respects your codebase conventions, and maintains awareness of the bigger picture across long sessions.

What's Next

You've now built an agent that knows how to behave (system prompts), has the right tools (with clear descriptions), operates in appropriate modes (agent vs chat), and has relevant context (todos, files, design system).

But there's one more piece: conditional tool availability. Not all tools should be available at all times. In the next guide, we'll cover how to enable and disable tools based on project state, user permissions, and task context.

That's the final piece of behavior control.