DEV Community

Cover image for Why Your AI Agent Orchestration Breaks Down (and How DSLs Help)
Alan West
Alan West

Posted on

Why Your AI Agent Orchestration Breaks Down (and How DSLs Help)

If you've spent any time wiring up multi-step AI agent workflows in Python or TypeScript, you've hit the wall. You know the one — your orchestration code starts as a clean function, then grows into a tangled mess of retry logic, context management, prompt chaining, and error handling that makes spaghetti code look organized.

I've been there. Last month I was debugging an agent pipeline that was supposed to summarize documents, extract entities, and then cross-reference them against a knowledge base. Three steps. Should be simple. Except the orchestration code was 400 lines of Python and the actual business logic was maybe 30 lines buried somewhere in the middle.

That's the core problem: general-purpose languages are terrible at expressing AI workflows declaratively.

The Root Cause: Impedance Mismatch

When you orchestrate AI agents in Python or JavaScript, you're fighting the language. These languages were designed for sequential, deterministic computation. AI agent workflows are fundamentally different:

  • They're non-deterministic — the same input can produce different outputs
  • They require context windows that need careful management
  • They involve structured data flowing between steps with type coercion
  • Error handling isn't just try/catch — it's "the model hallucinated, retry with a different prompt"

Here's what typical orchestration code looks like in Python:

async def process_document(doc: str) -> Result:
    # Step 1: Summarize
    summary = await call_llm(
        model="claude-sonnet-4-6",
        prompt=f"Summarize: {doc}",
        max_tokens=500
    )

    # Step 2: Extract entities — but what if summary is garbage?
    if not validate_summary(summary):
        # Retry with more context? Different model? Give up?
        summary = await call_llm(
            model="claude-sonnet-4-6",
            prompt=f"Summarize more carefully: {doc}",
            max_tokens=800  # more tokens, maybe that helps?
        )

    # Step 3: Now extract entities from the summary
    entities = await call_llm(
        model="claude-sonnet-4-6",
        prompt=f"Extract entities from: {summary}",
        response_format="json"
    )

    # Step 4: Parse the JSON... which might not be valid JSON
    try:
        parsed = json.loads(entities)
    except json.JSONDecodeError:
        # Here we go again
        entities = await call_llm(
            model="claude-sonnet-4-6",
            prompt=f"Extract entities as valid JSON: {summary}",
            response_format="json"
        )
        parsed = json.loads(entities)  # fingers crossed

    return Result(summary=summary, entities=parsed)
Enter fullscreen mode Exit fullscreen mode

See the problem? Half the code is dealing with the incidental complexity of working with non-deterministic systems using deterministic tools. The actual workflow is four lines. Everything else is duct tape.

The DSL Approach

This is exactly why projects like Weft — a programming language specifically designed for AI systems — are showing up on GitHub Trending. The idea is straightforward: instead of shoehorning AI orchestration into Python, build a language where AI-native concepts are first-class citizens.

I haven't done a deep dive into Weft's specific implementation yet, so I'll speak to the general pattern that AI-focused DSLs are converging on. The core insight is that AI workflows have a few primitives that deserve language-level support:

1. Declarative Pipeline Definitions

Instead of imperative step-by-step code, you declare what the pipeline is:

# Pseudocode representing the DSL pattern
pipeline document_analysis:
  input: document (text)

  step summarize:
    model: claude-sonnet-4-6
    prompt: "Summarize the following document"
    context: $document
    retry: 2
    validate: length > 50

  step extract_entities:
    model: claude-sonnet-4-6
    prompt: "Extract named entities as JSON"
    context: $summarize.output
    output_format: json
    retry: 3

  output:
    summary: $summarize.output
    entities: $extract_entities.output
Enter fullscreen mode Exit fullscreen mode

Notice what disappeared: the manual retry logic, the JSON parsing boilerplate, the validation plumbing. The DSL handles all of it because it understands what these operations are.

2. Built-in Retry and Validation Semantics

In a general-purpose language, retry logic for AI calls is always hand-rolled. In an AI-focused DSL, retry is a primitive with sensible defaults:

  • Retry with the same prompt (transient failures)
  • Retry with an augmented prompt (quality failures)
  • Retry with a different model (capability failures)
  • Fail gracefully with a fallback value

This isn't just convenience — it's correctness. I've seen production systems where a developer forgot to handle one retry path and the whole pipeline would silently return partial results.

3. Type-Aware Context Passing

The biggest footgun in agent orchestration is context management. When you chain steps together, you need to track what data flows where. DSLs can enforce this at the language level, catching errors before runtime.

Step-by-Step: Applying DSL Thinking Today

You don't need to adopt a new language tomorrow to benefit from this pattern. Here's how to apply DSL thinking to your existing orchestration code:

Step 1: Separate workflow definition from execution.

# Define the workflow as data, not code
workflow = {
    "steps": [
        {
            "name": "summarize",
            "model": "claude-sonnet-4-6",
            "prompt_template": "Summarize: {input}",
            "retry": {"max": 2, "strategy": "augment_prompt"},
            "validate": lambda output: len(output) > 50
        },
        {
            "name": "extract_entities",
            "model": "claude-sonnet-4-6",
            "prompt_template": "Extract entities from: {summarize.output}",
            "output_format": "json",
            "retry": {"max": 3, "strategy": "same_prompt"}
        }
    ]
}

# Generic executor handles all the plumbing
result = await execute_workflow(workflow, input=document)
Enter fullscreen mode Exit fullscreen mode

Step 2: Build a small executor that handles the common patterns. Retry logic, JSON parsing, validation — write it once in the executor, not in every pipeline.

Step 3: Add observability at the executor level. Log every step's input, output, latency, and retry count. When something breaks at 2 AM, you'll thank yourself.

Prevention: Designing for Non-Determinism

The deeper lesson here isn't about any specific tool. It's about acknowledging that AI orchestration is a fundamentally different programming paradigm. A few principles that have saved me headaches:

  • Never assume a single LLM call will succeed. Always have a retry strategy, even if it's just "try twice."
  • Validate outputs structurally before using them downstream. Don't just check for errors — check that the shape of the data is what you expect.
  • Keep prompts and orchestration logic separate. When you need to tweak a prompt, you shouldn't have to touch control flow code.
  • Treat context like a typed data pipeline. Know exactly what data each step receives and produces. If you can't draw it on a whiteboard, your pipeline is too complex.

Whether you end up using a dedicated DSL like Weft or building your own lightweight abstraction on top of Python, the key insight is the same: stop writing AI orchestration code like it's a regular web app. It isn't. The sooner your tools reflect that, the fewer 2 AM pages you'll get.

Worth Watching

The AI orchestration DSL space is still early. Projects like Weft are exploring what it means to make AI concepts first-class language primitives, and it's worth keeping an eye on how these approaches mature. If you're building anything with multi-step agent workflows, I'd recommend at least reading through Weft's repository to see what patterns they've identified — even if you don't adopt the language itself, the design decisions are informative.

Top comments (0)