Three Branches, One Recipe

How three features became one step

Recipes needed three hard capabilities at once

Run steps in parallel, loop until they converge, and pick the right model per step. Each started as its own experiment.

And each one really shipped — here is the proof.

Run in parallel
Bounded parallelism over items
Loop until convergence
While-loop convergence body
Pick the right model
Per-step model choice

All three shipped — and their branches are still on origin

Every capability landed on origin/main, and each one's original feature branch is still visible on origin.

But look at when they landed — one after another.

feat/rate-limiting-and-bounded-parallelism
tip 2da1a3c · 2026-01-07
feat/convergence-loops-and-expression-evaluator
tip bec8c1e · 2026-02-06
feat/model-class-routing
tip fa7e5d6 · 2026-02-27

Landing one after another risked three bolt-ons

The three commits arrived in sequence — each built alone, each able to ignore the others.

Sequential, independent landings are exactly how features fragment.

1
997b85e · 2026-01-07
bounded parallelism + recipe-level rate limiting
2
53089b1 · 2026-02-07
convergence loops + while-loop execution
3
b965b95 · 2026-02-28
model_role support (PR #41)

Instead of three add-ons, all three live in one Step

The three capabilities converged into a single Step model and a single recipe executor — not three separate bolt-ons.

One shared model is what makes composition possible.

939
lines in models.py — the single Step model
3054
lines in executor.py — the single recipe executor

parallel is only valid on a foreach step

Bounded parallelism isn't a standalone knob. Validation requires foreach, so it means bounded parallel iteration over items — enforced by asyncio.Semaphore.

One capability binds cleanly into the shared Step.

# models.py L512 — validation if self.parallel and not self.foreach: error("parallel requires foreach") # executor.py L2363 semaphore = asyncio.Semaphore(max_concurrent)

model_role stays disciplined by validation

Per-step model choice is agent-step-only and mutually exclusive with the legacy provider/model fields — so a second capability slots in without colliding.

Two capabilities, one Step, no collision.

# models.py L292 model_role: str | None = None # models.py L568-582 — validation # agent steps only # cannot combine with provider / model

One step carries all three capabilities at once

A single foreach step can carry bounded parallel, a while_condition convergence body, and a model_role — the three capabilities COMPOSE.

Three branches resolve into one composable declarative step.

# one declarative step, three capabilities foreach: items parallel: 3 while_condition: not converged break_when: done model_role: reviewer

Composition kept paying off after the merge

A later rename to model_role_resolver (PR #75, 2026-05-13) deleted duplicated resolver logic — tightening the single shared design.

Merging features into one model makes ongoing cleanup and reuse possible.

45300d8 · 2026-05-13
rename routing_matrix → model_role_resolver (PR #75)
Duplicated resolver logic removed
step resolution simplifies to resolver.resolve(step.model_role)
Sources

Research MethodologySHIPPED on origin/main

Repository: microsoft/amplifier-bundle-recipes

Data as of: 2026-05-20 — origin/main HEAD b6ee26d (PR #76)

Verification: every claim independently re-derived from git commands run against origin/main at b6ee26d after a fresh fetch.

Commands run:

Gaps: the literal token model_role appears 0 times in docs/*.md; the user-facing docs describe this as class-based routing via provider_preferences. No example YAML uses model_role.

Primary contributor: Brian Krabach (99 of last 120 commits; author of all three feature commits and the rename).

More Amplifier Stories