Files
Developer d3e5bd623a
CI / build-docs (push) Failing after 13m40s
docs: remove deprecated forms and make explicit syntax mandatory
- 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
2026-05-23 19:29:34 +00:00

3972 lines
152 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
1. The structural grammar and syntax of an Actor configuration document.
2. The complete set of allowed top-level sections, fields, types, and values.
3. The defined agent types and their configuration parameters.
4. The defined route types (`stream`, `graph`, `bridge`) and their internal structures.
5. The defined node types within graph routes.
6. The defined stream operators, including their parameters and semantics.
7. The defined condition types used for filtering, routing, and edge evaluation.
8. The defined message-routing rules within `message_router` nodes.
9. The reserved/built-in stream names and their semantics.
10. The template and instance mechanism for reusable component definitions.
11. The context-management mechanism (both per-Actor and global).
12. The state-management mechanism for stateful graph execution.
13. The environment-variable interpolation mechanism.
14. The template-rendering dialects (`JINJA2`, `MUSTACHE`, `SIMPLE`) and their semantics.
15. The configuration-validation rules and error semantics.
16. The runtime behavior, including message flow, error handling, concurrency, and lifecycle.
17. Security boundaries enforced by `safe`/`unsafe` modes.
This standard does **NOT** define:
1. Command-line interfaces, invocation methods, or host integration patterns.
2. Any specific programming-language binding, application-programming interface, or development-kit.
3. The internal mechanics of LLM providers, transport protocols, or model behavior.
4. Storage formats for persisted state beyond the schema visible at the Actor configuration boundary.
5. Distribution, deployment, packaging, or operational concerns.
### 1.3 Conformance
A compliant implementation MUST:
1. Accept any well-formed Actor configuration that conforms to this standard and reject any document that does not.
2. Apply the validation rules in §11 and signal configuration errors before any agent is instantiated or any message is processed.
3. Realize the runtime behavior described in §12, including message flow, ordering, and error routing.
4. 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.
5. Enforce the security model defined in §13.
6. Treat all built-in stream names (§5.2) as reserved.
A compliant implementation MAY:
1. 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.
2. Provide additional tooling, persistence formats, or observability features.
3. 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:
1. The `agents` section, with at least one valid agent definition, and
2. The `routes` section, with at least one valid route definition (and, when `routes` is present, a default router SHOULD be designated identifying a route present in `routes` — 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:**
```yaml
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:
```yaml
agents:
<agent_name>:
type: <agent_type> # REQUIRED
config: <mapping> # OPTIONAL (defaults to {})
```
OR, when instantiated from a template (§8):
```yaml
agents:
<agent_name>:
template: <template_name> # REQUIRED for template instances
params: <mapping> # OPTIONAL parameters
```
OR:
```yaml
agents:
<agent_name>:
agent_template: <template_name> # Alternative key for template
params: <mapping>
```
**Example — declaring multiple agents of different types:**
```yaml
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:**
```yaml
agents:
simple_chat:
type: llm
config:
provider: openai
model: gpt-4
```
**Example — LLM agent with memory and a custom system prompt:**
```yaml
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:**
```yaml
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:
```yaml
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:**
```yaml
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:
1. The agent MUST maintain a list of conversation entries, each having keys `role` (one of `"user"`, `"assistant"`) and `content` (string).
2. The list MUST be truncated to retain only the most recent `max_history` entries (counting each user/assistant pair as two entries).
3. When invoked, the agent MUST include the historical entries in the prompt sent to the provider, preserving order.
4. 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:**
```yaml
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):**
```yaml
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:
```yaml
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:
1. Restrict the available built-in functions and types to the **Restricted Built-ins for Inline Code** set defined in §13.2.1.
2. Expose a JSON parsing and serialization facility under the name `json`, providing at minimum the operations `loads` (parse a JSON string), `dumps` (serialize a value to JSON), and an error category `JSONDecodeError` signaled when parsing fails.
3. Provide the following local variables to the tool body:
- `input_data`: the raw input message (string).
- `message`: an alias for `input_data`.
- `context`: the active context dictionary (passed by reference; modifications propagate back to the caller).
- `result`: initially `null`; the tool body sets this to its return value.
4. When `result` is `null` after 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:**
```yaml
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:**
```yaml
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:
1. If the agent declares exactly one inline-code tool, that tool is invoked with the entire message as `input_data`.
2. Otherwise, the message is parsed as JSON. If valid and the JSON contains a `tool` key, the named tool is invoked with arguments from the `args` key.
3. 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.
4. Otherwise, the message is parsed as a space-separated command of the form `<tool_name> <arg1> <arg2> ...`.
5. If the named tool is not in the allowed `tools` list, an execution error MUST be signaled.
#### 4.5.4 Safe-Mode Restrictions
When `safe_mode` is true (the default):
1. The shell tool MUST refuse to execute any command containing any of the literal substrings: `rm`, `del`, `format`, `shutdown`, `reboot`, `kill` (case-insensitive comparison).
2. `file_read` MUST reject paths containing `..` (directory traversal).
3. `file_read` MUST reject absolute paths unless the invocation context contains `_unsafe_mode: true`.
4. `file_write` MUST 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 `~`.
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:
1. A bar of fixed width 24 cells, using `█` for filled cells and `░` for empty cells.
2. A percentage with one decimal place.
3. A counter of the form `(current/total)`.
4. An optional label combining `stage` and `message`, joined by ` • `.
The facility MUST:
1. Resolve unspecified arguments by inferring from the active context's `writing_stage`, `section_paths`, `current_section_index`, and `current_latex_index` keys when present. These keys are conventional progress-bar hooks (§10.3).
2. Clamp `current` to the range `[0, total]`.
3. Default `total` to `1` when no upstream value is available.
4. Emit a trailing newline when progress reaches 100%.
5. Be invocable both from the `progress_bar` tool (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:**
```yaml
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:
```yaml
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:
1. Receive input message.
2. Dispatch to the input component.
3. Allow internal flow per the component's own configuration.
4. Collect the output from the output component.
5. 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:
1. If `prompt`/`prompt_reference` is set, renders the prompt with a context augmented by `message = <input>`.
2. Iterates through `steps`, appending each step label to the running result with the separator ` -> `.
3. 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:
1. 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"`.
2. A **metadata** mapping — at minimum containing the agent's `name`, `type`, `capabilities`, and `reactive: 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:**
```yaml
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:**
```yaml
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.
```yaml
- 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:**
```yaml
routes:
chat_route:
type: stream
operators:
- type: map
params:
agent: my_chat_agent
publications: [__output__]
```
**Example — map operator applying a built-in `format` transform:**
```yaml
routes:
prefix_route:
type: stream
operators:
- type: map
params:
transform:
type: format
template: "[PROCESSED] {content}"
publications: [__output__]
```
##### `filter`
Filters messages based on a condition.
```yaml
- type: filter
params:
condition: <condition> # See §5.4
```
**Example — filter route that drops empty messages:**
```yaml
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.
```yaml
- 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.
```yaml
- 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.
```yaml
- type: throttle
params:
duration: <number> # Minimum interval between emissions, in seconds
```
##### `delay`
Delays each emission by a fixed duration.
```yaml
- type: delay
params:
duration: <number> # Seconds
```
##### `buffer`
Batches messages.
```yaml
- 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:**
```yaml
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:**
```yaml
routes:
time_batched:
type: stream
operators:
- type: buffer
params:
time: 2.0
publications: [__output__]
```
##### `scan` / `reduce`
Stateful aggregations.
```yaml
- 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.
```yaml
- 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:
1. For each case, evaluate the `condition` against the current message (§5.4).
2. On the first match:
- If the case has `operators`, apply them inline and emit the result downstream.
- Else if it has `target` and the target stream exists, forward the message to the named stream.
3. If no case matches:
- If `default_operators` is set, apply them inline.
- Else if `default` is set and the default stream exists, forward the message there.
- Otherwise, pass the message through unchanged.
A `target` (or `default`) naming an unknown stream is silently treated as no-match.
**Example — switch operator routing messages to different downstream lanes:**
```yaml
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__`.
```yaml
- type: catch
params: {}
```
##### `retry`
Retries on failure.
```yaml
- type: retry
params:
count: <integer> # default 3
```
##### `distinct`
Suppresses duplicate consecutive emissions.
```yaml
- type: distinct
params: {}
```
##### `take` / `skip`
```yaml
- type: take
params:
count: <integer> # default 1
- type: skip
params:
count: <integer> # default 1
```
##### `sample`
Periodic sampling.
```yaml
- type: sample
params:
interval: <number> # Seconds (default 1.0)
```
##### `graph_execute` (Bridge Operator)
Invokes a graph route from within a stream.
```yaml
- 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)
```yaml
- 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)
```yaml
- 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)
```yaml
- 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:
1. As a graph-bridge operator, it routes messages to named groups by evaluating a computed route key against a `routes` mapping (`routes: <mapping of route_name to condition>`) with an optional `default`.
2. When no graph bridge is engaged, it falls through to the `switch` semantics 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:
1. Resolve the context to pass to the agent in the following order: (a) the message's `metadata.context` if present, (b) the active global context reference, (c) the empty context.
2. Invoke the agent so that its asynchronous response is delivered back into the stream pipeline.
3. Enforce a 120-second hard timeout per agent invocation. On timeout, a message MUST be emitted with `content` equal to `"Processing timed out"` and metadata fields `timeout: true` and `error: true`.
4. On any other error, emit a message with `content` equal to `"Error: <reason>"`, with metadata fields `error: true` and `processed_by: <agent_name>`.
5. 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 `merge` subscribes its `target` stream to the union of emissions from all `sources`. If the target does not already exist, it is created as a `cold` stream.
- A `split` filters the `source` stream against each named condition and forwards matching emissions to the named target stream (creating the target if necessary).
- Declaring a stream's `publications` to include a target that is also a `merge` `source` or `split` source 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:**
```yaml
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:**
```yaml
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`)
```yaml
<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:
```yaml
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:
1. The current message content is injected as the agent's input.
2. A `context` mapping is built containing:
- `graph_state`: a dictionary representation of the graph state (per §6.3.1), with `messages` trimmed (see §6.3.2).
- `conversation_history`: the trimmed history.
- `full_context`: `true`.
- Any keys present in the graph state's `metadata` at the top level.
- Nested `context` keys flattened to the top level.
3. After the agent call returns, any modifications the agent made to the `context` mapping MUST be propagated back to the graph state's `metadata`, with the exception of the reserved internal keys (`graph_state`, `conversation_history`, `full_context`, `_history_truncated`, `_history_original_length`).
4. 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`)
```yaml
<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 `agent` node referencing a `tool` agent with inline code (§4.5.2);
- A `message_router` node 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`)
```yaml
<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`)
```yaml
<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`)
```yaml
<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:
1. `state.metadata[key]`
2. `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`:
1. The directory named by `checkpoint_dir` MUST be created if it does not exist.
2. Every ten state updates, the full state MUST be persisted to a JSON file named `checkpoint_<timestamp>.json` within that directory, where `<timestamp>` is a sortable date-time string.
3. The most recent checkpoint MUST be discoverable for recovery.
4. A facility MUST exist for restoring state from a specified checkpoint file.
When `enable_time_travel: true`:
1. Up to 100 historical state snapshots MUST be retained.
2. A time-travel operation MUST be available to restore an earlier state by specifying the number of steps to rewind.
3. 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:
1. The set of next-nodes available from the most recent execution step is computed.
2. The subset whose nodes have `parallel: true` is identified.
3. That subset is executed concurrently, with all results joined before continuing.
4. 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:
1. **Maximum recursion depth**: bounded by the greater of `2000` and `50` times the number of nodes in the graph. On overflow, a warning MUST be emitted and the most recent output MUST be returned unchanged.
2. **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_active` is true, this guard MUST be relaxed.
3. **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.
4. **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`, or `COMMAND_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:
1. The graph executes natively, without inserting any reactive-stream intermediaries between nodes.
2. Context persistence, when provided by the host environment, is propagated directly to and from the graph's execution.
3. After each execution, the final state's `metadata` MUST replace the active context; nested `context` keys MUST be merged into the top-level metadata for inspection.
4. 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):
1. **Adjacency analysis**: forward and reverse adjacency lists MUST be constructed.
2. **Cycle detection**: the presence of cycles MUST be detected. Cycles are permitted in the configuration; they MUST simply be known to the runtime.
3. **Topological levels**: nodes MUST be partitioned into topological levels for use by parallel scheduling.
4. **Reachability**: nodes unreachable from the entry point MUST be identified; a warning listing any such nodes SHOULD be emitted.
5. **Edge validation**: every edge MUST be verified to reference existing nodes (after `START`/`END` normalization).
6. **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
```yaml
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:**
```yaml
merges:
- sources: [__input__]
target: main
```
**Example — multiple sources merged into a single aggregator:**
```yaml
merges:
- sources: [research_stream, news_stream, social_stream]
target: aggregator
- sources: [__input__]
target: research_stream
```
### 7.2 The `splits` Section
```yaml
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:**
```yaml
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:**
```yaml
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)
```yaml
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):**
```yaml
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:
1. **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.
2. **Component-level templating (Phase B — Reusable Component Definitions)**: Named, parameterized template definitions under the `templates` section produce concrete agent, graph, or stream definitions when instantiated. Instantiation happens during configuration loading, after YAML parsing.
3. **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:**
```yaml
# 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:
```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:**
```yaml
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:**
```yaml
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.
```yaml
{# 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:
1. **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 23 below are skipped.
2. **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_prompt` fields (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.
3. **Phase A.1 — YAML parsing**: The rendered text is parsed as YAML.
4. **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).
5. **Phase B — Component instantiation**: After YAML parsing, any agent, graph, or stream declared via `template`, `agent_template`, or `route_template` is instantiated by applying the named template (§8.6) with the provided parameters.
6. **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_prompt` is 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:**
```yaml
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:
1. Before Phase A rendering, the raw text of the configuration is scanned. Within string values that occupy `system_prompt` and equivalent fields, the markers `{{`, `}}`, `{%`, `%}` are replaced with the sentinels defined in §15.5 (`<<<TEMPLATE_START>>>`, `<<<TEMPLATE_END>>>`, `<<<BLOCK_START>>>`, `<<<BLOCK_END>>>`).
2. Phase A rendering proceeds against a minimal seed context; the sentinels are inert and pass through unchanged.
3. Phase A.1 YAML parsing proceeds normally.
4. After parsing, the sentinels MUST be restored to their original template-syntax form within the parsed structure.
5. 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:
```yaml
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:**
```yaml
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):**
```yaml
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:
1. The validated parameter values, AND
2. 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):**
```yaml
agents:
<agent_name>:
template: <template_name>
params:
<param_name>: <value>
```
Routes MAY similarly be instantiated from templates using the `template_config` field:
```yaml
routes:
<route_name>:
template_config:
template: <template_name>
params:
<param_name>: <value>
```
**Example — instantiating multiple agents from the same template:**
```yaml
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:
1. The local context of the enclosing composite agent (if any).
2. 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:**
```yaml
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:
1. **Validate parameters**: Required parameters are checked for presence; declared types are validated and coerced per §8.6.2.
2. **Fill defaults**: Parameters with declared defaults that were not provided receive their default values.
3. **Deep-copy the template body**: A fresh copy of the template definition is produced so that instantiation cannot mutate the template.
4. **Remove the `parameters` block**: The copy's `parameters` block is removed and is not present in the instantiated component.
5. **Render expressions**: Template expressions in all string values of the body are rendered recursively, with the merged parameter context.
6. **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.
7. **Eliminate empty conditional blocks**: Any `{% if … %} … {% endif %}` block that evaluates to an empty string MUST be removed from the resulting structure.
8. **Resolve references**: `agent_ref` / `component_ref` values are resolved per §8.7.1.
9. **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:
```yaml
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:**
```yaml
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:**
```yaml
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:**
```yaml
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).
```yaml
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
```yaml
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:**
```yaml
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
1. At Actor load time, `context.global` is populated from the configuration.
2. At message-processing time, the global context is referenced (not copied) into each message's `metadata.context`.
3. Tool agents and graph nodes MAY modify the context dictionary in-place.
4. Modifications made during processing are visible to subsequent processing steps within the same execution.
5. 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
```yaml
prompts:
<template_name>: # Long form (Required)
content: <prompt_template_string>
```
Prompt templates MUST be declared using the long form with a `content` key:
1. If the value is a mapping with a `content` key, the value of that key is used as the template content.
2. 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:**
```yaml
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
1. The top-level document MUST parse as a YAML mapping.
2. The `agents` section MUST be present and MUST be a mapping.
3. Each value under `agents` MUST be a mapping with a `type` key.
4. Each agent's `config`, when present, MUST be a mapping (if absent, an empty mapping is supplied).
5. When `routes` is present, it MUST be a non-empty mapping.
6. When `routes` is present, a `default_router` parameter SHOULD identify a route in `routes`. Compliant implementations MAY treat the absence or a dangling reference as a strict error.
7. When `routes` is absent, the configuration is invalid.
### 11.2 Agent Validation
1. Every agent's `type` MUST be a registered agent type, or one of `composite`, `template_instance`, or instantiated via `template`/`agent_template`.
2. Names beginning with `__` MUST be rejected.
### 11.3 Route Validation
1. Every route MUST specify `type`.
2. The value of `type` MUST be one of `stream`, `graph`, `bridge` (case-insensitive).
3. For `stream` routes:
- Each subscription/publication MUST name a known route or a built-in stream.
- Each agent referenced by `agents` MUST exist in the `agents` section.
4. For `graph` routes:
- `nodes` MUST be a non-empty mapping (unless instantiated from a template).
- Each agent node's `agent` MUST exist in the `agents` section.
- The `entry_point` MUST refer to an existing node (after auto-injection of `start`/`end`).
- Each edge's `source` and `target` MUST refer to existing nodes (after `START`/`END` normalization).
5. For `bridge` routes: no specific structural validation beyond schema conformance.
### 11.4 Merge/Split Validation
1. Each merge MUST have a non-null `target`.
2. Each merge with non-empty `sources` MUST reference only known route/stream names.
3. Each split MUST have a non-null `source`.
4. Each split's `source` MUST be a known route/stream name.
### 11.5 Pipeline Validation
1. A pipeline stage of `type: graph` referring to a non-existent graph name SHOULD generate a warning rather than an error.
2. A pipeline stage of `type: stream` whose `name` collides 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:
1. **Load**: Parse all configuration files; deep-merge in order; interpolate environment variables; perform validation (§11).
2. **Register templates**: Build the template registry.
3. **Create agents**: Instantiate every agent; agent creation failures MUST abort startup.
4. **Set up routes**: Create stream and graph routes.
5. **Set up operations**: Process merges and splits.
6. **Set up pipelines**: Create hybrid pipelines.
7. **Run**: Begin accepting messages on `__input__`.
8. **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
1. Stream operators MAY process messages concurrently per reactive-stream semantics.
2. 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: true` in their config.
3. Bridge conversions MUST preserve relative ordering of messages.
4. 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:
1. 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.
2. 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.
3. 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.
4. 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.
5. 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_read` MUST reject `..` and absolute paths.
- `file_write` MUST be disabled.
In unsafe mode (the host is in unsafe mode AND `_unsafe_mode: true` is in the invocation context):
- `file_read` permits absolute paths.
- `file_write` is 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:
1. The tool agent has `allow_shell: true`, AND
2. 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:**
```yaml
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:
1. If the environment variable is set, its value MUST be substituted.
2. If the environment variable is unset and a default is given, the default MUST be substituted.
3. If the environment variable is unset and no default is given, a `ConfigurationError` MUST 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:**
```yaml
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` / `START`
- `end` / `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:
- `prompt` AND `prompt_reference` on the same agent.
- `template` AND `type` on the same agent (one form excludes the other).
- `agent_template` AND `template` on the same agent.
- `code` AND a tool name colliding with a built-in tool (the inline tool always wins, but implementations SHOULD warn).
- Routes with both `template_config` and 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:
1. **Minimal valid configurations**: A single agent and a single stream route publishing to `__output__`.
2. **All agent types**: One configuration per type (`llm`, `tool`, `composite`).
3. **All stream operators**: Configurations exercising each defined operator.
4. **All condition types**: Both passing and failing for each condition type.
5. **Graph routes**: Linear, branching, conditional, looping, parallel.
6. **Message router nodes**: All five match types.
7. **Templates and instances**: Parameter validation success and failure cases.
8. **Environment variables**: Required, defaulted, missing.
9. **Merges and splits**: Including multiple sources and conditional targets.
10. **Error scenarios**: Invalid configurations (missing `type`, dangling references, etc.).
11. **Security boundaries**: Configurations requiring unsafe mode in both safe and unsafe hosts.
12. **State persistence**: Graph routes with checkpointing enabled.
---
## 18. Appendix A — Canonical Examples
### 18.1 Minimal Chat Actor
```yaml
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
```yaml
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
```yaml
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
```yaml
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)
```yaml
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.
```yaml
{# 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.
```yaml
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.
```yaml
{% 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.
```yaml
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.
```yaml
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.
```yaml
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.
```yaml
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
```yaml
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
```yaml
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:
1. Use lowercase, underscore-separated identifiers (`my_agent`, `chat_stream`).
2. Use 2-space indentation.
3. Place required fields before optional fields.
4. Group related sections together (all agents first, then all routes, etc.).
5. Comment complex routing patterns with `#`-prefixed YAML comments.
6. Prefer environment-variable interpolation over hard-coded credentials.
7. Use templates to factor out repeated patterns.
8. 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.