Context Engineering for LLM Applications: A Complete Implementation Guide
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:
- Collect all relevant pieces – Gather prompts, instructions, retrieved documents, tool-call results, memory, and error information.
- 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.
- Feed as a single message – Pass the serialized string as the content of one
userorsystemmessage, 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, 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 file outlines a concrete five-step implementation process:
- Define an event model – Create types for your events (e.g.,
type,datafields) that represent actions in your system. - Create a serializer – Build a function that turns each event into a tag block, such as
<event_type> … </event_type>. - Join serialized events – Combine all serialized events into one string using a function like
thread_to_prompt. - Pass as user message – Send the concatenated string as the content of a single
usermessage when calling the LLM API. - 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 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:
// 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 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 file provides reusable helper functions that convert event objects to tagged strings:
# 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
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:
# 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
serializeForLLMorevent_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.tsfor the TypeScript agent logic andcontent/factor-03-own-your-context-window.mdfor 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—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 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 (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 provides reference implementations of event_to_prompt and thread_to_prompt helper functions that demonstrate the functional approach to context engineering.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →