Language Reference
This is a complete reference for the Agentic Context Description Language (ACDL). The language declaratively specifies context structures sent to large language models, separating structural concerns from content and implementation.
Specification Structure
An ACDL specification defines a named prompt template parameterized by optional indices. The body consists of an ordered sequence of prompt blocks—role messages, mark blocks, and control flow constructs—which may be freely interleaved:
PromptName[idx1, idx2, ...]: {
<prompt-blocks>
}
A single file may contain multiple specifications, each defining a separate prompt template.
PromptName1[idx1, idx2, ...]: {
<prompt-blocks>
}
PromptName2[idx1, idx2, ...]: {
<prompt-blocks>
}
Example
BasicPrompt[@T]: { S: INSTRUCTIONS U: env.user_question[@T] }
BasicPrompt[@T]:
Role Messages
Every message carries exactly one role. Four roles serve the chat format; a fifth pseudo-role serves the legacy completion format:
| Marker | Purpose |
|---|---|
S: | System—instructions, persona, tool descriptions, behavioral constraints |
U: | User—external input, observations, tool results |
A: | Assistant—prior model outputs, reasoning traces, chosen actions |
T: | Tool—structured tool call results |
N: | None—single unstructured text block (completion format only) |
Multi-Line Form
Role messages support two syntactic forms. The multi-line form encloses content in braces and permits any combination of content elements and control flow:
S: { INSTRUCTIONS AVAILABLE_TOOLS env.datetime[@t] }
AVAILABLE_TOOLS
env.datetime[@t]
Single-Line Form
The single-line form omits braces and accepts exactly one content element—a context variable, template, or function call:
U: env.user_question[@t] A: resp.answer[@t] S: INSTRUCTIONS
Important: Control flow constructs (ForEach, If, Switch) are not permitted in single-line role messages. To include control flow inside a role message, the multi-line braced form must be used:
U: { ForEach(item: env.items) { env.item_detail[@t, item] } }
Completion Format
The N: role (completion format) imposes two additional constraints: (1) exactly one N: block may appear per prompt, and (2) no chat roles (S:, U:, A:, T:) may appear in the same specification:
CompletionPrompt[@t]: { N: { TASK_DESCRIPTION env.context[@t] QUESTION } }
env.context[@t]
QUESTION
Scoping Rules
The language enforces a strict two-level scope that mirrors the structure of LLM chat APIs:
- Top level (the prompt body, outside any role block): only role messages, marker blocks, control flow constructs, name definitions, and comments are permitted.
- Inside role blocks (within braces): valid elements are context variables, functions, templates, control flow, comments, name definitions, marking blocks and
break/continue.
Role messages may not appear inside other role messages—the following is invalid:
U: { S: {INSTRUCTIONS} // ERROR: nested role }
Completion Prompts: For prompts using N:, the top level may contain only the single N: block—no other role messages or control flow may appear outside it.
Context Variables
Context variables reference dynamic runtime data. The general syntax is:
namespace.path[indices]
where namespace is one of four reserved prefixes:
| Namespace | Use Cases |
|---|---|
env | Environment—external inputs, observations, sensor readings, user queries, game state |
sys | System—agent state, memory contents, tool configurations, action histories |
resp | Response—prior LLM outputs, reasoning traces |
Paths may be nested using dot notation to reach into sub-fields of structured data. Indices may appear at any level of the path:
env.user_question[@T] // the user question at time T sys.agent_desc // the agent description (constant) sys.tool[@t].tool_response[@t] // tool response of tool at time t env.bomb_location[@T, bomb] // bomb location of bomb at time T
A variable without indices (e.g., sys.agent_desc) refers to data that does not vary over time or other dimensions.
Indices
Indices address specific elements along one or more dimensions. Two types are distinguished syntactically.
Time Indices
The special symbol @ denotes the primary time dimension that the prompt iterates over. What @ represents depends on the agent's structure: for a ReAct agent that operates within a single turn but loops over many steps, @ refers to the current step; for an agent that loops over multi-turn conversations, @ refers to the current turn, and sub-steps within each turn are indexed with ordinary index variables.
The current time is denoted with capital letters: @T for the main time step, and I, J, etc. for sub-steps. When iterating over time steps, the corresponding lower-case letters are used. Sub-steps are accessed using dot notation: @t.i refers to sub-step i within turn t, while @T.I refers to the current sub-step of the current turn. When iterating over the sub-steps of a previous turn, use @t.substeps to obtain the count.
Convention: Time indices start at 1, not 0. The first turn is @1.
@T // Current time step @T-1 // Previous time step @T.I // Current substep of current turn @t.i // Substep i of turn t (in loops)
Iteration Examples
// Iterating over all previous turns ForEach(t: range(1, @T-1)) { env.observation[@t] } // Iterating over substeps in the current turn ForEach(i: range(1, I)) { sys.action[@T.i] } // Nested: substeps within each previous turn ForEach(@t: range(1, @T-1)) { ForEach(i: range(1, @t.substeps)) { sys.action[@t.i] } }
Non-Time Indices
Non-time indices have no prefix and address other dimensions—named entities, or context-variable-valued keys:
[sys.agent_name]
[bomb]
Multiple indices are comma-separated: env.bomb_location[@t, bomb] addresses a specific bomb at a specific time step. Standard arithmetic operators (+, -, *, /, %) are permitted in all index positions, enabling expressions such as @t-1, @t+1, t-k, or @t % 25.
Templates
Templates are ALL_CAPS placeholders representing text blocks whose content is specified at instantiation time. They describe the semantic purpose of a text section without fixing its wording, separating prompt architecture from prompt prose. Words within a template name are separated by underscores: TASK_INTRO, MAP_DESCRIPTION.
Templates may accept arguments in parentheses, enabling parameterized text:
INSTRUCTIONS // Task explanation AVAILABLE_TOOLS // Tool list QUERY(sys.agent_name) // Parameterized
AVAILABLE_TOOLS // Tool list
QUERY(sys.agent_name) // Parameterized
An optional inline comment (after //) documents the intended content of the template.
Functions
Functions represent computed content—summarization, retrieval, formatting, or any transformation that cannot be expressed as a simple variable lookup. They are declared by name and purpose without defining their implementation; the name conveys semantic intent.
The syntax is functionName(arg1, arg2, ...)[indices]. Function names use camelCase (distinguishing them from ALL_CAPS templates). Arguments may be context variables, time or regular indices, numeric literals, arithmetic expressions, or nested function calls:
summarize(prompt.History[@t]) get_dialog_history(sys.agent_name) range(1, @T-1, 2)
get_dialog_history(sys.agent_name)
range(1, @T-1, 2)
Built-in Function: The range(start, stop, step) function generates numeric sequences for use in ForEach loops. The step argument is optional and defaults to 1. The range is inclusive, meaning range(1, 2) starts at 1 and ends at 2, including 2.
Control Flow
Three constructs govern dynamic prompt structure. All three may appear both at the top level (producing or gating entire role messages) and inside role blocks (controlling content within a single message).
ForEach
ForEach iterates over ranges or collections to produce repeated structures. The syntax is:
ForEach(variable: iterable) {
<body>
}
The iterable may be a range(start, stop, step) call or a collection-valued context variable. At the top level, the loop body may contain role messages, producing multiple messages per iteration. Inside a role block, it produces repeated content elements:
Top-level ForEach (produces multiple messages)
ForEach(@t: range(1, @T-1)) { U: env.user_question[@t] A: resp.answer[@t] }
ForEach inside a role (produces repeated content)
U: { ForEach(bomb: env.bombs) { env.bomb_location[@t, bomb] env.bomb_details[@t, bomb] } }
env.bomb_details[@t, bomb]
If / ElseIf / Else
If / ElseIf / Else conditionally includes or excludes blocks based on runtime state. Conditions may use comparison operators (==, !=, <, >) and logical connectives (&, |):
If sys.tool[@t] == clarify { U: env.user_input[@t] } ElseIf sys.tool[@t] == search { U: env.search_results[@t] } Else { A: sys.tool[@t].tool_response }
Conditionals can also guard entire sections including loops:
If @T > 1 { ForEach(t: range(1, @T-1)) { U: env.user_input[@t] A: resp.answer[@t] } }
Switch / Case / Default
Switch / Case / Default selects among multiple alternatives based on the value of an expression:
Switch sys.action_type[@t] { Case "search" { U: env.search_results[@t] } Case "calculate" { U: env.calculation[@t] } Default { U: env.fallback[@t] } }
The break and continue keywords are available inside loops with their standard semantics.
Early Termination
The PromptEndsHere when construct signals that, if the given condition is true, the prompt ends at that point—no further content is appended to the message sequence sent to the LLM for that turn. This is useful when certain conditions require a truncated prompt, such as an initial turn that needs no history or context beyond the setup. The syntax is:
PromptEndsHere when <condition>
For example, to end the prompt at the first time step:
Prompt[@T]: { S: INSTRUCTIONS U: env.user_input[@T] PromptEndsHere when (@T == 1) ForEach(@t: range(1, @T-1)) { A: resp.answer[@t] } }
Here, if the current time is the first step, the prompt contains only the system instructions and user input. Otherwise, the full history is appended.
Mark Blocks
A mark block annotates a section of the specification for visual emphasis in the rendered output. It places a bracket (]) along the side of the marked content, with a number identifier displayed beside it. Marks are purely presentational—they do not affect prompt semantics or scoping. They can wrap any prompt block, from a single content element to a large multi-message section:
Mark 1 {
<prompt-blocks>
}
The number appears next to the bracket in the visualization as ]1. Multiple marks with different numbers may be used to highlight distinct sections:
Mark 1 { S: { INSTRUCTIONS AVAILABLE_TOOLS } } Mark 2 { U: env.user_question[@T] }
AVAILABLE_TOOLS
Here, mark ]1 highlights the system setup, ]2 marks the current query.
Name Definitions
A name definition binds a symbolic name to an expression, allowing complex or frequently repeated expressions to be written once and referenced concisely throughout the specification. The definition syntax is:
Name docs := k_relevant_docs(env.query[@T]) ForEach(i: range(1, $docs.len)) { $docs[i].source $docs[i].content }
docs[i].content
Once defined, the name is referenced using the $ prefix: $var_name. Name definitions improve readability by replacing long or opaque expressions with descriptive identifiers. The bound expression can be any valid ACDL element—a context variable, function call, arithmetic expression, or string literal. Fields of the bound value can be accessed via dot notation on the reference.
Here, $docs binds the result of a retrieval function, avoiding repetition of the full function call. Its length and individual elements are then accessed via $docs.len and $docs[i].
List Comprehensions
Name definitions also support list comprehensions, which construct a list by iterating over a range or collection:
Name relevant_summaries := [sys.summary[@t] for t in range(@T, @T-900, 100)] compress_summaries($relevant_summaries)
This binds $relevant_summaries to a list of summaries sampled every 100 steps, which is then passed as an argument to a function. The reference syntax is the same regardless of whether the bound value is a single expression or a list.
Fragments
Fragments are reusable building blocks that encapsulate portions of a prompt specification. They enable modular prompt design by allowing common patterns to be defined once and invoked multiple times. Two kinds of fragments are supported, distinguished by what they produce when expanded.
String Fragments
String Fragments produce content pieces without an associated role. They are defined with the StrFrag keyword and may contain any elements valid inside a role block—context variables, functions, templates, control flow, other string fragments, name definitions, and comments. When invoked, the content expands in place and inherits the role of the enclosing message:
StrFrag DocumentContext[doc]: { env.doc_title[doc] env.doc_content[doc] summarize(env.doc_metadata[doc]) }
DocumentContext[doc]
SFenv.doc_content[doc]
summarize(env.doc_metadata[doc])
A more complex example with control flow:
StrFrag ConversationContext[@T]: { CONTEXT_HEADER ForEach(@t: range(@T-5, @T)) { sys.Summary[@t] If sys.has_tool_call[@t] { sys.tool_response[@t] } } }
ConversationContext[@T]
SFString fragments are invoked with the Frag keyword followed by the fragment name and arguments. They may appear anywhere a context variable, function, or template is valid—inside role blocks, within control flow bodies, or as arguments to other constructs:
U: { TASK_INSTRUCTIONS ForEach(doc: env.documents) { Frag DocumentContext[doc] } env.user_question[@T] }
Role Fragments
Role Fragments produce one or more complete role messages. They are defined with the RolesFrag keyword and may contain role messages, control flow, mark blocks, and other prompt-level constructs—the same elements valid at the top level of a prompt body:
RolesFrag ConversationTurn[@t]: { U: env.user_input[@t] A: resp.answer[@t] If sys.tool[@t] != none { T: sys.tool[@t].tool_response } }
ConversationTurn[@t]
RFRole fragments are invoked at the top level of a prompt, wherever a role message would be valid. They expand to the full sequence of role messages defined in the fragment body:
ChatAgent[@T]: { S: INSTRUCTIONS ForEach(@t: range(1, @T)) { Frag ConversationTurn[@t] } U: env.user_input[@T] }
ChatAgent[@T]:
Fragment Parameters and Invocation
Both fragment types accept parameters, enabling parameterized reuse. Parameters follow the fragment name in square brackets and may include indices, context variables, or other valid index expressions. The same invocation syntax (Frag Name[args]) is used for both types; the parser determines which kind based on context—invocations inside role blocks resolve to string fragments, while those at the top level resolve to role fragments.
Fragment definitions appear at the top level of an ACDL file, alongside prompt specifications. A single file may contain any combination of prompts and fragment definitions:
StrFrag ToolDescription[tool]: { sys.tool_name[tool] sys.tool_schema[tool] } RolesFrag ToolResult[@t, tool]: { A: sys.tool_call[@t, tool] T: sys.tool_response[@t, tool] } ToolAgent[@T]: { S: { INSTRUCTIONS ForEach(tool: sys.available_tools) { Frag ToolDescription[tool] } } ForEach(@t: range(1, @T)) { U: env.observation[@t] Frag ToolResult[@t, sys.selected_tool[@t]] } U: env.observation[@T] }
ToolDescription[tool]
SFsys.tool_schema[tool]
ToolResult[@t, tool]
RFToolAgent[@T]:
Frag ToolResult[@t, sys.selected_tool[@t]]
Comments
Comments use // syntax and may appear on any line—between prompt blocks, inside role blocks, or between specifications. Once a // is encountered, the remainder of that line is treated as a comment; no further ACDL elements may follow on the same line.
An inline comment, placed after a content element, renders alongside that element. A standalone comment, placed on its own line, renders at the current level of nesting:
S: { // This comment appears on its own line, // indented to the level of the S: block INSTRUCTIONS // Renders beside INSTRUCTIONS AVAILABLE_TOOLS // Another standalone comment env.datetime[@T] // Renders beside datetime } // This comment is at the top level ForEach(@t: range(1, @T-1)) { // This comment is inside the loop body U: env.user_question[@t] // Beside the message A: resp.answer[@t] }
Identifiers and Naming Conventions
Identifiers start with a letter or underscore and may contain letters, digits, and underscores. They are case-sensitive. The language enforces naming conventions to visually distinguish element types:
| Element | Convention | Example |
|---|---|---|
| Templates | ALL_CAPS | TASK_DESCRIPTION |
| Functions | camelCase | summarize, k_relevant_docs |
| Context variables | dot.separated | env.user_input |
| Time indices | @ prefix | @T, @t |
| Name references | $ prefix | $docs |
Example: Tool-Using Agent
A complete specification for a tool-using agent with conditional history replay:
ToolAgent[@T]: { S: { INSTRUCTIONS AVAILABLE_TOOLS } U: { env.user_input[@0] env.user_document[@0] } If t > 1 { ForEach(@t: range(1, @T-1)) { If sys.tool[@t] == clarify { U: env.user_input[@t] } Else { A: sys.tool[@t].tool_response } } } S: {REACT_INSTRUCTIONS} }
ToolAgent[@T]:
AVAILABLE_TOOLS
env.user_document[@0]
Tool-using agent. System messages frame the task; a conditional loop replays interaction history with role assignment determined by action type.
Example: Multi-Agent Prompt
A multi-agent prompt with collection iteration and dialog retrieval:
MultiAgent[@T, agent_name]: { S: sys.agent_desc[agent_name] U: env.datetime[@T] ForEach(other: sys.agent_names) { If env.in_dialog(other, @T) { U: get_dialog_history(other, @T) } } S: QUESTION(agent_name) }
MultiAgent[@T, agent_name]:
Multi-agent interaction. The prompt is parameterized by time and agent identity; dialog histories are conditionally included for agents currently in conversation.