Repository navigation
refactor(rocm): split chat.rs into summaries, MCP bridge, intent and turn modules, and fold in four related refactors - #936
jussielo-amd wants to merge 25 commits into
Conversation
…e freeform planner A model-proposed `rocm serve` is refused for four reasons: a public bind, a CPU device, a non-loopback host, and serving that is not managed. The chat tool policy and the provider-assisted planner each enforce them in their own code today, and nothing ties the two together. Add one table of refused and accepted `serve` argvs in test_support and run it against both entry points. For each refusal it asserts the exact message and that nothing is proposed or planned (no ChatRocmCommandAction from chat, no StructuredRequestPlan from the planner); for each accepted argv it asserts the argv comes through unchanged and is held for approval. Also pinned: the order the rules fire in, and the places the two surfaces differ today (the actor named in the message, the CPU message's second half, the case of the serve word, and the --foreground/--managed pair that only the chat check ever sees because clap refuses it first). Tests only; they pass on the unmodified code. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…y check The provider-assisted planner re-implemented the four rules a model-proposed `rocm serve` must satisfy (no public bind, no CPU device, a loopback host, managed serving without --foreground), which the chat tool policy already enforced in validate_chat_rocm_command_safety. The planner's `install sdk` arm already called the shared function, so the two surfaces kept two copies of one policy that could drift apart. Move the four rules into tool_policy::validate_serve_command_safety, taking the name of the surface that proposed the call. The chat check calls it with "local assistant" and the planner's serve arm with "provider planner", so each refusal still says who was refused while the rules, their order and their wording live in one place. The planner's copy is deleted, and it still does not import chat. One user-visible difference: the planner's CPU refusal now ends "ROCm GPU execution is required", as the chat one does, where it used to end "rocm-cli requires ROCm GPU execution". Every other refusal reads as before. The serve word is still matched exactly by the planner (clap refuses `Serve` before the safety check runs) and case-insensitively by the chat check, unchanged. Tests: the shared table of unsafe and safe argvs from the previous commit now holds the unified wording, and deleting any one rule from the shared check fails both the chat and the planner suites. Four mock-lane scenarios (chat-13 to chat-16) drive a provider plan that breaks one rule each through the real binary and check that the CLI falls back to its own plan, runs nothing, and says why. docs/architecture.md, docs/llm-tool-use.md and docs/testing.md describe the shared check. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…gain model_registry_adapter_availability_note was widened to pub(crate) when the rocm model catalog renderer moved out of examine_report_text.rs, only because one test that stayed behind called it. That test, linux_only_engine_runtime_status_is_explicit_on_native_windows, checked two things: the runtime: label from engine_runtime_status_label and the note the verbose model listing prints. Split it along that line. The label assertion keeps its name and stays in examine_report_text.rs. The note assertion moves, unchanged and with the same cfg!(windows) branches, to model_catalog_text.rs as vllm_adapter_availability_note_is_explicit_on_native_windows, next to the renderer that prints the note. The helper goes back to private, and the module docs and docs/architecture.md describe the new test location. Tests and visibility only; no behaviour change. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
….rs into background_helper.rs services_text.rs imported background_helper_note from service_launch.rs while service_launch.rs imports the stop-result renderers from services_text.rs, so the two modules depended on each other. The note (HelperNote, helper_note_for, background_helper_note) and the liveness check it reads (background_helper_already_running) move verbatim into a new leaf module that imports nothing from the service modules. services_text.rs and uninstall_plan.rs read the note from there, and service_launch.rs reads background_helper_already_running from there for its autostart decision; spawning the helper stays in service_launch.rs. Pure move: no behaviour change. The tests that cover the moved items move with them and keep their names; the runtime_state and set_helper_facts fixtures become pub(crate) because the autostart and stop-output tests that stay in service_launch.rs use them too. docs/architecture.md gains the new module's entry and drops the claim that services_text.rs still reaches into service_launch.rs. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…importing service_control service_launch.rs imported run_internal_sandbox_tool from service_control.rs only to stop the server when the user presses Ctrl-C in an attached serve, while service_control.rs imports the spawn helpers and the endpoint-key guard from service_launch.rs, so the two modules depended on each other. run_internal_sandbox_tool cannot move down, because it calls the stop and restart paths that live in service_control.rs, so the dependency is cut at its single call site instead: run_attached_service takes the stop as a parameter and serve_cmd.rs, which already sits above both modules, passes the same run_internal_sandbox_tool(StopServer, service id, native fallback allowed) call. The stop still runs at the same point, with the same arguments, and its answer goes to finish_attached_stop unchanged, so behaviour is the same. docs/architecture.md and the service_control.rs header now describe the one-way edge. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…ry-note-helper-private Signed-off-by: Jussi Elo <jussi.elo@amd.com> # Conflicts: # docs/architecture.md
…-shared-serve-safety-check Signed-off-by: Jussi Elo <jussi.elo@amd.com> # Conflicts: # docs/architecture.md
…s into chat/summaries.rs The assistant's deterministic summaries of read-only tool output (examine, model recipes, ComfyUI status, services, port status, engine inventory), the row types and parsers behind them, chat_tool_value and managed_service_running_state move out of chat.rs into a new leaf child module, chat/summaries.rs. chat.rs stays a file and gains a chat/ directory, the same layout providers.rs and comfyui.rs use, so the include_str! of the assistant skill and every path that names chat.rs are unchanged. A pure move: no behaviour change. The tests that exercise only these items move with them and keep their names; the tests that drive a summary through the turn loop's follow-up decision stay in chat.rs. Visibility widens only as far as chat.rs needs (pub(super)), and chat re-exports deterministic_comfyui_tool_summary under cfg(test) for comfyui.rs's tests. docs/architecture.md and the module doc comments are updated to match. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
There was a problem hiding this comment.
🟢 Approval recommended
There are no active review issues blocking approval of the tool-output summary module extraction.
No open issues 🎉
What changed in this PR
Extracts the chat assistant’s deterministic tool-output summaries into a leaf module, reducing chat.rs responsibilities without changing user-visible behavior.
Changes:
- Moves summary builders, parsers, row types, and nine existing tests into
chat/summaries.rs. - Preserves turn-loop integration and the test-only ComfyUI summary access path.
- Updates architecture documentation and module comments to describe the split.
| File | Description |
|---|---|
docs/architecture.md |
Documents the new summaries module and revised ownership. |
apps/rocm/src/managed_service_status.rs |
Updates a comment naming the engine-label helper’s consumer. |
apps/rocm/src/chat/summaries.rs |
Houses the extracted summaries, parsers, and tests. |
apps/rocm/src/chat.rs |
Replaces moved code with imports and a test-only re-export. |
Review effort: Balanced
The `comfyui` dispatch function (the match over `ComfyuiCommand`, about 160 lines) moves from `apps/rocm/src/main.rs` into `apps/rocm/src/comfyui.rs` as `comfyui::run`, next to the code it calls, the way `remote::run` and `bootstrap::run` are named. It is a pure move: the body is unchanged apart from dropping the `comfyui::` prefix on calls that are now local, and the module reaches `record_cli_audit_event` through `crate::cli_audit` like its sibling command modules, so no new module cycle appears. The one arm of `dispatch()` now calls `comfyui::run(command)`. The clap command tree already lives in `cli.rs`, so the convention text that said the command enum and its dispatch function usually stay in `main.rs` (with `comfyui()` as the example) is updated in the same change: docs/architecture.md (module-organization convention, the `main.rs` entry and the `comfyui.rs` entry) and AGENTS.md section 6. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
The re-export of deterministic_comfyui_tool_summary in chat.rs was
#[cfg(test)] only, but its sole readers outside summaries.rs are three
comfyui.rs tests that are all #[cfg(unix)]. On Windows the test build
therefore had an unused import, which -D warnings turns into a compile
error ("unused import: self::summaries::deterministic_comfyui_tool_summary").
It did not show on Linux, and before the move the function was defined in
chat.rs itself, so there was no import to be unused.
Gate the re-export with #[cfg(all(test, unix))] to match its readers.
No behaviour change.
Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Only docs/architecture.md conflicted: the clap-tree move re-pointed several module entries from main.rs to cli.rs where this branch added the background_helper.rs entry and reworded the service_launch.rs one. Takes the re-pointed entries from main, keeps this branch's new and reworded text, and applies the cli.rs re-pointing to the reworded service_launch.rs entry. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…-main' into comb-structure Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…port-cycles' into comb-structure Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…per-private' into comb-structure docs/architecture.md conflicted on the managed_service_status.rs and model_catalog_text.rs entries (adjacent lines). Kept both sides: the managed_service_status.rs entry stays as it is on this branch (its helper is read by chat/summaries.rs), and the model_catalog_text.rs entry takes the private-helper wording from the merged branch. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…ve-safety-check' into comb-structure Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…idge.rs The MCP tool dispatch (run_internal_mcp_call), the in-process runner for the read-only rocm commands the assistant may ask for, the helpers that wrap a command's output as an MCP tool result, the path_exists and port_status tools, the services tool's result text, the install sdk dry-run helpers and the logs flag parsers were mixed into chat.rs with the turn loop. They move to a new leaf child, chat/mcp_bridge.rs, declared from chat.rs (chat.rs stays a file beside its chat/ directory, like summaries.rs). Pure move: 17 functions and the 18 tests that exercise only them, with their original names. run_internal_mcp_call stays reachable as chat::run_internal_mcp_call (main.rs, dash_seam.rs) through a re-export; run_chat_read_only_tool, run_chat_path_exists_tool and loopback_host_key are pub(super) because the turn loop and the command-preview property tests in chat.rs still call them. The re-exports of ChatRocmCommandAction and rocm_chat_tool_requested_args become cfg(test), since chat.rs no longer calls them in production code. The advised_commands.rs path pin for the "rocm command requires at least one argument" error follows its string, and the module docs and docs/architecture.md entries that named chat.rs as the caller of the moved items now name chat/mcp_bridge.rs. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…to chat/intent.rs The functions that decide from a user's latest message what it asks for (fallback_rocm_tool_call_for_prompt and the config, engine-list and services-list fallbacks behind it, the prompt_* phrase predicates, and the rocm install sdk approval request built from an install or setup request) were mixed into chat.rs with the turn loop. They move to a new leaf child, chat/intent.rs, declared from chat.rs. Pure move: 13 functions and the 16 tests that call them directly, with their original names. latest_user_chat_message moves with them because three of them call it; leaving it with the turn loop would have made the two modules call each other. Ten functions are pub(super) for the turn loop that stays in chat.rs; the other three are private. The advised_commands.rs path pins for "rocm install folder", "rocm installed at", "rocm installed" and "rocm please" follow their strings, and the prompt_text.rs doc comment and the docs/architecture.md entry that named chat.rs as the heuristics' home now name chat/intent.rs. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
What remained in chat.rs after the summaries, MCP bridge and prompt-intent
moves was the turn loop: the entry point that takes a prompt and renders the
assistant's reply, the system prompt, the transcript helpers and most of the
module's tests. It moves to a new child, chat/turn.rs, declared from chat.rs.
chat.rs keeps the entry points other modules call (read_piped_prompt,
render_chat_text, render_chat_prompt_text), the sampling-flag value parsers,
the re-exports and the tests that cross the seams.
Pure move: 33 production items, the 28 tests that exercise them and their one
shared helper, with their original names. The assistant skill is now included
from one directory deeper, so the include_str! path gains a ../ and still
resolves to skills/rocm-cli-assistant/SKILL.md. render_chat_prompt_result_with_progress,
ChatPromptResult and mcp_tool_result_text are pub(super);
rocm_chat_tool_system_prompt_for_host stays pub(crate) and is re-exported for
dash.rs. The command-preview property tests, which drive the tool-call guards
across the policy, the bridge and the turn loop, stay in chat.rs and import the
two helpers they use from the siblings.
The advised_commands.rs path pins for "rocm tools: {}" and "rocm command"
follow their strings to chat/turn.rs. AGENTS.md and the skill-evals comment in
ci.yml said main.rs embeds the assistant skill; they now name chat/turn.rs.
The docs/architecture.md chat.rs entry is rewritten for the file-plus-children
layout, a turn.rs entry is added, and the module docs that named chat.rs as the
caller of items now in a child name the child.
Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Signed-off-by: Jussi Elo <jussi.elo@amd.com>
juhovainio
left a comment
There was a problem hiding this comment.
Requesting changes on one item: the ticket-closing claim. The code itself looks clean.
Blocking
- The PR body says
Closes ROCMAI-765, but that ticket's scope also includes theproviders.rssplit (providers/tool_schema.rs,providers/local_http.rs,providers/wire.rs). This diff only touches a comment inproviders.rs, and the body doesn't mention those seams. Merging would auto-close 765 with that work undelivered. Either narrow it to "Part of ROCMAI-765" (chat seams only) and keep the ticket open, or say where the providers seams landed.
Non-blocking
- The
mcp_bridgetests importmcp_tool_result_textfromturn, so the "leaf below turn" claim holds only for non-test code (inline comment). pub(crate) mod testsinbackground_helper.rswidens visibility for test fixtures (inline comment).- ROCMAI-858 is listed as closed, but the clap-tree move and the byte-identical
--help/completions proof aren't in this diff. Please confirm the earlier PR covered them. - Bundling six tickets departs from the "one PR per ticket" note on 765/881/885. The body explains why, so this is a process call for the owner.
Checked
- The moves look behavior-identical: no function dropped, cfg gates unchanged, no tests removed, and
validate_serve_command_safetykeeps the old rule order. Docs and AGENTS.md updates are consistent. - CI is green except
Sphinx docs build (-W), which hit a GitHub API 403 rate limit and the job timeout. It needs a rerun. - Not run: I did not build or re-run the mutation checks.
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use crate::chat::turn::mcp_tool_result_text; |
There was a problem hiding this comment.
Test-only edge back to turn (mcp_tool_result_text). Production stays one-way, but the "leaf below turn" statement in the docs is only true for non-test code. Consider placing this helper in summaries.rs or mcp_bridge.rs so the edge and the pub(super) go away.
There was a problem hiding this comment.
Fixed in 4ee1d10. mcp_tool_result_text only reads the text out of an MCP tool result, which is the shape this module builds, and the turn loop calls it too, so it now lives in chat/mcp_bridge.rs and chat/turn.rs imports it from there (an edge that already existed). The body is unchanged and its two tests moved with it under the same names; the test-only import from turn is gone, so the "nothing below the turn loop calls back" statement now holds for tests as well. The function stays pub(super) because the turn loop calls it in production code. docs/architecture.md and the module doc comments are updated in the same commit, and the leaf-name list of cargo test -p rocm --bin rocm -- --list is identical before and after.
| } | ||
|
|
||
| #[cfg(test)] | ||
| pub(crate) mod tests { |
There was a problem hiding this comment.
pub(crate) mod tests widens visibility so service_launch.rs's tests can reuse runtime_state and set_helper_facts. It is test-gated, but consider test_support.rs for shared fixtures, consistent with ROCMAI-885's aim of not widening visibility for tests.
There was a problem hiding this comment.
Fixed in fd18665. runtime_state and set_helper_facts moved, bodies unchanged, to the crate-level test_support.rs, both test modules import them from there, and background_helper's mod tests is private again. No test was renamed (the leaf-name list is identical before and after) and docs/architecture.md now says where the fixtures live.
|
On the blocking point (the ticket-closing claim): ROCMAI-765's scope also includes the On ROCMAI-858: the clap-tree move landed in #889 (commit 88c344d). It was merged without naming the key so the ticket stayed open; nothing for it is in this diff and this PR only closes it. #889's description states that the move changes no clap attribute, help text, subcommand order, default or alias. On bundling six tickets: that is the author's decision to cut review overhead, recorded in the description. If you would rather split, say so and I will not merge it as is. On the Sphinx docs job: it hit the GitHub API rate limit and the job timeout, and will be re-run. The two inline suggestions are being handled separately. |
…the bridge has no edge back to the turn loop mcp_tool_result_text only reads the text out of an MCP tool result, the shape that chat/mcp_bridge.rs builds, and the turn loop is one of its callers. It sat in chat/turn.rs, so the bridge's own tests imported it from there: a test-only edge from the bridge back up to the turn loop. Production stayed one-way, but the "nothing below the turn loop calls back" statement in the module docs held only for non-test code. Move the function (body unchanged) and its two tests, with their names, to chat/mcp_bridge.rs. chat/turn.rs and the command-preview property tests in chat.rs now import it from there; the turn -> mcp_bridge edge already existed. The function stays pub(super) because the turn loop calls it. No behavior change. docs/architecture.md and the module doc comments are updated to match. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…ort and make background_helper's tests module private again background_helper.rs declared `pub(crate) mod tests` so the tests in service_launch.rs could reuse its runtime_state and set_helper_facts fixtures. That widened the module's visibility only for tests. Move the two fixtures (bodies unchanged) to the crate-level test_support module, where the other fixtures shared across modules live, and have both test modules import them from there. background_helper's `mod tests` is private again. No behavior change and no test renamed; docs/architecture.md is updated to match. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Signed-off-by: Jussi Elo <jussi.elo@amd.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The refactor spans approval/MCP execution and service lifecycle boundaries, requiring maintainer or CI validation of combined-tree Linux/Windows behavior and the untested interactive Ctrl-C shutdown path.
No open issues 🎉
Review effort: Balanced
juhovainio
left a comment
There was a problem hiding this comment.
I found nothing blocking in this PR.
I compared the moved code function by function against the old chat.rs: everything lands in exactly one of summaries, mcp_bridge, intent or turn (or stays in chat.rs), with only visibility, the include_str! path depth, test wrappers and re-export gating differing. The comfyui::run body and the background_helper.rs bodies are identical to what they replaced, and all new files are under the 1,500-line limit. CI is green apart from the consolidated report that was still pending when I checked.
Small non-blocking notes:
- The first line closes six ROCMAI keys, but ROCMAI-765's automation warning says its closing PR names no other ROCMAI key, so please confirm the multi-key close is fine. The ROCMAI-858 wording says nothing for it is in this diff, yet the comfyui dispatch move is.
- This bundles five logical changes, one of them behavioral (the planner's CPU-serve refusal wording). It is disclosed and covered by ROCMAI-881, but it makes the "pure move" framing of ROCMAI-765 not strictly true.
- One inline comment on the new e2e step's negative assertion.
The PR currently shows merge conflicts with main, so it will need a rebase before it can land.
| "the plan shown must be the CLI's own:\n{out}" | ||
| ); | ||
| assert!( | ||
| !out.contains("\nexecution\n"), |
There was a problem hiding this comment.
This negative check passes if the plan's execution heading ever changes shape. Please show chat-13..16 fail with the planner's serve arm reverted (and say so in the PR body), or assert a positive marker of "nothing ran".
There was a problem hiding this comment.
Fixed in 0925fb0, and I ran the scenarios to show it. The step now also asserts a positive marker of nothing having run: the plan renderer indents everything under its one request plan heading, so the output's only unindented line must be that heading, and anything a run printed of its own would add another. On a debug build with the e2e hooks (cargo test -p e2e-cucumber --test e2e -- -n "chat-1[3-6]", Linux under WSL, mock-backed) chat-13 to chat-16 pass; with the planner's serve arm refusal in validate_provider_planner_tool_call removed all four fail, each on the assistant's rocm serve ... plan being shown; and with an extra unindented line appended to the rendered plan, the new assertion fails all four, which the old absent-heading check would not have caught. Both mutations were undone by targeted edits and the four pass again. The PR body says so.
Signed-off-by: Jussi Elo <jussi.elo@amd.com>
…st that no execution heading appears The step behind the planner's refused-serve scenarios (the CPU, public bind, non-local host and foreground cases) checked that nothing ran only by asserting the output lacks an "execution" heading. That passes vacuously if the heading ever changes shape. Add the positive statement: the plan renderer indents everything under its single `request plan` heading, so the output's only unindented lines must be that heading, and anything a run printed of its own would break it. Checked against a debug build with the e2e hooks: with the planner's serve arm refusal removed all four scenarios fail, and with a differently named unindented line appended to the plan the new assertion fails all four where the old heading check would not. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
After merging main, run_attached_service's `paths` is already a reference at the point where the Ctrl-C stop is handed to `stop_service`, so the extra `&` trips clippy's needless_borrow. Pass it directly. No behavior change. Signed-off-by: Jussi Elo <jussi.elo@amd.com>
|
Thank you for the review and the function-by-function comparison. On the notes: (a) the first line does close six keys. The epic's rule was changed on 2026-11-06 to one PR per ticket with a |
Closes ROCMAI-765, ROCMAI-833, ROCMAI-858, ROCMAI-871, ROCMAI-881 and ROCMAI-885.
Where the other parts of the closed tickets landed. ROCMAI-765 covers the whole assistant cluster, not only the
chat/seams in this diff. The rest is already on main: the three leaf modules that broke the chat and freeform-planner cycle (rocm_argv.rs,prompt_text.rs,tool_policy.rs) came in #888, and the threeproviders/modules (tool_schema.rs,local_http.rs,wire.rs) came in #887, which is why this diff touchesproviders.rsonly in a comment. What this PR adds is the remaining fourchat/seams (summaries.rs,mcp_bridge.rs,intent.rs,turn.rs), so with it every seam of the ticket's scope table is in its destination. ROCMAI-858's main work, moving the clap command tree intocli.rs, is already on main (#889, commit 88c344d, merged without naming the key so that the ticket stayed open); #889's own description states the move changes no clap attribute, help text, subcommand order, default or alias. What this diff delivers for ROCMAI-858 is the rest of its scope: the comfyui dispatch fold-in (the same change as ROCMAI-833, in the "From #938" section below, somain.rsends under the limit) and the two convention texts the ticket names,docs/architecture.mdand the new-subsystems bullet in AGENTS.md section 6, which now say that a dispatch function lives in its domain module. ROCMAI-833 is delivered entirely by this diff, by the same comfyui dispatch move.Summary
apps/rocmhad several structural problems that were being fixed one pull request at a time, in files that overlap:chat.rsstill bundled the whole chat assistant in one file of roughly 4,500 lines,main.rsstill held the one command dispatch function (comfyui) that every sibling command keeps in its own module, the managed-service modules imported each other in production code so that none of the three could be read or moved without the other two, one test-only visibility widening leaked a helper out of its module, and the freeform planner kept its own copy of the four rules the chat tool policy applies to a proposedrocm serve. Reviewing and merging these separately meant the same conflict-prone files (chat.rs,main.rs,docs/architecture.md) were edited in a queue of near-identical rebases, so this PR carries all of them as one reviewable unit: the original split of the deterministic summaries out ofchat.rs, four earlier pull requests absorbed as ordinary merges (each keeps its own commits), and the three remaining seams of thechat.rssplit.This PR supersedes #938, #933, #930 and #921, which can be closed once this one merges. Only one change in it alters behaviour (the planner's serve-safety check, from #921); everything else is a pure move or a visibility or import-direction change with no observable effect, and the sections below say which is which.
chat.rsstays a file with achat/directory beside it (the layoutproviders.rsandcomfyui.rsalready use) instead of becomingchat/mod.rs. That keeps theinclude_str!depth change to one extra../, avoids retargeting every doc and path pin that nameschat.rsin a rename, and does not make the older open PRs that editchat.rsconflict on a rename. The child modules are declared fromchat.rs(mod summaries;,mod mcp_bridge;,mod intent;,mod turn;).From #938: the comfyui dispatch function leaves main.rs
rocm comfyuiwas the one command whose dispatch function (the match overComfyuiCommandthat renders status, logs and the models path and runs install, start and stop, recording an audit event for each) was still defined inmain.rs, while its siblings keep theirs in their own module. It moves tocomfyui.rsaspub(crate) fn run(command: Option<ComfyuiCommand>), next to the code it calls, as a pure move: the body is unchanged apart from import paths (thecomfyui::prefix on calls into the module is dropped because they are now local, andcomfyui.rsimportscrate::cli::ComfyuiCommandandcrate::cli_audit::record_cli_audit_eventdirectly, so there is no new edge back intomain.rs). The one arm ofdispatch()becomesSome(Command::Comfyui { command }) => comfyui::run(command). The module-organization text indocs/architecture.mdand the new-subsystems bullet in AGENTS.md section 6 said the dispatch function usually stays inmain.rsand namedcomfyui()as the example; both now say what the code does. With the clap tree already incli.rs, this takesmain.rsto 1,202 production lines, under the 1,500-line limit.Verification claims carried over: fmt, clippy,
cargo test -p rocm, an identical leaf-name test list before and after (1,516 tests and 8 ignored),cargo xtask manifest --check,cargo xtask check-crate-edges, lychee, and the leak scan. Its Windows cross-check was not run on its own branch; on this branch it was (see Verification).From #933: the managed-service modules stop importing each other
services_text.rsandservice_launch.rswere a pair (the stop-result text needed the background-helper note from the launcher, and the launcher needed the stop-result renderers), andservice_launch.rsandservice_control.rswere a second pair (the launcher tookrun_internal_sandbox_toolfrom the control module for the attached-serve Ctrl-C, and the control module took the endpoint-key guard and the spawn helpers from the launcher); together they closed a three-module loop. Two commits make the graph one-way:background_helper_already_running,HelperNote,helper_note_forandbackground_helper_note(with their five tests) move verbatim into a new leafbackground_helper.rs, andrun_attached_servicetakes the Ctrl-C stop as a finalimpl FnOnce(&AppPaths, &str) -> Result<serde_json::Value>argument, whichserve_cmd.rs(its only caller, already above both modules) passes as the samerun_internal_sandbox_toolcall that was made before. The two fixtures that the helper's tests share with the autostart and stop-output tests inservice_launch.rs(runtime_stateandset_helper_facts) live in the crate-leveltest_support.rs, sobackground_helper.rs'smod testsstays private. The first commit is a move; the second is not a move and cannot be one, becauserun_internal_sandbox_toolcalls the stop and restart paths inservice_control.rs. The dependency order is nowservices_text.rsbelowservice_launch.rsbelowservice_control.rs, withservices_cmd.rs,runtime_services.rsandserve_cmd.rsabove them.Verification claims carried over: fmt, clippy, tests with an identical leaf-name list (including the five tests that moved to
background_helper.rs), manifest and crate-edge checks, and the end-to-end scenarios that traverse the changed paths (service-stop-01toservice-stop-05,automations-11and the sevenservice-cleanupscenarios), all green on its own branch. The changed Ctrl-C call site has no unit or scenario coverage before or after because it needs an interactive terminal, and was checked by reading it against the removed call; that still holds.From #930: a test-only visibility widening is undone
When the
rocm modelcatalog renderer moved intomodel_catalog_text.rs,model_registry_adapter_availability_notewas widened from private topub(crate)for one reason: a test left behind inexamine_report_text.rs(linux_only_engine_runtime_status_is_explicit_on_native_windows) also called it. That test was two checks in one body, theruntime:label (which belongs toexamine_report_text.rs) and the adapter-availability note (which belongs to the catalog renderer). The note assertions move, byte for byte and inside the same runtimeif cfg!(windows)split, tomodel_catalog_text.rsasvllm_adapter_availability_note_is_explicit_on_native_windows, and the helper is private again. No assertion was deleted: one test became two, so the test count rises by one.Verification claims carried over: the moved test was shown to fail when its target is broken (on Linux by making the
Allowedarm return a note; the Windows branch by temporarily forcing the condition), and themodel_readinessimport claim indocs/architecture.mdwas checked and is unchanged.From #921: the planner uses the shared serve-safety check (the one behaviour change)
When the provider-assisted planner proposed a
rocm serve, it refused it with its own copy of the four rules the chat assistant's tool policy applies to the same command: no--allow-public-bind, no CPU device, a loopback--hostif one is given, and managed serving (--managed, never--foreground). A rule added or reworded on one side would silently not reach the other. The four rules now live once intool_policy::validate_serve_command_safety(args, actor);validate_chat_rocm_command_safetycalls it withlocal assistantand the planner'sservearm calls it withprovider planner, so each refusal still names who was refused. The planner still does not importchat. The first commit adds only tests (one table of unsafe and safeserveargvs shared by both suites, pinning the current behaviour on unmodified code); the second makes the planner call the shared check and changes the table to the unified wording.The one message that changes is the planner's CPU refusal, which now ends
ROCm GPU execution is requiredas the chat one does (it used to endrocm-cli requires ROCm GPU execution); it reaches the user in the planner's failure note. No argv that either surface refused is accepted now, and none that it accepted is refused. Known differences deliberately left alone (the case of theserveword,--allow-public-bind=trueand--foregroundwith--managed, which clap refuses before the planner's check) are pinned by tests rather than changed, and the chatlaunch_servertool's own partial check is untouched.Verification claims carried over: eleven new unit tests (six in
freeform_planner, five intool_policy), mutation checks on each of the four rules and on the planner's call into the shared check, and the mock-lane scenarios below.The three remaining seams of the chat.rs split
The three commits at the end move the last parts of
chat.rsinto child modules, in this order: the MCP bridge, the prompt-intent heuristics and the turn loop. Each is a pure move: the functions are cut and pasted, tests move with the code they test and keep their leaf names, visibility is widened only where a caller stayed behind, and the docs are updated in the same commit. The symbol lists were re-located by name on the merged tree. The tool-policy, argv and prompt-text seams had already moved in earlier merged pull requests, and the summaries seam is the first commit of this PR.run_internal_mcp_call,internal_mcp_arguments,internal_mcp_tool_success,internal_mcp_tool_result_from_command,run_rocm_capture_for_paths,run_rocm_command_for_paths,run_rocm_read_only_in_process,render_install_sdk_dry_run_for_args,internal_mcp_install_sdk_args,run_chat_read_only_tool,run_chat_path_exists_tool,run_chat_port_status_tool,render_services_tool_result_text,loopback_tcp_port_is_reachable,loopback_host_key,parse_optional_query,parse_optional_lines,mcp_tool_result_text; 20 testschat/mcp_bridge.rs:run_internal_mcp_callispub(crate)and re-exported fromchat.rsformain.rsanddash_seam.rs;run_chat_read_only_tool,loopback_host_keyandmcp_tool_result_textarepub(super)for the turn loop, andrun_chat_path_exists_toolispub(super)for the command-preview property tests inchat.rs, which also readmcp_tool_result_text; the rest is privatelatest_user_chat_message,fallback_rocm_tool_call_for_prompt,fallback_config_tool_call_for_prompt,fallback_engine_list_tool_call,fallback_services_list_tool_call,prompt_can_use_read_only_without_local_assistant,prompt_mentions_serving_engine_or_service,prompt_asks_engine_install_state,prompt_asks_running_or_status,install_sdk_without_prefix_chat_approval,install_sdk_chat_approval_for_prompt,prompt_requests_rocm_install_or_setup,prompt_requests_install_action; 16 testschat/intent.rs: ten functions arepub(super)for the turn loop, three privateChatPromptResult,ChatToolRunResult,render_chat_prompt_result(test-only),render_chat_prompt_result_with_progress,report_chat_tool_progress,ROCM_CHAT_TOOL_SYSTEM_PROMPT,ROCM_CHAT_TOOL_SKILL,rocm_chat_tool_system_prompt,rocm_chat_tool_system_prompt_for_host,append_chat_tool_resultsand the rest of the turn-loop, display-label andlocal_*helpers,mcp_tool_result_is_error; 26 tests and their one helperchat/turn.rs:render_chat_prompt_result_with_progressandChatPromptResultarepub(super);rocm_chat_tool_system_prompt_for_hoststayspub(crate)and is re-exported fromchat.rsfordash.rs; the rest is privatechat.rskeepsread_piped_prompt,piped_prompt_from_input,render_chat_text,plain_provider_auth_status,render_chat_prompt_text, the three clap value parsers, the re-exports, the two piped-prompt tests and the command-preview property tests (which drive the tool-call guards, the planner's guards and the rendered command lines across the policy, the bridge and the turn loop, so they belong to no single seam). Production line counts, counted as every line outside a top-level#[cfg(test)] mod ... { }block (blank lines and comments included):chat.rs241,chat/turn.rs873,chat/mcp_bridge.rs862,chat/intent.rs565,chat/summaries.rs631,main.rs1,202.Import edges of each child, production code only:
summariesreadsmanaged_service_status,providersandrocm_core::placeholder;intentreadsprompt_text,rocm_argv,tool_policy,freeform_planner(format_structured_tool_call) andproviders;mcp_bridgereadssummaries(managed_service_running_state),tool_policy,rocm_argv,freeform_planner,managed_service_status, the read-only renderers it dispatches to and the crate-root bridge builders;turnreadsintent,mcp_bridge,summaries,tool_policy,rocm_argv,freeform_planner,engine_runtime_select,managed_service_status,chat_host_factsandproviders. Nothing belowturncalls back into it, andfreeform_planner.rs,tool_policy.rs,rocm_argv.rsandprompt_text.rscontain nochat::path.mcp_tool_result_textformats an MCP tool result and the turn loop is one of its callers, so it lives inmcp_bridgeandturnreads it from there; with that, no module belowturnreaches back into it, in tests either.Other changes the moves required, all in the same commits as the code they follow: the
advised_commands.rspath pins follow their strings (rocm command requires at least one argumenttochat/mcp_bridge.rs;rocm install folder,rocm installed at,rocm installedandrocm pleasetochat/intent.rs;rocm tools: {}androcm commandtochat/turn.rs); the assistant skill is included from one directory deeper, so theinclude_str!path gains a../and still resolves toskills/rocm-cli-assistant/SKILL.md; the re-exports ofChatRocmCommandActionandrocm_chat_tool_requested_argsbecome#[cfg(test)]becausechat.rsno longer calls them in production code; AGENTS.md and theskill-evalscomment inci.ymlsaidmain.rsembeds the assistant skill, and now namechat/turn.rs; and a number of module doc comments anddocs/architecture.mdentries that namedchat.rsas the caller of an item now in a child name the child. Thedocs/architecture.mdentry forchat.rsis rewritten for the file-plus-children layout and each child has its own entry.Verification
Ran, on this branch (Linux unless stated): after each of the four merges,
cargo fmt --all --check,cargo clippy -p rocm --all-targets -- -D warningson Linux and onx86_64-pc-windows-msvc,cargo test -p rocm --no-fail-fast,cargo xtask manifest --checkandcargo xtask check-crate-edges, all green; the unit-test total grows 1,516 (after #938 and #933), 1,517 (after #930, which adds one test), 1,528 (after #921, which adds eleven), with 8 ignored throughout, andcargo test -p rocmpassed at each step (1,520 passed and 8 ignored in therocmbinary at the last merge). The MCP bridge commit got the same full set again (1,520 passed and 8 ignored, Windows clippy over allrocmtargets, test list identical to its parent). After each of the three seam commits, and for the intent and turn commits in place of the full set:cargo fmt --all --check,cargo clippy -p rocm --bins -- -D warnings,cargo check -p rocm --tests, the targeted tests for the moved module and its neighbours (111 for the intent step, 334 for the turn step, which includes theadvised_commandspin tests),cargo xtask manifest --check,cargo xtask check-crate-edges, and the leaf-name test list ofcargo test -p rocm --bin rocm -- --listand-- --list --ignoreddiffed against the commit's parent, which is empty for each of the three seam commits (1,528 tests and 8 ignored). On the final tree:cargo fmt --all --check,cargo clippy -p rocm --bins -- -D warningsandcargo clippy -p rocm --tests -- -D warningsforx86_64-pc-windows-msvc(the C toolchain the Windows dependencies need is stubbed, since clippy never links), lychee offline overdocs/architecture.mdandAGENTS.md,git merge-tree --write-tree origin/main HEAD(exit 0), a conflict-marker grep and the leak scan againstorigin/main(the only hits are existing identifiers such asrun_internal_mcp_callandinternal_mcp_tool_success, and the wordprivatein architecture entries).After the two review-fix commits at the end (moving
mcp_tool_result_textintochat/mcp_bridge.rs, and the background-helper fixtures intotest_support.rs):cargo fmt --all --check,cargo clippy --locked -p rocm --all-targets -- -D warningson Linux,cargo xtask manifest --check,cargo xtask check-crate-edges, lychee offline overdocs/architecture.md, the leaf-name list ofcargo test -p rocm --bin rocm -- --listdiffed against the previous head (empty, 1,528 tests) and the 128 tests matchingchat::,background_helperandservice_launch, all passing. Neither commit touches platform-gated code, so the Windows cross-check was not repeated for them. After merging the currentorigin/maininto this branch (conflicts inservice_launch.rsimports and onedocs/architecture.mdentry, resolved by keeping both sides), the same fmt, clippy, manifest, crate-edge and lychee checks pass, no test present before the merge is missing fromcargo test -p rocm --bin rocm -- --list(1,566 tests now, main having added 38), and the 143 tests matchingchat::,background_helperandservice_launchpass.After the latest merge of
origin/main(conflicts inservice_launch.rs,tool_policy.rsanddocs/architecture.md, resolved by keeping both sides):cargo fmt --all --check,cargo clippy --locked -p rocm --all-targets -- -D warnings,cargo clippy --locked -p e2e-cucumber --test e2e -- -D warnings, the targeted tests forchat::,background_helper,service_launchandfreeform,cargo xtask manifest --check,cargo xtask check-crate-edgesand lychee ondocs/architecture.md, and no test that existed before the merge is missing from the--listleaf names. The merge brought one new rule into the chat serve check: main now also refuses a--portthatrocm servewould refuse. That check stays in the chat policy only and runs after the shared four rules, so the planner is unchanged by it; the one visible difference is that aserveargv that is both missing--managedand carries an invalid port now reports the missing--managedfirst.Test plan for the planner scenarios, which a review asked to see fail when the planner's serve arm is reverted: I built a debug
rocmandrocmdwith--features rocm/e2e-test-hooksand rancargo test -p e2e-cucumber --test e2e -- -n "chat-1[3-6]"withROCM_CLI_BINARYandROCM_CLI_ROCMD_BINARYpointing at them (Linux under WSL, mock-backed, no GPU). All four pass on this branch; with the planner'sservearm refusal removed fromvalidate_provider_planner_tool_call, all four fail, each on the assistant'srocm serve ...plan being shown (the revert was undone by a targeted edit and the four pass again). The review also noted that the "nothing ran" check looked only for an absentexecutionheading, so the step now also asserts the output's only unindented line is therequest planheading, which fails if anything a run printed appears; with an extra unindented line appended to the plan, that assertion fails all four scenarios, and the heading check alone would not.Not run:
cargo test -p rocmas one full run after the three seam commits (only the targeted filters above, plus the full run after each merge),cargo xtask e2eand the cucumber scenarios (chat-13tochat-16ran green on this combined tree as described above;chat-10tochat-12and the service scenarios named under #933 ran green on those pull requests' own branches, not on this combined tree, and the comfyui scenarios were not run locally even there),cargo test --workspace,scripts/smoke_local.pyandcargo xtask coverage. CI carries those. No test was added or edited by the three seam commits, so there is no new test to show failing; the added tests are those of #930 and #921, whose mutation checks are described above.User-visible behaviour
Only #921 changes what a user can observe: the planner's refusal of a CPU-device
servenow endsROCm GPU execution is required. Scenarios:chat-13(chat-planning-assistant-cpu-serve-refused) is the one that depends on the change, andchat-14(public bind),chat-15(non-local host) andchat-16(unmanaged serve) pass before and after and hold the shared rules at the binary level; all four are intests/e2e-cucumber/features/chat.featureon the mock lane, and each asserts the plan the CLI printed, that no execution section appears, that the plan is the only output, and the exact refusal note. The existing planner scenarioschat-10tochat-12never reach theservearm. Everything else is internal: the comfyui dispatch move, the service-module import changes, the visibility and test move, and the three chat seams change no command output, exit code, file or engine selection, so no new scenario applies, and the existing chat, comfyui and service scenarios exercise the same paths unmodified.Notes for reviewers
Reading order, oldest first. The two commits of the first group came with this branch:
refactor(rocm): move the chat assistant's deterministic tool summaries into chat/summaries.rs(pure move) andfix(rocm): gate the chat summary re-export for comfyui's tests to unix(the test-only re-export is an unused import on Windows). Then #938:refactor(rocm): move the comfyui dispatch function out of main.rsand its merge. Then #933:move the background-helper note out of service_launch.rs into background_helper.rs(move),hand run_attached_service the Ctrl-C stop instead of importing service_control(the one signature change), a merge oforigin/maininto that branch and the merge into this one. Then #930:make the model registry adapter note helper private again, its own merge oforigin/main, and the merge here, whose only conflict was two adjacent entries indocs/architecture.md(kept both sides). Then #921:pin the serve safety rules of the chat tool policy and the freeform planner(tests only),make the freeform planner call the shared serve safety check(the behaviour change, with the scenarios and docs), its merge oforigin/mainand the merge here. Last, the three seam commits:move the chat assistant's MCP bridge into chat/mcp_bridge.rs,move the chat assistant's prompt-intent heuristics into chat/intent.rsandmove the chat turn loop and transcript into chat/turn.rs. Review-fix commits follow. The first two are pure moves:move mcp_tool_result_text into chat/mcp_bridge.rs so the bridge has no edge back to the turn loopandmove the background-helper test fixtures to test_support and make background_helper's tests module private again. The third,test(e2e): assert the refused planner plan is the only output, not just that no execution heading appears, tightens one e2e step and changes no product code; a merge oforigin/mainsits before it. The last commit,drop a needless borrow in the attached Ctrl-C stop call, is the one-character clippy fix that the latest merge made necessary. For the seam commits,git diff --color-movedshows the bodies as moved; what is not a move is the new module's header and imports, thepub(super)keywords, the import blocks left inchat.rsand the docs.The merges of #938, #933 and #921 had no conflicts; the one in #930 is described above. Docs touched by the open pull requests that this one does not supersede (
docs/architecture.md,docs/testing.md,docs/llm-tool-use.mdandchat.feature) can still conflict mechanically, as can any open pull request that editschat.rs: after this changechat.rsis a small file, so such a rebase has to re-apply the edit in the child module that now holds the code.