Learn to describe LLM agent context structures in under 10 minutes
ACDL (Agentic Context Description Language) is a notation for describing the structure of agent contexts—the system instructions, user messages, tool calls, conversation history, and conditional logic that make up what your agent sends to the LLM.
A prompt is what gets sent to the LLM at a single point in time. A context is the pattern that describes how prompts are constructed and evolve across turns—which parts stay fixed, which parts accumulate, and how the structure changes dynamically. ACDL describes contexts, not just prompts.
Ad hoc prose or prompt excerpts are imprecise, hard to verify, and make comparing implementations difficult. ACDL provides a clean, precise notation that is easy to read and compare across different systems. As your agent grows more complex, ACDL helps you see the structure at a glance, share it with others, and reason about how context evolves over time.
Let's start with the simplest possible ACDL prompt. Every prompt is a series of messages, each marked with a role (System, User, Assistant, Tool) and containing different types of content. Here's what that looks like in ACDL:
HelloWorld[@T]: { S: INSTRUCTIONS U: CONTENT }
This describes an llm context with two messages. The first message is a message who's role is System and whose content is the template INSTRUCTIONS. The second message is a message who's role is User and whose content is the template CONTENT.
INSTRUCTIONS might expand
to a long system prompt, but in ACDL we just see the structural role it plays. This keeps specifications
clean and focused on the conversation flow.
S: (System), U: (User), A: (Assistant),
and T: (Tool response). These map directly to the roles in chat-based LLM APIs.
When a role message contains multiple elements, use braces to group them together:
CodingAssistant[@T]: { S: { SYSTEM_INSTRUCTIONS CODING_GUIDELINES AVAILABLE_TOOLS } U: env.user_input[@T] }
The S: { ... } form lets you include multiple templates, variables, or any combination
of content in a single message. Without braces, a role takes just one element.
Real prompts usually include dynamic content. ACDL uses context variables that come from three sources:
env. - Environment inputs (user inputs, task descriptions, external data)sys. - Agent state (memory contents, tool configurations, action histories)resp. - LLM responses (what the model previously generated)sys.config.role stays the same throughout
the system's lifetime. Values that change between steps are accessed with a time index:
env.user_input[@T] is the user input at the current step.
AssistantPrompt: { S: ROLE_DESCRIPTION(env.assistant_name) U: env.user_input }
env.user_input are placeholders. When you implement your agent, you'll fill
these in with actual values. ACDL doesn't care about the implementation - it just describes the structure.
Agents have conversations that evolve over multiple turns. ACDL uses time indices to describe which turn we're talking about:
ChatAgent[@T]: { S: ROLE // you are a helpful assistant... U: env.user_input[@T] }
The [@T] after the prompt name means "this prompt is parameterized by turn T".
Inside the prompt, env.user_input[@T] means "the user's input at turn T".
@T - The current turn (a parameter)@1, @2, etc. - Specific turn numbers@t - A loop variable (lowercase) for iterating through turns
Most agents need to include previous conversation turns. Use ForEach to loop through history:
ChatWithHistory[@T]: { S: INSTRUCTIONS // Previous turns ForEach(@t: range(1, @T)) { U: env.user_input[@t] A: resp.reply[@t] } // Current turn U: env.user_input[@T] }
ForEach(@t: range(1, @T)) iterates from turn 1 up to (but not including) the current turn @T.
The loop variable @t takes each value in that range.
Let's combine everything to describe a ReAct agent - an agent that reasons and uses tools in a loop:
ReactAgent[@T]: { S: { TASK_INSTRUCTIONS AVAILABLE_TOOLS } U: env.user_question // Action history ForEach(@t: range(1, @T)) { A: { resp.reasoning[@t] sys.tool_call[@t] } T: sys.tool_call[@t].response } S: CONTINUE_OR_ANSWER }
This describes the classic ReAct pattern:
Sometimes you need different context based on conditions. Use If, ElseIf, and
Else:
AdaptiveAgent[@T]: { S: BASE_INSTRUCTIONS If env.has_documents { S: { RAG_INSTRUCTIONS sys.retrieved_docs } } If env.has_tools { S: TOOL_INSTRUCTIONS } Else { S: NO_TOOLS_MESSAGE } U: env.user_input[@T] }
This agent adapts its instructions based on whether documents and tools are available.