- Remove alternative/shorthand forms (list-form routes, short-form prompts, sequence-form parameters, shorthand routing, dynamic_router) - Change SHOULD/MAY to MUST for explicit canonical forms - Deprecate agent_template, top-level instances, and merges-based default router - Expand forbidden forms in §16.2 and add field combination rules - Update cross-references and remove obsolete examples
152 KiB
The Actor Configuration Standard
Version: 1.0.0 Status: Normative
0. Document Conventions
This document uses the following conventions to specify normative requirements:
- MUST, REQUIRED, SHALL indicate an absolute requirement.
- MUST NOT, SHALL NOT indicate an absolute prohibition.
- SHOULD, RECOMMENDED indicate that there may exist valid reasons in particular circumstances to ignore a particular requirement, but the full implications must be understood and carefully weighed.
- SHOULD NOT, NOT RECOMMENDED indicate that there may exist valid reasons in particular circumstances when the particular behavior is acceptable, but the full implications should be understood and the case carefully weighed.
- MAY, OPTIONAL indicate that an item is truly optional.
Code and configuration examples are presented in YAML 1.2 syntax. Internal identifiers, types, and values are presented in monospaced font.
A "compliant implementation" is any software that loads, interprets, and executes Actor configurations in conformance with the requirements of this document.
The terms "configuration", "Actor", and "Actor configuration" are used interchangeably throughout this document to refer to a complete, valid document conforming to this standard.
1. Introduction
1.1 Purpose
This document defines the Actor Configuration Standard: a declarative, YAML-based format for describing networks of cooperating AI agents, the data flow between them, and the runtime behaviors of the resulting system. An Actor configuration captures, in a single document or set of documents, every aspect of an agent-network's structure and semantics required to produce equivalent observable behavior across all conforming systems.
1.2 Scope
This standard normatively defines:
- The structural grammar and syntax of an Actor configuration document.
- The complete set of allowed top-level sections, fields, types, and values.
- The defined agent types and their configuration parameters.
- The defined route types (
stream,graph,bridge) and their internal structures. - The defined node types within graph routes.
- The defined stream operators, including their parameters and semantics.
- The defined condition types used for filtering, routing, and edge evaluation.
- The defined message-routing rules within
message_routernodes. - The reserved/built-in stream names and their semantics.
- The template and instance mechanism for reusable component definitions.
- The context-management mechanism (both per-Actor and global).
- The state-management mechanism for stateful graph execution.
- The environment-variable interpolation mechanism.
- The template-rendering dialects (
JINJA2,MUSTACHE,SIMPLE) and their semantics. - The configuration-validation rules and error semantics.
- The runtime behavior, including message flow, error handling, concurrency, and lifecycle.
- Security boundaries enforced by
safe/unsafemodes.
This standard does NOT define:
- Command-line interfaces, invocation methods, or host integration patterns.
- Any specific programming-language binding, application-programming interface, or development-kit.
- The internal mechanics of LLM providers, transport protocols, or model behavior.
- Storage formats for persisted state beyond the schema visible at the Actor configuration boundary.
- Distribution, deployment, packaging, or operational concerns.
1.3 Conformance
A compliant implementation MUST:
- Accept any well-formed Actor configuration that conforms to this standard and reject any document that does not.
- Apply the validation rules in §11 and signal configuration errors before any agent is instantiated or any message is processed.
- Realize the runtime behavior described in §12, including message flow, ordering, and error routing.
- Support at least all defined agent types (§4), route types (§5), node types (§6.2), operators (§5.3), and conditions (§5.4) as defined in this document.
- Enforce the security model defined in §13.
- Treat all built-in stream names (§5.2) as reserved.
A compliant implementation MAY:
- Provide additional agent types, node types, operators, conditions, or routing match types beyond those defined here, provided they do not conflict with the names defined in this standard.
- Provide additional tooling, persistence formats, or observability features.
- Optimize execution where the optimization is not observable through the Actor configuration's defined behavior.
2. Terminology
| Term | Definition |
|---|---|
| Actor | A complete, valid configuration document that conforms to this standard. |
| Agent | A named processing unit that accepts a message as input and produces a message as output. |
| Route | A named pathway through which messages flow. May be of type stream, graph, or bridge. |
| Stream | A reactive route consisting of an ordered sequence of operators that process messages as they flow through. |
| Graph | A stateful, directed-graph route consisting of nodes and edges, supporting conditional routing and persistent state. |
| Bridge | A meta-route that defines dynamic conversion between stream and graph forms. |
| Node | A vertex in a graph route, representing a single processing step. |
| Edge | A directed connection between two nodes in a graph route, optionally guarded by a condition. |
| Operator | A single processing step within a stream route. |
| Merge | A configuration that combines the output of multiple streams into a single target stream. |
| Split | A configuration that routes messages from a single source stream to multiple target streams based on conditions. |
| Template | A named, reusable, parameterized definition of an agent, graph, or stream. |
| Instance | A concrete agent, route, or stream produced by applying parameters to a template. |
| Context | A keyed dictionary that provides shared state to agents during message processing. |
| Global Context | The Actor-wide context, declared under context.global. |
| Graph State | The per-execution state object of a graph route. |
| Message | A unit of data flowing through routes; consists of content, metadata, source_stream, and timestamp. |
| Built-in Stream | One of the system-reserved streams: __input__, __output__, __error__. |
| Compliant Implementation | Software that satisfies the conformance requirements of §1.3. |
3. Document Structure
3.1 File Format
An Actor configuration MUST be a valid YAML 1.2 document encoded in UTF-8. The top-level YAML value MUST be a mapping (dictionary). Documents that are not mappings, or that fail to parse as YAML, MUST be rejected with a configuration error.
A compliant implementation MUST support the merging of multiple Actor configuration files, applied in the order specified, using the following semantics:
- For each key encountered in a subsequent file:
- If the key is absent from the merged result, it is added.
- If the key is present and both values are mappings, they are deeply merged (recursively).
- If the key is present and both values are sequences, the new sequence is appended to the existing sequence.
- Otherwise, the new value replaces the existing value.
3.2 Top-Level Sections
The following table enumerates all permitted top-level sections of an Actor configuration. Any top-level key not listed here MAY be either ignored or rejected by a compliant implementation; a warning SHOULD be emitted when an unknown top-level section is encountered.
| Section | Required | Type | Purpose |
|---|---|---|---|
agents |
Yes | mapping | Defines all agents in the Actor. (§4) |
routes |
Yes | mapping | Defines all routes. (§5, §6) |
merges |
No | sequence | Stream merge operations. (§7.1) |
splits |
No | sequence | Stream split operations. (§7.2) |
templates |
No | mapping | Reusable component templates. (§8) |
instances |
No | mapping | Template instances (deprecated form). (§8.7) |
context |
No | mapping | Global context variables. (§10) |
prompts |
No | mapping | Named, reusable prompt templates. (§10.4) |
pipelines |
No | mapping | Hybrid (multi-stage) pipelines. (§7.3) |
template_strings |
No | mapping | Raw template strings (Jinja2-style syntax) for deferred preprocessing. (§8.9) |
name |
No | string | Optional human-readable name of the Actor. |
3.3 Minimum Valid Document
The minimum valid Actor configuration MUST contain at least:
- The
agentssection, with at least one valid agent definition, and - The
routessection, with at least one valid route definition (and, whenroutesis present, a default router SHOULD be designated identifying a route present inroutes— see §3.4).
Additionally, for the configured network to produce externally observable output, at least one route MUST publish to __output__ (directly or via merges/splits), and at least one route MUST be reachable from __input__ (directly or via merges).
Example — minimum valid Actor:
agents:
echo:
type: tool
config:
tools: [echo]
routes:
main:
type: stream
operators:
- type: map
params:
agent: echo
publications: [__output__]
merges:
- sources: [__input__]
target: main
This Actor accepts any message on __input__, routes it through the echo tool agent, and emits the result on __output__.
3.4 The Default Router Requirement
When a routes section is present and is non-empty, the Actor MUST designate a default router: a route present in routes that receives messages submitted to the Actor when no explicit target is specified by the consumer. The mechanism by which an implementation accepts the designation is outside the scope of this standard; see §9 for the parameter description.
A compliant implementation MUST satisfy the default-router requirement through the default_router parameter. The alternative approach using a merges entry connecting __input__ to a route is deprecated.
4. Agents
4.1 The agents Section
The agents section MUST be a mapping. Each key MUST be a unique agent name (a non-empty string), and each value MUST be a mapping defining the agent. Agent definitions MUST conform to the following schema:
agents:
<agent_name>:
type: <agent_type> # REQUIRED
config: <mapping> # OPTIONAL (defaults to {})
OR, when instantiated from a template (§8):
agents:
<agent_name>:
template: <template_name> # REQUIRED for template instances
params: <mapping> # OPTIONAL parameters
OR:
agents:
<agent_name>:
agent_template: <template_name> # Alternative key for template
params: <mapping>
Example — declaring multiple agents of different types:
agents:
greeter:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
system_prompt: "Greet the user warmly."
calculator:
type: tool
config:
tools: [math]
formatter:
type: tool
config:
tools:
- name: pretty_format
code: |
result = "RESULT: " + str(input_data)
research_pipeline:
type: composite
config:
components:
agents:
searcher:
type: tool
config:
tools: [http_request]
analyzer:
type: llm
config:
provider: openai
model: gpt-4
routing:
input:
type: agent
name: searcher
output:
type: agent
name: analyzer
4.2 Agent Names
Agent names MUST be unique within an Actor. Implementations SHOULD restrict agent names to the character class [A-Za-z_][A-Za-z0-9_]*. Names beginning with double underscore (__) are reserved for built-in use and MUST NOT be defined by user configurations.
4.3 Defined Agent Types
The following agent types are defined by this standard. Compliant implementations MUST support all defined types.
| Type | Required | Description |
|---|---|---|
llm |
Yes | An agent backed by a large language model. (§4.4) |
tool |
Yes | An agent that executes deterministic tools or inline code. (§4.5) |
composite |
Yes | An agent composed of nested agents, graphs, and streams. (§4.6) |
chain |
Optional | An agent that applies an ordered sequence of textual steps to a message. (§4.7) |
template_instance |
Yes | An agent instantiated from a template. (§4.8) |
Implementations MAY define additional agent types beyond those listed above, but the names llm, tool, composite, chain, and template_instance MUST retain their defined semantics. Additional agent types MUST be made available before any agent of that type is created.
4.4 LLM Agents (type: llm)
LLM agents process messages by invoking a large language model. The following configuration fields are defined:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
provider |
string | No | "openai" |
Name of the LLM provider. (§4.4.1) |
model |
string | No | (provider-specific) | Identifier of the model to invoke. (§4.4.2) |
temperature |
number | No | 0.7 |
Sampling temperature in the range [0.0, 2.0]. |
max_tokens |
integer | No | 1000 |
Maximum number of tokens the model may generate in a single response. |
system_prompt |
string | No | "You are a helpful assistant." |
The system message; MAY contain template expressions in the active template engine. |
memory_enabled |
boolean | No | false |
Whether the agent maintains conversation history. |
max_history |
integer | No | 10 |
Maximum number of historical message pairs to retain when memory_enabled is true. |
api_key |
string | No | (from environment) | Explicit API key; MAY use ${ENV_VAR} interpolation. |
template |
string | No | (none) | Name of a prompt template (registered under prompts) used to format the user message. |
template_vars |
mapping | No | {} |
Additional variables passed to the named template. |
response_format |
mapping | No | (none) | Provider-specific structured-output specification (e.g., JSON schema). (§4.4.3) |
Example — minimal LLM agent:
agents:
simple_chat:
type: llm
config:
provider: openai
model: gpt-4
Example — LLM agent with memory and a custom system prompt:
agents:
helpful_assistant:
type: llm
config:
provider: openai
model: gpt-4
temperature: 0.8
max_tokens: 2000
memory_enabled: true
max_history: 15
system_prompt: |
You are a thoughtful, careful assistant.
Always cite your sources when discussing factual matters.
Example — LLM agent using an environment-variable API key:
agents:
external_api_agent:
type: llm
config:
provider: anthropic
model: claude-3-5-sonnet-20241022
api_key: "${ANTHROPIC_API_KEY}"
system_prompt: "You are an analytical assistant."
4.4.1 Providers
The following providers MUST be supported. The provider name comparison is case-insensitive.
| Provider | Environment Variables (consulted in order, first match used) |
|---|---|
openai |
OPENAI_API_KEY |
anthropic |
ANTHROPIC_API_KEY |
google |
GOOGLE_API_KEY, GOOGLEAI_API_KEY |
Additional providers MAY be supported.
When an API key is not supplied (neither as an api_key field nor through a corresponding environment variable), a ConfigurationError MUST be signaled with a message identifying the missing variables.
When the requested provider is not available in the host, an AgentCreationError MUST be signaled with a clear message describing the unavailable provider.
4.4.2 Default Models
When model is unspecified, the following defaults apply:
| Provider | Default Model |
|---|---|
openai |
gpt-3.5-turbo |
anthropic |
claude-3-5-sonnet-20241022 |
google |
gemini-1.5-flash |
4.4.3 Structured Output
When response_format is provided, it MUST be a mapping conformant to the provider's structured-output mechanism. The standard recognizes the following structure:
response_format:
type: json_schema
json_schema:
name: <string>
strict: <boolean>
schema: <JSON Schema mapping>
The structure MUST be forwarded to the LLM provider's structured-output mechanism. When the provider does not support structured output, an execution error MUST be signaled.
Example — agent that parses a table of contents into structured JSON:
agents:
toc_parser:
type: llm
config:
provider: google
model: gemini-2.0-flash
memory_enabled: false
response_format:
type: json_schema
json_schema:
name: table_of_contents
strict: true
schema:
type: object
properties:
sections:
type: array
items:
type: object
properties:
title:
type: string
description: The section title without numbering
subsections:
type: array
items:
type: object
properties:
title:
type: string
required: [title]
additionalProperties: false
required: [title]
additionalProperties: false
required: [sections]
additionalProperties: false
system_prompt: |
Extract the section titles and their hierarchy from the provided
table of contents. Return them as a nested JSON structure.
4.4.4 Memory and History
When memory_enabled is true:
- The agent MUST maintain a list of conversation entries, each having keys
role(one of"user","assistant") andcontent(string). - The list MUST be truncated to retain only the most recent
max_historyentries (counting each user/assistant pair as two entries). - When invoked, the agent MUST include the historical entries in the prompt sent to the provider, preserving order.
- After each successful invocation, the new user message and the assistant response MUST be appended to the history.
When memory_enabled is false, the agent MUST NOT retain any history between invocations. However, if the invoking context (e.g., a graph node) provides conversation_history in the context dictionary, the agent MUST use that history for the current invocation (without persisting it).
4.4.5 Temperature Override
A _temperature_override value present in the context dictionary at invocation time MUST replace the configured temperature for the duration of that invocation. _temperature_override is set by the host environment outside the configuration document; it MUST NOT be declared by an Actor itself.
4.4.6 System Prompt Rendering
The system_prompt field MAY contain template expressions in the configured template engine (§9.2). At invocation time, the prompt MUST be rendered with a context that, at minimum, contains:
context: the merged context dictionary (global context + invocation-specific context).message: the user message that is being processed.
If rendering fails, a warning MUST be emitted and the literal, unrendered prompt MUST be used as a fallback.
4.5 Tool Agents (type: tool)
Tool agents execute deterministic operations without invoking a language model. The following configuration fields are defined:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
tools |
sequence | Yes | [] |
List of tools the agent may execute. Each element is either a string (built-in tool name) or a mapping (inline tool definition). |
safe_mode |
boolean | No | true |
Enforces sandbox restrictions on tool execution. |
allow_shell |
boolean | No | false |
Whether the agent may execute arbitrary shell commands. |
timeout |
number | No | 1 |
Maximum execution time in seconds for any single tool invocation. |
context |
mapping | No | {} |
Per-agent persistent context, merged with any invocation-time context. |
Example — tool agent that accepts JSON requests for math and JSON parsing:
agents:
utilities:
type: tool
config:
tools:
- math
- json_parse
- echo
safe_mode: true
timeout: 5
A message such as {"tool": "math", "args": {"expression": "2 + 2 * 3"}} would cause the agent to evaluate the expression and return "8".
Example — tool agent with a single inline-code tool (no JSON wrapping needed):
agents:
shouter:
type: tool
config:
tools:
- name: shout
code: |
result = str(input_data).upper() + "!"
A message of "hello world" produces the response "HELLO WORLD!".
4.5.1 Built-in Tools
A compliant implementation MUST support the following built-in tools when listed by name in the tools array. The exact identifiers are case-sensitive.
| Name | Description | Required Arguments |
|---|---|---|
echo |
Returns the input verbatim. | text (string) OR args (list of strings) |
math |
Evaluates a numeric expression in a restricted arithmetic environment exposing only: abs, round, min, max, sum, pow, int, float. Returns the result as a string. |
expression (string) OR args[0] |
json_parse |
Parses a JSON string and returns a re-serialized form indented with two spaces. | json (string) OR args[0] |
http_request |
Performs an HTTP request. The response is returned as a string of the form "Status: <code>\n\n<body>". |
url (string); optional method (default "GET"), headers (mapping), data (mapping serialized as a JSON request body) |
file_read |
Reads a UTF-8 file from the filesystem. Returns a string of the form: [FILE_READ_SUCCESS]📄 File: <path> | Lines: <n> | Size: <n> chars\n[FILE_CONTENT_START]\n<content>\n[FILE_CONTENT_END]. |
file (string path) OR args[0] |
file_write |
Writes/appends/inserts content to a file. | file (string), content (string); optional mode (w, a, insert), position |
progress_bar |
Updates the global progress-bar facility and writes a rendered bar to the host's standard-output channel. (§4.5.6) | stage, phase, current, done, total, steps, count, remaining, sections_total, sections_completed, completed_sections, message, label (all optional) |
For each built-in tool, an absent argument MAY default to a sensible fallback. The standard arguments listed above MUST be honored; additional arguments MAY be silently ignored.
4.5.2 Inline Tools
A tool definition MAY be specified as a mapping:
tools:
- name: <tool_name> # REQUIRED, unique within the agent
code: <code_body> # REQUIRED for inline-code tools (string)
When code is provided, the tool executes the given code body in a sandboxed environment. The execution environment MUST:
- Restrict the available built-in functions and types to the Restricted Built-ins for Inline Code set defined in §13.2.1.
- Expose a JSON parsing and serialization facility under the name
json, providing at minimum the operationsloads(parse a JSON string),dumps(serialize a value to JSON), and an error categoryJSONDecodeErrorsignaled when parsing fails. - Provide the following local variables to the tool body:
input_data: the raw input message (string).message: an alias forinput_data.context: the active context dictionary (passed by reference; modifications propagate back to the caller).result: initiallynull; the tool body sets this to its return value.
- When
resultisnullafter the tool body completes, fall back to one of (in order):output,response,answer, defaulting to the empty string if none are set.
Inline-code tools MUST be enabled only when the host is operating in unsafe mode (see §13).
Example — inline tool that routes a message based on its content:
agents:
router_tool:
type: tool
config:
tools:
- name: route_by_prefix
code: |
# `input_data` holds the incoming message.
# `context` holds the active context.
# `result` MUST be set to the value the tool returns.
text = (input_data or "").strip()
if text.startswith("!"):
result = f"GOTO_COMMAND_HANDLER:{text}"
else:
stage = context.get("current_stage") or "default"
result = f"GOTO_{stage.upper()}:{text}"
Example — inline tool that mutates context and returns the modified state:
agents:
set_topic:
type: tool
config:
tools:
- name: save_topic
code: |
import json
# input_data is expected to be JSON: {"topic": "<value>"}
try:
payload = json.loads(input_data)
topic = payload.get("topic", "")
except json.JSONDecodeError:
topic = input_data
# Mutating `context` propagates back to the caller.
context["current_topic"] = topic
result = f"Topic set to: {topic}"
4.5.3 Tool Invocation Protocol
When a tool agent receives a message, it MUST execute the following algorithm:
- If the agent declares exactly one inline-code tool, that tool is invoked with the entire message as
input_data. - Otherwise, the message is parsed as JSON. If valid and the JSON contains a
toolkey, the named tool is invoked with arguments from theargskey. - If the message appears to be JSON (starts with
{and ends with}) but fails to parse, an execution error reporting "Invalid JSON in tool request" MUST be signaled. - Otherwise, the message is parsed as a space-separated command of the form
<tool_name> <arg1> <arg2> .... - If the named tool is not in the allowed
toolslist, an execution error MUST be signaled.
4.5.4 Safe-Mode Restrictions
When safe_mode is true (the default):
- The shell tool MUST refuse to execute any command containing any of the literal substrings:
rm,del,format,shutdown,reboot,kill(case-insensitive comparison). file_readMUST reject paths containing..(directory traversal).file_readMUST reject absolute paths unless the invocation context contains_unsafe_mode: true.file_writeMUST reject:- Calls when the invocation context does not contain
_unsafe_mode: true. - Paths containing
..directory traversal. - Paths resolving outside the current working directory (unless
_unsafe_mode: true). - Paths beginning with
~.
- Calls when the invocation context does not contain
When safe_mode is false, these restrictions are relaxed. However, file_write still requires _unsafe_mode: true to perform any write operation.
4.5.5 file_write Modes
The file_write tool accepts a mode argument with the following defined values:
| Mode | Description |
|---|---|
w |
Overwrite the file with the given content. (Default) |
a |
Append content to the end of the file. If content begins with # (Markdown section), insertion includes a separating blank line. |
insert |
Insert content at a specified position. |
The position argument applies only to insert mode and accepts:
| Value | Description |
|---|---|
"start" |
Insert at line 1. |
"end" |
Insert at the end (equivalent to append). |
| integer N | Insert before line N (1-indexed; clamped to [1, len(lines)+1]). |
null / absent |
Defaults to "end". |
file_write MUST return a success message of the form "Successfully wrote/appended/inserted <n> characters [to|at line N in] <path>".
4.5.6 The Progress-Bar Facility
The progress_bar tool drives a progress-bar facility that is safe to invoke from concurrent contexts. The facility maintains a single global snapshot consisting of (stage, current, total, message) and emits a textual rendering to the host's standard-output channel. The rendering MUST consist of:
- A bar of fixed width 24 cells, using
█for filled cells and░for empty cells. - A percentage with one decimal place.
- A counter of the form
(current/total). - An optional label combining
stageandmessage, joined by•.
The facility MUST:
- Resolve unspecified arguments by inferring from the active context's
writing_stage,section_paths,current_section_index, andcurrent_latex_indexkeys when present. These keys are conventional progress-bar hooks (§10.3). - Clamp
currentto the range[0, total]. - Default
totalto1when no upstream value is available. - Emit a trailing newline when progress reaches 100%.
- Be invocable both from the
progress_bartool (with the arguments listed in §4.5.1) and from inline tool code that needs to update progress mid-stream.
4.5.7 Tool Output Conventions
Tool output is returned as a string. Non-string return values MUST be coerced to strings using their canonical string representation. When a tool produces no value, the empty string MUST be returned.
When a tool's invocation mutates the active context, those mutations MUST be visible to subsequent processing steps. An Actor's host environment MAY additionally retain context snapshots for replay or audit; such snapshots are not part of the externally observable behavior of this standard.
4.6 Composite Agents (type: composite)
Composite agents encapsulate other agents, graphs, and streams behind a single agent interface. The following configuration fields are defined:
| Field | Type | Required | Description |
|---|---|---|---|
components |
mapping | Yes | Internal components: agents, graphs, streams. |
routing |
mapping | Yes | Routing configuration: input, output. |
expose_params |
mapping | No | Parameters exposed for override at instantiation time. |
Example — composite agent wrapping a researcher + summarizer pipeline:
agents:
research_then_summarize:
type: composite
config:
components:
agents:
researcher:
type: llm
config:
provider: openai
model: gpt-4
system_prompt: "Gather facts on the topic, citing each one."
summarizer:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
system_prompt: "Summarize the facts in 3 sentences."
routing:
input:
type: agent
name: researcher
output:
type: agent
name: summarizer
4.6.1 Components
The components mapping accepts three optional keys: agents, graphs, streams. Each is itself a mapping from a local component name to a component definition (using the same schema as the corresponding top-level section).
4.6.2 Routing
The routing mapping MUST specify the entry and exit components using the explicit form:
routing:
input:
type: <agent|graph|stream> # REQUIRED
name: <component_name> # REQUIRED
output:
type: <agent|graph|stream>
name: <component_name>
The composite agent's processing logic is:
- Receive input message.
- Dispatch to the input component.
- Allow internal flow per the component's own configuration.
- Collect the output from the output component.
- Return that output as the composite's response.
4.6.3 Restrictions
A strategy key at the composite level is prohibited. A configuration containing strategy under a composite agent's config MUST be rejected.
4.7 Chain Agents (type: chain)
Chain agents apply an ordered sequence of textual processing steps to a message. They are optional in implementations of this standard but, where supported, MUST conform to the following schema:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
steps |
sequence of strings | No | [] |
Ordered list of step labels appended to the message in turn. |
prompt |
string | No | (none) | Inline prompt template applied to the input message. |
prompt_reference |
string | No | (none) | Name of a registered prompt template (see §10.4) to apply. |
prompt and prompt_reference MUST NOT both be specified; doing so MUST be rejected with ConfigurationError.
When invoked, the chain agent:
- If
prompt/prompt_referenceis set, renders the prompt with a context augmented bymessage = <input>. - Iterates through
steps, appending each step label to the running result with the separator->. - Returns the final string prefixed by
"ChainAgent processed: ".
Chain agents are intentionally simple and are primarily useful for testing and composition. They MUST NOT make any external calls (LLM or otherwise).
4.8 Template Instance Agents (type: template_instance)
Agents declared with template, agent_template, or template_instance are instantiated from a template registered under templates.agents. See §8. During configuration parsing, agents containing a top-level template or agent_template key are automatically marked with the internal type template_instance; this type tag is not normally written by hand.
4.9 Agent Context Inheritance
When an agent is created, the Actor's global context (context.global) MUST be merged into the agent's config.context unless the agent already declares a config.context of its own. The merged context becomes the per-agent persistent context.
4.10 Agent Type Availability
The agent types llm and tool MUST be available before any agent is instantiated. The composite and chain types, when supported, MUST likewise be available. Custom agent types beyond those defined by this standard MUST be made available before configuration loading begins; making a custom type available after configuration loading has begun is outside the scope of this standard.
4.11 Capabilities and Metadata
Every agent exposes:
- A capabilities list — a sequence of strings describing the agent's published abilities. Standard capability identifiers include
"text-generation","conversation","reasoning","analysis","creative-writing","structured-output","tool-execution","command-execution","http-requests","file-operations","math-evaluation","composite","stateful-workflow","reactive-processing", and"chain-processing". - A metadata mapping — at minimum containing the agent's
name,type,capabilities, andreactive: true.
LLM agents additionally expose provider, model, temperature, max_tokens, memory_enabled, and supports_structured_output in their metadata. Tool agents additionally expose tools, allow_shell, safe_mode, and timeout. Composite agents additionally expose the union of capabilities of their internal components.
These properties are observable but not part of the configuration grammar; they are exposed for inspection by tooling and orchestration layers.
5. Routes — Common Concepts
5.1 The routes Section
The routes section is a mapping. Each key is a unique route name; each value is a mapping defining the route. Every route MUST specify a type field with one of the values:
| Type | Section |
|---|---|
stream |
§5.3 |
graph |
§6 |
bridge |
§5.6 |
Route names MUST be unique across the Actor. The following names are reserved and MUST NOT be used for user-defined routes: __input__, __output__, __error__.
5.2 Built-in Streams
The following streams MUST exist implicitly in every Actor and MUST behave as defined:
| Name | Direction | Semantics |
|---|---|---|
__input__ |
Producer | Messages submitted to the Actor enter here. |
__output__ |
Consumer | Messages emitted to __output__ become the externally observed result of the Actor. |
__error__ |
Consumer | Messages emitted here represent errors observed during execution. |
All three built-in streams MUST be of the cold stream type and MUST be available for use as targets/sources in routes, merges, and splits, even if not declared explicitly.
5.3 Stream Routes (type: stream)
A stream route is an ordered, reactive pipeline of operators acting on messages. The following fields are defined:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | Yes | — | MUST be "stream". |
stream_type |
string | No | "cold" |
One of cold, hot, replay. (§5.3.1) |
operators |
sequence | No | [] |
Ordered list of operator configurations. (§5.3.2) |
subscriptions |
sequence | No | [] |
Names of source streams this stream consumes from. |
publications |
sequence | No | [] |
Names of target streams this stream publishes to. |
agents |
sequence | No | [] |
Names of agents referenced by operators. (Informational/validation hint.) |
initial_value |
any | No | null |
Initial value emitted by hot streams. |
buffer_size |
integer | No | 1 |
Buffer length for replay streams. |
bridge |
mapping | No | (none) | Bridge configuration. (§5.6) |
metadata |
mapping | No | {} |
Free-form metadata. |
template_config |
mapping | No | (none) | Template-instance configuration. (§8) |
Example — simple stream that processes messages through one agent:
routes:
chat_route:
type: stream
stream_type: cold
operators:
- type: map
params:
agent: chat_agent
publications:
- __output__
Example — multi-operator stream with filter, throttle, and agent:
routes:
rate_limited:
type: stream
stream_type: cold
operators:
- type: filter
params:
condition:
type: content_not_contains
text: ""
- type: throttle
params:
duration: 1.0
- type: map
params:
agent: my_agent
publications: [__output__]
5.3.1 Stream Types
| Type | Semantics |
|---|---|
cold |
Begins emitting only when subscribed to. Each subscriber receives the full sequence independently. |
hot |
Always emits, replaying the most recent value to new subscribers. initial_value MAY be specified to seed the stream; if omitted, the stream begins with no value. |
replay |
Replays the most recent buffer_size values to each new subscriber. |
5.3.2 Operators
Each operator is a mapping with type (string) and params (mapping). The following operators MUST be supported:
map
Transforms each message.
- type: map
params:
agent: <agent_name> # Use a named agent to transform
# OR
function: <function_name> # Built-in or registered function
# OR
transform: # Built-in transformation
type: <transform_type> # See §5.3.5
<transform-specific keys>
Exactly one of agent, function, or transform MUST be specified.
Example — map operator routing every message through an agent:
routes:
chat_route:
type: stream
operators:
- type: map
params:
agent: my_chat_agent
publications: [__output__]
Example — map operator applying a built-in format transform:
routes:
prefix_route:
type: stream
operators:
- type: map
params:
transform:
type: format
template: "[PROCESSED] {content}"
publications: [__output__]
filter
Filters messages based on a condition.
- type: filter
params:
condition: <condition> # See §5.4
Example — filter route that drops empty messages:
routes:
non_empty:
type: stream
operators:
- type: filter
params:
condition:
type: content_not_contains
text: "" # Equivalent to "drop empty content"
- type: map
params:
agent: my_agent
publications: [__output__]
transform
Applies an inline expression to message content, or applies a built-in transform.
- type: transform
params:
fn: "<expression>" # Inline expression evaluated in a restricted sandbox
# OR
type: <transform_type> # See §5.3.5
<transform-specific keys>
When fn is provided, the expression MUST be evaluated using the restricted expression sandbox defined in §13.2.2. The expression takes the form of a single-argument anonymous function whose parameter is bound to the message's content field (NOT the full message envelope). The expression's value replaces the message content; the surrounding message and metadata are otherwise unchanged.
When type is provided, the params mapping is passed directly to the transform handler (§5.3.5), and the operator behaves identically to a map operator with the same transform.
An operator declaring neither fn nor type MUST be rejected with a stream-routing error.
debounce
Suppresses emissions during periods of activity, emitting only after a quiet period.
- type: debounce
params:
duration: <number> # Duration in seconds
In single-shot execution contexts, the effective debounce time MUST be capped at 50 ms to avoid hangs.
throttle
Limits emission rate.
- type: throttle
params:
duration: <number> # Minimum interval between emissions, in seconds
delay
Delays each emission by a fixed duration.
- type: delay
params:
duration: <number> # Seconds
buffer
Batches messages.
- type: buffer
params:
count: <integer> # Emit every N messages
timeout: <number> # Optional max wait, in seconds (default 0.1)
# OR
time: <number> # Buffer for N seconds and flush
The buffer operator MUST flatten the buffered list back into individual emissions downstream, ensuring downstream operators continue to see individual messages.
Example — buffer to batch every 5 messages:
routes:
batched:
type: stream
operators:
- type: buffer
params:
count: 5
- type: map
params:
agent: batch_processor
publications: [__output__]
Example — buffer to flush every 2 seconds:
routes:
time_batched:
type: stream
operators:
- type: buffer
params:
time: 2.0
publications: [__output__]
scan / reduce
Stateful aggregations.
- type: scan # Emits intermediate results
params:
accumulator:
type: <collect|concat|sum>
- type: reduce # Emits only the final result
params:
accumulator:
type: <collect|concat|sum>
switch / conditional_route
Conditional routing within a stream. Cases are evaluated in declaration order.
- type: switch # alias: conditional_route
params:
cases:
- condition: <condition>
target: <stream_name> # OPTIONAL — route matching messages to this stream
operators: <sequence> # OPTIONAL — apply inline operators when matched
default: <stream_name> # OPTIONAL — fallback stream when no case matches
default_operators: <sequence> # OPTIONAL — fallback inline operators
Semantics:
- For each case, evaluate the
conditionagainst the current message (§5.4). - On the first match:
- If the case has
operators, apply them inline and emit the result downstream. - Else if it has
targetand the target stream exists, forward the message to the named stream.
- If the case has
- If no case matches:
- If
default_operatorsis set, apply them inline. - Else if
defaultis set and the default stream exists, forward the message there. - Otherwise, pass the message through unchanged.
- If
A target (or default) naming an unknown stream is silently treated as no-match.
Example — switch operator routing messages to different downstream lanes:
routes:
classifier:
type: stream
operators:
- type: switch
params:
cases:
- condition:
type: content_contains
text: "?"
target: question_lane
- condition:
type: content_contains
text: "!"
target: urgent_lane
default: normal_lane
publications: []
question_lane:
type: stream
operators:
- type: map
params:
agent: question_agent
publications: [__output__]
urgent_lane:
type: stream
operators:
- type: map
params:
agent: urgent_agent
publications: [__output__]
normal_lane:
type: stream
operators:
- type: map
params:
agent: chat_agent
publications: [__output__]
catch
Catches stream errors and emits them to __error__.
- type: catch
params: {}
retry
Retries on failure.
- type: retry
params:
count: <integer> # default 3
distinct
Suppresses duplicate consecutive emissions.
- type: distinct
params: {}
take / skip
- type: take
params:
count: <integer> # default 1
- type: skip
params:
count: <integer> # default 1
sample
Periodic sampling.
- type: sample
params:
interval: <number> # Seconds (default 1.0)
graph_execute (Bridge Operator)
Invokes a graph route from within a stream.
- type: graph_execute
params:
graph: <graph_route_name>
The operator MUST execute the named graph for each incoming message, wait for the result, and forward a message whose content is the final state's last assistant message (or the full state dictionary if no messages are present) and whose metadata includes graph, execution_history, and final_state keys.
state_update (Bridge Operator)
- type: state_update
params:
graph: <graph_route_name>
Applies the message content as a state update to the named graph and forwards the message annotated with metadata.state_updated = true.
state_checkpoint (Bridge Operator)
- type: state_checkpoint
params:
graph: <graph_route_name>
Triggers a checkpoint save on the named graph's state (see §6.6) and forwards the message annotated with metadata.checkpointed = true.
graph_node (Bridge Operator)
- type: graph_node
params:
graph: <graph_route_name>
node: <node_name>
Executes a single node within the named graph, returning the node's output.
conditional_route Ambiguity
The operator name conditional_route is recognized in two contexts:
- As a graph-bridge operator, it routes messages to named groups by evaluating a computed route key against a
routesmapping (routes: <mapping of route_name to condition>) with an optionaldefault. - When no graph bridge is engaged, it falls through to the
switchsemantics defined in §5.3.2.
The graph-bridge interpretation MUST take precedence when the route engages a graph bridge. Authors are encouraged to use the switch name to disambiguate within a stream context.
5.3.3 Function References
The function parameter (used in map operators) names a built-in transformer defined by this standard or registered as an extension. The named transformer MUST be resolved to a registered transformer. When no such transformer is registered, the operator MUST behave as the identity transformation (passing each message through unchanged).
The function parameter MUST NOT be used to declare or evaluate arbitrary code expressed in the configuration document. Authors who wish to apply an inline expression to a message SHOULD use the transform operator with the fn parameter (§5.3.2). The fn expression receives the message's content (not the full message envelope), and its return value replaces the message content.
5.3.4 Agent Mappers
When agent is specified for a map operator, the named agent is invoked once per message. The agent's response replaces the message content; the original metadata is preserved and augmented with processed_by: <agent_name>.
Agent invocation in stream routes MUST:
- Resolve the context to pass to the agent in the following order: (a) the message's
metadata.contextif present, (b) the active global context reference, (c) the empty context. - Invoke the agent so that its asynchronous response is delivered back into the stream pipeline.
- Enforce a 120-second hard timeout per agent invocation. On timeout, a message MUST be emitted with
contentequal to"Processing timed out"and metadata fieldstimeout: trueanderror: true. - On any other error, emit a message with
contentequal to"Error: <reason>", with metadata fieldserror: trueandprocessed_by: <agent_name>. - After successful execution, propagate the context reference into the outgoing message's
metadata.context.
5.3.5 Transform Types
The transform family supports the following type values:
| Type | Parameters | Effect |
|---|---|---|
extract |
field: <key> |
If content is a mapping, replace content with content[field]. |
wrap |
wrapper: <mapping> |
Replace content with {**wrapper, "data": content}. |
format |
template: <format_string> |
Replace content with the result of substituting the current content into the format string. The format string MUST recognize the placeholder {content}, which is replaced by the stringified current content. |
replace |
old: <string>, new: <string> |
String replacement on content. |
identity (default) |
— | No change. |
5.3.6 Accumulators
| Type | Effect |
|---|---|
collect |
Appends content to a list. |
concat |
Concatenates content as a string. |
sum |
Numeric sum (ignores non-numeric content). |
5.4 Conditions
Conditions are mappings with a type key. They are used by multiple subsystems, each of which supports a specific subset of the condition vocabulary. The following table enumerates all defined condition types, the subsystems that honor them, and the default behavior for unknown types.
| Type | Parameters | Semantics | Honored By |
|---|---|---|---|
always |
— | Always true. | All subsystems. |
never |
— | Always false. | Conditional nodes, stream router (returns True). |
content_contains |
text: <string> |
True if the substring is in the message content (stringified). | All subsystems. |
content_not_contains |
text: <string> |
True if the substring is NOT in the message content. | Stream router, conditional nodes, bridge router. |
content_starts_with |
text: <string> |
True if content (stringified) starts with the text. | Conditional nodes. |
content_type |
value: <type_name> |
True if the canonical type name of the message content equals value. Recognized type names include str, int, float, bool, list, dict, NoneType. |
Bridge conditional_route only. |
metadata_has |
key: <string> |
True if the key is in the message's metadata. | Stream router, bridge router. |
metadata_check |
key: <string>, value: <any> |
True if state.metadata[key] == value. |
Conditional graph nodes only. |
source_is |
source: <stream_name> |
True if message.source_stream == source. |
Stream router only. |
has_messages |
— | True if the graph state has at least one message. | Conditional graph nodes only. |
message_count |
operator: <gt|lt|eq>, value: <integer> |
True if the state's message count satisfies the comparison. | Conditional graph nodes only. |
context_value |
key: <string>, value: <any> |
True if state's metadata[key] == value, also checking nested context. (§6.5.1) |
Pure-graph edges only. |
output_pattern |
pattern: <string> |
True if state.metadata.last_output starts with the pattern. |
Pure-graph edges only. |
message_pattern |
pattern: <string> |
True if the substring is in the last message's content. | Pure-graph edges only. |
custom |
function: <callable> |
True if the bound callable returns truthy. | Conditional graph nodes only. |
5.4.1 Subsystem-Specific Behavior
The condition-evaluation behavior differs by subsystem; implementations MUST adhere to the rules below:
| Subsystem | Default for Unknown Condition Type | Default When type Is Absent |
|---|---|---|
Stream-router conditions (filter, switch, splits) |
True ("pass") | always (true) |
Bridge conditional_route |
False ("no-match") | always (true) |
| Pure-graph edge conditions | True (warning emitted) | True (always traverse) |
| Conditional graph nodes | True | True |
Authors SHOULD always specify an explicit type rather than relying on defaults; implementations SHOULD emit warnings when an unknown condition type is encountered.
5.4.2 Conditions Not Honored Across Subsystems
A condition declared in a subsystem that does not honor it MUST follow the subsystem's default behavior. For example, content_type declared on a stream-router filter MUST evaluate as if the type were unknown, with the stream-router default (True/pass).
Implementations MAY introduce additional condition types beyond those listed here; such extensions MUST NOT redefine the semantics of any condition type listed above.
5.5 Subscriptions and Publications
A stream's publications list declares the streams that MUST receive every emission produced by this stream. Setting up publications is automatic at stream-creation time; each publication target MUST receive every value, error, and completion notification emitted by the source stream.
A stream's subscriptions list, by contrast, is informational only: the canonical wiring mechanism for incoming messages is the merges section (§7.1). Implementations MUST validate that names in subscriptions reference known routes or built-in streams but MUST NOT use those names to create automatic subscriptions at stream-creation time. Authors who wish to connect a source to a target SHOULD use a merges entry. The subscriptions field exists for documentation purposes and to aid validation tools.
Interactions with merges and splits:
- A
mergesubscribes itstargetstream to the union of emissions from allsources. If the target does not already exist, it is created as acoldstream. - A
splitfilters thesourcestream against each named condition and forwards matching emissions to the named target stream (creating the target if necessary). - Declaring a stream's
publicationsto include a target that is also amergesourceorsplitsource MUST NOT cause double-delivery; implementations MUST guarantee at-most-once delivery per logical message-route pair.
5.6 Bridge Routes (type: bridge)
Bridge routes define conversion logic between stream and graph forms but do not themselves transmit messages directly. Fields:
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | MUST be "bridge". |
upgrade_conditions |
mapping | No | Conditions for converting stream → graph. |
downgrade_conditions |
mapping | No | Conditions for converting graph → stream. |
state_extractor |
string | No | Inline expression extracting state for stream → graph conversion. Evaluated using the restricted expression sandbox (§13.2.2). |
state_flattener |
string | No | Inline expression producing a flat result from graph state. Evaluated using the restricted expression sandbox (§13.2.2). |
preserve_subscriptions |
boolean | No | Whether to preserve subscriptions across the transition. Default true. |
preserve_checkpointing |
boolean | No | Whether to preserve checkpointing. Default true. |
metadata |
mapping | No | Free-form metadata. |
A bridge MAY also be embedded inside a stream or graph route to describe its own conversion characteristics in-place. Bridge definitions are advisory: implementations MAY choose not to act on them but MUST NOT reject configurations containing valid bridge declarations.
Bridge condition keys recognized by this standard:
| Key | Type | Used For |
|---|---|---|
needs_state |
boolean | Upgrade trigger when stateful processing is required. |
message_count |
integer | Upgrade trigger after N messages. |
custom_predicate |
string (expression) | Custom evaluator taking two parameters bound to the incoming message and the route configuration. Evaluated using the restricted expression sandbox (§13.2.2). |
idle_time |
number (seconds) | Downgrade trigger after inactivity. |
state_size |
integer | Downgrade trigger when state is minimal. |
no_conditionals_used |
boolean | Downgrade trigger when conditional logic is unused. |
complexity_threshold |
number | Pure-bridge upgrade trigger. |
6. Graph Routes (type: graph)
6.1 Graph Route Structure
A graph route defines a stateful, directed graph of nodes connected by edges. Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | Yes | — | MUST be "graph". |
nodes |
mapping | Yes (when not a template instance) | — | Node definitions. (§6.2) |
edges |
sequence | Yes (when not a template instance) | — | Edge definitions. (§6.5) |
entry_point |
string | No | "start" |
Name of the entry node. |
checkpointing |
boolean | No | false |
Whether to persist state to disk. (§6.6) |
checkpoint_dir |
string | No | (none) | Directory for checkpoint files; required when checkpointing is true. |
enable_time_travel |
boolean | No | false |
Enable state-history retention. |
parallel_execution |
boolean | No | true |
Allow parallel node execution when next-node set has > 1 element and they are not data-dependent. |
state_class |
string | No | (none) | Fully qualified identifier of a custom state schema known to the host. |
bridge |
mapping | No | (none) | Bridge configuration. (§5.6) |
metadata |
mapping | No | {} |
Free-form metadata. |
template_config |
mapping | No | (none) | Template-instance configuration. (§8) |
Example — linear graph route:
routes:
linear:
type: graph
entry_point: start
nodes:
process:
type: agent
agent: my_agent
edges:
- source: start
target: process
- source: process
target: end
Example — graph route with checkpointing and time travel:
routes:
resilient_workflow:
type: graph
entry_point: start
checkpointing: true
checkpoint_dir: "./checkpoints/resilient"
enable_time_travel: true
parallel_execution: false
nodes:
step1:
type: agent
agent: validator
step2:
type: agent
agent: processor
edges:
- source: start
target: step1
- source: step1
target: step2
- source: step2
target: end
6.2 Nodes
Each entry in nodes is a mapping with at least a type field. The following node types are defined.
| Type | Section |
|---|---|
start |
§6.2.1 |
end |
§6.2.1 |
agent |
§6.2.2 |
function |
§6.2.3 |
tool |
§6.2.3 |
conditional |
§6.2.4 |
subgraph |
§6.2.5 |
message_router |
§6.2.6 |
When written in YAML, these MAY appear as either lowercase or uppercase (AGENT/agent). The type name MUST be matched case-insensitively. The names start and end are reserved.
6.2.1 start and end Nodes
start and end nodes MUST be added implicitly to every graph route if not already present. The start node receives the initial message; the end node terminates execution and emits the final state as output.
6.2.2 Agent Nodes (type: agent)
<node_name>:
type: agent
agent: <agent_name> # REQUIRED
retry_policy: <mapping> # OPTIONAL
timeout: <number> # OPTIONAL, in seconds
parallel: <boolean> # OPTIONAL, default false
metadata: <mapping> # OPTIONAL
retry_policy accepts:
retry_policy:
max_retries: <integer> # default 3
delay: <number> # default 1.0 seconds
When the named agent processes the message, the following behavior MUST occur:
- The current message content is injected as the agent's input.
- A
contextmapping is built containing:graph_state: a dictionary representation of the graph state (per §6.3.1), withmessagestrimmed (see §6.3.2).conversation_history: the trimmed history.full_context:true.- Any keys present in the graph state's
metadataat the top level. - Nested
contextkeys flattened to the top level.
- After the agent call returns, any modifications the agent made to the
contextmapping MUST be propagated back to the graph state'smetadata, with the exception of the reserved internal keys (graph_state,conversation_history,full_context,_history_truncated,_history_original_length). - The value
last_agent_node = <node_name>MUST be recorded in the state's metadata.
6.2.2.1 Agent Node Metadata Keys
The following keys in a node's metadata MUST be recognized, with the defined defaults:
| Key | Default | Effect |
|---|---|---|
max_history_messages |
20 |
Maximum messages passed to the agent. |
max_history_chars |
12000 |
Maximum total characters of history passed. |
6.2.3 Function and Tool Nodes (type: function, type: tool)
<node_name>:
type: function
function: <function_name> # REQUIRED
metadata: <mapping> # OPTIONAL
The function field MUST name a built-in function. Compliant implementations MUST support the following built-in functions:
| Name | Effect |
|---|---|
summarize |
Adds metadata.summary = "Summary of N messages". |
route |
Adds metadata.route = "question" (if last message contains ?) or "statement". |
validate |
Adds metadata.valid = true/false. |
Limitations: The function field does NOT accept inline code, expressions, or arbitrary callable references declared in the configuration. An unknown function name MUST be silently treated as a no-op (returning an empty mapping). Authors who need custom logic SHOULD use one of the following:
- An
agentnode referencing atoolagent with inline code (§4.5.2); - A
message_routernode for routing logic (§6.2.6); or - An extension function registered with the host environment as an addition to the standard set.
Tool nodes (type: tool) execute the named tools sequentially, populating metadata.tool_results with one entry per tool. Each entry has the form {"tool": <name>, "result": "Executed <name>"}.
6.2.4 Conditional Nodes (type: conditional)
<node_name>:
type: conditional
condition: <condition> # See §5.4
A conditional node evaluates its condition against the graph state and adds metadata.condition_result: <bool>. Routing is performed by edge conditions, not by the conditional node itself.
6.2.5 Subgraph Nodes (type: subgraph)
<agent_name>:
agent_template: <template_name> # DEPRECATED - use `template` instead
params: <mapping>
Subgraph nodes mark a position for nested graph execution. Compliant implementations MUST record metadata.subgraph = <graph_route_name> and metadata.subgraph_pending = true.
6.2.6 Message Router Nodes (type: message_router)
<node_name>:
type: message_router
rules: # REQUIRED, ordered list
- match_type: <prefix|suffix|contains|regex|exact> # REQUIRED
pattern: <string> # REQUIRED
target: <node_name> # REQUIRED
extract_message: <boolean> # OPTIONAL, default true (per match_type variations)
separator: <string> # OPTIONAL, default ":"
set_state: <mapping> # OPTIONAL
6.2.6.1 Rule Evaluation Order
Rules MUST be evaluated in declaration order. The first matching rule's target is set as the state's next_node. Subsequent rules MUST NOT be evaluated for the same message.
6.2.6.2 Match Types
| match_type | Match Semantics | Default extract_message Behavior |
|---|---|---|
prefix |
True when the message starts with pattern |
Strips pattern followed by separator (or pattern alone if the separator is not present) from the start of the message. |
suffix |
True when the message ends with pattern |
Strips pattern from the end of the message. |
contains |
True when pattern appears anywhere in the message |
If pattern followed by separator is present in the message, the extracted message is the text after that occurrence. |
regex |
True when the regular expression pattern matches anywhere in the message |
When the pattern contains capture groups, the first group is the extracted message; else the entire message is returned. |
exact |
True when the message equals pattern exactly |
Returns the empty string. |
When extract_message: false is set, the message is passed through unchanged.
6.2.6.3 State Updates
When a rule matches and includes set_state, each key-value pair in set_state MUST be written to the graph state's top level (or metadata).
6.2.6.4 Routing Effect
After a match, the state's next_node MUST be set to the rule's target, the last message's content MUST be modified per the extraction rules, and any set_state entries MUST be applied. Downstream edges may then route based on the next_node value using a context_value condition (§6.5.1).
When no rule matches, the state MUST be returned unchanged (no next_node is set), and downstream edge evaluation determines the next node by other means (e.g., unconditional edges).
6.3 Graph State
6.3.1 The Graph State Structure
The default state object has the following fields:
| Field | Type | Default | Description |
|---|---|---|---|
messages |
list of mappings | [] |
Conversation messages: [{role, content, ...}, ...]. |
metadata |
mapping | {} |
Arbitrary key-value metadata. |
current_node |
string | null | null |
Name of the currently executing node. |
execution_count |
integer | 0 |
Number of state updates. |
error |
string | null | null |
Last recorded error, if any. |
messages MUST be capped at 50 entries; older entries are dropped during merge/append updates.
6.3.2 State Update Modes
Updates are applied via one of three modes:
| Mode | Effect |
|---|---|
replace |
Setter replaces the existing field value. |
merge (default) |
Dictionaries are recursively merged; lists are extended; scalars are replaced. |
append |
Lists have the value appended (or extended if value is a list). |
6.3.3 Custom State Schemas
state_class accepts a string identifier (typically of the form <namespace>.<name>) referring to a custom state schema known to the host. The named schema MUST be resolvable and SHOULD extend the default graph-state structure defined in §6.3.1. When the schema cannot be resolved, the default graph state MUST be used as a fallback and a warning SHOULD be emitted.
6.4 Edges
Each entry in edges is a mapping:
| Field | Type | Required | Description |
|---|---|---|---|
source |
string | Yes | Source node name (or "start"/"END"). |
target |
string | Yes | Target node name (or "end"/"END"). |
condition |
mapping | No | Edge condition. (§5.4) |
metadata |
mapping | No | Free-form metadata. |
The identifiers END (uppercase) and end (lowercase) MUST be normalized to refer to the same end node. The same applies to START/start.
6.5 Edge Conditions
If condition is omitted, the edge is always traversed. If present, the condition is evaluated against the graph state per §5.4. Edges are traversed in declaration order; multiple edges with satisfied conditions cause parallel or sequential traversal per parallel_execution.
6.5.1 context_value Condition Resolution
The context_value condition is graph-specific and resolves its key against the state by checking, in this order:
state.metadata[key]state.metadata["context"][key](nested form)
Equality MUST be checked using value-equality semantics (deep equality for collections, identity-equivalent equality for scalars).
6.6 Checkpointing and Time Travel
When checkpointing: true:
- The directory named by
checkpoint_dirMUST be created if it does not exist. - Every ten state updates, the full state MUST be persisted to a JSON file named
checkpoint_<timestamp>.jsonwithin that directory, where<timestamp>is a sortable date-time string. - The most recent checkpoint MUST be discoverable for recovery.
- A facility MUST exist for restoring state from a specified checkpoint file.
When enable_time_travel: true:
- Up to 100 historical state snapshots MUST be retained.
- A time-travel operation MUST be available to restore an earlier state by specifying the number of steps to rewind.
- Snapshots MUST be evicted in first-in-first-out order when the limit is exceeded.
6.7 Parallel Execution
The graph-level parallel_execution: true flag declares that the graph may execute multiple ready nodes concurrently. The flag does not by itself cause parallelism; each individual node MUST additionally declare parallel: true in its node configuration to participate in parallel execution. The following behavior MUST apply:
- The set of next-nodes available from the most recent execution step is computed.
- The subset whose nodes have
parallel: trueis identified. - That subset is executed concurrently, with all results joined before continuing.
- Non-parallel-marked nodes are executed sequentially.
When parallel_execution: false is set on the graph route, all nodes MUST execute sequentially regardless of their individual parallel flags.
6.8 Loop and Cycle Detection
Graphs MUST be protected by the following runtime guards to prevent unbounded execution:
- Maximum recursion depth: bounded by the greater of
2000and50times the number of nodes in the graph. On overflow, a warning MUST be emitted and the most recent output MUST be returned unchanged. - Node-message revisit: when the same non-router node is visited twice with the same message fingerprint (taken as the first 200 characters of the message content), execution MUST stop and emit the current output. Exception: when
context.auto_finish_activeis true, this guard MUST be relaxed. - Router-agent ping-pong: when a pattern of
[router → agentX → router → agentX](i.e., the same agent appearing twice with a router between each pair) is detected in the recent execution path, execution MUST stop and emit the output. Exception: same as above. - Agent return-without-routing: when an agent node's output does not contain any of the routing tokens
GOTO_,ROUTE_,SET_,CMD_,DISCOVERY_RESPONSE,AUTO_SECTIONS_COMPLETE, orCOMMAND_OUTPUT, and the only downstream edge from the agent leads to a router node, the agent's output MUST be returned to the consumer rather than continuing the loop.
6.9 Pure Graph Mode
When every route in a configuration is of type: graph, a compliant implementation MAY use a "pure graph" execution path that does not involve reactive streams. The externally observable semantics MUST be identical to mixed-mode operation, but pure-graph execution MUST support single-shot invocation without timeout.
In pure-graph mode:
- The graph executes natively, without inserting any reactive-stream intermediaries between nodes.
- Context persistence, when provided by the host environment, is propagated directly to and from the graph's execution.
- After each execution, the final state's
metadataMUST replace the active context; nestedcontextkeys MUST be merged into the top-level metadata for inspection. - Recursion depth and loop-detection guards per §6.8 still apply.
6.10 Graph-Internal Streams (Non-Pure Mode)
When a graph route runs alongside one or more stream routes, per-node internal streams of the form __<graph_name>_node_<node_name>__ MUST be created to wire node-execution events into the stream layer. These names are reserved for internal use and MUST NOT be referenced by user configuration. Additional internal streams named __<graph_name>_control__ and __<graph_name>_state__ MUST also be created for graph control and state management, respectively.
6.11 Graph Analysis
For each graph route, the following analyses MUST be performed at setup time, and the results MUST be available to the runtime guards (§6.8) and parallel scheduler (§6.7):
- Adjacency analysis: forward and reverse adjacency lists MUST be constructed.
- Cycle detection: the presence of cycles MUST be detected. Cycles are permitted in the configuration; they MUST simply be known to the runtime.
- Topological levels: nodes MUST be partitioned into topological levels for use by parallel scheduling.
- Reachability: nodes unreachable from the entry point MUST be identified; a warning listing any such nodes SHOULD be emitted.
- Edge validation: every edge MUST be verified to reference existing nodes (after
START/ENDnormalization). - Entry-point validation: the entry point MUST be verified to exist as a node in the graph.
7. Inter-Route Composition
7.1 The merges Section
merges:
- sources: [<route_or_stream_name>, ...] # REQUIRED, non-empty
target: <route_or_stream_name> # REQUIRED
Each merge subscribes the target stream to receive the union of emissions from the listed sources. The order of merged emissions is non-deterministic. Merges into a stream that does not yet exist MUST create that stream as cold.
A merge with an empty sources list MUST be silently ignored. A merge with no target MUST be rejected.
Example — single-source merge connecting the Actor's input to the main route:
merges:
- sources: [__input__]
target: main
Example — multiple sources merged into a single aggregator:
merges:
- sources: [research_stream, news_stream, social_stream]
target: aggregator
- sources: [__input__]
target: research_stream
7.2 The splits Section
splits:
- source: <route_or_stream_name> # REQUIRED
targets: # mapping <name>: <condition>
<target_name_1>: <condition>
<target_name_2>: <condition>
Each split creates (or reuses) target streams and routes messages from the source to each target whose condition evaluates to true. Targets MAY share messages (a single message matching multiple conditions is forwarded to each).
A split with no source MUST be rejected. A split with empty targets MAY be silently ignored.
Example — splitting a single input stream into urgent and normal lanes:
splits:
- source: __input__
targets:
urgent_lane:
type: content_contains
text: "URGENT"
normal_lane:
type: content_not_contains
text: "URGENT"
Example — splitting based on metadata for error handling:
splits:
- source: processing_stream
targets:
success_handler:
type: content_not_contains
text: "Error:"
error_handler:
type: content_contains
text: "Error:"
7.3 The pipelines Section (Hybrid Pipelines)
pipelines:
<pipeline_name>:
stages:
- type: <stream|graph>
...
metadata: <mapping>
A pipeline is an ordered sequence of stages. Each stage MAY be a stream or graph configuration. The output of stage N feeds the input of stage N+1. Pipelines are advisory: implementations MUST accept valid pipeline declarations, but MAY emit warnings about discouraged patterns.
Example — three-stage hybrid pipeline (filter → graph → finalize):
pipelines:
analysis_pipeline:
stages:
- type: stream
name: stage1_filter
operators:
- type: filter
params:
condition:
type: content_contains
text: "analyze"
publications: [stage2_input]
- type: graph
config:
name: deep_analysis
nodes:
analyze:
type: agent
agent: analyzer
summarize:
type: function
function: summarize
edges:
- source: start
target: analyze
- source: analyze
target: summarize
- source: summarize
target: end
input_from: stage2_input
output_to: stage3_input
- type: stream
name: stage3_finalize
subscriptions: [stage3_input]
operators:
- type: distinct
publications: [__output__]
metadata:
description: "Filter, deep-analyze, then de-duplicate."
8. Templates and Instances
Templating in the Actor Configuration Standard operates at three distinct layers, each with its own purpose and timing:
- Document-level templating (Phase A — Pre-YAML): Template expressions embedded directly in the YAML source MAY produce or transform the YAML structure itself. This phase runs before YAML parsing and MAY contain Jinja2-style constructs that, when expanded, would produce additional YAML keys, list items, or value substitutions. This is the most powerful layer because it can manipulate the YAML grammar itself.
- Component-level templating (Phase B — Reusable Component Definitions): Named, parameterized template definitions under the
templatessection produce concrete agent, graph, or stream definitions when instantiated. Instantiation happens during configuration loading, after YAML parsing. - Runtime templating (Phase C — Per-Message Rendering): Template expressions in
system_prompt,prompts, and similar runtime-bearing fields are rendered each time an agent processes a message, with the active message and context as variables.
Each layer is described in detail below. §8.1–§8.5 cover the document-level layer; §8.6–§8.9 cover the component-level layer; §8.10 covers the runtime-rendering layer.
8.1 Document-Level Templating (Phase A)
An Actor configuration MAY be authored as a template document that contains Jinja2-style expressions and control-flow constructs at any position. The expressions are evaluated before YAML parsing; the rendered output MUST be a valid YAML 1.2 document.
The recognized expression forms at this layer are:
| Syntax | Purpose |
|---|---|
{{ expression }} |
Variable substitution. Replaced by the string representation of expression. |
{% statement %} |
Control-flow statement: for, if/elif/else/endif, for/endfor, macro/endmacro, block/endblock, with/endwith. |
{# comment #} |
Comment. Removed entirely before YAML parsing. |
{{ value | filter }} |
Filter pipeline. Applies a named filter (§8.3) to the value. |
8.1.1 Generating Structure via for Loops
A for loop MAY emit multiple YAML mapping entries or list items. Indentation MUST be preserved across iterations.
Example — generating multiple agents from a count parameter:
# Pre-render template (Phase A); not yet valid YAML.
agents:
{% for i in range(agent_count) %}
worker_{{ i }}:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
system_prompt: "You are worker number {{ i }}."
{% endfor %}
When rendered with the context {agent_count: 3}, the result is the valid YAML:
agents:
worker_0:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
system_prompt: "You are worker number 0."
worker_1:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
system_prompt: "You are worker number 1."
worker_2:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
system_prompt: "You are worker number 2."
8.1.2 Conditional Inclusion via if Blocks
A {% if %} ... {% endif %} block MAY include or exclude entire sub-trees of YAML based on a condition.
Example — conditionally including memory support:
agents:
chat_agent:
type: llm
config:
provider: openai
model: gpt-4
{% if include_memory %}
memory_enabled: true
max_history: 20
{% else %}
memory_enabled: false
{% endif %}
8.1.3 Inline Value Interpolation
A {{ expression }} MAY appear anywhere a scalar value is permitted in YAML.
Example — substituting a model name:
agents:
assistant:
type: llm
config:
provider: openai
model: "{{ chosen_model }}"
temperature: {{ default_temperature }}
8.1.4 Comments
Comments MUST be stripped before YAML parsing. They do not appear in the rendered output.
{# This comment is removed before the YAML is parsed #}
agents:
agent_a:
type: llm
config:
provider: openai
8.2 Document-Level Rendering Lifecycle
When an Actor configuration is loaded, the following phases MUST be applied in order:
- Detect templating: The raw configuration text is scanned for the presence of
{{,{%, or{#. If none are found, the document MUST be parsed directly as YAML and phases 2–3 below are skipped. - Phase A — Pre-YAML rendering: The raw text is rendered as a Jinja2-style template. If a rendering context is provided at load time, the expressions are evaluated against it; otherwise, expressions that appear inside
system_promptfields (and equivalent runtime-bearing fields) MUST be protected so they pass through unchanged, while other expressions are rendered using a minimal seed context (see §8.4). The rendered output MUST be valid YAML 1.2. - Phase A.1 — YAML parsing: The rendered text is parsed as YAML.
- Phase A.2 — Restore deferred expressions: After parsing, any template expressions that were protected in step 2 MUST be restored to their original form inside the parsed structure, so they can be rendered later at runtime (Phase C).
- Phase B — Component instantiation: After YAML parsing, any agent, graph, or stream declared via
template,agent_template, orroute_templateis instantiated by applying the named template (§8.6) with the provided parameters. - Phase C — Runtime rendering: Template expressions surviving from Phase A.2 are rendered each time a message is processed (e.g., when an agent's
system_promptis sent to a provider).
8.3 Document-Level Template Variables, Filters, and Built-ins
When Phase A rendering occurs against a context, the following built-in callables MUST be available in the rendering namespace (in addition to any caller-supplied variables):
| Name | Description |
|---|---|
range(stop) / range(start, stop) / range(start, stop, step) |
Standard integer sequence. |
len(x) |
Length of a string, list, or mapping. |
enumerate(x) |
Pairs (index, item) for items in x. |
zip(a, b, ...) |
Zips sequences together. |
min(...), max(...) |
Minimum and maximum. |
sum(iterable) |
Sum of a numeric iterable. |
abs(x), round(x) |
Numeric absolute value and rounding. |
int(x), float(x), str(x), bool(x), list(x), dict(x) |
Type conversion. |
The following filters MUST be available via the value | filter pipeline syntax:
| Filter | Effect |
|---|---|
tojson |
Serializes a value to a single-line YAML/JSON-compatible flow form. |
yaml |
Equivalent to tojson — emits flow-style YAML. |
indent(n) |
Indents every line of the value by n spaces. |
sum(attribute) |
Sums an attribute over a list of mappings/objects. |
selectattr(attr, op, value) |
Selects items whose attribute satisfies the operator (==, !=, >, <, >=, <=). |
default(d) |
Returns the value if defined and not None; otherwise returns d. |
length |
Standard length filter (alias of len). |
| Standard Jinja2-compatible filters | upper, lower, trim, replace, join, split, etc. |
Example — using tojson to safely embed dynamic values inside a prompt:
agents:
paper_writer:
type: llm
config:
provider: openai
model: gpt-4-turbo
system_prompt: |
Write a paper meeting these requirements:
- Topic: {{ context.paper_details.topic | tojson }}
- Length: {{ context.paper_details.length | tojson }} words
- Audience: {{ context.paper_details.audience | tojson }}
The tojson filter is particularly important for embedding user-supplied or context-derived strings inside multi-line YAML scalar values; it guarantees that the rendered substitution is properly quoted/escaped.
8.4 Deferred Rendering and Field Protection
A configuration loaded without a rendering context (or with only a partial seed context) MUST defer the rendering of template expressions that appear inside system_prompt fields (and any other field that is documented as supporting runtime templating, such as content under prompts).
The protection mechanism MUST work as follows:
- Before Phase A rendering, the raw text of the configuration is scanned. Within string values that occupy
system_promptand equivalent fields, the markers{{,}},{%,%}are replaced with the sentinels defined in §15.5 (<<<TEMPLATE_START>>>,<<<TEMPLATE_END>>>,<<<BLOCK_START>>>,<<<BLOCK_END>>>). - Phase A rendering proceeds against a minimal seed context; the sentinels are inert and pass through unchanged.
- Phase A.1 YAML parsing proceeds normally.
- After parsing, the sentinels MUST be restored to their original template-syntax form within the parsed structure.
- The restored expressions are then available for Phase C runtime rendering.
This design allows the same configuration file to:
- Be loaded once and reused for many invocations, each with a different runtime context, AND
- Use document-level templating to generate the YAML structure itself (which is finalized at load time and not re-evaluated per invocation).
8.5 Document-Level Template Storage and Reuse
When a configuration is loaded for deferred rendering (no context available), the entire document MAY be stored verbatim by the host. The host MAY tag such documents with markers such as _raw_template, _is_template, _needs_preprocessing, __jinja_template__, or __is_template__ to indicate that the document still requires template processing.
These tag names are reserved keys; user configurations MUST NOT use them.
8.6 The templates Section (Component-Level — Phase B)
The templates section declares named, reusable component definitions keyed by component type and template name:
templates:
agents:
<template_name>: <agent_template_definition>
graphs:
<template_name>: <graph_template_definition>
streams:
<template_name>: <stream_template_definition>
A template definition is a complete agent, graph, or stream configuration that MAY contain template expressions of the form {{ ... }} and conditional blocks of the form {% if ... %} ... {% endif %}. These expressions are rendered each time the template is instantiated (Phase B), with the instance's params as the rendering context.
Example — defining a reusable LLM-agent template:
templates:
agents:
research_assistant:
parameters:
model:
type: string
default: gpt-4
domain:
type: string
required: true
temperature:
type: float
default: 0.7
type: llm
config:
provider: openai
model: "{{ model }}"
temperature: {{ temperature }}
system_prompt: |
You are a research assistant specializing in {{ domain }}.
Provide accurate, well-cited responses.
8.6.1 Parameter Declarations
A template MUST declare a parameters block listing its parameters as a mapping.
Mapping form (Required):
parameters:
<param_name>:
type: <string|int|float|boolean|enum|list|dict|agent_ref|component_ref>
default: <value> # OPTIONAL
required: <boolean> # OPTIONAL, default false
values: [<allowed>...] # REQUIRED for enum
description: <string> # OPTIONAL, free-form documentation
8.6.2 Parameter Types
| Type | Validation |
|---|---|
string |
Coerced to its canonical string representation. |
int |
Coerced to the integer represented by the value. |
float |
Coerced to the floating-point number represented by the value. |
boolean |
Strings "true"/"yes"/"1"/"on" (case-insensitive) MUST be coerced to true; any other value MUST be coerced by its truthiness (non-empty strings, non-zero numbers, and non-empty collections are true). |
enum |
MUST match one of values. |
list |
Single values are wrapped into a single-element list. |
dict |
Passed through. |
agent_ref |
Reference to a named agent (§8.7); passed through and resolved during instantiation. |
component_ref |
Reference to a named component (agent, graph, or stream); passed through and resolved during instantiation. |
If a required parameter is not provided and has no default, instantiation MUST fail with ConfigurationError.
8.6.3 Filters Available at Component Instantiation
At Phase B (component instantiation), the same filters and built-ins defined in §8.3 are available. The rendering context for a template instance is the union of:
- The validated parameter values, AND
- The standard built-ins listed in §8.3.
8.7 Template Instances
Component instances MUST be declared inline within the agents or routes sections.
Form — Inline (Required):
agents:
<agent_name>:
template: <template_name>
params:
<param_name>: <value>
Routes MAY similarly be instantiated from templates using the template_config field:
routes:
<route_name>:
template_config:
template: <template_name>
params:
<param_name>: <value>
Example — instantiating multiple agents from the same template:
templates:
agents:
domain_expert:
parameters:
domain:
type: string
required: true
model:
type: string
default: gpt-4
type: llm
config:
provider: openai
model: "{{ model }}"
system_prompt: "You are an expert in {{ domain }}."
agents:
legal_expert:
template: domain_expert
params:
domain: contract law
medical_expert:
template: domain_expert
params:
domain: clinical pharmacology
finance_expert:
template: domain_expert
params:
domain: corporate finance
model: gpt-4-turbo
8.7.1 Reference Parameter Resolution
Parameters of type agent_ref or component_ref declare references to other named components. References MUST be resolved within the following scopes, in order:
- The local context of the enclosing composite agent (if any).
- The outer Actor scope (the top-level
agents,routes, etc.).
Unresolved references at the end of instantiation MUST cause a ConfigurationError.
Example — composite template using agent_ref parameters:
templates:
agents:
pipeline:
parameters:
first:
type: agent_ref
required: true
second:
type: agent_ref
required: true
type: composite
config:
components:
agents:
stage1:
ref: "{{ first }}"
stage2:
ref: "{{ second }}"
routing:
input:
type: agent
name: stage1
output:
type: agent
name: stage2
agents:
preprocessor:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
summarizer:
type: llm
config:
provider: openai
model: gpt-4
my_pipeline:
template: pipeline
params:
first: preprocessor
second: summarizer
8.8 Template Rendering Semantics (Phase B)
When a template is instantiated, the following steps MUST be performed in order:
- Validate parameters: Required parameters are checked for presence; declared types are validated and coerced per §8.6.2.
- Fill defaults: Parameters with declared defaults that were not provided receive their default values.
- Deep-copy the template body: A fresh copy of the template definition is produced so that instantiation cannot mutate the template.
- Remove the
parametersblock: The copy'sparametersblock is removed and is not present in the instantiated component. - Render expressions: Template expressions in all string values of the body are rendered recursively, with the merged parameter context.
- Coerce rendered scalars: Rendered string values that look like booleans, integers, floats, JSON lists, or JSON mappings are coerced to their natural typed forms:
- Strings
"true"/"false"(case-insensitive) → boolean. - Strings of integer form (e.g.,
"42") → integer. - Strings of float form (e.g.,
"3.14") → float. - Strings beginning with
[and ending with], or{and}, MAY be parsed as JSON.
- Strings
- Eliminate empty conditional blocks: Any
{% if … %} … {% endif %}block that evaluates to an empty string MUST be removed from the resulting structure. - Resolve references:
agent_ref/component_refvalues are resolved per §8.7.1. - Return: The resulting concrete definition is used in place of the template instance.
8.9 The template_strings Section
Raw, un-preprocessed templates (typically free-form template source containing complex Jinja2-style expressions that produce structural YAML) MUST be declared using the template_strings section:
template_strings:
agents:
<name>: |
<raw template body>
graphs:
<name>: |
<raw template body>
streams:
<name>: |
<raw template body>
These templates MUST be preserved verbatim until instantiation, with their template expressions intact. Compliant implementations MUST internally mark such templates as needing deferred preprocessing.
Templates that contain expressions that would themselves break YAML parsing (such as {% for %} loops that generate new YAML keys at instantiation time) MUST be declared in the template_strings section rather than the templates section.
When a template definition supplied via the templates section contains template syntax ({{ or {%), its rendering MUST be automatically deferred until instantiation time.
8.10 Composite Templates
Composite agent templates extend the standard template form with a components section, mirroring §4.6. References inside the composite template's body MAY use parameters (agent_ref, etc.) to bind to other instantiated components.
Example — a composite agent template that bundles three internal agents:
templates:
agents:
research_pipeline:
parameters:
topic:
type: string
required: true
depth:
type: enum
values: [shallow, medium, deep]
default: medium
type: composite
config:
components:
agents:
searcher:
type: tool
config:
tools: [http_request]
analyzer:
type: llm
config:
provider: openai
model: gpt-4
system_prompt: |
Analyze findings on "{{ topic }}" with {{ depth }} depth.
summarizer:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
system_prompt: "Summarize the analysis concisely."
routing:
input:
type: agent
name: searcher
output:
type: agent
name: summarizer
8.11 Runtime Template Rendering (Phase C)
Template expressions that survive Phase A and Phase B (typically those inside an LLM agent's system_prompt or under the prompts section) are rendered each time an agent processes a message.
The rendering context at this layer MUST include:
| Variable | Value |
|---|---|
context |
The active context dictionary (global context merged with per-invocation context). |
message |
The user message currently being processed. |
| Standard built-ins (§8.3) | range, len, enumerate, type constructors, filters, etc. |
| Any keys present in the context at the top level | Flattened from context so that {{ topic }} is equivalent to {{ context.topic }}. |
Example — a runtime-rendered system prompt:
agents:
topical_assistant:
type: llm
config:
provider: openai
model: gpt-4
memory_enabled: true
system_prompt: |
You are an assistant specialized in {{ context.topic }}.
{% if context.style %}
Respond in a {{ context.style }} tone.
{% endif %}
{% if context.history_size > 5 %}
We have been talking for a while; reference our prior discussion when helpful.
{% endif %}
Each invocation of topical_assistant re-renders the prompt against the live context, so the prompt naturally adapts to changing state.
8.12 Render-Failure Policy
When rendering at any phase fails (for example, an undefined variable is referenced or a filter receives an invalid argument):
| Phase | Behavior on Failure |
|---|---|
| Phase A (Document) | The configuration MUST fail to load and a ConfigurationError MUST be signaled. |
| Phase B (Instance) | The instantiation MUST fail with ConfigurationError. |
| Phase C (Runtime) | A warning MUST be emitted; the literal, unrendered text MUST be used as a fallback so processing continues. |
9. Actor Parameters
9.1 Overview
This section enumerates parameters that affect Actor behavior. The mechanism by which an implementation accepts these parameters is outside the scope of this standard; each parameter is described by name, type, default, and effect. Implementations MAY accept these parameters via any configuration mechanism appropriate to the host environment.
9.2 template_engine
The template_engine parameter selects the templating dialect used at all three template phases described in §8.
Recognized values:
| Value | Dialect | Supported Constructs |
|---|---|---|
JINJA2 (default) |
Jinja2-compatible templating dialect | Full variable interpolation ({{ var }}), control flow ({% if %}, {% for %}, {% macro %}, {% block %}, {% with %}), filters ({{ value | filter }}), and comments ({# … #}). |
MUSTACHE |
Mustache-compatible logic-less templating dialect | Variable interpolation ({{ var }}), sections ({{#section}}…{{/section}}), inverted sections ({{^section}}…{{/section}}), and comments ({{! comment }}). Does NOT support arbitrary expressions or filters. |
SIMPLE |
Format-string templating with {{ }} pre-pass |
First, {{ expression }} placeholders are evaluated against the context using the restricted expression sandbox (§13.2.2); then the result is passed through a standard format-string substitution using the context as named parameters. |
The value comparison is case-insensitive; the canonical form is uppercase.
When template_engine is unset, the default JINJA2 engine MUST be used for all three phases described in §8.
Example — selecting the engine:
cleveragents:
template_engine: JINJA2
agents:
greeter:
type: llm
config:
provider: openai
model: gpt-4
system_prompt: |
Greet the user {{ context.user_name | default('friend') }}.
{% if context.tone %}
Use a {{ context.tone }} tone.
{% endif %}
The full processing pipeline for templated configurations is defined normatively in §8.2 (Document-Level Rendering Lifecycle) and §8.4 (Deferred Rendering and Field Protection). The template_engine parameter selects which dialect those phases use.
9.3 default_router
When a routes section is present and is non-empty, an Actor MUST designate a default_router: the name of a route present in routes that receives messages submitted to the Actor when no explicit target is specified by the consumer. See §3.4 for the requirement.
| Field | Type | Default | Description |
|---|---|---|---|
default_router |
string | (none) | Name of a route present in routes. |
A compliant implementation MAY enforce this requirement strictly (signaling ConfigurationError when the designated route does not exist), or MAY rely on a merges entry from __input__ to fulfill the same role.
9.4 unsafe
An Actor MAY declare that it requires unsafe mode by setting a truthy unsafe value in context.global. When this value is true and the host has not been explicitly placed in unsafe mode, the Actor MUST be refused execution. The error category for this rejection is UnsafeConfigurationError (§20).
context:
global:
unsafe: true
9.5 verbose and logging.level
These fields are advisory only; compliant implementations are not required to read them from the configuration. The verbose / log-level setting is typically controlled by the host environment rather than by the configuration document.
When a compliant implementation does honor these fields, verbose: true SHOULD be equivalent to logging.level: DEBUG. The standard log levels are CRITICAL, ERROR, WARNING, INFO, DEBUG.
9.6 version
Implementations MAY accept a version parameter. This field is informational only and MAY be used to select compatibility behaviors. The current version of this standard is "1.0.0".
10. Context
10.1 The context Section
context:
global:
<key>: <value>
...
context.global declares the Actor-wide context dictionary. All keys here are visible to all agents during processing.
Example — context declaring multiple variables:
context:
global:
unsafe: true
app_name: "Scientific Paper Writer"
workflow_stage: "intro"
stage_order:
- intro
- discovery
- brainstorming
- structure
- writing
- review
paper_details:
topic: null
length: null
audience: null
collected_sources: []
These keys are visible inside every agent's invocation context. An LLM agent's system_prompt MAY refer to them via Phase C templating (e.g., {{ context.app_name }}); a tool agent's inline code MAY read and write them via the context variable.
10.2 Context Lifecycle
- At Actor load time,
context.globalis populated from the configuration. - At message-processing time, the global context is referenced (not copied) into each message's
metadata.context. - Tool agents and graph nodes MAY modify the context dictionary in-place.
- Modifications made during processing are visible to subsequent processing steps within the same execution.
- If the host environment provides persistence (e.g., named contexts), modifications MAY be persisted between Actor invocations.
10.3 Reserved Context Keys
The following context keys are reserved for system use:
| Key | Type | Set By | Semantics |
|---|---|---|---|
_temperature_override |
number | Host (not configuration) | Overrides LLM temperature for the current invocation. |
_unsafe_mode |
boolean | Host environment | Indicates the host is operating in unsafe mode. Required for file_write and other privileged operations. |
auto_finish_active |
boolean | Workflow logic | When true, relaxes loop-detection guards (see §6.8). |
auto_finish_state |
mapping | Workflow logic | Free-form auto-finish bookkeeping, structured by the configuration that uses it. |
auto_finish_final_stage |
boolean | Workflow logic | Marks final stage during auto-finish. |
auto_finish_last_output |
string | Workflow logic | Last output during auto-finish. |
graph_state |
mapping | Graph route | Injected by graph routes (read-only inside agents). |
conversation_history |
sequence | Graph route | Injected by graph routes (read-only inside agents). |
full_context |
boolean | Graph route | Indicator that full context was provided. |
_history_truncated |
boolean | Graph node | Set when history was truncated to fit max_history_messages/max_history_chars. |
_history_original_length |
integer | Graph node | Original message count before truncation. |
next_node |
string | Message router | Set by routers to direct downstream context_value edge conditions. |
last_output |
string | Graph route | Updated after each node execution to the most recent output. |
last_agent_node |
string | Graph route | Name of the most recent agent node to execute. |
current_message |
string | Graph route | The message currently being routed to a node. |
current_node |
string | Graph route | Name of the currently executing node. |
current_section_path |
string | Workflow convention | Conventional progress-bar / section-writing hook. |
current_section_index |
integer | Workflow convention | Conventional progress-bar / section-writing hook. |
section_paths |
sequence | Workflow convention | Conventional progress-bar / section-writing hook. |
writing_stage |
string | Workflow convention | Conventional progress-bar / section-writing hook. |
current_latex_index |
integer | Workflow convention | Conventional progress-bar hook. |
User-defined keys MUST NOT begin with double underscore (__). User-defined keys beginning with a single underscore (_) are STRONGLY DISCOURAGED to avoid collision with system keys.
10.4 The prompts Section
prompts:
<template_name>: # Long form (Required)
content: <prompt_template_string>
Prompt templates MUST be declared using the long form with a content key:
- If the value is a mapping with a
contentkey, the value of that key is used as the template content. - If the value does not conform to this structure, the entry is skipped silently.
Prompt templates are registered with the active template engine (§9.2) and may be referenced by LLM agents via their template field. The body MAY contain template expressions in the active engine and is rendered at agent-invocation time with the current message and context.
Example — declaring prompt templates:
prompts:
greeting:
content: "Hello {{ context.user_name | default('friend') }}!"
research_briefing:
content: |
You are researching the topic "{{ context.topic }}" for an audience
of "{{ context.audience }}".
{% if context.constraints %}
Constraints to observe:
{% for c in context.constraints %}
- {{ c }}
{% endfor %}
{% endif %}
Begin by summarizing the most-recent finding.
agents:
greeter:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
template: greeting
researcher:
type: llm
config:
provider: openai
model: gpt-4
memory_enabled: true
template: research_briefing
Each LLM agent that references a named prompt template renders that template at invocation time, with the current context and message available as variables.
11. Validation Rules
The following rules MUST be enforced at configuration-load time, before any agent is instantiated or any message is processed. Violations MUST be reported as ConfigurationError.
11.1 Structural Validation
- The top-level document MUST parse as a YAML mapping.
- The
agentssection MUST be present and MUST be a mapping. - Each value under
agentsMUST be a mapping with atypekey. - Each agent's
config, when present, MUST be a mapping (if absent, an empty mapping is supplied). - When
routesis present, it MUST be a non-empty mapping. - When
routesis present, adefault_routerparameter SHOULD identify a route inroutes. Compliant implementations MAY treat the absence or a dangling reference as a strict error. - When
routesis absent, the configuration is invalid.
11.2 Agent Validation
- Every agent's
typeMUST be a registered agent type, or one ofcomposite,template_instance, or instantiated viatemplate/agent_template. - Names beginning with
__MUST be rejected.
11.3 Route Validation
- Every route MUST specify
type. - The value of
typeMUST be one ofstream,graph,bridge(case-insensitive). - For
streamroutes:- Each subscription/publication MUST name a known route or a built-in stream.
- Each agent referenced by
agentsMUST exist in theagentssection.
- For
graphroutes:nodesMUST be a non-empty mapping (unless instantiated from a template).- Each agent node's
agentMUST exist in theagentssection. - The
entry_pointMUST refer to an existing node (after auto-injection ofstart/end). - Each edge's
sourceandtargetMUST refer to existing nodes (afterSTART/ENDnormalization).
- For
bridgeroutes: no specific structural validation beyond schema conformance.
11.4 Merge/Split Validation
- Each merge MUST have a non-null
target. - Each merge with non-empty
sourcesMUST reference only known route/stream names. - Each split MUST have a non-null
source. - Each split's
sourceMUST be a known route/stream name.
11.5 Pipeline Validation
- A pipeline stage of
type: graphreferring to a non-existent graph name SHOULD generate a warning rather than an error. - A pipeline stage of
type: streamwhosenamecollides with an existing route SHOULD generate a warning.
11.6 Environment Variable Validation
When a required environment variable (${VAR_NAME} without a default) is unset, configuration loading MUST fail with a ConfigurationError indicating the missing variable.
12. Runtime Behavior
12.1 Lifecycle
The lifecycle of an Actor MUST follow this order:
- Load: Parse all configuration files; deep-merge in order; interpolate environment variables; perform validation (§11).
- Register templates: Build the template registry.
- Create agents: Instantiate every agent; agent creation failures MUST abort startup.
- Set up routes: Create stream and graph routes.
- Set up operations: Process merges and splits.
- Set up pipelines: Create hybrid pipelines.
- Run: Begin accepting messages on
__input__. - Cleanup (on shutdown): Dispose of all subscriptions, agents, and streams.
12.2 Message Flow
12.2.1 Message Structure
A message has the following observable properties:
| Property | Type | Description |
|---|---|---|
content |
any | The principal payload. |
metadata |
mapping | Auxiliary key-value data. |
source_stream |
string | Name of the stream that produced the message. |
timestamp |
number | Wall-clock or event-loop timestamp. |
A message MAY be copied to produce a new message with modified fields. When such a copy is produced, the metadata.context entry MUST be preserved by reference (not by copy). All other metadata entries MUST be shallow-copied (each value is copied one level deep; nested structures within those values continue to share references).
Standard Metadata Keys
The following metadata keys are populated by the system during message flow. Authors SHOULD NOT overwrite these keys in user-defined operators.
| Key | Populated By | Semantics |
|---|---|---|
context |
send_message caller |
Active context dictionary (reference, not copy). |
_unsafe_mode |
Host environment | Whether the host is in unsafe mode. |
processed_by |
Agent mapper | Name of the agent that processed the message. |
error |
Stream router on error | true when the message represents an error. |
error_type |
Stream router on error | The error category from the taxonomy in §20. |
timeout |
Stream router on timeout | true when message represents a timed-out invocation. |
state_updated |
state_update operator |
true after state update. |
checkpointed |
state_checkpoint operator |
true after checkpoint. |
graph |
Graph operators | Name of the graph that produced the message. |
node |
Node-level operators | Name of the node that emitted the message. |
execution_history |
graph_execute operator |
List of nodes that executed during graph run. |
execution_count |
Node executor | Per-node invocation counter. |
final_state |
graph_execute operator |
Snapshot of the graph state at completion. |
source |
Bridge operators | Identifier of the originating subsystem (for example, an implementation-defined identifier when emitted by the graph subsystem). |
12.2.2 Ordering Guarantees
Within a single stream, message order MUST be preserved. Across streams (e.g., emissions joined through merges), ordering is non-deterministic.
12.2.3 Error Handling
Any agent or operator that signals an error MUST cause a message to be emitted on __error__ with:
content: a string describing the error.metadata.error:true.metadata.error_type: the error category from the taxonomy in §20 (where available).
The downstream pipeline MUST continue processing other messages.
12.2.4 Timeouts
Per §5.3.4, agent processing inside a stream map operator MUST honor a hard timeout of 120 seconds; messages that exceed this MUST be emitted with timeout metadata as defined in §5.3.4.
Per-message completion in single-shot execution modes MUST be bounded by a soft timeout (default 30 seconds), after which an execution error MUST be signaled. Continuous/interactive execution modes SHOULD use a similar timeout for per-message completion.
12.3 Concurrency
- Stream operators MAY process messages concurrently per reactive-stream semantics.
- Graph nodes MAY execute in parallel when:
parallel_execution: true(the default), AND- The next-node set has more than one element, AND
- The nodes have
parallel: truein their config.
- Bridge conversions MUST preserve relative ordering of messages.
- Context mutations MUST be visible to all subsequent processing steps within the same execution, regardless of concurrency.
12.4 Tool Command Processing in Outputs
When an LLM agent's output contains a [TOOL_EXECUTE:<tool_name>] ... [/TOOL_EXECUTE] block, compliant implementations MAY (but are not required to) extract the JSON payload between the markers and execute the named tool. When this feature is supported:
- Tool blocks MUST be detected by locating each occurrence of
[TOOL_EXECUTE:<name>]followed by an opening JSON payload and a corresponding[/TOOL_EXECUTE]closing marker. The pattern MUST match across multiple lines. - The captured JSON payload MUST be sanitized for unescaped control characters before parsing: literal backslashes MUST be doubled; raw newline (
\n), carriage-return (\r), tab (\t), backspace (\b), form-feed (\f), and double-quote (") characters inside string values MUST be escaped. - When the sanitized payload is valid JSON, the named tool MUST be executed with the parsed parameters; otherwise a human-readable error message MUST be substituted in place of the tool block.
- Tool blocks MUST be processed in reverse order of their occurrence in the output (last match first) so that earlier text offsets remain valid as substitutions are applied.
- The result of each tool execution MUST replace the corresponding
[TOOL_EXECUTE:...]...[/TOOL_EXECUTE]block in the output.
12.5 Routing Prefix Conventions
The following prefix conventions are recommended for inter-route routing (e.g., when used with message_router nodes) but are not normative requirements of this standard:
| Prefix | Common Meaning |
|---|---|
GOTO_<TARGET>: |
Route to the named target with the trailing message. |
ROUTE_<TARGET>: |
Similar; used in alternate routing schemes. |
SET_<KEY>: |
Update a context key. |
CMD_<KEY>: |
Issue a command. |
COMMAND_OUTPUT: |
Mark output as terminal command output. |
DISCOVERY_RESPONSE: |
Mark output as discovery-stage response. |
AUTO_SECTIONS_COMPLETE: |
Signal completion of section processing. |
When a message exits the Actor via __output__, any of the above prefixes SHOULD be stripped from the final output presented to the consumer, so that internal routing tokens are not exposed externally. The list of strippable prefixes MAY be extended by the host environment.
12.6 Loop Detection in Stream Routes
Stream-based publications/subscriptions that form a cycle (Stream A publishes to Stream B; Stream B publishes back to Stream A) MUST be detected at configuration load time or at runtime and MUST be reported as an error if they would result in an unbounded message loop.
13. Security Model
13.1 Safe and Unsafe Modes
Every Actor execution operates in one of two modes:
- Safe (default): Inline code, shell execution, and unsandboxed file writes are prohibited.
- Unsafe: All operations permitted.
The mode is set by the host environment, not by the Actor itself. However, an Actor MAY declare (via context.global.unsafe: true) that it requires unsafe mode to function. When this declaration is present but the host is in safe mode, the Actor MUST be refused execution with UnsafeConfigurationError.
13.2 Sandboxed Code Execution
Inline tool code (§4.5.2) MUST execute in a restricted environment. This standard defines two named sets of built-in facilities that bound the capabilities available to evaluated code: a broader set for inline tool bodies (§13.2.1) and a narrower set for inline expressions (§13.2.2).
13.2.1 Restricted Built-ins for Inline Code
When an inline-code tool body is executed, the following built-in facilities MUST be available to it, and no other built-in facilities MAY be exposed:
| Category | Facilities |
|---|---|
| Type constructors | bool, dict, float, int, list, set, str, tuple |
| Iteration / aggregation | abs, all, any, enumerate, len, max, min, range, reversed, round, sorted, sum, zip |
| Type inspection | isinstance, type |
| Local-frame access | locals |
| Diagnostic output | print |
| Error categories | ValueError, IndexError, KeyError, TypeError |
| Module access | A facility exposing only the JSON parsing/serialization module under the name json |
13.2.2 Restricted Built-ins for Expression Evaluation
When an inline expression (such as the transform.fn parameter or the bridge custom_predicate/state_extractor/state_flattener fields) is evaluated, only the following minimal set of built-in facilities MUST be available:
| Category | Facilities |
|---|---|
| Type constructors | bool, dict, float, int, list, set, str, tuple |
| Numeric / iteration utilities | abs, all, any, len, max, min, round, sum |
13.2.3 Prohibited Capabilities
Neither sandbox MAY expose any of the following:
- Performing arbitrary I/O against the filesystem, network, or system.
- Dynamic evaluation or compilation of arbitrary source code.
- Introspection of the host or surrounding execution environment.
- Dynamic import of modules other than those explicitly listed.
- Subprocess spawning or signaling.
Any attempt to expose these capabilities MUST be refused at configuration load time or at invocation time.
13.3 Filesystem Boundaries
In safe mode:
file_readMUST reject..and absolute paths.file_writeMUST be disabled.
In unsafe mode (the host is in unsafe mode AND _unsafe_mode: true is in the invocation context):
file_readpermits absolute paths.file_writeis enabled but still refuses..traversal and home-directory expansion (~).
13.4 Network Boundaries
The http_request tool MAY be invoked in either mode. Outbound network access SHOULD be restricted according to host policy; that policy is outside the scope of this standard.
13.5 Shell Execution
Shell execution is enabled only when both of the following hold:
- The tool agent has
allow_shell: true, AND - The host is in unsafe mode.
The safe-mode blocklist defined in §4.5.4 (rm, del, format, shutdown, reboot, kill) MUST always be enforced regardless of mode.
14. Environment Variable Interpolation
14.1 Syntax
Any string scalar in the configuration MAY contain references of the form:
${NAME}— required environment variable.${NAME:default}— environment variable with a default.
Names MUST match [A-Za-z0-9_]+.
Example — required and defaulted variables side by side:
agents:
prod_agent:
type: llm
config:
provider: openai
model: "${OPENAI_MODEL:gpt-4}" # Defaulted: uses gpt-4 if OPENAI_MODEL is unset.
api_key: "${OPENAI_API_KEY}" # Required: configuration fails if not set.
temperature: ${LLM_TEMPERATURE:0.7}
max_tokens: ${LLM_MAX_TOKENS:1500}
14.2 Substitution Semantics
For each occurrence:
- If the environment variable is set, its value MUST be substituted.
- If the environment variable is unset and a default is given, the default MUST be substituted.
- If the environment variable is unset and no default is given, a
ConfigurationErrorMUST be signaled.
14.3 Type Coercion
After substitution, the resulting full-string value MUST be coerced as follows:
"true"/"false"(case-insensitive) → boolean.- Strings matching
-?\d+→ integer. - Strings matching
-?\d+\.\d+→ float. - Other strings → unchanged.
Coercion applies only when the substituted result is the entire string value (not when interpolation is embedded in surrounding text).
Example — type coercion at work:
context:
global:
is_debug: "${DEBUG:false}" # Coerced to boolean false
max_retries: "${MAX_RETRIES:5}" # Coerced to integer 5
sample_rate: "${SAMPLE_RATE:0.25}" # Coerced to float 0.25
greeting: "Hello, ${USER:friend}!" # Embedded; result remains a string
14.4 Recursive Interpolation
Interpolation MUST be applied recursively to all nested mappings, sequences, and strings. The order of processing is unspecified, but each string is interpolated exactly once.
15. Reserved Names
The following names are reserved by this standard and MUST NOT be used for user-defined agents, routes, streams, nodes, or context keys (where applicable):
15.1 Stream Names
__input____output____error__
15.2 Node Names
start/STARTend/END
15.3 Context Keys
Any key beginning with _ SHOULD be considered system-reserved. The keys explicitly reserved are listed in §10.3.
15.4 Agent and Route Names
Any name beginning with __ is reserved for internal use.
15.5 Sentinel Strings
The strings <<<TEMPLATE_START>>>, <<<TEMPLATE_END>>>, <<<BLOCK_START>>>, and <<<BLOCK_END>>> are reserved as sentinels used by the template-loading subsystem (§9.2.1) and MUST NOT appear in source documents.
15.6 Internal Stream Name Patterns
The patterns __<graph_name>_node_<node_name>__, __<graph_name>_control__, and __<graph_name>_state__ are reserved for internal use by graph routes (§6.10) and MUST NOT be referenced from user configuration.
16. Forbidden Patterns and Discouraged Forms
This section enumerates constructs that this standard prohibits, together with discouraged forms that are accepted but for which an alternative is preferred.
16.1 Forbidden Top-Level Constructs
Compliant implementations MUST reject configurations that rely on the following constructs:
| Pattern | Replacement |
|---|---|
Top-level streams: section |
Use routes with type: stream. |
Top-level graphs: section |
Use routes with type: graph. |
composite agent with strategy: key |
Use composite agent with components: and routing:. |
16.2 Forbidden Forms
The following forms are prohibited and MUST be rejected by compliant implementations:
| Pattern | Replacement |
|---|---|
type: dynamic_router node |
Use type: message_router. |
List-form routes with from/to shorthand |
Use the mapping form (§5.1). |
Top-level pipelines: section |
Use composite agents or chained routes. |
Inline expressions placed in the function parameter of operators |
Define a named agent or built-in function instead, or use the transform operator with fn. |
Template instances declared using agent_template key |
Use template key (§8.7). |
| Template parameters declared as sequence form | Use mapping form (§8.6.1). |
| Prompt templates declared using short form | Use long form with content key (§10.4). |
| Composite agent routing using shorthand form | Use explicit input/output mapping form (§4.6.2). |
Templates with expressions in templates section instead of template_strings |
Use template_strings section for templates that generate YAML structure (§8.9). |
16.3 Forbidden Field Combinations
The following field combinations on a single mapping MUST be rejected:
promptANDprompt_referenceon the same agent.templateANDtypeon the same agent (one form excludes the other).agent_templateANDtemplateon the same agent.codeAND a tool name colliding with a built-in tool (the inline tool always wins, but implementations SHOULD warn).- Routes with both
template_configand other route-type-specific fields (e.g.,nodes,operators) — the template determines the structure.
17. Compliance Test Vectors
A compliant implementation SHOULD be tested against (at minimum) the following classes of test vectors:
- Minimal valid configurations: A single agent and a single stream route publishing to
__output__. - All agent types: One configuration per type (
llm,tool,composite). - All stream operators: Configurations exercising each defined operator.
- All condition types: Both passing and failing for each condition type.
- Graph routes: Linear, branching, conditional, looping, parallel.
- Message router nodes: All five match types.
- Templates and instances: Parameter validation success and failure cases.
- Environment variables: Required, defaulted, missing.
- Merges and splits: Including multiple sources and conditional targets.
- Error scenarios: Invalid configurations (missing
type, dangling references, etc.). - Security boundaries: Configurations requiring unsafe mode in both safe and unsafe hosts.
- State persistence: Graph routes with checkpointing enabled.
18. Appendix A — Canonical Examples
18.1 Minimal Chat Actor
agents:
chat_agent:
type: llm
config:
provider: openai
model: gpt-4
temperature: 0.7
memory_enabled: true
routes:
chat_stream:
type: stream
stream_type: cold
operators:
- type: map
params:
agent: chat_agent
publications:
- __output__
merges:
- sources: [__input__]
target: chat_stream
18.2 Conditional Graph Actor
agents:
classifier:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
system_prompt: "Classify the input as either 'question' or 'statement'."
qa_agent:
type: llm
config:
provider: openai
model: gpt-4
ack_agent:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
routes:
main:
type: graph
entry_point: start
nodes:
classify:
type: agent
agent: classifier
handle_question:
type: agent
agent: qa_agent
handle_statement:
type: agent
agent: ack_agent
edges:
- source: start
target: classify
- source: classify
target: handle_question
condition:
type: content_contains
text: "question"
- source: classify
target: handle_statement
condition:
type: content_contains
text: "statement"
- source: handle_question
target: end
- source: handle_statement
target: end
input_stream:
type: stream
operators:
- type: graph_execute
params:
graph: main
publications:
- __output__
merges:
- sources: [__input__]
target: input_stream
18.3 Message-Router-Based Actor
agents:
code_agent:
type: llm
config:
provider: openai
model: gpt-4
system_prompt: "You help with coding tasks."
research_agent:
type: llm
config:
provider: openai
model: gpt-4
system_prompt: "You help with research."
default_agent:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
routes:
main:
type: graph
entry_point: start
nodes:
router:
type: message_router
rules:
- match_type: prefix
pattern: "CODE"
target: code
extract_message: true
separator: ":"
- match_type: prefix
pattern: "RESEARCH"
target: research
extract_message: true
separator: ":"
- match_type: suffix
pattern: ""
target: default
extract_message: false
code:
type: agent
agent: code_agent
research:
type: agent
agent: research_agent
default:
type: agent
agent: default_agent
edges:
- source: start
target: router
- source: router
target: code
condition:
type: context_value
key: next_node
value: code
- source: router
target: research
condition:
type: context_value
key: next_node
value: research
- source: router
target: default
condition:
type: context_value
key: next_node
value: default
- source: code
target: end
- source: research
target: end
- source: default
target: end
merges:
- sources: [__input__]
target: main
18.4 Tool Agent with Inline Code
context:
global:
unsafe: true
agents:
echo_tool:
type: tool
config:
tools:
- name: echo_with_prefix
code: |
result = "PREFIX: " + str(input_data)
routes:
main:
type: stream
stream_type: cold
operators:
- type: map
params:
agent: echo_tool
publications:
- __output__
merges:
- sources: [__input__]
target: main
18.5 Template-Based Actor (Component-Level Templating, Phase B)
templates:
agents:
basic_llm:
parameters:
model:
type: string
default: gpt-4
system_message:
type: string
required: true
type: llm
config:
provider: openai
model: "{{ model }}"
temperature: 0.7
system_prompt: "{{ system_message }}"
agents:
helper:
template: basic_llm
params:
system_message: "You are a friendly assistant."
expert:
template: basic_llm
params:
model: gpt-4-turbo
system_message: "You are a domain expert."
routes:
main:
type: stream
operators:
- type: map
params:
agent: helper
publications:
- __output__
merges:
- sources: [__input__]
target: main
18.6 Document-Level Templating (Phase A)
This example demonstrates Phase A templating: the YAML document itself contains template expressions that are evaluated before YAML parsing. The result of expansion is the actual Actor configuration.
{# This is a Phase A comment. It is stripped before YAML parsing. #}
{% set worker_models = ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo"] %}
agents:
{% for model in worker_models %}
worker_{{ loop.index }}:
type: llm
config:
provider: openai
model: {{ model }}
temperature: 0.7
system_prompt: |
You are worker #{{ loop.index }}, running on {{ model }}.
{% endfor %}
aggregator:
type: llm
config:
provider: openai
model: gpt-4
system_prompt: |
Aggregate the outputs from {{ worker_models | length }} workers
into a single coherent answer.
routes:
main:
type: stream
operators:
{% for model in worker_models %}
- type: map
params:
agent: worker_{{ loop.index }}
{% endfor %}
- type: map
params:
agent: aggregator
publications: [__output__]
merges:
- sources: [__input__]
target: main
After Phase A rendering with no external context (the loop reads from the template's own {% set %}), the YAML expands to three concrete worker_N agents and three corresponding map operators in the stream. The fully expanded form is what the YAML parser then sees.
18.7 Runtime Templating (Phase C)
This example demonstrates Phase C templating: template expressions inside system_prompt are preserved across YAML loading and re-rendered each time the agent processes a message.
context:
global:
paper_details:
topic: null
length: null
audience: null
agents:
paper_brainstormer:
type: llm
config:
provider: openai
model: gpt-4-turbo
memory_enabled: true
system_prompt: |
You are a creative brainstorming partner for scientific papers.
The paper must be written to meet the following requirements:
- Topic: {{ context.paper_details.topic | tojson }}
- Length: {{ context.paper_details.length | tojson }} words
- Audience: {{ context.paper_details.audience | tojson }}
{% if context.paper_details.topic %}
Focus your suggestions on the declared topic.
{% else %}
Help the user choose a topic first.
{% endif %}
routes:
main:
type: stream
operators:
- type: map
params:
agent: paper_brainstormer
publications: [__output__]
merges:
- sources: [__input__]
target: main
Each time the agent processes a message, the system_prompt is re-rendered with the current context. If context.paper_details.topic is null, the else branch is rendered; once the topic is set (perhaps by a tool agent earlier in the pipeline), the if branch is selected on subsequent invocations.
18.8 Combined Phase A + Phase C Templating
This example combines both templating phases: Phase A generates multiple agents from a list, and Phase C renders each agent's system prompt at invocation time.
{% set specialties = [
{"name": "legal_expert", "domain": "contract law", "model": "gpt-4"},
{"name": "medical_expert", "domain": "clinical research", "model": "gpt-4-turbo"},
{"name": "engineering_expert", "domain": "systems engineering", "model": "gpt-4"}
] %}
agents:
{% for spec in specialties %}
{{ spec.name }}:
type: llm
config:
provider: openai
model: {{ spec.model }}
memory_enabled: true
# The system_prompt below contains a Phase C expression that will
# be rendered at message-processing time, NOT at YAML-load time.
system_prompt: |
You are an expert in {{ spec.domain }}.
{% raw %}
Current user context: {{ context.user_name | default('anonymous') }}
{% endraw %}
{% endfor %}
router:
type: tool
config:
tools:
- name: select_specialty
code: |
text = (input_data or "").lower()
if "legal" in text or "contract" in text:
result = "GOTO_LEGAL_EXPERT:" + input_data
elif "medical" in text or "clinic" in text:
result = "GOTO_MEDICAL_EXPERT:" + input_data
else:
result = "GOTO_ENGINEERING_EXPERT:" + input_data
routes:
main:
type: graph
entry_point: start
nodes:
route:
type: agent
agent: router
router_node:
type: message_router
rules:
{% for spec in specialties %}
- match_type: prefix
pattern: "GOTO_{{ spec.name | upper }}"
target: {{ spec.name }}
extract_message: true
separator: ":"
{% endfor %}
{% for spec in specialties %}
{{ spec.name }}:
type: agent
agent: {{ spec.name }}
{% endfor %}
edges:
- source: start
target: route
- source: route
target: router_node
{% for spec in specialties %}
- source: router_node
target: {{ spec.name }}
condition:
type: context_value
key: next_node
value: {{ spec.name }}
- source: {{ spec.name }}
target: end
{% endfor %}
context:
global:
unsafe: true
user_name: null
merges:
- sources: [__input__]
target: main
Note the use of {% raw %}...{% endraw %} to escape Phase A rendering of the inner Phase C expression. Without {% raw %}, Phase A would attempt to render the expression at load time (and fail, since context.user_name is not yet defined). With {% raw %}, the expression is passed through unchanged to be rendered at Phase C.
18.9 Multi-Stage Workflow with State
This example shows a stateful, multi-stage workflow using a graph route with checkpointing and a message-router-driven workflow controller.
cleveragents:
template_engine: JINJA2
context:
global:
unsafe: true
workflow_stage: null
stage_order: [intro, discovery, brainstorming, structure, writing, review]
collected_data: {}
agents:
# Workflow controller decides which stage to enter.
workflow_controller:
type: tool
config:
tools:
- name: dispatch
code: |
text = (input_data or "").strip()
if text.startswith("!"):
result = f"GOTO_COMMAND_HANDLER:{text}"
else:
stage = context.get("workflow_stage") or "intro"
result = f"GOTO_{stage.upper()}:{text}"
command_handler:
type: tool
config:
tools:
- name: handle_command
code: |
text = (input_data or "").strip()
parts = text.split(maxsplit=1)
command = parts[0]
if command == "!next":
stage_order = context.get("stage_order", [])
current = context.get("workflow_stage") or "intro"
try:
idx = stage_order.index(current)
if idx + 1 < len(stage_order):
next_stage = stage_order[idx + 1]
context["workflow_stage"] = next_stage
result = f"GOTO_{next_stage.upper()}:"
else:
result = "COMMAND_OUTPUT:Final stage already reached."
except ValueError:
result = "COMMAND_OUTPUT:Unknown current stage."
elif command == "!stage":
result = f"COMMAND_OUTPUT:Current stage is {context.get('workflow_stage')}"
else:
result = f"COMMAND_OUTPUT:Unknown command {command}"
# One agent per stage; the system_prompt uses Phase C templating.
intro_agent:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
system_prompt: |
You are an onboarding assistant.
Greet the user and instruct them to type `!next` to begin.
discovery_agent:
type: llm
config:
provider: openai
model: gpt-4
memory_enabled: true
system_prompt: |
Discover the user's needs.
So far we know: {{ context.collected_data | tojson }}
brainstorming_agent:
type: llm
config:
provider: openai
model: gpt-4-turbo
memory_enabled: true
system_prompt: |
Brainstorm based on the discovered needs:
{{ context.collected_data | tojson }}
structure_agent:
type: llm
config:
provider: openai
model: gpt-4
memory_enabled: true
system_prompt: |
Produce a structured outline.
writing_agent:
type: llm
config:
provider: openai
model: gpt-4-turbo
memory_enabled: true
max_tokens: 4000
system_prompt: |
Write the requested content in full.
review_agent:
type: llm
config:
provider: openai
model: gpt-4
memory_enabled: true
system_prompt: |
Review the produced content for accuracy and clarity.
routes:
main:
type: graph
entry_point: start
checkpointing: true
checkpoint_dir: "./checkpoints/workflow"
nodes:
controller:
type: agent
agent: workflow_controller
router:
type: message_router
rules:
- match_type: prefix
pattern: "GOTO_COMMAND_HANDLER"
target: command_handler
extract_message: true
separator: ":"
- match_type: prefix
pattern: "GOTO_INTRO"
target: intro
extract_message: true
separator: ":"
- match_type: prefix
pattern: "GOTO_DISCOVERY"
target: discovery
extract_message: true
separator: ":"
- match_type: prefix
pattern: "GOTO_BRAINSTORMING"
target: brainstorming
extract_message: true
separator: ":"
- match_type: prefix
pattern: "GOTO_STRUCTURE"
target: structure
extract_message: true
separator: ":"
- match_type: prefix
pattern: "GOTO_WRITING"
target: writing
extract_message: true
separator: ":"
- match_type: prefix
pattern: "GOTO_REVIEW"
target: review
extract_message: true
separator: ":"
- match_type: prefix
pattern: "COMMAND_OUTPUT"
target: end
extract_message: true
separator: ":"
command_handler:
type: agent
agent: command_handler
intro:
type: agent
agent: intro_agent
discovery:
type: agent
agent: discovery_agent
brainstorming:
type: agent
agent: brainstorming_agent
structure:
type: agent
agent: structure_agent
writing:
type: agent
agent: writing_agent
review:
type: agent
agent: review_agent
edges:
- source: start
target: controller
- source: controller
target: router
- source: router
target: command_handler
condition: {type: context_value, key: next_node, value: command_handler}
- source: router
target: intro
condition: {type: context_value, key: next_node, value: intro}
- source: router
target: discovery
condition: {type: context_value, key: next_node, value: discovery}
- source: router
target: brainstorming
condition: {type: context_value, key: next_node, value: brainstorming}
- source: router
target: structure
condition: {type: context_value, key: next_node, value: structure}
- source: router
target: writing
condition: {type: context_value, key: next_node, value: writing}
- source: router
target: review
condition: {type: context_value, key: next_node, value: review}
- source: router
target: end
condition: {type: context_value, key: next_node, value: end}
- source: command_handler
target: router
- source: intro
target: end
- source: discovery
target: end
- source: brainstorming
target: end
- source: structure
target: end
- source: writing
target: end
- source: review
target: end
merges:
- sources: [__input__]
target: main
18.10 Composite Template with Document-Level Generation
This example combines a templates definition with Phase A loops to instantiate the same template multiple times with different parameter sets.
templates:
agents:
domain_advisor:
parameters:
domain:
type: string
required: true
depth:
type: enum
values: [novice, intermediate, expert]
default: intermediate
type: llm
config:
provider: openai
model: gpt-4
memory_enabled: true
system_prompt: |
You are an advisor specializing in {{ domain }}.
Tailor your answers to a {{ depth }}-level audience.
{% set domains = [
{"id": "tax", "domain": "personal taxation", "depth": "intermediate"},
{"id": "investing", "domain": "long-term investing", "depth": "novice"},
{"id": "crypto", "domain": "cryptocurrency markets", "depth": "expert"}
] %}
agents:
{% for d in domains %}
{{ d.id }}_advisor:
template: domain_advisor
params:
domain: "{{ d.domain }}"
depth: {{ d.depth }}
{% endfor %}
triage:
type: tool
config:
tools:
- name: triage
code: |
text = (input_data or "").lower()
if "tax" in text:
result = "GOTO_TAX_ADVISOR:" + input_data
elif "stock" in text or "invest" in text:
result = "GOTO_INVESTING_ADVISOR:" + input_data
elif "bitcoin" in text or "crypto" in text:
result = "GOTO_CRYPTO_ADVISOR:" + input_data
else:
result = "GOTO_INVESTING_ADVISOR:" + input_data
routes:
main:
type: graph
entry_point: start
nodes:
triage_node:
type: agent
agent: triage
router:
type: message_router
rules:
{% for d in domains %}
- match_type: prefix
pattern: "GOTO_{{ d.id | upper }}_ADVISOR"
target: {{ d.id }}_advisor
extract_message: true
separator: ":"
{% endfor %}
{% for d in domains %}
{{ d.id }}_advisor:
type: agent
agent: {{ d.id }}_advisor
{% endfor %}
edges:
- source: start
target: triage_node
- source: triage_node
target: router
{% for d in domains %}
- source: router
target: {{ d.id }}_advisor
condition:
type: context_value
key: next_node
value: {{ d.id }}_advisor
- source: {{ d.id }}_advisor
target: end
{% endfor %}
context:
global:
unsafe: true
merges:
- sources: [__input__]
target: main
18.11 template_strings for Structural Templates
When a template body contains expressions that themselves produce YAML structure (such as {% for %} generating multiple keys), the template_strings section preserves the body as a raw string until instantiation.
template_strings:
agents:
fanout_team: |
parameters:
team_size:
type: int
default: 3
domain:
type: string
required: true
# Below, the loop is evaluated at instantiation time and produces
# `team_size` worker agents with the given `domain`.
{% for i in range(team_size) %}
worker_{{ i }}:
type: llm
config:
provider: openai
model: gpt-3.5-turbo
system_prompt: |
You are worker #{{ i }} in a {{ domain }} team.
{% endfor %}
agents:
legal_team:
template: fanout_team
params:
team_size: 4
domain: "contract review"
ops_team:
template: fanout_team
params:
team_size: 2
domain: "incident response"
routes:
main:
type: stream
operators:
- type: map
params:
agent: legal_team
publications: [__output__]
merges:
- sources: [__input__]
target: main
18.12 Stream with Aggregation, Filter, and Error Handling
This example shows a stream route that filters short messages, processes through an agent, retries on failure, batches outputs, and catches errors.
agents:
analyzer:
type: llm
config:
provider: openai
model: gpt-4
system_prompt: "Analyze the input and return a numeric score 0-100."
formatter:
type: tool
config:
tools:
- name: format
code: |
result = "[REPORT] " + str(input_data)
routes:
analysis_pipeline:
type: stream
stream_type: cold
operators:
- type: filter
params:
condition:
type: content_not_contains
text: ""
- type: throttle
params:
duration: 0.5
- type: map
params:
agent: analyzer
- type: retry
params:
count: 3
- type: buffer
params:
count: 5
timeout: 2.0
- type: map
params:
agent: formatter
- type: distinct
- type: catch
params: {}
publications: [__output__]
merges:
- sources: [__input__]
target: analysis_pipeline
18.13 Splits and Merges with Error Channel
agents:
worker:
type: llm
config:
provider: openai
model: gpt-4
error_handler:
type: tool
config:
tools:
- name: log_error
code: |
result = "Logged error: " + str(input_data)
routes:
main_route:
type: stream
operators:
- type: map
params:
agent: worker
publications: [__output__]
error_route:
type: stream
operators:
- type: map
params:
agent: error_handler
publications: [__output__]
splits:
- source: __input__
targets:
main_route:
type: content_not_contains
text: "ERROR:"
error_route:
type: content_contains
text: "ERROR:"
18.14 Environment Variables and Secrets
agents:
prod_agent:
type: llm
config:
provider: anthropic
model: "${ANTHROPIC_MODEL:claude-3-5-sonnet-20241022}"
api_key: "${ANTHROPIC_API_KEY}"
temperature: ${LLM_TEMPERATURE:0.7}
max_tokens: ${LLM_MAX_TOKENS:1500}
system_prompt: |
You are running in the ${ENVIRONMENT:production} environment.
routes:
main:
type: stream
operators:
- type: map
params:
agent: prod_agent
publications: [__output__]
merges:
- sources: [__input__]
target: main
19. Appendix B — YAML Style Conventions
These conventions are non-normative recommendations for authors of Actor configurations:
- Use lowercase, underscore-separated identifiers (
my_agent,chat_stream). - Use 2-space indentation.
- Place required fields before optional fields.
- Group related sections together (all agents first, then all routes, etc.).
- Comment complex routing patterns with
#-prefixed YAML comments. - Prefer environment-variable interpolation over hard-coded credentials.
- Use templates to factor out repeated patterns.
- For multi-line strings (system prompts, inline code), use the
|(literal) block scalar style.
20. Appendix C — Error Taxonomy
Compliant implementations SHOULD signal the following error categories with the indicated semantics:
| Error | Semantic |
|---|---|
ConfigurationError |
Generic configuration-validation error. |
UnsafeConfigurationError |
Configuration requires unsafe mode; host is in safe mode. |
AgentCreationError |
Agent could not be instantiated. |
RoutingError |
Generic routing error. |
TemplateError |
Template rendering error. |
ExecutionError |
Runtime execution error within an agent or operator. |
ApplicationError |
Application-level error not covered above. |
StreamRoutingError |
Stream-routing or operator error. |
21. Versioning of This Standard
This document is Version 1.0.0 of the Actor Configuration Standard. Future revisions:
- A patch version (1.0.x) corrects errors and clarifies semantics without changing required behavior.
- A minor version (1.x.0) adds new optional features without breaking conformance of existing implementations.
- A major version (x.0.0) introduces breaking changes; non-backward-compatible.
Conformance MUST be declared against a specific version of this standard. Implementations MAY support multiple major versions concurrently.