feat(runtime): plan_batch + path-overlap planner for tool dispatch (PR-2/6)#3129
Merged
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Deploying librefang-docs with
|
| Latest commit: |
fa6171c
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://e58e7bc6.librefang-docs-web.pages.dev |
| Branch Preview URL: | https://feat-parallel-tool-calls-m2.librefang-docs-web.pages.dev |
…R-2/6)
New `parallel_dispatch` module — the planner the agent loop's batch
dispatcher will consume in PR-4/5. Still passive: nothing calls
`plan_batch()` yet, so runtime behaviour is unchanged.
Module contents (`crates/librefang-runtime/src/parallel_dispatch.rs`):
- `ParallelPlan { groups: Vec<Vec<usize>> }` — output shape: each
group's calls may run concurrently, groups themselves run in
order. Concatenating groups yields 0..N (order preservation, a
hard contract for Anthropic Messages API tool_result alignment).
Helpers: `call_count`, `is_fully_sequential`.
- `NormalizedPath` enum: `Real(PathBuf)` for filesystem scopes
(component-aware prefix overlap) and `Virtual(String)` for tool
families whose scope is logical (`skill::<name>`).
- `lexical_clean` — folds `.` and `..` without touching the FS,
since target files for `WriteScoped` calls may not yet exist.
- `paths_overlap` — `Real` paths overlap when one is a Path-level
prefix of the other; `Virtual` overlap iff string-equal; mixed
kinds never overlap.
- `extract_scope_path` — projects a `NormalizedPath` from a
WriteScoped call's input. Handles `file_write` / `file_edit` /
`apply_patch` (`path` field) and `skill_evolve_*` (virtual
`skill::<name>` from the `name` field).
- `plan_batch` — contiguous-bucket scheduler:
* empty / single → trivial.
* any Exclusive → every call gets its own group (whole batch
serialises).
* walk in original order, accumulating into a `current` bucket.
`WriteShared` flushes + owns its bucket (acts as a barrier so
later ReadOnly calls cannot be reordered before it). `ReadOnly`
appends. `WriteScoped` checks path overlap with the bucket's
reserved scopes and flushes on conflict; an unprojectable scope
is treated as a per-call bucket.
The barrier semantics are the key correctness property: if the model
emits `read /a, shell, read /c`, dispatching `[read /a, read /c] +
[shell]` would let the second read observe pre-shell state — exactly
what the model didn't intend. Contiguous bucketing keeps `read /c`
strictly after the shell.
Companion fix: `tool_classifier::classify_by_name` now matches the
`skill_evolve_` prefix and projects to `Mutating`. Without this PR-1
left those tools at `Unknown` -> `WriteShared`, defeating the virtual
scope projection that `plan_batch` relies on.
Tests: 14 new in `parallel_dispatch::tests`:
- order preservation helper `assert_plan_covers_all`
- empty / single-call shapes
- `three_reads_one_group`
- `read_plus_write_disjoint_one_group`
- `two_writes_sibling_files_one_group`
- `parent_child_overlap_splits`
- `component_aware_prefix_does_not_split` (`/a/b` vs `/a/bc`)
- `trailing_slash_and_parent_dir_normalise`
- `shell_exec_isolated_in_its_bucket` (barrier semantics)
- `interactive_forces_full_serial`
- `skill_evolve_virtual_scope` (same skill splits, different skills
share a group)
- `write_scoped_without_path_is_isolated`
- `real_vs_virtual_paths_do_not_overlap`
- `lexical_clean_handles_dot_and_double_dot`
Plus 1 new in `tool_classifier::tests`:
- `skill_evolve_prefix_is_mutating` (covers all 6 concrete names +
the bare `skill_evolve` non-match case)
Verification:
- cargo test -p librefang-runtime --lib -- parallel_dispatch
tool_classifier: 39 ok (24 baseline + 14 new dispatch + 1 new
classifier)
- cargo check --workspace --lib: clean
- cargo clippy -p librefang-runtime --all-targets -- -D warnings:
clean
Stack: this PR is based on feat/parallel-tool-calls-m1 (PR #3127).
PR-3 will add `RuntimeConfig.parallel_tools` (feature flag); PR-4
wires `plan_batch` into `agent_loop.rs:3410`.
houko
force-pushed
the
feat/parallel-tool-calls-m2
branch
from
April 25, 2026 13:47
e11b4e6 to
d427b72
Compare
This was referenced Apr 25, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Builds on PR-1 (
feat/parallel-tool-calls-m1). Adds the planner the agent-loop dispatcher will consume in PR-4/5. Still passive —plan_batchhas no callers yet, runtime behaviour is unchanged.What's new
crates/librefang-runtime/src/parallel_dispatch.rs(new):ParallelPlan { groups: Vec<Vec<usize>> }— output shape: each group's calls may run concurrently, groups themselves run in order. Concatenating groups yields0..N(order preservation — a hard contract for Anthropic's Messages APItool_resultalignment). Helpers:call_count,is_fully_sequential.NormalizedPath—Real(PathBuf)for filesystem scopes (component-aware prefix overlap) andVirtual(String)for logical scopes (skill::<name>).lexical_clean— folds./..without touching the filesystem (target files forWriteScopedcalls may not yet exist, sostd::fs::canonicalizeis unsafe).paths_overlap—Realoverlap iff component-prefix;Virtualoverlap iff string-equal; mixed kinds never overlap.extract_scope_path— projects aNormalizedPathfrom aWriteScopedcall's input. Handlesfile_write/file_edit/apply_patch(pathfield) andskill_evolve_*(virtualskill::<name>from thenamefield).plan_batch— contiguous-bucket scheduler:Exclusivecall → every call gets its own group (whole batch serialises).currentbucket.WriteSharedflushes + owns its bucket (acts as a barrier — laterReadOnlycalls cannot be reordered before it).ReadOnlyappends.WriteScopedchecks path overlap with reserved scopes; conflict triggers a flush. Calls without a projectable scope sit alone.The barrier semantics matter
If the model emits
read /a, shell, read /c, dispatching[read /a, read /c] + [shell]would let the second read observe pre-shell state — exactly what the model didn't intend. Contiguous bucketing keepsread /cstrictly after the shell. This is why I rewrote the greedy O(N²) algorithm from the design plan as a contiguous-pass; it produced visibly wrong groups for that case (caught by theshell_exec_isolated_in_its_buckettest).Companion classifier fix
tool_classifier::classify_by_namenow matches theskill_evolve_prefix and projects toMutating. Without this, PR-1 leftskill_evolve_*tools atUnknown→WriteShared, defeating the virtual scope projection thatplan_batchrelies on. The fix is one match arm + 1 test (skill_evolve_prefix_is_mutatingcovering all 6 concrete names plus theskill_evolvenon-match case).Tests
14 new in
parallel_dispatch::tests:empty_batch_produces_empty_plansingle_call_is_one_groupthree_reads_one_groupread_plus_write_disjoint_one_grouptwo_writes_sibling_files_one_group/a/xand/a/yparalleliseparent_child_overlap_splits/a/band/a/b/csplitcomponent_aware_prefix_does_not_split/a/band/a/bcparallelise (component-aware)trailing_slash_and_parent_dir_normalise/a/b/and/a/b/c/..resolve to overlapshell_exec_isolated_in_its_bucketinteractive_forces_full_serialskill_evolve_virtual_scopewrite_scoped_without_path_is_isolatedreal_vs_virtual_paths_do_not_overlaplexical_clean_handles_dot_and_double_dot/a/./b→/a/b,/a/b/../c→/a/cPlus the
assert_plan_covers_allhelper that assertsgroups.iter().flatten()is a permutation of0..N— used in every plan-shape test as a structural invariant.1 new in
tool_classifier::tests:skill_evolve_prefix_is_mutatingTest plan
cargo test -p librefang-runtime --lib -- parallel_dispatch tool_classifier— 39 ok (24 baseline + 14 new dispatch + 1 new classifier)cargo check --workspace --lib— cleancargo clippy -p librefang-runtime --all-targets -- -D warnings— cleanStack
Base: PR-1 (
feat/parallel-tool-calls-m1, PR #3127).PR-3 will add
RuntimeConfig.parallel_tools(feature flag); PR-4 wiresplan_batchintoagent_loop.rs:3410(non-streaming); PR-5 wires the streaming path; PR-6 fixes the MCPannotationsbug + addsmcp_readonly_allowlist.See
.plans/parallel-tool-calls.mdfor full design.