Skip to content

feat(core): combinators MVP — sequence, parallel, map#102

Merged
vreshch merged 1 commit intomasterfrom
feat/combinators
Apr 15, 2026
Merged

feat(core): combinators MVP — sequence, parallel, map#102
vreshch merged 1 commit intomasterfrom
feat/combinators

Conversation

@vreshch
Copy link
Copy Markdown
Contributor

@vreshch vreshch commented Apr 15, 2026

Summary

Phase 3 of `projects/work/specs/agent-composition.md`. Three pure functions over `ctx.run` — each combinator is itself an `Agent`, so they recurse without limit. Runs in-process (via `makeCtxRun`) or through the daemon (via `runtime.dispatch`) with no code changes.

```ts
import { sequence, parallel, map } from '@agentage/core';

// Chain
export default sequence(fetcher, enricher, summarizer);

// Fan-out with post-processing
export default sequence(
parallel([prReviewer, lintChecker, testRunner]),
reporter,
);

// Per-item
export default map(['repo-a', 'repo-b'], (r) => agentForRepo(r));
```

Combinators

`sequence(...steps: StepRef[]): Agent`

  • Runs steps in order.
  • Input threading: if a step's `result.output` is an object, it becomes the next step's `input.config`; otherwise the next step gets the original input unchanged.
  • Halt on first failure. The combinator's output is the failing step's output; siblings skipped.
  • Overall output = last step's output.

`parallel(steps: StepRef[]): Agent`

  • Runs concurrently with the same input.
  • Event interleaving via `mergeGenerators` — child events stream through the parent's generator in arrival order.
  • Returns a tuple of outputs in input order regardless of completion order.
  • Any child failure flips overall `success=false` but does NOT cancel siblings — caller decides via the result (per spec's "no auto-bubble" rule).

`map(items: T[], factory: (item, i) => StepRef): Agent`

  • Fan-out: one child per item, all concurrent.
  • Each child's input gets `config.item` (merged over parent `input.config`).
  • Output is array of child outputs in item order.

`StepRef = Agent | string`

String refs resolve via `ctx.run`'s registry — same rules as `ctx.run` itself.

Helper

`mergeGenerators<T, R>(gens): AsyncGenerator<T, R[], void>` — interleaves yielded values in arrival order, collects returns into `R[]` in input order. The single primitive that makes `parallel` and `map` work without losing events.

Test coverage (+16, total 123)

merge (3): multi-source yield + return collection, empty input, fast/slow arrival ordering

sequence (5): empty, object-output threaded as config, halt-on-first-failure (confirms step B is NOT called), string ref via registry, event forwarding across steps

parallel (4): empty, outputs in input order, partial failure (flips success=false, still collects all), event interleaving

map (4): empty items, factory per item, `{ item }` injection into `config`, partial failure

Verify

`npm run verify` — 123/123 tests, type-check, lint, format, build all clean.

Decisions locked (per spec)

  • Combinators are Agents. No separate Workflow type. Recursion is free.
  • Step refs: Agent or string. String resolves via registry; keeps the call-site clean.
  • Failure policy: halt for sequence, collect-all for parallel/map, caller inspects. Matches spec's "no auto-bubble" rule.
  • Output threading: object → config. Primitive outputs pass through opaquely (next step gets base input). Simple rule, predictable behavior.

Next

  • Phase 7 dogfood: convert `pr-reviewer` / `git-log-summarizer` to declare output schemas; build one real orchestrator using `sequence` + `parallel` (e.g. `daily-standup`).
  • Phase 4 control-flow (`iff`, `switchAgent`, `whileAgent`): same shape as these three, ~half a day. Skip until we hit a concrete need.

Phase 3. Pure functions over ctx.run — each combinator IS an Agent,
so they recurse without limit. Runs in-process via makeCtxRun or
through the daemon via runtime.dispatch transparently.

Combinators (src/combinators/index.ts)
- sequence(...steps) — run in order; if step's result.output is an
  object it becomes next step's input.config; halt on first failure
- parallel(steps) — concurrent; tuple of outputs in input order;
  any child failure flips overall success=false, does NOT cancel
  siblings (caller decides via result)
- map<T>(items, factory) — fan-out; each child's input.config gains
  { item }; output is array in item order

Helper (src/combinators/merge.ts)
- mergeGenerators<T, R>(gens): interleaves yielded T in arrival
  order, collects R[] returns in input order. Used by parallel/map
  to stream child events as they arrive while preserving result
  ordering.

Accept StepRef = Agent | string; string refs resolve via ctx.run's
registry (same rules as ctx.run itself).

Tests (+16, total 123)
- merge: multi-source yield + return collection, empty, fast/slow
  arrival ordering
- sequence: empty, object output threads as config, halt on failure,
  string ref via registry, event forwarding across steps
- parallel: empty, outputs in input order, partial failure, event
  interleaving
- map: empty items, factory per-item, { item } injection into
  config, partial failure

Exports: sequence, parallel, map, StepRef, MapFactory from
@agentage/core.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 15, 2026

🎉 PR Validation ✅ PASSED

Commit: 1fb6016129a31b994023b05854525da93d78c14e
Branch: feat/combinators

Checks:

  • ✅ Release guard (no version/changelog changes)
  • ✅ Dependencies installed
  • ✅ Type check passed
  • ✅ Linting passed
  • ✅ Format check passed
  • ✅ Tests + coverage passed
  • ✅ Build successful

Ready to merge!


🔗 View workflow run
⏰ Generated at: 2026-04-15T21:00:40.600Z

@vreshch vreshch marked this pull request as ready for review April 15, 2026 21:11
@vreshch vreshch merged commit dd4bac3 into master Apr 15, 2026
2 checks passed
@vreshch vreshch deleted the feat/combinators branch April 15, 2026 21:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant