# Context Engineering for LLM Applications: A Complete Implementation Guide

> Implement context engineering for LLM applications. Learn to control LLM input by serializing prompts, tool results, and memory into a custom format. Read our complete guide.

- Repository: [HumanLayer/12-factor-agents](https://github.com/humanlayer/12-factor-agents)
- Tags: how-to-guide
- Published: 2026-05-19

---

**Context engineering is the practice of explicitly controlling exactly what an LLM sees in its context window by serializing prompts, tool results, and memory into a compact custom format—such as XML-style tags—and feeding it as a single user message rather than using standard message list structures.**

Context engineering for LLM applications is the disciplined practice of shaping your model's input window to maximize performance and token efficiency. According to the `humanlayer/12-factor-agents` repository, this concept is formalized as **Factor 3 – Own your context window**, where you move beyond standard `[{role, content}]` message arrays to fully customized serialization formats. By implementing the patterns found in the source code, you can dramatically improve information density, error handling, and safety in your agent systems.

## What is Context Engineering in LLM Applications?

Context engineering is fundamentally about serialization strategy. Instead of allowing your agent framework to automatically structure context as a list of chat messages, you explicitly define how events, tool calls, memories, and errors are formatted before they reach the model.

At its core, the process involves three stages:

1. **Collect all relevant pieces** – Gather prompts, instructions, retrieved documents, tool-call results, memory, and error information.
2. **Serialize into a compact format** – Convert these pieces into a token-efficient representation using custom XML-style tags, YAML blobs, or domain-specific languages the model understands.
3. **Feed as a single message** – Pass the serialized string as the content of one `user` or `system` message, allowing the model to treat the entire history as "what happened so far, what's the next step."

## Why Custom Context Formats Outperform Standard Message Lists

Standard message list formats waste tokens on repetitive field names and structural boilerplate. According to the design notes in [`content/factor-03-own-your-context-window.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-03-own-your-context-window.md), custom formats provide four critical advantages:

- **Information density** – You can drop JSON field names and use concise tags like `<multiply> a: 3 b: 4 </multiply>`, packing more semantic meaning into fewer tokens.
- **Error handling** – Embed errors directly adjacent to the failing step using `<error> … </error>` tags, enabling the model to react or request clarification immediately.
- **Safety** – Explicitly control which data gets passed to the model, stripping secrets or sensitive information before serialization.
- **Flexibility** – Experiment with XML, YAML, or any DSL without changing your underlying agent framework, as demonstrated in the Chapter 7 walkthrough.

## Implementation Steps for Context Engineering

The [`workshops/2025-05/sections/07-context-window/README.md`](https://github.com/humanlayer/12-factor-agents/blob/main/workshops/2025-05/sections/07-context-window/README.md) file outlines a concrete five-step implementation process:

1. **Define an event model** – Create types for your events (e.g., `type`, `data` fields) that represent actions in your system.
2. **Create a serializer** – Build a function that turns each event into a tag block, such as `<event_type> … </event_type>`.
3. **Join serialized events** – Combine all serialized events into one string using a function like `thread_to_prompt`.
4. **Pass as user message** – Send the concatenated string as the content of a single `user` message when calling the LLM API.
5. **Update your tests** – Modify your BAML tests or test suite to expect the new XML-style output, ensuring your serialization remains consistent with expectations.

## Code Implementation: Custom Serialization in TypeScript

The workshop implementation in [`src/agent.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/src/agent.ts) demonstrates the evolution from plain JSON to a compact XML-style format. The `Agent` class implements a `serializeForLLM` method that maps events to custom tags:

```typescript
// src/agent.ts – custom serialization for the LLM
class Agent {
  private events: Event[] = [];

  // ------------------------------------------------------------------
  // 1️⃣  Pretty‑print JSON (optional early step)
  // ------------------------------------------------------------------
  // return JSON.stringify(this.events, null, 2);

  // ------------------------------------------------------------------
  // 2️⃣  XML‑style serialization (final step)
  // ------------------------------------------------------------------
  serializeForLLM(): string {
    return this.events.map(e => this.serializeOneEvent(e)).join("\n");
  }

  private trimLeadingWhitespace(s: string): string {
    return s.replace(/^[ \t]+/gm, "");
  }

  private serializeOneEvent(e: Event): string {
    return this.trimLeadingWhitespace(`
      <${e.data?.intent || e.type}>
      ${
        typeof e.data !== "object"
          ? e.data
          : Object.keys(e.data)
              .filter(k => k !== "intent")
              .map(k => `${k}: ${e.data[k]}`)
              .join("\n")
      }
      </${e.data?.intent || e.type}>
    `);
  }
}

```

*Source: [`src/agent.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/src/agent.ts) in the 07-context-window workshop*

This implementation shifts from `JSON.stringify` to a custom `serializeOneEvent` helper that uses `trimLeadingWhitespace` to clean up template literals, producing clean XML-style output that the LLM can parse efficiently.

## Reusable Serialization Helpers in Python

For applications requiring a more functional approach, the [`content/factor-03-own-your-context-window.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-03-own-your-context-window.md) file provides reusable helper functions that convert event objects to tagged strings:

```python

# Helper functions from factor‑03 documentation

class Thread:
    events: List[Event]

class Event:
    type: Literal["list_git_tags", "deploy_backend", ...]
    data: Union[str, ListGitTags, DeployBackend, ...]

def event_to_prompt(event: Event) -> str:
    data = event.data if isinstance(event.data, str) else stringifyToYaml(event.data)
    return f"<{event.type}>\n{data}\n</{event.type}>"

def thread_to_prompt(thread: Thread) -> str:
    return "\n\n".join(event_to_prompt(e) for e in thread.events)

```

*Source: [`factor-03-own-your-context-window.md`](https://github.com/humanlayer/12-factor-agents/blob/main/factor-03-own-your-context-window.md)*

The `event_to_prompt` function handles type checking to determine whether to serialize data as a raw string or as YAML, while `thread_to_prompt` joins all events with double newlines to create clear separation between context blocks.

## Testing Your Custom Context Format

When implementing context engineering for LLM applications, you must keep your test suite synchronized with your serialization changes. The 12-factor-agents workshop uses BAML (BoundaryML) tests defined in `baml_src/agent.baml` to verify that the agent output matches the expected XML structure. Update these fixtures to expect the new tag-based format rather than JSON arrays.

Run the agent with the new context format using:

```bash

# Run the agent with the new context format

BAML_LOG=info npx tsx src/index.ts \
  "can you multiply 3 and 4, then divide the result by 2 and then add 12 to that result"

```

This command executes the TypeScript agent while logging BAML information, allowing you to verify that the custom serialization produces valid context windows for the LLM.

## Summary

- **Context engineering** is the practice of explicitly controlling what an LLM sees by using custom serialization formats instead of standard message lists.
- **Custom formats** (XML-style tags, YAML) provide higher information density, better error handling, improved safety, and greater flexibility than JSON message arrays.
- **Implementation** involves defining event models, creating serializers like `serializeForLLM` or `event_to_prompt`, joining events into a single string, and passing that string as a user message.
- **Key files** in the reference implementation include [`src/agent.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/src/agent.ts) for the TypeScript agent logic and [`content/factor-03-own-your-context-window.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-03-own-your-context-window.md) for design patterns and helper functions.
- **Testing** requires updating BAML fixtures or equivalent tests to expect the new compact format instead of traditional message structures.

## Frequently Asked Questions

### What is context engineering in LLM applications?

Context engineering is the practice of shaping exactly what an LLM sees in its context window by serializing prompts, tool results, memory, and errors into a compact, custom format. According to the `humanlayer/12-factor-agents` repository, this is **Factor 3 – Own your context window**, where you replace standard `[{role, content}]` message arrays with optimized representations like XML-style tags to improve token efficiency and model performance.

### Why not use standard OpenAI message formats?

Standard message formats waste tokens on repetitive JSON field names and offer limited control over information density. By implementing custom serialization—as shown in [`content/factor-03-own-your-context-window.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-03-own-your-context-window.md)—you can embed errors directly next to failing steps, strip sensitive data before it reaches the model, and format tool calls more concisely using domain-specific tags rather than verbose JSON objects.

### How do I handle errors in custom context formats?

Embed errors directly adjacent to the relevant event using explicit tags such as `<error> … </error>`. This allows the LLM to immediately correlate the error with the specific action that failed, enabling it to react, correct its approach, or ask for clarification in the next turn. The `serializeOneEvent` method in [`src/agent.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/src/agent.ts) demonstrates how to structure these tags alongside event data.

### What files implement the serialization logic in the 12-factor-agents workshop?

The serialization logic is implemented in two primary locations: [`src/agent.ts`](https://github.com/humanlayer/12-factor-agents/blob/main/src/agent.ts) (in the 07-context-window workshop section) contains the `Agent` class with `serializeForLLM` and `serializeOneEvent` methods for XML-style formatting, while [`content/factor-03-own-your-context-window.md`](https://github.com/humanlayer/12-factor-agents/blob/main/content/factor-03-own-your-context-window.md) provides reference implementations of `event_to_prompt` and `thread_to_prompt` helper functions that demonstrate the functional approach to context engineering.