Files
cleveractors-core/docs/index.md
2026-05-26 22:13:33 +01:00

152 KiB
Raw Permalink Blame History

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:

agents:
  echo:
    type: tool
    config:
      tools: [echo]

routes:
  main:
    type: stream
    operators:
      - type: map
        params:
          agent: echo
    publications: [__output__]

merges:
  - sources: [__input__]
    target: main

This Actor accepts any message on __input__, routes it through the echo tool agent, and emits the result on __output__.

3.4 The Default Router Requirement

When a routes section is present and is non-empty, the Actor MUST designate a default router: a route present in routes that receives messages submitted to the Actor when no explicit target is specified by the consumer. The mechanism by which an implementation accepts the designation is outside the scope of this standard; see §9 for the parameter description.

A compliant implementation MUST satisfy the default-router requirement through the default_router parameter. The alternative approach using a merges entry connecting __input__ to a route is deprecated.


4. Agents

4.1 The agents Section

The agents section MUST be a mapping. Each key MUST be a unique agent name (a non-empty string), and each value MUST be a mapping defining the agent. Agent definitions MUST conform to the following schema:

agents:
  <agent_name>:
    type: <agent_type>          # REQUIRED
    config: <mapping>           # OPTIONAL (defaults to {})

OR, when instantiated from a template (§8):

agents:
  <agent_name>:
    template: <template_name>   # REQUIRED for template instances
    params: <mapping>           # OPTIONAL parameters

OR:

agents:
  <agent_name>:
    agent_template: <template_name>   # Alternative key for template
    params: <mapping>

Example — declaring multiple agents of different types:

agents:
  greeter:
    type: llm
    config:
      provider: openai
      model: gpt-3.5-turbo
      system_prompt: "Greet the user warmly."

  calculator:
    type: tool
    config:
      tools: [math]

  formatter:
    type: tool
    config:
      tools:
        - name: pretty_format
          code: |
            result = "RESULT: " + str(input_data)

  research_pipeline:
    type: composite
    config:
      components:
        agents:
          searcher:
            type: tool
            config:
              tools: [http_request]
          analyzer:
            type: llm
            config:
              provider: openai
              model: gpt-4
      routing:
        input:
          type: agent
          name: searcher
        output:
          type: agent
          name: analyzer

4.2 Agent Names

Agent names MUST be unique within an Actor. Implementations SHOULD restrict agent names to the character class [A-Za-z_][A-Za-z0-9_]*. Names beginning with double underscore (__) are reserved for built-in use and MUST NOT be defined by user configurations.

4.3 Defined Agent Types

The following agent types are defined by this standard. Compliant implementations MUST support all defined types.

Type Required Description
llm Yes An agent backed by a large language model. (§4.4)
tool Yes An agent that executes deterministic tools or inline code. (§4.5)
composite Yes An agent composed of nested agents, graphs, and streams. (§4.6)
chain Optional An agent that applies an ordered sequence of textual steps to a message. (§4.7)
template_instance Yes An agent instantiated from a template. (§4.8)

Implementations MAY define additional agent types beyond those listed above, but the names llm, tool, composite, chain, and template_instance MUST retain their defined semantics. Additional agent types MUST be made available before any agent of that type is created.

4.4 LLM Agents (type: llm)

LLM agents process messages by invoking a large language model. The following configuration fields are defined:

Field Type Required Default Description
provider string No "openai" Name of the LLM provider. (§4.4.1)
model string No (provider-specific) Identifier of the model to invoke. (§4.4.2)
temperature number No 0.7 Sampling temperature in the range [0.0, 2.0].
max_tokens integer No 1000 Maximum number of tokens the model may generate in a single response.
system_prompt string No "You are a helpful assistant." The system message; MAY contain template expressions in the active template engine.
memory_enabled boolean No false Whether the agent maintains conversation history.
max_history integer No 10 Maximum number of historical message pairs to retain when memory_enabled is true.
api_key string No (from environment) Explicit API key; MAY use ${ENV_VAR} interpolation.
template string No (none) Name of a prompt template (registered under prompts) used to format the user message.
template_vars mapping No {} Additional variables passed to the named template.
response_format mapping No (none) Provider-specific structured-output specification (e.g., JSON schema). (§4.4.3)

Example — minimal LLM agent:

agents:
  simple_chat:
    type: llm
    config:
      provider: openai
      model: gpt-4

Example — LLM agent with memory and a custom system prompt:

agents:
  helpful_assistant:
    type: llm
    config:
      provider: openai
      model: gpt-4
      temperature: 0.8
      max_tokens: 2000
      memory_enabled: true
      max_history: 15
      system_prompt: |
        You are a thoughtful, careful assistant.
        Always cite your sources when discussing factual matters.

Example — LLM agent using an environment-variable API key:

agents:
  external_api_agent:
    type: llm
    config:
      provider: anthropic
      model: claude-3-5-sonnet-20241022
      api_key: "${ANTHROPIC_API_KEY}"
      system_prompt: "You are an analytical assistant."

4.4.1 Providers

The following providers MUST be supported. The provider name comparison is case-insensitive.

Provider Environment Variables (consulted in order, first match used)
openai OPENAI_API_KEY
anthropic ANTHROPIC_API_KEY
google GOOGLE_API_KEY, GOOGLEAI_API_KEY

Additional providers MAY be supported.

When an API key is not supplied (neither as an api_key field nor through a corresponding environment variable), a ConfigurationError MUST be signaled with a message identifying the missing variables.

When the requested provider is not available in the host, an AgentCreationError MUST be signaled with a clear message describing the unavailable provider.

4.4.2 Default Models

When model is unspecified, the following defaults apply:

Provider Default Model
openai gpt-3.5-turbo
anthropic claude-3-5-sonnet-20241022
google gemini-1.5-flash

4.4.3 Structured Output

When response_format is provided, it MUST be a mapping conformant to the provider's structured-output mechanism. The standard recognizes the following structure:

response_format:
  type: json_schema
  json_schema:
    name: <string>
    strict: <boolean>
    schema: <JSON Schema mapping>

The structure MUST be forwarded to the LLM provider's structured-output mechanism. When the provider does not support structured output, an execution error MUST be signaled.

Example — agent that parses a table of contents into structured JSON:

agents:
  toc_parser:
    type: llm
    config:
      provider: google
      model: gemini-2.0-flash
      memory_enabled: false
      response_format:
        type: json_schema
        json_schema:
          name: table_of_contents
          strict: true
          schema:
            type: object
            properties:
              sections:
                type: array
                items:
                  type: object
                  properties:
                    title:
                      type: string
                      description: The section title without numbering
                    subsections:
                      type: array
                      items:
                        type: object
                        properties:
                          title:
                            type: string
                        required: [title]
                        additionalProperties: false
                  required: [title]
                  additionalProperties: false
            required: [sections]
            additionalProperties: false
      system_prompt: |
        Extract the section titles and their hierarchy from the provided
        table of contents. Return them as a nested JSON structure.

4.4.4 Memory and History

When memory_enabled is true:

  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:

agents:
  utilities:
    type: tool
    config:
      tools:
        - math
        - json_parse
        - echo
      safe_mode: true
      timeout: 5

A message such as {"tool": "math", "args": {"expression": "2 + 2 * 3"}} would cause the agent to evaluate the expression and return "8".

Example — tool agent with a single inline-code tool (no JSON wrapping needed):

agents:
  shouter:
    type: tool
    config:
      tools:
        - name: shout
          code: |
            result = str(input_data).upper() + "!"

A message of "hello world" produces the response "HELLO WORLD!".

4.5.1 Built-in Tools

A compliant implementation MUST support the following built-in tools when listed by name in the tools array. The exact identifiers are case-sensitive.

Name Description Required Arguments
echo Returns the input verbatim. text (string) OR args (list of strings)
math Evaluates a numeric expression in a restricted arithmetic environment exposing only: abs, round, min, max, sum, pow, int, float. Returns the result as a string. expression (string) OR args[0]
json_parse Parses a JSON string and returns a re-serialized form indented with two spaces. json (string) OR args[0]
http_request Performs an HTTP request. The response is returned as a string of the form "Status: <code>\n\n<body>". url (string); optional method (default "GET"), headers (mapping), data (mapping serialized as a JSON request body)
file_read Reads a UTF-8 file from the filesystem. Returns a string of the form: [FILE_READ_SUCCESS]📄 File: <path> | Lines: <n> | Size: <n> chars\n[FILE_CONTENT_START]\n<content>\n[FILE_CONTENT_END]. file (string path) OR args[0]
file_write Writes/appends/inserts content to a file. file (string), content (string); optional mode (w, a, insert), position
progress_bar Updates the global progress-bar facility and writes a rendered bar to the host's standard-output channel. (§4.5.6) stage, phase, current, done, total, steps, count, remaining, sections_total, sections_completed, completed_sections, message, label (all optional)

For each built-in tool, an absent argument MAY default to a sensible fallback. The standard arguments listed above MUST be honored; additional arguments MAY be silently ignored.

4.5.2 Inline Tools

A tool definition MAY be specified as a mapping:

tools:
  - name: <tool_name>           # REQUIRED, unique within the agent
    code: <code_body>           # REQUIRED for inline-code tools (string)

When code is provided, the tool executes the given code body in a sandboxed environment. The execution environment MUST:

  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:

agents:
  router_tool:
    type: tool
    config:
      tools:
        - name: route_by_prefix
          code: |
            # `input_data` holds the incoming message.
            # `context` holds the active context.
            # `result` MUST be set to the value the tool returns.
            text = (input_data or "").strip()
            if text.startswith("!"):
                result = f"GOTO_COMMAND_HANDLER:{text}"
            else:
                stage = context.get("current_stage") or "default"
                result = f"GOTO_{stage.upper()}:{text}"

Example — inline tool that mutates context and returns the modified state:

agents:
  set_topic:
    type: tool
    config:
      tools:
        - name: save_topic
          code: |
            import json
            # input_data is expected to be JSON: {"topic": "<value>"}
            try:
                payload = json.loads(input_data)
                topic = payload.get("topic", "")
            except json.JSONDecodeError:
                topic = input_data

            # Mutating `context` propagates back to the caller.
            context["current_topic"] = topic
            result = f"Topic set to: {topic}"

4.5.3 Tool Invocation Protocol

When a tool agent receives a message, it MUST execute the following algorithm:

  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:

agents:
  research_then_summarize:
    type: composite
    config:
      components:
        agents:
          researcher:
            type: llm
            config:
              provider: openai
              model: gpt-4
              system_prompt: "Gather facts on the topic, citing each one."
          summarizer:
            type: llm
            config:
              provider: openai
              model: gpt-3.5-turbo
              system_prompt: "Summarize the facts in 3 sentences."
      routing:
        input:
          type: agent
          name: researcher
        output:
          type: agent
          name: summarizer

4.6.1 Components

The components mapping accepts three optional keys: agents, graphs, streams. Each is itself a mapping from a local component name to a component definition (using the same schema as the corresponding top-level section).

4.6.2 Routing

The routing mapping MUST specify the entry and exit components using the explicit form:

routing:
  input:
    type: <agent|graph|stream>  # REQUIRED
    name: <component_name>      # REQUIRED
  output:
    type: <agent|graph|stream>
    name: <component_name>

The composite agent's processing logic is:

  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:

routes:
  chat_route:
    type: stream
    stream_type: cold
    operators:
      - type: map
        params:
          agent: chat_agent
    publications:
      - __output__

Example — multi-operator stream with filter, throttle, and agent:

routes:
  rate_limited:
    type: stream
    stream_type: cold
    operators:
      - type: filter
        params:
          condition:
            type: content_not_contains
            text: ""
      - type: throttle
        params:
          duration: 1.0
      - type: map
        params:
          agent: my_agent
    publications: [__output__]

5.3.1 Stream Types

Type Semantics
cold Begins emitting only when subscribed to. Each subscriber receives the full sequence independently.
hot Always emits, replaying the most recent value to new subscribers. initial_value MAY be specified to seed the stream; if omitted, the stream begins with no value.
replay Replays the most recent buffer_size values to each new subscriber.

5.3.2 Operators

Each operator is a mapping with type (string) and params (mapping). The following operators MUST be supported:

map

Transforms each message.

- type: map
  params:
    agent: <agent_name>          # Use a named agent to transform
    # OR
    function: <function_name>    # Built-in or registered function
    # OR
    transform:                   # Built-in transformation
      type: <transform_type>     # See §5.3.5
      <transform-specific keys>

Exactly one of agent, function, or transform MUST be specified.

Example — map operator routing every message through an agent:

routes:
  chat_route:
    type: stream
    operators:
      - type: map
        params:
          agent: my_chat_agent
    publications: [__output__]

Example — map operator applying a built-in format transform:

routes:
  prefix_route:
    type: stream
    operators:
      - type: map
        params:
          transform:
            type: format
            template: "[PROCESSED] {content}"
    publications: [__output__]
filter

Filters messages based on a condition.

- type: filter
  params:
    condition: <condition>       # See §5.4

Example — filter route that drops empty messages:

routes:
  non_empty:
    type: stream
    operators:
      - type: filter
        params:
          condition:
            type: content_not_contains
            text: ""              # Equivalent to "drop empty content"
      - type: map
        params:
          agent: my_agent
    publications: [__output__]
transform

Applies an inline expression to message content, or applies a built-in transform.

- type: transform
  params:
    fn: "<expression>"           # Inline expression evaluated in a restricted sandbox
    # OR
    type: <transform_type>       # See §5.3.5
    <transform-specific keys>

When fn is provided, the expression MUST be evaluated using the restricted expression sandbox defined in §13.2.2. The expression takes the form of a single-argument anonymous function whose parameter is bound to the message's content field (NOT the full message envelope). The expression's value replaces the message content; the surrounding message and metadata are otherwise unchanged.

When type is provided, the params mapping is passed directly to the transform handler (§5.3.5), and the operator behaves identically to a map operator with the same transform.

An operator declaring neither fn nor type MUST be rejected with a stream-routing error.

debounce

Suppresses emissions during periods of activity, emitting only after a quiet period.

- type: debounce
  params:
    duration: <number>           # Duration in seconds

In single-shot execution contexts, the effective debounce time MUST be capped at 50 ms to avoid hangs.

throttle

Limits emission rate.

- type: throttle
  params:
    duration: <number>           # Minimum interval between emissions, in seconds
delay

Delays each emission by a fixed duration.

- type: delay
  params:
    duration: <number>           # Seconds
buffer

Batches messages.

- type: buffer
  params:
    count: <integer>             # Emit every N messages
    timeout: <number>            # Optional max wait, in seconds (default 0.1)
    # OR
    time: <number>               # Buffer for N seconds and flush

The buffer operator MUST flatten the buffered list back into individual emissions downstream, ensuring downstream operators continue to see individual messages.

Example — buffer to batch every 5 messages:

routes:
  batched:
    type: stream
    operators:
      - type: buffer
        params:
          count: 5
      - type: map
        params:
          agent: batch_processor
    publications: [__output__]

Example — buffer to flush every 2 seconds:

routes:
  time_batched:
    type: stream
    operators:
      - type: buffer
        params:
          time: 2.0
    publications: [__output__]
scan / reduce

Stateful aggregations.

- type: scan      # Emits intermediate results
  params:
    accumulator:
      type: <collect|concat|sum>
- type: reduce    # Emits only the final result
  params:
    accumulator:
      type: <collect|concat|sum>
switch / conditional_route

Conditional routing within a stream. Cases are evaluated in declaration order.

- type: switch     # alias: conditional_route
  params:
    cases:
      - condition: <condition>
        target: <stream_name>     # OPTIONAL — route matching messages to this stream
        operators: <sequence>     # OPTIONAL — apply inline operators when matched
    default: <stream_name>        # OPTIONAL — fallback stream when no case matches
    default_operators: <sequence> # OPTIONAL — fallback inline operators

Semantics:

  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:

routes:
  classifier:
    type: stream
    operators:
      - type: switch
        params:
          cases:
            - condition:
                type: content_contains
                text: "?"
              target: question_lane
            - condition:
                type: content_contains
                text: "!"
              target: urgent_lane
          default: normal_lane
    publications: []

  question_lane:
    type: stream
    operators:
      - type: map
        params:
          agent: question_agent
    publications: [__output__]

  urgent_lane:
    type: stream
    operators:
      - type: map
        params:
          agent: urgent_agent
    publications: [__output__]

  normal_lane:
    type: stream
    operators:
      - type: map
        params:
          agent: chat_agent
    publications: [__output__]
catch

Catches stream errors and emits them to __error__.

- type: catch
  params: {}
retry

Retries on failure.

- type: retry
  params:
    count: <integer>             # default 3
distinct

Suppresses duplicate consecutive emissions.

- type: distinct
  params: {}
take / skip
- type: take
  params:
    count: <integer>             # default 1
- type: skip
  params:
    count: <integer>             # default 1
sample

Periodic sampling.

- type: sample
  params:
    interval: <number>           # Seconds (default 1.0)
graph_execute (Bridge Operator)

Invokes a graph route from within a stream.

- type: graph_execute
  params:
    graph: <graph_route_name>

The operator MUST execute the named graph for each incoming message, wait for the result, and forward a message whose content is the final state's last assistant message (or the full state dictionary if no messages are present) and whose metadata includes graph, execution_history, and final_state keys.

state_update (Bridge Operator)
- type: state_update
  params:
    graph: <graph_route_name>

Applies the message content as a state update to the named graph and forwards the message annotated with metadata.state_updated = true.

state_checkpoint (Bridge Operator)
- type: state_checkpoint
  params:
    graph: <graph_route_name>

Triggers a checkpoint save on the named graph's state (see §6.6) and forwards the message annotated with metadata.checkpointed = true.

graph_node (Bridge Operator)
- type: graph_node
  params:
    graph: <graph_route_name>
    node: <node_name>

Executes a single node within the named graph, returning the node's output.

conditional_route Ambiguity

The operator name conditional_route is recognized in two contexts:

  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:

routes:
  linear:
    type: graph
    entry_point: start
    nodes:
      process:
        type: agent
        agent: my_agent
    edges:
      - source: start
        target: process
      - source: process
        target: end

Example — graph route with checkpointing and time travel:

routes:
  resilient_workflow:
    type: graph
    entry_point: start
    checkpointing: true
    checkpoint_dir: "./checkpoints/resilient"
    enable_time_travel: true
    parallel_execution: false
    nodes:
      step1:
        type: agent
        agent: validator
      step2:
        type: agent
        agent: processor
    edges:
      - source: start
        target: step1
      - source: step1
        target: step2
      - source: step2
        target: end

6.2 Nodes

Each entry in nodes is a mapping with at least a type field. The following node types are defined.

Type Section
start §6.2.1
end §6.2.1
agent §6.2.2
function §6.2.3
tool §6.2.3
conditional §6.2.4
subgraph §6.2.5
message_router §6.2.6

When written in YAML, these MAY appear as either lowercase or uppercase (AGENT/agent). The type name MUST be matched case-insensitively. The names start and end are reserved.

6.2.1 start and end Nodes

start and end nodes MUST be added implicitly to every graph route if not already present. The start node receives the initial message; the end node terminates execution and emits the final state as output.

6.2.2 Agent Nodes (type: agent)

<node_name>:
  type: agent
  agent: <agent_name>            # REQUIRED
  retry_policy: <mapping>        # OPTIONAL
  timeout: <number>              # OPTIONAL, in seconds
  parallel: <boolean>            # OPTIONAL, default false
  metadata: <mapping>            # OPTIONAL

retry_policy accepts:

retry_policy:
  max_retries: <integer>         # default 3
  delay: <number>                # default 1.0 seconds

When the named agent processes the message, the following behavior MUST occur:

  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)

<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)

<node_name>:
  type: conditional
  condition: <condition>         # See §5.4

A conditional node evaluates its condition against the graph state and adds metadata.condition_result: <bool>. Routing is performed by edge conditions, not by the conditional node itself.

6.2.5 Subgraph Nodes (type: subgraph)

  <agent_name>:
    agent_template: <template_name>   # DEPRECATED - use `template` instead
    params: <mapping>

Subgraph nodes mark a position for nested graph execution. Compliant implementations MUST record metadata.subgraph = <graph_route_name> and metadata.subgraph_pending = true.

6.2.6 Message Router Nodes (type: message_router)

<node_name>:
  type: message_router
  rules:                         # REQUIRED, ordered list
    - match_type: <prefix|suffix|contains|regex|exact>  # REQUIRED
      pattern: <string>          # REQUIRED
      target: <node_name>        # REQUIRED
      extract_message: <boolean> # OPTIONAL, default true (per match_type variations)
      separator: <string>        # OPTIONAL, default ":"
      set_state: <mapping>       # OPTIONAL
6.2.6.1 Rule Evaluation Order

Rules MUST be evaluated in declaration order. The first matching rule's target is set as the state's next_node. Subsequent rules MUST NOT be evaluated for the same message.

6.2.6.2 Match Types
match_type Match Semantics Default extract_message Behavior
prefix True when the message starts with pattern Strips pattern followed by separator (or pattern alone if the separator is not present) from the start of the message.
suffix True when the message ends with pattern Strips pattern from the end of the message.
contains True when pattern appears anywhere in the message If pattern followed by separator is present in the message, the extracted message is the text after that occurrence.
regex True when the regular expression pattern matches anywhere in the message When the pattern contains capture groups, the first group is the extracted message; else the entire message is returned.
exact True when the message equals pattern exactly Returns the empty string.

When extract_message: false is set, the message is passed through unchanged.

6.2.6.3 State Updates

When a rule matches and includes set_state, each key-value pair in set_state MUST be written to the graph state's top level (or metadata).

6.2.6.4 Routing Effect

After a match, the state's next_node MUST be set to the rule's target, the last message's content MUST be modified per the extraction rules, and any set_state entries MUST be applied. Downstream edges may then route based on the next_node value using a context_value condition (§6.5.1).

When no rule matches, the state MUST be returned unchanged (no next_node is set), and downstream edge evaluation determines the next node by other means (e.g., unconditional edges).

6.3 Graph State

6.3.1 The Graph State Structure

The default state object has the following fields:

Field Type Default Description
messages list of mappings [] Conversation messages: [{role, content, ...}, ...].
metadata mapping {} Arbitrary key-value metadata.
current_node string | null null Name of the currently executing node.
execution_count integer 0 Number of state updates.
error string | null null Last recorded error, if any.

messages MUST be capped at 50 entries; older entries are dropped during merge/append updates.

6.3.2 State Update Modes

Updates are applied via one of three modes:

Mode Effect
replace Setter replaces the existing field value.
merge (default) Dictionaries are recursively merged; lists are extended; scalars are replaced.
append Lists have the value appended (or extended if value is a list).

6.3.3 Custom State Schemas

state_class accepts a string identifier (typically of the form <namespace>.<name>) referring to a custom state schema known to the host. The named schema MUST be resolvable and SHOULD extend the default graph-state structure defined in §6.3.1. When the schema cannot be resolved, the default graph state MUST be used as a fallback and a warning SHOULD be emitted.

6.4 Edges

Each entry in edges is a mapping:

Field Type Required Description
source string Yes Source node name (or "start"/"END").
target string Yes Target node name (or "end"/"END").
condition mapping No Edge condition. (§5.4)
metadata mapping No Free-form metadata.

The identifiers END (uppercase) and end (lowercase) MUST be normalized to refer to the same end node. The same applies to START/start.

6.5 Edge Conditions

If condition is omitted, the edge is always traversed. If present, the condition is evaluated against the graph state per §5.4. Edges are traversed in declaration order; multiple edges with satisfied conditions cause parallel or sequential traversal per parallel_execution.

6.5.1 context_value Condition Resolution

The context_value condition is graph-specific and resolves its key against the state by checking, in this order:

  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

merges:
  - sources: [<route_or_stream_name>, ...]   # REQUIRED, non-empty
    target: <route_or_stream_name>           # REQUIRED

Each merge subscribes the target stream to receive the union of emissions from the listed sources. The order of merged emissions is non-deterministic. Merges into a stream that does not yet exist MUST create that stream as cold.

A merge with an empty sources list MUST be silently ignored. A merge with no target MUST be rejected.

Example — single-source merge connecting the Actor's input to the main route:

merges:
  - sources: [__input__]
    target: main

Example — multiple sources merged into a single aggregator:

merges:
  - sources: [research_stream, news_stream, social_stream]
    target: aggregator

  - sources: [__input__]
    target: research_stream

7.2 The splits Section

splits:
  - source: <route_or_stream_name>           # REQUIRED
    targets:                                 # mapping <name>: <condition>
      <target_name_1>: <condition>
      <target_name_2>: <condition>

Each split creates (or reuses) target streams and routes messages from the source to each target whose condition evaluates to true. Targets MAY share messages (a single message matching multiple conditions is forwarded to each).

A split with no source MUST be rejected. A split with empty targets MAY be silently ignored.

Example — splitting a single input stream into urgent and normal lanes:

splits:
  - source: __input__
    targets:
      urgent_lane:
        type: content_contains
        text: "URGENT"
      normal_lane:
        type: content_not_contains
        text: "URGENT"

Example — splitting based on metadata for error handling:

splits:
  - source: processing_stream
    targets:
      success_handler:
        type: content_not_contains
        text: "Error:"
      error_handler:
        type: content_contains
        text: "Error:"

7.3 The pipelines Section (Hybrid Pipelines)

pipelines:
  <pipeline_name>:
    stages:
      - type: <stream|graph>
        ...
    metadata: <mapping>

A pipeline is an ordered sequence of stages. Each stage MAY be a stream or graph configuration. The output of stage N feeds the input of stage N+1. Pipelines are advisory: implementations MUST accept valid pipeline declarations, but MAY emit warnings about discouraged patterns.

Example — three-stage hybrid pipeline (filter → graph → finalize):

pipelines:
  analysis_pipeline:
    stages:
      - type: stream
        name: stage1_filter
        operators:
          - type: filter
            params:
              condition:
                type: content_contains
                text: "analyze"
        publications: [stage2_input]

      - type: graph
        config:
          name: deep_analysis
          nodes:
            analyze:
              type: agent
              agent: analyzer
            summarize:
              type: function
              function: summarize
          edges:
            - source: start
              target: analyze
            - source: analyze
              target: summarize
            - source: summarize
              target: end
        input_from: stage2_input
        output_to: stage3_input

      - type: stream
        name: stage3_finalize
        subscriptions: [stage3_input]
        operators:
          - type: distinct
        publications: [__output__]
    metadata:
      description: "Filter, deep-analyze, then de-duplicate."

8. Templates and Instances

Templating in the Actor Configuration Standard operates at three distinct layers, each with its own purpose and timing:

  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:

# Pre-render template (Phase A); not yet valid YAML.
agents:
  {% for i in range(agent_count) %}
  worker_{{ i }}:
    type: llm
    config:
      provider: openai
      model: gpt-3.5-turbo
      system_prompt: "You are worker number {{ i }}."
  {% endfor %}

When rendered with the context {agent_count: 3}, the result is the valid YAML:

agents:
  worker_0:
    type: llm
    config:
      provider: openai
      model: gpt-3.5-turbo
      system_prompt: "You are worker number 0."
  worker_1:
    type: llm
    config:
      provider: openai
      model: gpt-3.5-turbo
      system_prompt: "You are worker number 1."
  worker_2:
    type: llm
    config:
      provider: openai
      model: gpt-3.5-turbo
      system_prompt: "You are worker number 2."

8.1.2 Conditional Inclusion via if Blocks

A {% if %} ... {% endif %} block MAY include or exclude entire sub-trees of YAML based on a condition.

Example — conditionally including memory support:

agents:
  chat_agent:
    type: llm
    config:
      provider: openai
      model: gpt-4
      {% if include_memory %}
      memory_enabled: true
      max_history: 20
      {% else %}
      memory_enabled: false
      {% endif %}

8.1.3 Inline Value Interpolation

A {{ expression }} MAY appear anywhere a scalar value is permitted in YAML.

Example — substituting a model name:

agents:
  assistant:
    type: llm
    config:
      provider: openai
      model: "{{ chosen_model }}"
      temperature: {{ default_temperature }}

8.1.4 Comments

Comments MUST be stripped before YAML parsing. They do not appear in the rendered output.

{# This comment is removed before the YAML is parsed #}
agents:
  agent_a:
    type: llm
    config:
      provider: openai

8.2 Document-Level Rendering Lifecycle

When an Actor configuration is loaded, the following phases MUST be applied in order:

  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:

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:

templates:
  agents:
    <template_name>: <agent_template_definition>
  graphs:
    <template_name>: <graph_template_definition>
  streams:
    <template_name>: <stream_template_definition>

A template definition is a complete agent, graph, or stream configuration that MAY contain template expressions of the form {{ ... }} and conditional blocks of the form {% if ... %} ... {% endif %}. These expressions are rendered each time the template is instantiated (Phase B), with the instance's params as the rendering context.

Example — defining a reusable LLM-agent template:

templates:
  agents:
    research_assistant:
      parameters:
        model:
          type: string
          default: gpt-4
        domain:
          type: string
          required: true
        temperature:
          type: float
          default: 0.7
      type: llm
      config:
        provider: openai
        model: "{{ model }}"
        temperature: {{ temperature }}
        system_prompt: |
          You are a research assistant specializing in {{ domain }}.
          Provide accurate, well-cited responses.

8.6.1 Parameter Declarations

A template MUST declare a parameters block listing its parameters as a mapping.

Mapping form (Required):

parameters:
  <param_name>:
    type: <string|int|float|boolean|enum|list|dict|agent_ref|component_ref>
    default: <value>           # OPTIONAL
    required: <boolean>        # OPTIONAL, default false
    values: [<allowed>...]     # REQUIRED for enum
    description: <string>      # OPTIONAL, free-form documentation

8.6.2 Parameter Types

Type Validation
string Coerced to its canonical string representation.
int Coerced to the integer represented by the value.
float Coerced to the floating-point number represented by the value.
boolean Strings "true"/"yes"/"1"/"on" (case-insensitive) MUST be coerced to true; any other value MUST be coerced by its truthiness (non-empty strings, non-zero numbers, and non-empty collections are true).
enum MUST match one of values.
list Single values are wrapped into a single-element list.
dict Passed through.
agent_ref Reference to a named agent (§8.7); passed through and resolved during instantiation.
component_ref Reference to a named component (agent, graph, or stream); passed through and resolved during instantiation.

If a required parameter is not provided and has no default, instantiation MUST fail with ConfigurationError.

8.6.3 Filters Available at Component Instantiation

At Phase B (component instantiation), the same filters and built-ins defined in §8.3 are available. The rendering context for a template instance is the union of:

  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):

agents:
  <agent_name>:
    template: <template_name>
    params:
      <param_name>: <value>

Routes MAY similarly be instantiated from templates using the template_config field:

routes:
  <route_name>:
    template_config:
      template: <template_name>
      params:
        <param_name>: <value>

Example — instantiating multiple agents from the same template:

templates:
  agents:
    domain_expert:
      parameters:
        domain:
          type: string
          required: true
        model:
          type: string
          default: gpt-4
      type: llm
      config:
        provider: openai
        model: "{{ model }}"
        system_prompt: "You are an expert in {{ domain }}."

agents:
  legal_expert:
    template: domain_expert
    params:
      domain: contract law

  medical_expert:
    template: domain_expert
    params:
      domain: clinical pharmacology

  finance_expert:
    template: domain_expert
    params:
      domain: corporate finance
      model: gpt-4-turbo

8.7.1 Reference Parameter Resolution

Parameters of type agent_ref or component_ref declare references to other named components. References MUST be resolved within the following scopes, in order:

  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:

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:

template_strings:
  agents:
    <name>: |
      <raw template body>
  graphs:
    <name>: |
      <raw template body>
  streams:
    <name>: |
      <raw template body>

These templates MUST be preserved verbatim until instantiation, with their template expressions intact. Compliant implementations MUST internally mark such templates as needing deferred preprocessing.

Templates that contain expressions that would themselves break YAML parsing (such as {% for %} loops that generate new YAML keys at instantiation time) MUST be declared in the template_strings section rather than the templates section.

When a template definition supplied via the templates section contains template syntax ({{ or {%), its rendering MUST be automatically deferred until instantiation time.

8.10 Composite Templates

Composite agent templates extend the standard template form with a components section, mirroring §4.6. References inside the composite template's body MAY use parameters (agent_ref, etc.) to bind to other instantiated components.

Example — a composite agent template that bundles three internal agents:

templates:
  agents:
    research_pipeline:
      parameters:
        topic:
          type: string
          required: true
        depth:
          type: enum
          values: [shallow, medium, deep]
          default: medium
      type: composite
      config:
        components:
          agents:
            searcher:
              type: tool
              config:
                tools: [http_request]
            analyzer:
              type: llm
              config:
                provider: openai
                model: gpt-4
                system_prompt: |
                  Analyze findings on "{{ topic }}" with {{ depth }} depth.
            summarizer:
              type: llm
              config:
                provider: openai
                model: gpt-3.5-turbo
                system_prompt: "Summarize the analysis concisely."
        routing:
          input:
            type: agent
            name: searcher
          output:
            type: agent
            name: summarizer

8.11 Runtime Template Rendering (Phase C)

Template expressions that survive Phase A and Phase B (typically those inside an LLM agent's system_prompt or under the prompts section) are rendered each time an agent processes a message.

The rendering context at this layer MUST include:

Variable Value
context The active context dictionary (global context merged with per-invocation context).
message The user message currently being processed.
Standard built-ins (§8.3) range, len, enumerate, type constructors, filters, etc.
Any keys present in the context at the top level Flattened from context so that {{ topic }} is equivalent to {{ context.topic }}.

Example — a runtime-rendered system prompt:

agents:
  topical_assistant:
    type: llm
    config:
      provider: openai
      model: gpt-4
      memory_enabled: true
      system_prompt: |
        You are an assistant specialized in {{ context.topic }}.
        {% if context.style %}
        Respond in a {{ context.style }} tone.
        {% endif %}
        {% if context.history_size > 5 %}
        We have been talking for a while; reference our prior discussion when helpful.
        {% endif %}

Each invocation of topical_assistant re-renders the prompt against the live context, so the prompt naturally adapts to changing state.

8.12 Render-Failure Policy

When rendering at any phase fails (for example, an undefined variable is referenced or a filter receives an invalid argument):

Phase Behavior on Failure
Phase A (Document) The configuration MUST fail to load and a ConfigurationError MUST be signaled.
Phase B (Instance) The instantiation MUST fail with ConfigurationError.
Phase C (Runtime) A warning MUST be emitted; the literal, unrendered text MUST be used as a fallback so processing continues.

9. Actor Parameters

9.1 Overview

This section enumerates parameters that affect Actor behavior. The mechanism by which an implementation accepts these parameters is outside the scope of this standard; each parameter is described by name, type, default, and effect. Implementations MAY accept these parameters via any configuration mechanism appropriate to the host environment.

9.2 template_engine

The template_engine parameter selects the templating dialect used at all three template phases described in §8.

Recognized values:

Value Dialect Supported Constructs
JINJA2 (default) Jinja2-compatible templating dialect Full variable interpolation ({{ var }}), control flow ({% if %}, {% for %}, {% macro %}, {% block %}, {% with %}), filters ({{ value | filter }}), and comments ({# … #}).
MUSTACHE Mustache-compatible logic-less templating dialect Variable interpolation ({{ var }}), sections ({{#section}}…{{/section}}), inverted sections ({{^section}}…{{/section}}), and comments ({{! comment }}). Does NOT support arbitrary expressions or filters.
SIMPLE Format-string templating with {{ }} pre-pass First, {{ expression }} placeholders are evaluated against the context using the restricted expression sandbox (§13.2.2); then the result is passed through a standard format-string substitution using the context as named parameters.

The value comparison is case-insensitive; the canonical form is uppercase.

When template_engine is unset, the default JINJA2 engine MUST be used for all three phases described in §8.

Example — selecting the engine:

cleveragents:
  template_engine: JINJA2

agents:
  greeter:
    type: llm
    config:
      provider: openai
      model: gpt-4
      system_prompt: |
        Greet the user {{ context.user_name | default('friend') }}.
        {% if context.tone %}
        Use a {{ context.tone }} tone.
        {% endif %}

The full processing pipeline for templated configurations is defined normatively in §8.2 (Document-Level Rendering Lifecycle) and §8.4 (Deferred Rendering and Field Protection). The template_engine parameter selects which dialect those phases use.

9.3 default_router

When a routes section is present and is non-empty, an Actor MUST designate a default_router: the name of a route present in routes that receives messages submitted to the Actor when no explicit target is specified by the consumer. See §3.4 for the requirement.

Field Type Default Description
default_router string (none) Name of a route present in routes.

A compliant implementation MAY enforce this requirement strictly (signaling ConfigurationError when the designated route does not exist), or MAY rely on a merges entry from __input__ to fulfill the same role.

9.4 unsafe

An Actor MAY declare that it requires unsafe mode by setting a truthy unsafe value in context.global. When this value is true and the host has not been explicitly placed in unsafe mode, the Actor MUST be refused execution. The error category for this rejection is UnsafeConfigurationError (§20).

context:
  global:
    unsafe: true

9.5 verbose and logging.level

These fields are advisory only; compliant implementations are not required to read them from the configuration. The verbose / log-level setting is typically controlled by the host environment rather than by the configuration document.

When a compliant implementation does honor these fields, verbose: true SHOULD be equivalent to logging.level: DEBUG. The standard log levels are CRITICAL, ERROR, WARNING, INFO, DEBUG.

9.6 version

Implementations MAY accept a version parameter. This field is informational only and MAY be used to select compatibility behaviors. The current version of this standard is "1.0.0".


10. Context

10.1 The context Section

context:
  global:
    <key>: <value>
    ...

context.global declares the Actor-wide context dictionary. All keys here are visible to all agents during processing.

Example — context declaring multiple variables:

context:
  global:
    unsafe: true
    app_name: "Scientific Paper Writer"
    workflow_stage: "intro"
    stage_order:
      - intro
      - discovery
      - brainstorming
      - structure
      - writing
      - review
    paper_details:
      topic: null
      length: null
      audience: null
    collected_sources: []

These keys are visible inside every agent's invocation context. An LLM agent's system_prompt MAY refer to them via Phase C templating (e.g., {{ context.app_name }}); a tool agent's inline code MAY read and write them via the context variable.

10.2 Context Lifecycle

  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

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:

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:

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:

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

agents:
  chat_agent:
    type: llm
    config:
      provider: openai
      model: gpt-4
      temperature: 0.7
      memory_enabled: true

routes:
  chat_stream:
    type: stream
    stream_type: cold
    operators:
      - type: map
        params:
          agent: chat_agent
    publications:
      - __output__

merges:
  - sources: [__input__]
    target: chat_stream

18.2 Conditional Graph Actor

agents:
  classifier:
    type: llm
    config:
      provider: openai
      model: gpt-3.5-turbo
      system_prompt: "Classify the input as either 'question' or 'statement'."

  qa_agent:
    type: llm
    config:
      provider: openai
      model: gpt-4

  ack_agent:
    type: llm
    config:
      provider: openai
      model: gpt-3.5-turbo

routes:
  main:
    type: graph
    entry_point: start
    nodes:
      classify:
        type: agent
        agent: classifier
      handle_question:
        type: agent
        agent: qa_agent
      handle_statement:
        type: agent
        agent: ack_agent
    edges:
      - source: start
        target: classify
      - source: classify
        target: handle_question
        condition:
          type: content_contains
          text: "question"
      - source: classify
        target: handle_statement
        condition:
          type: content_contains
          text: "statement"
      - source: handle_question
        target: end
      - source: handle_statement
        target: end

  input_stream:
    type: stream
    operators:
      - type: graph_execute
        params:
          graph: main
    publications:
      - __output__

merges:
  - sources: [__input__]
    target: input_stream

18.3 Message-Router-Based Actor

agents:
  code_agent:
    type: llm
    config:
      provider: openai
      model: gpt-4
      system_prompt: "You help with coding tasks."

  research_agent:
    type: llm
    config:
      provider: openai
      model: gpt-4
      system_prompt: "You help with research."

  default_agent:
    type: llm
    config:
      provider: openai
      model: gpt-3.5-turbo

routes:
  main:
    type: graph
    entry_point: start
    nodes:
      router:
        type: message_router
        rules:
          - match_type: prefix
            pattern: "CODE"
            target: code
            extract_message: true
            separator: ":"
          - match_type: prefix
            pattern: "RESEARCH"
            target: research
            extract_message: true
            separator: ":"
          - match_type: suffix
            pattern: ""
            target: default
            extract_message: false
      code:
        type: agent
        agent: code_agent
      research:
        type: agent
        agent: research_agent
      default:
        type: agent
        agent: default_agent
    edges:
      - source: start
        target: router
      - source: router
        target: code
        condition:
          type: context_value
          key: next_node
          value: code
      - source: router
        target: research
        condition:
          type: context_value
          key: next_node
          value: research
      - source: router
        target: default
        condition:
          type: context_value
          key: next_node
          value: default
      - source: code
        target: end
      - source: research
        target: end
      - source: default
        target: end

merges:
  - sources: [__input__]
    target: main

18.4 Tool Agent with Inline Code

context:
  global:
    unsafe: true

agents:
  echo_tool:
    type: tool
    config:
      tools:
        - name: echo_with_prefix
          code: |
            result = "PREFIX: " + str(input_data)

routes:
  main:
    type: stream
    stream_type: cold
    operators:
      - type: map
        params:
          agent: echo_tool
    publications:
      - __output__

merges:
  - sources: [__input__]
    target: main

18.5 Template-Based Actor (Component-Level Templating, Phase B)

templates:
  agents:
    basic_llm:
      parameters:
        model:
          type: string
          default: gpt-4
        system_message:
          type: string
          required: true
      type: llm
      config:
        provider: openai
        model: "{{ model }}"
        temperature: 0.7
        system_prompt: "{{ system_message }}"

agents:
  helper:
    template: basic_llm
    params:
      system_message: "You are a friendly assistant."

  expert:
    template: basic_llm
    params:
      model: gpt-4-turbo
      system_message: "You are a domain expert."

routes:
  main:
    type: stream
    operators:
      - type: map
        params:
          agent: helper
    publications:
      - __output__

merges:
  - sources: [__input__]
    target: main

18.6 Document-Level Templating (Phase A)

This example demonstrates Phase A templating: the YAML document itself contains template expressions that are evaluated before YAML parsing. The result of expansion is the actual Actor configuration.

{# This is a Phase A comment. It is stripped before YAML parsing. #}

{% set worker_models = ["gpt-3.5-turbo", "gpt-4", "gpt-4-turbo"] %}

agents:
  {% for model in worker_models %}
  worker_{{ loop.index }}:
    type: llm
    config:
      provider: openai
      model: {{ model }}
      temperature: 0.7
      system_prompt: |
        You are worker #{{ loop.index }}, running on {{ model }}.
  {% endfor %}

  aggregator:
    type: llm
    config:
      provider: openai
      model: gpt-4
      system_prompt: |
        Aggregate the outputs from {{ worker_models | length }} workers
        into a single coherent answer.

routes:
  main:
    type: stream
    operators:
      {% for model in worker_models %}
      - type: map
        params:
          agent: worker_{{ loop.index }}
      {% endfor %}
      - type: map
        params:
          agent: aggregator
    publications: [__output__]

merges:
  - sources: [__input__]
    target: main

After Phase A rendering with no external context (the loop reads from the template's own {% set %}), the YAML expands to three concrete worker_N agents and three corresponding map operators in the stream. The fully expanded form is what the YAML parser then sees.

18.7 Runtime Templating (Phase C)

This example demonstrates Phase C templating: template expressions inside system_prompt are preserved across YAML loading and re-rendered each time the agent processes a message.

context:
  global:
    paper_details:
      topic: null
      length: null
      audience: null

agents:
  paper_brainstormer:
    type: llm
    config:
      provider: openai
      model: gpt-4-turbo
      memory_enabled: true
      system_prompt: |
        You are a creative brainstorming partner for scientific papers.

        The paper must be written to meet the following requirements:
          - Topic: {{ context.paper_details.topic | tojson }}
          - Length: {{ context.paper_details.length | tojson }} words
          - Audience: {{ context.paper_details.audience | tojson }}

        {% if context.paper_details.topic %}
        Focus your suggestions on the declared topic.
        {% else %}
        Help the user choose a topic first.
        {% endif %}

routes:
  main:
    type: stream
    operators:
      - type: map
        params:
          agent: paper_brainstormer
    publications: [__output__]

merges:
  - sources: [__input__]
    target: main

Each time the agent processes a message, the system_prompt is re-rendered with the current context. If context.paper_details.topic is null, the else branch is rendered; once the topic is set (perhaps by a tool agent earlier in the pipeline), the if branch is selected on subsequent invocations.

18.8 Combined Phase A + Phase C Templating

This example combines both templating phases: Phase A generates multiple agents from a list, and Phase C renders each agent's system prompt at invocation time.

{% set specialties = [
  {"name": "legal_expert", "domain": "contract law", "model": "gpt-4"},
  {"name": "medical_expert", "domain": "clinical research", "model": "gpt-4-turbo"},
  {"name": "engineering_expert", "domain": "systems engineering", "model": "gpt-4"}
] %}

agents:
  {% for spec in specialties %}
  {{ spec.name }}:
    type: llm
    config:
      provider: openai
      model: {{ spec.model }}
      memory_enabled: true
      # The system_prompt below contains a Phase C expression that will
      # be rendered at message-processing time, NOT at YAML-load time.
      system_prompt: |
        You are an expert in {{ spec.domain }}.
        {% raw %}
        Current user context: {{ context.user_name | default('anonymous') }}
        {% endraw %}
  {% endfor %}

  router:
    type: tool
    config:
      tools:
        - name: select_specialty
          code: |
            text = (input_data or "").lower()
            if "legal" in text or "contract" in text:
                result = "GOTO_LEGAL_EXPERT:" + input_data
            elif "medical" in text or "clinic" in text:
                result = "GOTO_MEDICAL_EXPERT:" + input_data
            else:
                result = "GOTO_ENGINEERING_EXPERT:" + input_data

routes:
  main:
    type: graph
    entry_point: start
    nodes:
      route:
        type: agent
        agent: router
      router_node:
        type: message_router
        rules:
          {% for spec in specialties %}
          - match_type: prefix
            pattern: "GOTO_{{ spec.name | upper }}"
            target: {{ spec.name }}
            extract_message: true
            separator: ":"
          {% endfor %}
      {% for spec in specialties %}
      {{ spec.name }}:
        type: agent
        agent: {{ spec.name }}
      {% endfor %}
    edges:
      - source: start
        target: route
      - source: route
        target: router_node
      {% for spec in specialties %}
      - source: router_node
        target: {{ spec.name }}
        condition:
          type: context_value
          key: next_node
          value: {{ spec.name }}
      - source: {{ spec.name }}
        target: end
      {% endfor %}

context:
  global:
    unsafe: true
    user_name: null

merges:
  - sources: [__input__]
    target: main

Note the use of {% raw %}...{% endraw %} to escape Phase A rendering of the inner Phase C expression. Without {% raw %}, Phase A would attempt to render the expression at load time (and fail, since context.user_name is not yet defined). With {% raw %}, the expression is passed through unchanged to be rendered at Phase C.

18.9 Multi-Stage Workflow with State

This example shows a stateful, multi-stage workflow using a graph route with checkpointing and a message-router-driven workflow controller.

cleveragents:
  template_engine: JINJA2

context:
  global:
    unsafe: true
    workflow_stage: null
    stage_order: [intro, discovery, brainstorming, structure, writing, review]
    collected_data: {}

agents:
  # Workflow controller decides which stage to enter.
  workflow_controller:
    type: tool
    config:
      tools:
        - name: dispatch
          code: |
            text = (input_data or "").strip()
            if text.startswith("!"):
                result = f"GOTO_COMMAND_HANDLER:{text}"
            else:
                stage = context.get("workflow_stage") or "intro"
                result = f"GOTO_{stage.upper()}:{text}"

  command_handler:
    type: tool
    config:
      tools:
        - name: handle_command
          code: |
            text = (input_data or "").strip()
            parts = text.split(maxsplit=1)
            command = parts[0]
            if command == "!next":
                stage_order = context.get("stage_order", [])
                current = context.get("workflow_stage") or "intro"
                try:
                    idx = stage_order.index(current)
                    if idx + 1 < len(stage_order):
                        next_stage = stage_order[idx + 1]
                        context["workflow_stage"] = next_stage
                        result = f"GOTO_{next_stage.upper()}:"
                    else:
                        result = "COMMAND_OUTPUT:Final stage already reached."
                except ValueError:
                    result = "COMMAND_OUTPUT:Unknown current stage."
            elif command == "!stage":
                result = f"COMMAND_OUTPUT:Current stage is {context.get('workflow_stage')}"
            else:
                result = f"COMMAND_OUTPUT:Unknown command {command}"

  # One agent per stage; the system_prompt uses Phase C templating.
  intro_agent:
    type: llm
    config:
      provider: openai
      model: gpt-3.5-turbo
      system_prompt: |
        You are an onboarding assistant.
        Greet the user and instruct them to type `!next` to begin.

  discovery_agent:
    type: llm
    config:
      provider: openai
      model: gpt-4
      memory_enabled: true
      system_prompt: |
        Discover the user's needs.
        So far we know: {{ context.collected_data | tojson }}

  brainstorming_agent:
    type: llm
    config:
      provider: openai
      model: gpt-4-turbo
      memory_enabled: true
      system_prompt: |
        Brainstorm based on the discovered needs:
          {{ context.collected_data | tojson }}

  structure_agent:
    type: llm
    config:
      provider: openai
      model: gpt-4
      memory_enabled: true
      system_prompt: |
        Produce a structured outline.

  writing_agent:
    type: llm
    config:
      provider: openai
      model: gpt-4-turbo
      memory_enabled: true
      max_tokens: 4000
      system_prompt: |
        Write the requested content in full.

  review_agent:
    type: llm
    config:
      provider: openai
      model: gpt-4
      memory_enabled: true
      system_prompt: |
        Review the produced content for accuracy and clarity.

routes:
  main:
    type: graph
    entry_point: start
    checkpointing: true
    checkpoint_dir: "./checkpoints/workflow"

    nodes:
      controller:
        type: agent
        agent: workflow_controller

      router:
        type: message_router
        rules:
          - match_type: prefix
            pattern: "GOTO_COMMAND_HANDLER"
            target: command_handler
            extract_message: true
            separator: ":"
          - match_type: prefix
            pattern: "GOTO_INTRO"
            target: intro
            extract_message: true
            separator: ":"
          - match_type: prefix
            pattern: "GOTO_DISCOVERY"
            target: discovery
            extract_message: true
            separator: ":"
          - match_type: prefix
            pattern: "GOTO_BRAINSTORMING"
            target: brainstorming
            extract_message: true
            separator: ":"
          - match_type: prefix
            pattern: "GOTO_STRUCTURE"
            target: structure
            extract_message: true
            separator: ":"
          - match_type: prefix
            pattern: "GOTO_WRITING"
            target: writing
            extract_message: true
            separator: ":"
          - match_type: prefix
            pattern: "GOTO_REVIEW"
            target: review
            extract_message: true
            separator: ":"
          - match_type: prefix
            pattern: "COMMAND_OUTPUT"
            target: end
            extract_message: true
            separator: ":"

      command_handler:
        type: agent
        agent: command_handler

      intro:
        type: agent
        agent: intro_agent
      discovery:
        type: agent
        agent: discovery_agent
      brainstorming:
        type: agent
        agent: brainstorming_agent
      structure:
        type: agent
        agent: structure_agent
      writing:
        type: agent
        agent: writing_agent
      review:
        type: agent
        agent: review_agent

    edges:
      - source: start
        target: controller
      - source: controller
        target: router

      - source: router
        target: command_handler
        condition: {type: context_value, key: next_node, value: command_handler}
      - source: router
        target: intro
        condition: {type: context_value, key: next_node, value: intro}
      - source: router
        target: discovery
        condition: {type: context_value, key: next_node, value: discovery}
      - source: router
        target: brainstorming
        condition: {type: context_value, key: next_node, value: brainstorming}
      - source: router
        target: structure
        condition: {type: context_value, key: next_node, value: structure}
      - source: router
        target: writing
        condition: {type: context_value, key: next_node, value: writing}
      - source: router
        target: review
        condition: {type: context_value, key: next_node, value: review}
      - source: router
        target: end
        condition: {type: context_value, key: next_node, value: end}

      - source: command_handler
        target: router
      - source: intro
        target: end
      - source: discovery
        target: end
      - source: brainstorming
        target: end
      - source: structure
        target: end
      - source: writing
        target: end
      - source: review
        target: end

merges:
  - sources: [__input__]
    target: main

18.10 Composite Template with Document-Level Generation

This example combines a templates definition with Phase A loops to instantiate the same template multiple times with different parameter sets.

templates:
  agents:
    domain_advisor:
      parameters:
        domain:
          type: string
          required: true
        depth:
          type: enum
          values: [novice, intermediate, expert]
          default: intermediate
      type: llm
      config:
        provider: openai
        model: gpt-4
        memory_enabled: true
        system_prompt: |
          You are an advisor specializing in {{ domain }}.
          Tailor your answers to a {{ depth }}-level audience.

{% set domains = [
  {"id": "tax", "domain": "personal taxation", "depth": "intermediate"},
  {"id": "investing", "domain": "long-term investing", "depth": "novice"},
  {"id": "crypto", "domain": "cryptocurrency markets", "depth": "expert"}
] %}

agents:
  {% for d in domains %}
  {{ d.id }}_advisor:
    template: domain_advisor
    params:
      domain: "{{ d.domain }}"
      depth: {{ d.depth }}
  {% endfor %}

  triage:
    type: tool
    config:
      tools:
        - name: triage
          code: |
            text = (input_data or "").lower()
            if "tax" in text:
                result = "GOTO_TAX_ADVISOR:" + input_data
            elif "stock" in text or "invest" in text:
                result = "GOTO_INVESTING_ADVISOR:" + input_data
            elif "bitcoin" in text or "crypto" in text:
                result = "GOTO_CRYPTO_ADVISOR:" + input_data
            else:
                result = "GOTO_INVESTING_ADVISOR:" + input_data

routes:
  main:
    type: graph
    entry_point: start
    nodes:
      triage_node:
        type: agent
        agent: triage
      router:
        type: message_router
        rules:
          {% for d in domains %}
          - match_type: prefix
            pattern: "GOTO_{{ d.id | upper }}_ADVISOR"
            target: {{ d.id }}_advisor
            extract_message: true
            separator: ":"
          {% endfor %}
      {% for d in domains %}
      {{ d.id }}_advisor:
        type: agent
        agent: {{ d.id }}_advisor
      {% endfor %}
    edges:
      - source: start
        target: triage_node
      - source: triage_node
        target: router
      {% for d in domains %}
      - source: router
        target: {{ d.id }}_advisor
        condition:
          type: context_value
          key: next_node
          value: {{ d.id }}_advisor
      - source: {{ d.id }}_advisor
        target: end
      {% endfor %}

context:
  global:
    unsafe: true

merges:
  - sources: [__input__]
    target: main

18.11 template_strings for Structural Templates

When a template body contains expressions that themselves produce YAML structure (such as {% for %} generating multiple keys), the template_strings section preserves the body as a raw string until instantiation.

template_strings:
  agents:
    fanout_team: |
      parameters:
        team_size:
          type: int
          default: 3
        domain:
          type: string
          required: true
      # Below, the loop is evaluated at instantiation time and produces
      # `team_size` worker agents with the given `domain`.
      {% for i in range(team_size) %}
      worker_{{ i }}:
        type: llm
        config:
          provider: openai
          model: gpt-3.5-turbo
          system_prompt: |
            You are worker #{{ i }} in a {{ domain }} team.
      {% endfor %}

agents:
  legal_team:
    template: fanout_team
    params:
      team_size: 4
      domain: "contract review"

  ops_team:
    template: fanout_team
    params:
      team_size: 2
      domain: "incident response"

routes:
  main:
    type: stream
    operators:
      - type: map
        params:
          agent: legal_team
    publications: [__output__]

merges:
  - sources: [__input__]
    target: main

18.12 Stream with Aggregation, Filter, and Error Handling

This example shows a stream route that filters short messages, processes through an agent, retries on failure, batches outputs, and catches errors.

agents:
  analyzer:
    type: llm
    config:
      provider: openai
      model: gpt-4
      system_prompt: "Analyze the input and return a numeric score 0-100."

  formatter:
    type: tool
    config:
      tools:
        - name: format
          code: |
            result = "[REPORT] " + str(input_data)

routes:
  analysis_pipeline:
    type: stream
    stream_type: cold
    operators:
      - type: filter
        params:
          condition:
            type: content_not_contains
            text: ""
      - type: throttle
        params:
          duration: 0.5
      - type: map
        params:
          agent: analyzer
      - type: retry
        params:
          count: 3
      - type: buffer
        params:
          count: 5
          timeout: 2.0
      - type: map
        params:
          agent: formatter
      - type: distinct
      - type: catch
        params: {}
    publications: [__output__]

merges:
  - sources: [__input__]
    target: analysis_pipeline

18.13 Splits and Merges with Error Channel

agents:
  worker:
    type: llm
    config:
      provider: openai
      model: gpt-4

  error_handler:
    type: tool
    config:
      tools:
        - name: log_error
          code: |
            result = "Logged error: " + str(input_data)

routes:
  main_route:
    type: stream
    operators:
      - type: map
        params:
          agent: worker
    publications: [__output__]

  error_route:
    type: stream
    operators:
      - type: map
        params:
          agent: error_handler
    publications: [__output__]

splits:
  - source: __input__
    targets:
      main_route:
        type: content_not_contains
        text: "ERROR:"
      error_route:
        type: content_contains
        text: "ERROR:"

18.14 Environment Variables and Secrets

agents:
  prod_agent:
    type: llm
    config:
      provider: anthropic
      model: "${ANTHROPIC_MODEL:claude-3-5-sonnet-20241022}"
      api_key: "${ANTHROPIC_API_KEY}"
      temperature: ${LLM_TEMPERATURE:0.7}
      max_tokens: ${LLM_MAX_TOKENS:1500}
      system_prompt: |
        You are running in the ${ENVIRONMENT:production} environment.

routes:
  main:
    type: stream
    operators:
      - type: map
        params:
          agent: prod_agent
    publications: [__output__]

merges:
  - sources: [__input__]
    target: main

19. Appendix B — YAML Style Conventions

These conventions are non-normative recommendations for authors of Actor configurations:

  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.