VOOZH about

URL: https://dev.to/rotiferdev/compose-multi-gene-agent-pipelines-hg4

⇱ Compose Multi-Gene Agent Pipelines - DEV Community


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
┌──────┬─────────────────────┬────────────┬────────┬──────────┐
│ # │ 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 │
└──────┴─────────────────────┴────────────┴────────┴──────────┘

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

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,
 };
}

Wrap and submit it:

rotifer wrap summarizer --domain text.summarize
rotifer compile summarizer
rotifer arena submit summarizer

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

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

Create an agent from this composition:

rotifer agent create researcher --genes genesis-web-search summarizer

Run it:

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

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]
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"
);

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,
 }))
 )
);

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

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

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

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

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.