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]
}
Rendered Output

BasicPrompt[@T]:

Role: System
INSTRUCTIONS
Role: User
env.user_question[@T]

Role Messages

Every message carries exactly one role. Four roles serve the chat format; a fifth pseudo-role serves the legacy completion format:

MarkerPurpose
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]
}
Rendered Output
Role: System
INSTRUCTIONS
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
Rendered Output
Role: User
env.user_question[@t]
Role: Assistant
resp.answer[@t]
Role: System
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]
    }
}
Rendered Output
Role: User
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
    }
}
Rendered Output
Role: None
TASK_DESCRIPTION
env.context[@t]
QUESTION

Scoping Rules

The language enforces a strict two-level scope that mirrors the structure of LLM chat APIs:

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:

NamespaceUse Cases
envEnvironment—external inputs, observations, sensor readings, user queries, game state
sysSystem—agent state, memory contents, tool configurations, action histories
respResponse—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
Rendered Output
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
env.bomb_location[@T, bomb] // bomb location

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)
Rendered Output
@T // Current time step
@T-1 // Previous time step
@T.I // Current substep
@t.i // Substep i of turn t

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]
    }
}
Rendered Output
// Iterating over all previous turns
ForEach t : 1 ... @T-1
env.observation[@t]
// Iterating over substeps
ForEach i : 1 ... I
sys.action[@T.i]
// Nested: substeps within each turn
ForEach @t : 1 ... @T-1
ForEach i : 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]
Rendered Output
[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
Rendered Output
INSTRUCTIONS // Task explanation
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)
Rendered Output
summarize(prompt.History[@t])
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]
}
Rendered Output
ForEach @t : 1 ... @T-1
Role: User
env.user_question[@t]
Role: Assistant
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]
    }
}
Rendered Output
Role: User
ForEach bomb : env.bombs
env.bomb_location[@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
}
Rendered Output
If sys.tool[@t] == clarify
Role: User
env.user_input[@t]
ElseIf sys.tool[@t] == search
Role: User
env.search_results[@t]
Else
Role: Assistant
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]
    }
}
Rendered Output
If @T > 1
ForEach t : 1 ... @T-1
Role: User
env.user_input[@t]
Role: Assistant
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]
    }
}
Rendered Output
Switch sys.action_type[@t]
Case "search"
Role: User
env.search_results[@t]
Case "calculate"
Role: User
env.calculation[@t]
Default
Role: User
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]
    }
}
Rendered Output
Role: System
INSTRUCTIONS
Role: User
env.user_input[@T]
PromptEndsHere when @T == 1

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]
}
Rendered Output
Role: System
INSTRUCTIONS
AVAILABLE_TOOLS
1
Role: User
env.user_question[@T]
2

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
}
Rendered Output
Name docs := k_relevant_docs(env.query[@T])
ForEach i : 1..docs.len
docs[i].source
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)
Rendered Output
Name relevant_summaries := [sys.summary[@t] | t@T ... @T-900 every 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])
}
Rendered Output

DocumentContext[doc]

SF
env.doc_title[doc]
env.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]
        }
    }
}
Rendered Output

ConversationContext[@T]

SF
CONTEXT_HEADER
ForEach @t : @T-5 ... @T
sys.Summary[@t]
If sys.has_tool_call[@t]
sys.tool_response[@t]

String 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]
}
Rendered Output
Role: User
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
    }
}
Rendered Output

ConversationTurn[@t]

RF
Role: User
env.user_input[@t]
Role: Assistant
resp.answer[@t]
If sys.tool[@t] != none
Role: Tool
sys.tool[@t].tool_response

Role 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]
}
Rendered Output

ChatAgent[@T]:

Role: System
INSTRUCTIONS
ForEach @t : 1 ... @T
Frag ConversationTurn[@t]
Role: User
env.user_input[@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]
}
Rendered Output

ToolDescription[tool]

SF
sys.tool_name[tool]
sys.tool_schema[tool]

ToolResult[@t, tool]

RF
Role: Assistant
sys.tool_call[@t, tool]
Role: Tool
sys.tool_response[@t, tool]

ToolAgent[@T]:

Role: System
INSTRUCTIONS
ForEach tool : sys.available_tools
Frag ToolDescription[tool]
ForEach @t : 1 ... @T
Role: User
env.observation[@t]
Frag ToolResult[@t, sys.selected_tool[@t]]
Role: User
env.observation[@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]
}
Rendered Output
Role: System
// 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 : 1 ... @T-1
// This comment is inside the loop body
Role: User
env.user_question[@t] // Beside the message
Role: Assistant
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:

ElementConventionExample
TemplatesALL_CAPSTASK_DESCRIPTION
FunctionscamelCasesummarize, k_relevant_docs
Context variablesdot.separatedenv.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}
}
Rendered Output

ToolAgent[@T]:

Role: System
INSTRUCTIONS
AVAILABLE_TOOLS
Role: User
env.user_input[@0]
env.user_document[@0]
If t > 1
ForEach @t : 1 ... @T-1
If sys.tool[@t] == clarify
Role: User
env.user_input[@t]
Else
Role: Assistant
sys.tool[@t].tool_response
Role: System
REACT_INSTRUCTIONS

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)
}
Rendered Output

MultiAgent[@T, agent_name]:

Role: System
sys.agent_desc[agent_name]
Role: User
env.datetime[@T]
ForEach other : sys.agent_names
If env.in_dialog(other, @T)
Role: User
get_dialog_history(other, @T)
Role: System
QUESTION(agent_name)

Multi-agent interaction. The prompt is parameterized by time and agent identity; dialog histories are conditionally included for agents currently in conversation.