# Agent ensembles arrive — scheduler-driven fan-out, Channel.await fan-in, and a clock for agents (RFC S, v0.25)

> Source: <https://loomcycle.dev/blog/agent-ensembles-arrive.html>
> Published: 2026-06-10 16:00:00+00:00

Experiment 5 ran end-to-end as a real agent ensemble: five RSS collectors (Hacker News, Wired, Engadget, Ars Technica, TechCrunch) fired in parallel by loomcycle's scheduler every five minutes, each on success pinging a fan-in channel via the scheduler's on_complete hook, a consolidator scheduled one minute after each crossing calling the new Channel.await {channels, mode: at_least, n: 5, wait_ms: 120s} combinator and waking when all five collectors had reported in (or the budget elapsed), URL-deduplication across 25 items, one Telegram digest as output. Three cycles then max_fires=3 auto-retired every collector schedule. Both static and fully-dynamic variants (every entity created at runtime via REST) validated on v0.25.3 with zero workarounds. The v0.25 "agentic-ensemble" release ships RFC S — three primitives the substrate had been missing for ensemble-shaped pipelines. P0: Context op=time gives every agent a wall-clock (closes F34 — pre-RFC-S Context had op=tools/doc/help/self but no op=time, so cycle bucketing required shelling to Bash date which dragged Bash into the collector's allowed_tools surface for no other reason). P1: Channel.await {channels, mode: any|all|at_least, n, wait_ms} is the fan-in combinator across N channels with race / rendezvous / count semantics and a hard deadline (closes F35 — pre-RFC-S Channel.subscribe was single-channel with no AT_LEAST_N / OR / AND combinator; a consolidator had to poll one channel in a loop and count distinct ids, burning max_iterations with no clean timeout; the only native AND-barrier (Agent.parallel_spawn) bypasses the scheduler by coupling N children to one parent run, exactly the coupling an ensemble is supposed to avoid). Symmetric Channel.broadcast publishes one payload to N channels in one call. P2: schedule max_fires retires a def after N successful fires (closes F36 — pre-RFC-S a ScheduledRun fired indefinitely until manually retired; a test ensemble had no way to express "this is bounded"; a forked def can lift the cap with explicit max_fires:0). Three follow-up fixes each surfaced by exp5 itself, all shipped within 24 hours. F37 (#422, v0.25.1, RFC T): the scheduler's on_complete: channel.publish hook was publishing under the run's user scope, ignoring a channel's declared scope: global, so a global fan-in channel's pings landed at user/exp5 and a global Channel.await reader saw 0 messages; the v0.25.0 workaround was scope: user, sort-of-worked because all five collectors shared user_id but a fan-in channel SHOULD be global. The fix is a ResolveChannelScope resolver wired into the scheduler so the publish hook uses the channel's declared scope, not the run's. F38 (#424, v0.25.2, RFC U): the fully-dynamic exp5 variant (every entity at runtime) failed with "unknown agent: exp5-collector" — the scheduler fire path was resolving agents from the yaml-static registry only, not the AgentDef substrate store; same shape as the F30 webhook-spawn fix from exp4. Fix: scheduled runs now stamp the def's tenant on spawn, agent resolves against both registries. F39 (#426, v0.25.3, RFC V): the fully-dynamic Telegram leg got HTTP 404 because the bot received the literal string ${LOOMCYCLE_TELEGRAM_BOT_TOKEN} as its API token — dynamic stdio MCPServerDef env values weren't being interpolated against the runtime's env at spawn time, only at YAML-load (which the dynamic path bypasses by design). Same envelope-of-references pattern hit in MCPServerDef header expansion back at v0.18. Fix: expand ${ENV} references in the dynamic stdio MCP def's env map at spawn time, using the same prefix-allowlisted resolver the rest of the substrate uses. The agent-ensemble term: a multi-agent system is not an ensemble. An ensemble needs primitives that survive the loss of any single agent's run. Each agent has its own run (independent run_id, independent lifecycle, billed and OTEL-spanned independently). Coordination flows through the substrate (channels, memory, scheduling, Channel.await), not through call stacks. The ensemble outlives any single agent. The ensemble can be authored declaratively (data, not control flow). Companion change: every prior experiment (exp1-exp4) now ships as a self-contained directory under loomcycle/examples — each carries its own loomcycle.yaml, run.sh launcher, .env.local.example template, and reproducible README, routing Anthropic-OAuth-primary with a DeepSeek fallback so an operator can clone and exercise the substrate in minutes. exp5 joins next. The substrate is now ensemble-shaped, not just agent-shaped.
