DEV Community

Rotifer Protocol
Rotifer Protocol

Posted on • Originally published at rotifer.dev

Compose Multi-Gene Agent Pipelines

Rotifer genes are powerful on their own, but the real magic happens when you compose them. The gene algebra — Seq, Par, Cond, Try, and Transform — lets you wire simple genes into complex agent pipelines that are type-safe, verifiable, and automatically optimizable.

In this tutorial, you'll build a real-world pipeline: search the web → summarize results → format output — then extend it with parallel execution, conditional branching, and error recovery.

Prerequisites

  • Rotifer CLI installed (npm i -g @rotifer/playground)
  • A project initialized (rotifer init my-pipeline && cd my-pipeline)
  • Familiarity with basic gene concepts (see Your First Gene in 5 Minutes)

Step 1: Understand the Building Blocks

Your project already includes genesis genes. Let's check what we have:

rotifer arena list
Enter fullscreen mode Exit fullscreen mode
┌──────┬─────────────────────┬────────────┬────────┬──────────┐
│ #    │ Name                │ Domain     │ F(g)   │ Fidelity │
├──────┼─────────────────────┼────────────┼────────┼──────────┤
│ 1    │ genesis-web-search  │ search.web │ 0.9200 │ Native   │
│ 2    │ genesis-search-lite │ search.web │ 0.7100 │ Native   │
│ 3    │ genesis-code-format │ code.format│ 0.8800 │ Native   │
└──────┴─────────────────────┴────────────┴────────┴──────────┘
Enter fullscreen mode Exit fullscreen mode

We'll use genesis-web-search as the first stage. Now we need a summarizer gene.

Step 2: Create a Summarizer Gene

mkdir -p genes/summarizer
Enter fullscreen mode Exit fullscreen mode

Write genes/summarizer/index.ts:

export async function express(input: {
  text: string;
  maxLength?: number;
}) {
  const maxLen = input.maxLength || 200;
  const sentences = input.text.split(/[.!?]+/).filter(Boolean);

  let summary = "";
  for (const sentence of sentences) {
    if ((summary + sentence).length > maxLen) break;
    summary += sentence.trim() + ". ";
  }

  return {
    summary: summary.trim(),
    wordCount: summary.split(/\s+/).length,
  };
}
Enter fullscreen mode Exit fullscreen mode

Wrap and submit it:

rotifer wrap summarizer --domain text.summarize
rotifer compile summarizer
rotifer arena submit summarizer
Enter fullscreen mode Exit fullscreen mode

Step 3: Your First Composition — Seq

Seq executes genes sequentially, piping each output as input to the next:

Seq(A, B, C) = A → B → C
Enter fullscreen mode Exit fullscreen mode

But wait — the output of genesis-web-search is { results: [...] }, while summarizer expects { text: string }. We need a Transform to bridge the schemas:

import { Seq, Transform } from "@rotifer/algebra";

const searchAndSummarize = Seq(
  "genesis-web-search",
  Transform((searchResult) => ({
    text: searchResult.results.map(r => r.snippet).join(" "),
    maxLength: 200,
  })),
  "summarizer"
);
Enter fullscreen mode Exit fullscreen mode

Create an agent from this composition:

rotifer agent create researcher --genes genesis-web-search summarizer
Enter fullscreen mode Exit fullscreen mode

Run it:

rotifer agent run researcher --input '{"query": "quantum computing 2026"}' --verbose
Enter fullscreen mode Exit fullscreen mode

The --verbose flag shows intermediate inputs and outputs at each stage.

Step 4: Add Parallel Execution — Par

What if you want to search multiple sources simultaneously? Par executes genes concurrently:

Par(A, B, C) = A ‖ B ‖ C → [resultA, resultB, resultC]
Enter fullscreen mode Exit fullscreen mode
import { Par, Seq, Transform } from "@rotifer/algebra";

const multiSourceSearch = Seq(
  Par(
    "genesis-web-search",
    "genesis-web-search-lite"
  ),
  Transform((results) => ({
    text: results.flat().map(r => r.results?.map(x => x.snippet)).flat().join(" "),
    maxLength: 300,
  })),
  "summarizer"
);
Enter fullscreen mode Exit fullscreen mode

Both searches run in parallel using the thread pool. The results are collected into an array, then merged by the Transform and fed to the summarizer.

Step 5: Add Conditional Branching — Cond

Cond lets you route execution based on a runtime predicate:

import { Cond, Seq, Transform } from "@rotifer/algebra";

const adaptivePipeline = Seq(
  "genesis-web-search",
  Cond(
    (result) => result.results.length > 10,
    Seq(
      Transform((r) => ({
        text: r.results.map(x => x.snippet).join(" "),
        maxLength: 500,
      })),
      "summarizer"
    ),
    Transform((r) => ({
      summary: r.results[0]?.snippet || "No results found.",
      wordCount: 0,
    }))
  )
);
Enter fullscreen mode Exit fullscreen mode

If the search returns more than 10 results, we summarize them. Otherwise, we just return the top snippet directly.

Step 6: Add Error Recovery — Try

Try attempts a primary gene and falls back to a secondary if it fails:

import { Try, Seq, Transform } from "@rotifer/algebra";

const resilientPipeline = Seq(
  Try(
    "genesis-web-search",
    "genesis-web-search-lite"
  ),
  Transform((r) => ({
    text: r.results.map(x => x.snippet).join(" "),
    maxLength: 200,
  })),
  "summarizer"
);
Enter fullscreen mode Exit fullscreen mode

If the primary search fails (network error, rate limit, etc.), execution automatically falls back to the lite version. No manual error handling needed.

Step 7: The Complete Pipeline

Combining all operators into one production-grade pipeline:

import { Seq, Par, Cond, Try, Transform } from "@rotifer/algebra";

const productionPipeline = Seq(
  // Stage 1: Resilient multi-source search
  Try(
    Par("genesis-web-search", "genesis-web-search-lite"),
    Par("genesis-web-search-lite")  // fallback: single source
  ),

  // Stage 2: Merge parallel results
  Transform((results) => ({
    text: results.flat()
      .map(r => r.results?.map(x => x.snippet))
      .flat()
      .filter(Boolean)
      .join(" "),
    resultCount: results.flat().reduce((n, r) => n + (r.results?.length || 0), 0),
  })),

  // Stage 3: Adaptive summarization
  Cond(
    (data) => data.resultCount > 5,
    Seq(
      Transform((d) => ({ text: d.text, maxLength: 400 })),
      "summarizer"
    ),
    Transform((d) => ({ summary: d.text.slice(0, 200), wordCount: 0 }))
  ),

  // Stage 4: Format output
  "genesis-code-format"
);
Enter fullscreen mode Exit fullscreen mode

Create and run the agent:

rotifer agent create research-bot \
  --genes genesis-web-search genesis-web-search-lite summarizer genesis-code-format

rotifer agent run research-bot \
  --input '{"query": "rotifer protocol gene evolution"}' \
  --verbose
Enter fullscreen mode Exit fullscreen mode

Type Safety

The composition algebra enforces schema compatibility at composition time. If you wire two incompatible genes, you get a clear error:

Error[E0032]: Type mismatch in Seq composition
  → gene 'web-search' output: { results: SearchResult[] }
  → gene 'summarizer' input:  { text: string, maxLength?: number }

  help: Add a Transform between the genes to reshape the data
Enter fullscreen mode Exit fullscreen mode

This catches data flow bugs before runtime.

Fitness of Composed Genes

Compositions have their own fitness scores:

Operator Fitness Formula
Seq min(F(components)) × latency_penalty
Par avg(F(components)) × parallelism_bonus
Try F(primary) × success_rate + F(fallback) × (1 - success_rate)

The Arena evaluates compositions as a whole, so there's selection pressure toward efficient structures.

What You've Learned

  • Seq chains genes into sequential pipelines
  • Par runs genes concurrently for speed and redundancy
  • Cond routes execution based on runtime conditions
  • Try provides automatic error recovery with fallbacks
  • Transform bridges schema mismatches between genes
  • Compositions are type-safe and have composite fitness scores

Deep Dive: See the full Composition Patterns guide for all operators, type constraints, and fitness formulas. For agent CLI commands, see the Agent Reference.

Top comments (0)