#4902 DSL: AI Agent Orchestration

#4902 DSL: AI Agent Orchestration

#4902 Describe a domain specific language (DSL) for AI agent orchestration. All available AI agents are defined in a folder/text repository and can be referenced directly from the DSL

A Domain-Specific Language (DSL) for AI agent orchestration acts as a declarative coordination layer that separates agent capabilities (defined externally) from workflow execution logic (orchestration, routing, control flow, and safety gates).

Core Architecture & Mental Model

 ┌───────────────────────────┐      ┌───────────────────────────┐
 │   Agent Repository        │      │    DSL Orchestration File │
 │   (Directory / Markdown)  │      │    (.flow / YAML / HCL)   │
 │                           │      │                           │
 │   agents/                 │      │  workflow IncidentTriage: │
 │    ├── triage_agent.md    │◄─────┼── pipeline:                  │
│ ├── db_specialist.md │ │ - step: classify │ │ └── security_lead.md │ │ call: triage_agent │ └───────────────────────────┘ └─────────────┬─────────────┘ │ ▼ ┌───────────────────────────┐ │ Execution Engine │ │ - Context / State bus │ │ - Concurrency & Routing │ │ - Tool / Guardrail gates │ └───────────────────────────┘
  1. Agent Repository: Each agent lives as an individual text artifact (typically .agent.md, .yaml, or front-matter Markdown) specifying its role, persona, model tier, attached tools, and output schema.

  2. DSL Runtime: Discovers available agents via path or package reference (e.g., @agents/db_specialist), loads their interface contracts, and enforces step transitions, state propagation, retries, and validations.

DSL Syntax Design: AgentFlow

Below is a declarative, human-readable DSL syntax designed specifically for direct repository linkage.

# workflow: automated_incident_response.flow
name: AutomatedIncidentResponse
version: "1.0"

# Explicit agent imports mapped to the repository
agents:
  triage: import "agents/triage_agent.md"
  db_expert: import "agents/db_specialist.md"
  sec_analyst: import "agents/security_lead.md"
  summarizer: import "agents/reporter.md"

# Global state schema passed across steps
state:
  incident_raw: string
  severity: enum [P1, P2, P3, P4] = P3
  findings: list[string] = []
  resolution_report: string = ""

# Workflow execution definition
pipeline:
  # 1. Sequential classification step
  - step: TriageIncident
    agent: triage
    input:
      description: state.incident_raw
    output:
      severity: $.severity
      suspected_domains: $.tags
    timeout: 30s

  # 2. Branching & Dynamic Fan-Out
  - step: DeepInvestigation
    branch:
      when: state.severity in [P1, P2]
      parallel:
        - step: InvestigateDatabase
          if: "'database' in state.suspected_domains"
          agent: db_expert
          input:
            logs: context.events
          retry:
            max_attempts: 3
            backoff: exponential

        - step: InvestigateSecurity
          if: "'auth' in state.suspected_domains or state.severity == P1"
          agent: sec_analyst
          input:
            audit_trail: context.security_logs
          guardrail:
            reject_if: "$.confidence < 0.70"

  # 3. Consensus / Deliberation Pattern
  - step: DebateActionPlan
    mode: consensus
    rounds: 2
    participants: [db_expert, sec_analyst]
    topic: "Agree on immediate containment measures without dropping prod connections."
    output:
      agreed_plan: $.consensus_statement

  # 4. Final Aggregation
  - step: GenerateReport
    agent: summarizer
    input:
      triage: state.severity
      investigations: [InvestigateDatabase.output, InvestigateSecurity.output]
      plan: DebateActionPlan.agreed_plan
    output:
      resolution_report: $.markdown

Primitives & Key Language Constructs

Language ConstructPurposeExample
Agent BindingLinks identifiers directly to repository paths or specs.agent: triage import "agents/triage.md"
Execution TopologiesSupports sequential, fan-out/fan-in (parallel), and collaborative turns.mode: consensus, parallel: [...]
State & ScopeContext bus with explicit schema definitions preventing prompt bleed.state.incident_raw, $.output_field
Guardrails & FiltersAssertion layers checking output structures, confidence, or regex constraints.guardrail: reject_if: "$.score < 0.8"
Fault ResilienceDeclarative retry policies, fallbacks, model degradations, and timeouts.fallback: call agents/fallback_agent.md
Human-in-the-LoopNative pause hooks awaiting external approval tokens or webhook calls.await_approval: role == "lead_sre"

Agent Definition Contract in the Repository

For the DSL to bind to agents unambiguously, agents in the text repository define clear input/output contracts alongside prompts:

<!-- agents/triage_agent.md -->
---
name: triage_agent
model: gpt-4o-mini
tools: [query_datadog, query_pagerduty]
temperature: 0.1
schema:
  input:
    description: string
  output:
    severity: string (P1|P2|P3|P4)
    tags: array[string]
---

You are an automated triage SRE. Categorize incoming alerts by severity and domain tags.

Compilation & Runtime Pipeline

  1. Static Analysis & Link Checking: The compiler parses the DSL file and scans the designated folder repository to confirm every referenced agent file exists and that declared step inputs match the agent's schema.

  2. DAG Construction: Control flow statements (step, parallel, branch, if) are transformed into a directed acyclic graph (DAG) or a state machine.

  3. Context Isolation: When a node executes, only its declared input keys are injected into the agent's prompt context, avoiding context window exhaustion.

  4. Telemetry & Tracing: Emits OpenTelemetry events for step transitions, token usage, latency, and agent handoffs.


How can this work along with Pydantic classes for each agent?

Integrating Pydantic with the DSL provides compile-time structural verification, runtime JSON-schema enforcement, and automatic model validation at agent boundaries.

In this architecture, each agent in the repository defines its interface using Pydantic models (either directly via Python modules or via Pydantic-compatible JSON schemas inside Markdown frontmatter). The DSL runtime binds to these models to validate inputs before invoking an LLM and parse outputs into typed objects.

Architectural Layout

agents/
  ├── triage/
  │    ├── schemas.py       # Pydantic Input/Output models
  │    └── prompt.md        # System prompt & persona
  └── db_expert/
       ├── schemas.py
       └── prompt.md
workflows/
  └── incident_flow.yaml    # DSL referencing the agent definitions

Step 1: Defining Agent Contracts with Pydantic

Each agent exports two core schemas: Input and Output.

# agents/triage/schemas.py
from enum import Enum
from pydantic import BaseModel, Field

class SeverityEnum(str, Enum):
    P1 = "P1"
    P2 = "P2"
    P3 = "P3"
    P4 = "P4"

class TriageInput(BaseModel):
    alert_raw: str = Field(..., description="Raw incoming alert or stack trace")
    service: str = Field(..., description="Source service reporting the alert")

class TriageOutput(BaseModel):
    severity: SeverityEnum
    domains: list[str] = Field(default_factory=list, description="Affected subsystems")
    summary: str
    confidence: float = Field(ge=0.0, le=1.0)

The accompanying prompt references these contracts:

<!-- agents/triage/prompt.md -->
---
name: triage_agent
schemas: agents.triage.schemas
input_model: TriageInput
output_model: TriageOutput
model: gpt-4o-mini
---
You are an SRE incident classifier. Given an incoming alert, extract the severity, affected domains, and provide an initial summary.

Step 2: Referencing Typed Agents in the DSL

The DSL maps execution steps to agent paths, automatically inheriting the Pydantic type definitions for validation:

# workflows/incident_flow.yaml
name: TriagePipeline

agents:
  triage: import "agents/triage"
  db_expert: import "agents/db_expert"

state:
  raw_alert: string
  origin_service: string

pipeline:
  - step: TriageAlert
    agent: triage
    # The DSL engine validates these keys against TriageInput at parse time
    input:
      alert_raw: state.raw_alert
      service: state.origin_service

  - step: InvestigateDB
    agent: db_expert
    if: "'database' in TriageAlert.output.domains"
    # Validated against DBExpertInput
    input:
      context: TriageAlert.output.summary
      severity: TriageAlert.output.severity

Step 3: Runtime Execution & Validation Lifecycle

When the orchestration engine processes a step, Pydantic sits at both boundaries:

[DSL Step Inputs]
       │
       ▼
1. Validate & Pack ──► `TriageInput(**step_inputs)` (Raises DSLBindingError if mismatched)
       │
       ▼
2. LLM Call ─────────► Injects `TriageOutput.model_json_schema()` as structured output schema
       │
       ▼
3. Validate & Cast ──► `TriageOutput.model_validate_json(raw_llm_response)`
       │
       ▼
[State Bus Updated] ──► Available to downstream steps with dot-notation access

Python Runtime Implementation Hook

Here is how the DSL interpreter dynamically resolves the Pydantic classes and enforces contracts:

import importlib
from pathlib import Path
from pydantic import BaseModel, ValidationError

class AgentExecutor:
    def __init__(self, agent_dir: str):
        self.path = Path(agent_dir)
        # Dynamically load the agent's schema module
        module_path = str(self.path / "schemas.py").replace("/", ".").rstrip(".py")
        self.schemas = importlib.import_module(f"agents.{self.path.name}.schemas")
        
        self.input_model: type[BaseModel] = getattr(self.schemas, f"{self.path.name.capitalize()}Input")
        self.output_model: type[BaseModel] = getattr(self.schemas, f"{self.path.name.capitalize()}Output")

    def execute_step(self, raw_inputs: dict, llm_client) -> BaseModel:
        # 1. Pre-execution validation
        try:
            validated_input = self.input_model(**raw_inputs)
        except ValidationError as e:
            raise RuntimeError(f"DSL input binding failed schema check: {e}")

        # 2. Structured LLM Call using JSON Schema generated by Pydantic
        response = llm_client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": (self.path / "prompt.md").read_text()},
                {"role": "user", "content": validated_input.model_dump_json()}
            ],
            response_format={
                "type": "json_schema",
                "json_schema": {
                    "name": self.output_model.__name__,
                    "schema": self.output_model.model_json_schema()
                }
            }
        )

        # 3. Post-execution response casting
        return self.output_model.model_validate_json(response.choices[0].message.content)

Key Advantages of Combining DSLs with Pydantic

FeatureWithout PydanticWith Pydantic Integration
Static VerificationErrors caught only when LLM responds unpredictably.Syntax/type mismatches caught at DSL load time before sending API calls.
Prompt EngineeringManual JSON formatting instructions written inside prompts.Uses model_json_schema() to leverage provider-native structured output engines.
State PropagationUnstructured JSON parsing (res["data"]["id"]).Strongly-typed field access (StepName.output.field) with autocomplete/IDE support.
Recovery / RetriesBlanket retries on generic errors.Targeted prompt retries providing Pydantic's ValidationError.errors() directly back to the model.


From <https://gemini.google.com/app/1f0a7b7c230a83ea>  Google Gemini (3.8 Flash)

Comments

Popular posts from this blog

#4297 ISS Astronaut Unable to Speak

#3356 PHP and JavaScript to Retrieve and Set an API_Key

#4188 Hybrid/GraphRAG