Skip to content

feat(workflows): per-item fan-out — dispatch N harnesses from one node - #124

Merged
senamakel merged 1 commit into
mainfrom
parallel-fanout
Jul 31, 2026
Merged

feat(workflows): per-item fan-out — dispatch N harnesses from one node#124
senamakel merged 1 commit into
mainfrom
parallel-fanout

Conversation

@senamakel

@senamakel senamakel commented Jul 31, 2026

Copy link
Copy Markdown
Member

What

Top of a three-repo chain that lets a workflow node multiply an array of input into N concurrent units of work — array in, array out, like a bounded Promise.all.

// one harness session per file, four at a time
{ "id": "review", "kind": "agent", "config": {
    "execution": "per_item",
    "concurrency": 4,
    "input_context": "=item",
    "prompt": "Review the single file named in the input context."
} }

Until now the only way to run harnesses in parallel was to hand-author N sibling nodes, so the width had to be known when the graph was written. Now it follows the data: split_outagent(concurrency: 4)merge works through whatever list reaches it and hands the merge one result per item.

examples/workflows/fan-out-per-file.json is the shape end to end.

The chain

Repo PR Contains
tinyflows tinyhumansai/tinyflows#26 The engine: concurrency / on_item_error on every mapping node, plus per_item on sub_workflow
openhuman tinyhumansai/openhuman#5302 Engine bump + a process-wide harness ceiling
medulla this openhuman bump + the run-scoped ceiling and authoring surface

Merge bottom-up. This PR's vendor/openhuman bump is only meaningful once the two below land.

Changes here

vendor/openhuman gitlink bump, which carries the engine.

workflows.maxParallelAgents (default 4) — new config, surfaced on workflow_host, threaded into CapabilitySettings and enforced as a semaphore in HarnessAgentRunner.

The engine bounds a single node's width. This bounds the run, and on this host that is the limit that matters: a fanned-out item is a whole coding session occupying a worker for the length of a task, so the useful ceiling is what the pool can serve, not what a graph asks for. An over-wide fan-out waits for a slot rather than failing — raising concurrency past the ceiling slows a run down, it does not break it.

Two details worth a reviewer's eye:

  • The agent runner and the LLM provider share one limiter. Both dispatch to the same worker pool, so leaving each with its own semaphore would make the run's real ceiling double what the operator configured. Built once in build_capabilities_inner and handed to both via with_limiter.
  • A zero ceiling falls back to the default. A zero-permit semaphore would leave every agent node awaiting a permit that can never arrive — a hang, which reads as the feature being broken rather than as a misconfiguration. Same reasoning as the existing zero-timeout fallback beside it.

Authoring surface. node_contracts.rs already delegates to the engine catalogue, so the new execution / concurrency / on_item_error fields reach the MCP tools and the copilot automatically. Added two host notes: that a per-item agent node is a full harness session per item (fan out over an already-narrowed list, not a raw fetch), and that the ceiling throttles rather than refuses.

Validation

  • cargo test — 1793 lib + all integration suites green
  • cargo clippy --all-targets -- -D warnings — clean (remaining warnings are pre-existing, in vendored openhuman)
  • cargo fmt --check — clean

New tests in src/sdk/src/workflows/run/tests/cases.rs run through the real engine and real capability seam with only the harness dispatch stubbed:

  • a per-item agent node dispatches one task per item and they genuinely overlap (a probe records peak in-flight, so a regression to a sequential loop fails rather than passing quietly);
  • the run ceiling wins over a graph asking for concurrency: "all", and the run still succeeds;
  • a per-item node with no concurrency stays strictly sequential — the back-compat guard.

Live per-item progress

A fanned-out node now renders as N live workers, not one long step.

The engine reports each item as it starts and settles (on_item_start / on_item_finish), and WorkflowRunObserver turns each into its own sub-agent — opened when the item actually starts, closed when it settles. The existing work pane renders it with no new drawing code, which is the whole reason this observer speaks the harness_work vocabulary rather than inventing one.

Two details:

  • The node-level sub-agent is suppressed for a fanned-out node. on_step_finish still fires for the node itself, and emitting both would show N+1 workers for N items. A node that runs once is silent on the item callbacks, so it keeps the single-sub-agent path unchanged.
  • on_step_start is now wired. The engine has had it since this pin; the module doc claiming otherwise was stale. Which node is active comes from the engine instead of being inferred from what has finished — an inference that was exact for a sequential graph but named an arbitrary one of several in a parallel fan-out.

Not in this PR

The TUI still discards the live fold (let (sink, _fold) = folding_sink() in event_loop/cmd_dispatch/workflows.rs and commands/workflow.rs), so these frames reach the daemon/orchestrator path but not the TUI'''s own workflow view. Keeping and rendering that fold is a self-contained follow-up.

Bumps vendor/openhuman to the per-item fan-out engine, so an agent node
can dispatch one harness task per input item concurrently — array in,
array out — instead of the author hand-writing N sibling nodes.

Adds workflows.maxParallelAgents (default 4). Here a fanned-out item is a
whole coding session, so the useful ceiling is what the worker pool can
serve, not what a graph asks for; an over-wide fan-out is throttled, never
refused. The agent runner and the LLM provider share one limiter, since
both dispatch to the same pool and separate semaphores would make the
real ceiling double what the operator configured.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

senamakel has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 46 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ee571721-e92e-4139-afd3-bddcf24ae86d

📥 Commits

Reviewing files that changed from the base of the PR and between f207e47 and 52d5406.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • examples/workflows/fan-out-per-file.json
  • src/sdk/src/config/types/orchestration.rs
  • src/sdk/src/flow_engine/caps/agent.rs
  • src/sdk/src/flow_engine/caps/mod.rs
  • src/sdk/src/flow_engine/mod.rs
  • src/sdk/src/flow_engine/settings.rs
  • src/sdk/src/flow_engine/settings_tests.rs
  • src/sdk/src/workflows/node_contracts.rs
  • src/sdk/src/workflows/ops/graph.rs
  • src/sdk/src/workflows/run/tests/cases.rs
  • vendor/openhuman

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 52d5406a6c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +576 to +581
#[derive(Default)]
pub(super) struct ConcurrencyProbe {
live: std::sync::atomic::AtomicUsize,
peak: std::sync::atomic::AtomicUsize,
calls: std::sync::atomic::AtomicUsize,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Split the oversized fan-out test module

Move the new fan-out fixtures and tests into a focused submodule: this addition grows cases.rs from 468 to approximately 606 nonblank, non-comment lines, exceeding the repository's mandatory 500-line ceiling for edited Rust files.

AGENTS.md reference: AGENTS.md:L58-L60

Useful? React with 👍 / 👎.

"httpAllowlist": config.http_allowlist,
"allowCode": config.allow_code,
"runTimeoutSecs": config.run_timeout_secs,
"maxParallelAgents": config.max_parallel_agents,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report the normalized parallel-agent ceiling

When an operator explicitly configures maxParallelAgents = 0, CapabilitySettings::from_config deliberately normalizes it to the default of 4, but workflow_host reports the raw value 0. Since the new agent contract tells workflow authors to consult this tool for the current ceiling, they receive a value that is not actually enforced; normalize this field before returning the host facts.

Useful? React with 👍 / 👎.

@senamakel
senamakel merged commit 1c0182f into main Jul 31, 2026
6 checks passed
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