Skip to content

refactor(rocm): break the chat and freeform-planner import cycle with three leaf modules - #888

Merged
jussielo-amd merged 7 commits into
ROCm:mainfrom
jussielo-amd:refactor-assistant-break-chat-planner-cycle
Nov 5, 2026
Merged

jussielo-amd merged 7 commits into
ROCm:mainfrom
jussielo-amd:refactor-assistant-break-chat-planner-cycle

Conversation

@jussielo-amd

Copy link
Copy Markdown
Collaborator

Summary

chat.rs and freeform_planner.rs in apps/rocm imported each other: the planner called back into chat:: for a handful of small helpers while chat.rs calls the planner to build its plans. Mutual imports like that make it hard to split either file, because every helper either side shares has to stay reachable from both. This PR removes the cycle without changing behaviour, by moving the three groups of leaf helpers the planner reached into chat into modules of their own. It is part of the ongoing split of the assistant code; the larger splits of chat.rs and providers.rs follow separately.

After the last commit freeform_planner.rs has no chat import in production code and the only edge left between the two modules is chat -> freeform_planner (plan_freeform_request_with_context).

What moved

Every item moved verbatim. Visibility widened only where the item is now called across a module boundary.

Symbols From To
chat_cli_arg_value, chat_cli_arg_value_checked, chat_cli_has_flag, ensure_flag chat.rs rocm_argv.rs (new, no crate:: dependencies)
format_structured_tool_call, quote_tool_arg, serve_args_request_cpu_device, device_policy_is_cpu_mode freeform_planner.rs rocm_argv.rs
any_substring, requested_install_prefix_from_prompt, requested_bare_install_prefix_from_prompt, clean_requested_install_prefix, looks_like_user_path, requested_therock_build_date_from_prompt, requested_therock_version_from_prompt, looks_like_therock_runtime_version chat.rs prompt_text.rs (new, depends only on therock::RuntimeVersionSelector)
infer_engine_from_request and its test infer_engine_from_request_does_not_match_aim_as_a_substring freeform_planner.rs prompt_text.rs
ChatRocmCommandAction, ChatToolApprovalRequest, chat_tool_approval_request, validate_chat_tool_call, the validate_chat_*_tool_call family, validate_required_chat_string, validate_optional_chat_integer, chat_rocm_command_action, normalized_chat_rocm_command_args, chat_install_driver_action, chat_rocm_command_action_from_args, canonicalize_chat_rocm_command, validate_chat_rocm_command_safety, chat_install_prefix_is_system, chat_tool_call_is_read_only, rocm_chat_tool_requested_args, rocm_chat_tool_requested_command, push_optional_json_cli_arg, json_string, plus the 37 chat tests that exercise only these items chat.rs tool_policy.rs (new)

Widened to pub(crate): chat_cli_arg_value_checked and device_policy_is_cpu_mode (rocm_argv), requested_therock_version_from_prompt (prompt_text), and normalized_chat_rocm_command_args, json_string, chat_install_prefix_is_system, chat_rocm_command_action, rocm_chat_tool_requested_command (tool_policy).

Every other module keeps its path through re-exports: chat::ensure_flag (freeform_execution.rs), chat::any_substring (freeform_answer.rs), chat::validate_chat_tool_call, chat::chat_tool_call_is_read_only and chat::chat_tool_approval_request (dash_seam.rs), freeform_planner::format_structured_tool_call (comfyui.rs, freeform_execution.rs, providers.rs tests), and the chat:: paths that driver_plan.rs and system_packages.rs read from their test modules (a re-export gated on cfg(test) where chat.rs itself does not call the item). The one deliberate exception is that freeform_planner.rs now calls the new modules directly, because importing through chat would keep the cycle. freeform_planner.rs no longer re-exports serve_args_request_cpu_device: its only outside reader was validate_chat_rocm_command_safety, which moved.

Production line counts: chat.rs 4111 -> 3009, freeform_planner.rs 1293 -> 1242, new rocm_argv.rs 123, prompt_text.rs 182, tool_policy.rs 963.

Import edges between the modules involved (production code only, use crate:: and crate::<module> paths, before this PR from origin/main and after it):

module before after
chat automations, chat_host_facts, comfyui, driver_install, engine_runtime_select, freeform_planner, managed_service_status, providers, runtimes_cmd, service_launch, services_text, therock, uninstall_plan, update_cmd the same, minus service_launch, plus prompt_text, rocm_argv, tool_policy
freeform_planner chat, driver_install, providers, service_launch driver_install, prompt_text, providers, rocm_argv, service_launch, tool_policy
rocm_argv (new) none
prompt_text (new) therock
tool_policy (new) driver_install, providers, rocm_argv, service_launch, services_text, therock

Before: chat <-> freeform_planner, the only two-way edge touching any of these modules. After: no two-way edge between any of them and any other module (chat -> freeform_planner is one-way). A grep for chat:: or use crate::chat in freeform_planner.rs finds nothing.

Verification

Ran:

  • cargo fmt --all --check and cargo clippy -p rocm -p xtask --all-targets -- -D warnings: clean (clippy on rocm re-run after the final rebase)
  • cargo test -p rocm --no-fail-fast before the final rebase: all pass (the rocm binary: 1458 passed, 7 ignored; the other rocm targets pass too)
  • the leaf-name test list diff, cargo test -p rocm --bin rocm -- --list and -- --list --ignored, against origin/main, after each commit: empty (1465 tests, 7 ignored, none lost or added; module paths change, leaf names do not). After the final rebase onto a newer origin/main the diff shows only additions, the 10 property tests that main gained in the meantime (gpu_selection_proptests.rs, providers/sse_emitter_proptests.rs, test_support.rs); none is missing
  • cargo test -p rocm advised_commands: pass (also after the final rebase) (the pins on apps/rocm/src/chat.rs still match their strings, none of which moved)
  • cargo xtask manifest --check, cargo xtask check-crate-edges, cargo test -p xtask ascii: pass
  • cargo test -p e2e-cucumber --lib: 196 passed
  • leak scan of the diff against origin/main: only identifiers such as run_internal_mcp_call and the existing words "private" in docs matched; nothing internal

Not run: cargo xtask e2e and scripts/smoke_local.py (left to CI); the Windows cross-check (cargo clippy --target x86_64-pc-windows-msvc) cannot run on the Linux build host because aws-lc-sys needs a Windows C toolchain, so the Windows lane is the check. No item gated on cfg(windows) moved; the one cfg(unix) test that moved, the_assistant_cannot_name_a_system_install_folder_by_respelling_it, keeps its gate. No new tests were added and no test body changed, so there was nothing new to break-test.

User-visible behaviour

None: a pure move of code and its tests, with no change to any command, output, exit code, file or engine selection. No Gherkin scenario applies. The existing chat and freeform scenarios in tests/e2e-cucumber cover these paths and are unmodified; CI runs them.

Notes for reviewers

Read the commits in order; each of the first three compiles and passes clippy and the tests on its own, and the fourth changes only comments and docs.

  1. rocm_argv.rs: the argv helpers (leaf, no crate:: dependencies). The planner's chat::chat_cli_* calls switch to it.
  2. prompt_text.rs: the phrase, install-folder, TheRock date/version and engine readers. The planner's chat::any_substring and chat::requested_* calls switch to it.
  3. tool_policy.rs: the tool-call validation and approval policy and its tests. The planner's one remaining chat:: call switches to it. This is the large diff, but nearly all of it is lines deleted from chat.rs and added verbatim to the new file.
  4. A docs-only commit that corrects who calls the moved helpers in the module docs and docs/architecture.md (prompt_text is used by chat.rs's prompt-intent code and the planner, not by tool_policy) and completes the dependency list of tool_policy.

git diff --color-moved=dimmed-zebra makes the moves easy to confirm.

Deviations worth a look: freeform_planner.rs stops re-exporting serve_args_request_cpu_device (no reader left); the small rocm_command_call test helper is duplicated into the new test module because a chat test that stays also uses it; chat_tool_call_requests_therock_install_without_prefix and rocm_command_args_install_sdk_without_prefix stay in chat.rs (they are intent heuristics, not policy). Module docs that named chat.rs as the reader of moved code (services_text.rs, driver_install.rs, quoting.rs) and the test-name comment in main.rs are updated; docs/architecture.md describes the three new modules and the chat.rs and freeform_planner.rs entries.

Open PRs that touch the same code, for merge order: #563 rewrites chat_tool_approval_request, validate_chat_tool_call and normalized_chat_rocm_command_args and edits quote_tool_arg (and its mention in quoting.rs), so it conflicts with commits 1 and 3. #243 edits validate_chat_launch_server_tool_call and validate_chat_rocm_command_safety and adds tests that call validate_chat_tool_call, so it conflicts with commit 3. #299, #678 and #724 touch chat.rs only outside the moved code (chat_tool_call_display_label, run_internal_mcp_call, render_services_tool_result_text, the imports) and #818 only a test, so at most the use block of chat.rs needs a manual merge. #243, #299, #563, #678, #723, #724, #725, #817 and others also touch docs/architecture.md.

`chat.rs` imports from `freeform_planner.rs` and `freeform_planner.rs` calls back into `chat::`, so the two modules form an import cycle. Part of it is a handful of small argv helpers that live in one module and are used by the other.

Move them into a new leaf module, `rocm_argv.rs`, with no `crate::` dependencies: `chat_cli_arg_value`, `chat_cli_arg_value_checked`, `chat_cli_has_flag` and `ensure_flag` from `chat.rs`, and `format_structured_tool_call`, `quote_tool_arg`, `serve_args_request_cpu_device` and `device_policy_is_cpu_mode` from `freeform_planner.rs`. The bodies are unchanged; `chat_cli_arg_value_checked` and `device_policy_is_cpu_mode` become `pub(crate)` because they are now called across a module boundary.

`freeform_planner.rs` calls `rocm_argv` directly instead of `chat::chat_cli_*`. Every other caller keeps its path: `chat.rs` re-exports `ensure_flag` (read by `freeform_execution.rs`) and `freeform_planner.rs` re-exports `format_structured_tool_call` and `serve_args_request_cpu_device`.

Pure move: no behaviour change. docs/architecture.md and the one comment that named `quote_tool_arg`'s old file are updated.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
The planner and the chat tool policy both read the same facts out of a natural-language request: which phrases it contains, which install folder, TheRock build date or TheRock version it names, and which serving engine it asks for. They lived in `chat.rs` (and one in `freeform_planner.rs`), so `freeform_planner.rs` had to import `chat` for them, which is half of the import cycle between the two modules.

Move them into a new leaf module, `prompt_text.rs`, which depends only on `therock::RuntimeVersionSelector`: `any_substring`, `requested_install_prefix_from_prompt`, `requested_bare_install_prefix_from_prompt`, `clean_requested_install_prefix`, `looks_like_user_path`, `requested_therock_build_date_from_prompt`, `requested_therock_version_from_prompt` and `looks_like_therock_runtime_version` from `chat.rs`, and `infer_engine_from_request` from `freeform_planner.rs`. `infer_engine_from_request_does_not_match_aim_as_a_substring` moves with its function. The bodies are unchanged; `requested_therock_version_from_prompt` becomes `pub(crate)` because `chat.rs` now calls it across a module boundary.

`freeform_planner.rs` calls `prompt_text` directly instead of `chat::any_substring` and `chat::requested_*`. `chat.rs` re-exports `any_substring`, which `freeform_answer.rs` still reaches as `chat::any_substring`.

Pure move: no behaviour change. docs/architecture.md is updated.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
`freeform_planner.rs` still imported `chat` for one thing after the argv and prompt-text helpers moved out: `chat::validate_chat_rocm_command_safety`, the check that refuses a CPU or public-bind `serve` and a system install folder. That was the last edge back from the planner to `chat.rs`, so the two modules still imported each other.

Move the tool-call validation and approval policy into a new module, `tool_policy.rs`: `ChatRocmCommandAction`, `ChatToolApprovalRequest`, `chat_tool_approval_request`, `validate_chat_tool_call` and the `validate_chat_*_tool_call` family, `validate_required_chat_string`, `validate_optional_chat_integer`, `chat_rocm_command_action`, `normalized_chat_rocm_command_args`, `chat_install_driver_action`, `chat_rocm_command_action_from_args`, `canonicalize_chat_rocm_command`, `validate_chat_rocm_command_safety`, `chat_install_prefix_is_system`, `chat_tool_call_is_read_only`, `rocm_chat_tool_requested_args`, `rocm_chat_tool_requested_command`, `push_optional_json_cli_arg` and `json_string`. The 37 tests that exercise only these items move with them; their names are unchanged. The bodies are unchanged. `normalized_chat_rocm_command_args`, `json_string`, `chat_install_prefix_is_system`, `chat_rocm_command_action` and `rocm_chat_tool_requested_command` become `pub(crate)` because `chat.rs` calls them across the new module boundary.

The module calls nothing in `chat.rs` or `freeform_planner.rs`; it uses `rocm_argv`, `providers::ChatToolCall`, `service_launch::is_loopback_host`, `services_text::validate_service_id`, `therock` and `driver_install::host_install_driver_only_reports`, none of which imports `chat`. `freeform_planner.rs` now calls it directly, so the only edge left between the two is `chat` -> `freeform_planner`. `chat.rs` re-exports what other modules reach as `chat::<item>`: `validate_chat_tool_call`, `chat_tool_call_is_read_only` and `chat_tool_approval_request` for `dash_seam.rs`, and the items that only other modules' tests read (`driver_plan.rs`, `system_packages.rs`).

`freeform_planner.rs` no longer re-exports `serve_args_request_cpu_device`: its only outside reader was `chat::validate_chat_rocm_command_safety`, which moved. The small `rocm_command_call` test helper is duplicated into the new test module because a staying chat test uses it too.

Pure move: no behaviour change. docs/architecture.md, the module docs that named `chat.rs` as the reader of moved code, and the test-name comment in `main.rs` are updated.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
The module doc of `prompt_text.rs` and the `docs/architecture.md` entries for it and for `rocm_argv.rs` said the helpers were shared by the chat tool policy. `tool_policy.rs` does not use `prompt_text`; the readers are called by the prompt-intent heuristics in `chat.rs` and by the freeform planner (and `freeform_answer.rs` reaches `any_substring` through `chat`). `rocm_argv` is called by `chat.rs`, `tool_policy.rs` and the planner.

Also add `therock::RuntimeVersionSelector` to the dependencies listed for `tool_policy.rs`, and say that `canonicalize_chat_rocm_command` is called by the argv classifier rather than by `validate_chat_rocm_command_safety`.

Docs and comments only.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

No active review issues are identified for the shared-helper extraction.

No open issues 🎉

What changed in this PR

This refactor removes the mutual dependency between chat and freeform planning by extracting shared helpers into three modules, preparing the assistant code for further splits without intended behavior changes.

Changes:

  • Move argument handling, prompt-text extraction, and tool-call policy into dedicated modules.
  • Update the planner to use those modules directly, preserving existing caller paths through re-exports.
  • Relocate existing tests and update architecture documentation and source comments.
File Description
docs/​architecture.md Documents the extracted modules and revised dependencies.
crates/​rocm-core/​src/​quoting.rs Updates the comment identifying the tool-argument quoter’s location.
apps/​rocm/​src/​tool_policy.rs Houses relocated tool validation, approval policy, and tests.
apps/​rocm/​src/​services_text.rs Updates comments describing validation callers.
apps/​rocm/​src/​rocm_argv.rs Collects shared argument parsing, flag handling, and command formatting.
apps/​rocm/​src/​prompt_text.rs Collects prompt extraction and engine inference helpers with the existing inference test.
apps/​rocm/​src/​main.rs Registers the three modules and updates a test-reference comment.
apps/​rocm/​src/​freeform_planner.rs Replaces chat imports with direct helper-module calls and preserves the formatting re-export.
apps/​rocm/​src/​driver_install.rs Updates the comment naming the policy helper’s caller.

Review effort: Balanced

@jussielo-amd
jussielo-amd marked this pull request as ready for review November 4, 2026 16:12
@jussielo-amd
jussielo-amd requested a review from a team as a code owner November 4, 2026 16:12
@jussielo-amd
jussielo-amd requested a review from r0x0r November 4, 2026 16:12

@nowycondro nowycondro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Automated review (Claude Code) · 2daed56

This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.

Review — no blocking findings

Full review of the whole change (fetched the PR head read-only, never checked out; diffed against the merge-base with main). No ticket named for this change — it's framed as one incremental step in an ongoing, undirected split of the assistant code (the larger chat.rs/providers.rs splits "follow separately"); no specific tracking ticket is claimed to be closed by this PR.

Blocking

None

Non-blocking

None

Decisions for the author

None

Positive signals

  • Every moved function and test body was verified byte-for-byte identical between its origin in chat.rs/freeform_planner.rs at the base commit and its new location in rocm_argv.rs, prompt_text.rs, and tool_policy.rs — including the safety-critical validate_chat_rocm_command_safety and chat_install_prefix_is_system (no-CPU-fallback and system-install-folder refusal logic), confirmed unchanged apart from the documented pub(crate) visibility widenings.
  • The stated goal is independently verifiable and true: freeform_planner.rs has zero chat::/use crate::chat references at HEAD, and none of the three new modules' own use blocks import chat or freeform_planner, so the cycle is genuinely broken rather than just hidden behind a re-export.
  • Every old external call path (chat::ensure_flag, chat::any_substring, chat::validate_chat_tool_call, chat::chat_tool_call_is_read_only, chat::chat_tool_approval_request, freeform_planner::format_structured_tool_call, plus the #[cfg(test)]-gated paths driver_plan.rs and system_packages.rs use) resolves correctly through the new re-export shims; a crate-wide grep found no stale reference to a moved symbol.
  • Test count is exactly preserved: 137 #[test] occurrences in chat.rs+freeform_planner.rs at base, 137 across the five touched files at head (73+26+0+1+37) — matching the PR's claim that tests were moved, not dropped or duplicated.
  • Module doc comments and docs/architecture.md updates were checked against the actual use statements and call sites in the new/changed files and are accurate.

Deployment notes

  • Pure internal refactor; no user-visible behavior, no migration, no new dependency.
  • The author's own description lists nine open PRs (#563, #243, #299, #678, #723, #724, #725, #817, #818) that touch the same files and will need manual merge-order handling; GitHub currently reports mergeable_state: blocked (most likely pending required review/status, not a real conflict against main as of the reviewed commit).
  • At review time, four hardware-runner E2E check runs (W7900, MI300X, Strix Halo Ubuntu nightly/release) were still queued rather than completed; every check that had completed was green (build-and-test, clippy, CodeQL, Windows build/test, Commit signatures+sign-off, Coverage ratcheted, E2E consolidated report and the completed E2E hardware lanes).

What this covered

Full review of all 10 changed files between base 3944380 and head 2daed56 (apps/rocm/src/{chat.rs,freeform_planner.rs,freeform_answer.rs,rocm_argv.rs,prompt_text.rs,tool_policy.rs,main.rs,driver_install.rs,services_text.rs}, crates/rocm-core/src/quoting.rs, docs/architecture.md), each read completely alongside the moved symbols' full bodies at both commits, plus the call sites in dash_seam.rs, comfyui.rs, freeform_execution.rs, system_packages.rs, driver_plan.rs, and providers.rs that reach the moved items. Agent-instruction adherence was checked against the repo's AGENTS.md (architecture guardrails, coverage-floor mechanism, doc-link convention); nothing in this diff violates it. History was checked via git log on chat.rs/freeform_planner.rs: recent activity is the same ongoing module-splitting series this PR belongs to, with no sign of an unrelated bugfix being silently reverted by the move.

Did not run: cargo check/cargo test (no Rust toolchain in the review environment) — compile and test-pass correctness was instead verified by manual, exhaustive cross-referencing of every import, visibility, and call site, and by reading the PR's own green CI (clippy, build-and-test on Linux/Windows, the full affected-crate test suite, and the e2e suite all passed on the reviewed commit). The "prior-changes" pass (do past review comments on these files apply here) found nothing to check — no human review threads existed on this PR beyond an automated bot's "no issues" comment. Not reconciled against any human discussion thread, since none existed at review time.

🤖 Generated with Claude Code

@juhovainio juhovainio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I read this as a pure move, and it holds up. Comparing removed and added lines (ignoring whitespace), the code in the three new modules matches what left chat.rs and freeform_planner.rs. The only differences are the nine visibility widenings, the new use blocks, re-wrapped signatures, call-site prefixes, and the duplicated test helper you disclosed. Test counts balance (37 moved out of chat.rs, 1 out of freeform_planner.rs), and I found no leaks of internal names.

Nothing blocking. Three small things, left inline: a wrong caller attribution in docs/architecture.md (serve, not install sdk), a comment in chat.rs that disagrees with the re-export gating, and a ragged comment re-wrap in prompt_text.rs.

Two things for you to weigh: the chat_cli_* names now live in rocm_argv, and nothing enforces the "planner never imports chat" rule at module level (the crate-edges check is crate-level only). Also, merge order against #563 and #243 needs sequencing.

One thing I couldn't check from the diff: that none of tool_policy's dependencies (providers, therock) import chat.

CI: the two Strix Halo Ubuntu E2E lanes (nightly and release) failed in the "Run E2E tests on Strix Halo" step and I couldn't get the logs. The diff changes no behaviour and the other E2E lanes passed, so it looks like a lane flake, but a re-run would confirm. W7900 and MI300X were still pending. This is a diff-level read, not a runtime check.

Comment thread docs/architecture.md Outdated
- [`freeform_answer.rs`](../apps/rocm/src/freeform_answer.rs) — mechanical relocation, no owned types: the answers a freeform request can get without running a command — `render_freeform_read_only_answer`, which answers a single approval-free `examine`, `comfyui status` or `comfyui logs` action, and `render_freeform_examine_answer`, the plain-language ROCm status it prints for `examine`. `run_freeform` in [`main.rs`](../apps/rocm/src/main.rs) is the only caller of the first, which is `pub(crate)` and re-imported there; the second is private. Besides `chat.rs` (`any_substring`, re-exported there from [`prompt_text.rs`](../apps/rocm/src/prompt_text.rs)), it depends on `comfyui.rs`, `runtimes_cmd.rs` and `therock.rs`, none of which calls back into it. No unit test calls either function directly.
- [`freeform_execution.rs`](../apps/rocm/src/freeform_execution.rs) — mechanical relocation, dispatch-adjacent, owns a `pub(crate)` type (as of Phase 6e's follow-up, ROCMAI-726; see the Phase 6a variant note above for the owned-types pattern). Holds the freeform natural-language planner's plan-*execution*/consent half that [`freeform_planner.rs`](../apps/rocm/src/freeform_planner.rs) below deliberately left out: `validate_freeform_execution_action()`, `prepare_freeform_execution()`, `apply_freeform_execution_consent()`, `render_freeform_execution_header()`, and the `pub(crate)` `FreeformExecution` struct that `prepare_freeform_execution` returns and `render_freeform_execution_header` consumes. `main.rs`'s `execute_freeform_next_action()` and `run_freeform()` stay in `main.rs` — both are dispatch-level, not part of deciding whether a plan's next action is safe to execute — and `execute_freeform_next_action()` calls in via the unqualified re-import `use crate::freeform_execution::{prepare_freeform_execution, render_freeform_execution_header};` (`run_freeform()` only calls `execute_freeform_next_action()`, never this module directly), matching [`driver_install.rs`](../apps/rocm/src/driver_install.rs)'s dispatch-adjacent form rather than `freeform_planner.rs`'s own qualified-path form. `prepare_freeform_execution` takes the already-built `plan` by value rather than rebuilding it from `request`/`paths`/`config` (ROCMAI-725's single-build guarantee, inherited unchanged from `main.rs`); `prepare_freeform_execution`/`render_freeform_execution_header` are `pub(crate)` for that cross-module call; `FreeformExecution` was already `pub(crate)` (read directly by `main.rs` and this module's own tests); `validate_freeform_execution_action`/`apply_freeform_execution_consent` have no caller outside this module and stay module-private, unchanged from before the move. The cluster's six regression tests moved out of `main.rs`'s shared `mod tests` into this module's own `mod tests`, inheriting the qualified `freeform_planner::` path they already used to reach `freeform_planner.rs`'s items as `main.rs`'s former tests — this module reaches in from outside using the same qualified path `main.rs` and `providers.rs`'s tests use (unlike `chat.rs`/`comfyui.rs`, which import it unqualified).
- [`freeform_planner.rs`](../apps/rocm/src/freeform_planner.rs) — full domain extraction (Phase 6e, ROCMAI-87). Owns the freeform natural-language planner cluster's types (`StructuredRequestPlan`, `PlannerIntent`, `PlannedToolCall`, `FreeformPlanAction`, `ProviderPlannerResponse`, `ProviderPlannerToolCall`, all module-private except `FreeformPlanAction`, `StructuredRequestPlan` and `PlannedToolCall`, which are `pub(crate)`), the deterministic plan builder (`build_freeform_plan*`), the provider-assisted path (`resolve_freeform_plan_with_provider`, `build_provider_planner_prompt`, `provider_planner_response_to_plan`, `validate_provider_planner_tool_call`), and plan rendering (`render_structured_request_plan`). [`main.rs`](../apps/rocm/src/main.rs) calls `build_freeform_plan_with_context`/`render_structured_request_plan` directly for the dispatch/review flow ([`examine_report_text.rs`](../apps/rocm/src/examine_report_text.rs)'s `render_launch_summary` → `host_default_engine` is another call into this module, unrelated to planning: bare `rocm`'s non-interactive launch summary), while [`freeform_execution.rs`](../apps/rocm/src/freeform_execution.rs) above calls `plan_next_action`/`format_structured_tool_call` for the execution flow and reads `FreeformPlanAction`/`StructuredRequestPlan` by type for its execution-validation functions — those live there, not here, because they decide whether a plan's next action is safe to *execute*, a dispatch-adjacent concern distinct from *building* the plan; `main.rs`'s `run_freeform` builds a plan exactly once via `build_freeform_plan_with_context` and threads that same value through `execute_freeform_next_action` to `prepare_freeform_execution` rather than rebuilding it (ROCMAI-725: building a plan can call a configured cloud `planner_provider`, which is not guaranteed deterministic, so a second independent build could silently disagree with the first). [`chat.rs`](../apps/rocm/src/chat.rs)'s `natural_language_plan` MCP tool gets the same single-build guarantee from `plan_freeform_request_with_context`, which builds once and returns both the rendered text and the next action; `chat.rs` also calls `format_structured_tool_call` directly, like [`comfyui.rs`](../apps/rocm/src/comfyui.rs) does, and its `validate_chat_rocm_command_safety` reads `serve_args_request_cpu_device` directly as a no-CPU-fallback safety check on provider-proposed `serve` args. `host_default_engine`, `build_freeform_plan_with_context`, `render_structured_request_plan`, `plan_next_action`, `plan_freeform_request_with_context`, `provider_planner_response_to_plan`, `format_structured_tool_call`, and `serve_args_request_cpu_device` are `pub(crate)` for these reasons, as are `infer_engine_from_request` (read from `chat.rs`) and `plan_driver_request` (read from [`driver_plan.rs`](../apps/rocm/src/driver_plan.rs)'s test module); `freeform_plan_next_action` and `render_freeform_plan` are test-only (`#[cfg(test)]`) and `FreeformPlanAction` was already `pub(crate)` before the split. `parse_freeform_invocation`/`should_treat_as_freeform` (deciding whether an invocation is freeform at all) stay in `main.rs`, a dispatch-level concern outside both this module and `freeform_execution.rs`. The cluster's regression tests live in `freeform_planner.rs`'s own `mod tests`; the handful of `main.rs` tests that exercise the invocation classifier/clap-suggestion-avoidance stay in `main.rs`'s shared `mod tests`, since neither is specific to this module or to `freeform_execution.rs` (and neither touches `freeform_planner::` at all), and the ROCMAI-725 regression tests proving the single-build guarantee live in `providers.rs`'s `mod tests` (reusing its mock local-provider server), reaching this module's items through the qualified `freeform_planner::` path like any other external caller.
- [`freeform_planner.rs`](../apps/rocm/src/freeform_planner.rs) — full domain extraction (Phase 6e, ROCMAI-87). Owns the freeform natural-language planner cluster's types (`StructuredRequestPlan`, `PlannerIntent`, `PlannedToolCall`, `FreeformPlanAction`, `ProviderPlannerResponse`, `ProviderPlannerToolCall`, all module-private except `FreeformPlanAction`, `StructuredRequestPlan` and `PlannedToolCall`, which are `pub(crate)`), the deterministic plan builder (`build_freeform_plan*`), the provider-assisted path (`resolve_freeform_plan_with_provider`, `build_provider_planner_prompt`, `provider_planner_response_to_plan`, `validate_provider_planner_tool_call`), and plan rendering (`render_structured_request_plan`). [`main.rs`](../apps/rocm/src/main.rs) calls `build_freeform_plan_with_context`/`render_structured_request_plan` directly for the dispatch/review flow ([`examine_report_text.rs`](../apps/rocm/src/examine_report_text.rs)'s `render_launch_summary` → `host_default_engine` is another call into this module, unrelated to planning: bare `rocm`'s non-interactive launch summary), while [`freeform_execution.rs`](../apps/rocm/src/freeform_execution.rs) above calls `plan_next_action`/`format_structured_tool_call` for the execution flow and reads `FreeformPlanAction`/`StructuredRequestPlan` by type for its execution-validation functions — those live there, not here, because they decide whether a plan's next action is safe to *execute*, a dispatch-adjacent concern distinct from *building* the plan; `main.rs`'s `run_freeform` builds a plan exactly once via `build_freeform_plan_with_context` and threads that same value through `execute_freeform_next_action` to `prepare_freeform_execution` rather than rebuilding it (ROCMAI-725: building a plan can call a configured cloud `planner_provider`, which is not guaranteed deterministic, so a second independent build could silently disagree with the first). [`chat.rs`](../apps/rocm/src/chat.rs)'s `natural_language_plan` MCP tool gets the same single-build guarantee from `plan_freeform_request_with_context`, which builds once and returns both the rendered text and the next action; `chat.rs` also calls `format_structured_tool_call` directly, like [`comfyui.rs`](../apps/rocm/src/comfyui.rs) does, while [`tool_policy.rs`](../apps/rocm/src/tool_policy.rs)'s `validate_chat_rocm_command_safety` reads `serve_args_request_cpu_device` from [`rocm_argv.rs`](../apps/rocm/src/rocm_argv.rs) directly as a no-CPU-fallback safety check on provider-proposed `serve` args; `validate_provider_planner_tool_call` calls that same function for an `install sdk` call. `host_default_engine`, `build_freeform_plan_with_context`, `render_structured_request_plan`, `plan_next_action`, `plan_freeform_request_with_context` and `provider_planner_response_to_plan` are `pub(crate)` for these reasons, as is `plan_driver_request` (read from [`driver_plan.rs`](../apps/rocm/src/driver_plan.rs)'s test module); `format_structured_tool_call` lives in [`rocm_argv.rs`](../apps/rocm/src/rocm_argv.rs) and is re-exported here with `pub(crate) use`, so the callers above keep their `freeform_planner::` path, while the planner's own argv lookups call `rocm_argv`, its phrase and engine readers call [`prompt_text.rs`](../apps/rocm/src/prompt_text.rs) and its safety check calls `tool_policy.rs`, directly: the planner no longer imports `chat`, and only `chat` imports the planner (`plan_freeform_request_with_context`); `freeform_plan_next_action` and `render_freeform_plan` are test-only (`#[cfg(test)]`) and `FreeformPlanAction` was already `pub(crate)` before the split. `parse_freeform_invocation`/`should_treat_as_freeform` (deciding whether an invocation is freeform at all) stay in `main.rs`, a dispatch-level concern outside both this module and `freeform_execution.rs`. The cluster's regression tests live in `freeform_planner.rs`'s own `mod tests`; the handful of `main.rs` tests that exercise the invocation classifier/clap-suggestion-avoidance stay in `main.rs`'s shared `mod tests`, since neither is specific to this module or to `freeform_execution.rs` (and neither touches `freeform_planner::` at all), and the ROCMAI-725 regression tests proving the single-build guarantee live in `providers.rs`'s `mod tests` (reusing its mock local-provider server), reaching this module's items through the qualified `freeform_planner::` path like any other external caller.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The sentence saying validate_provider_planner_tool_call calls that same function "for an install sdk call" is inaccurate: the install sdk branch calls tool_policy::validate_chat_rocm_command_safety, and serve_args_request_cpu_device is called in the serve branch. Please say serve, or name the function the install-sdk path really calls.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Correct, that was wrong: validate_provider_planner_tool_call calls serve_args_request_cpu_device in its own serve branch and hands an install sdk call to tool_policy::validate_chat_rocm_command_safety. The sentence now says exactly that (5339c70).

Comment thread apps/rocm/src/prompt_text.rs Outdated
//!
//! A leaf module: it depends only on `therock::RuntimeVersionSelector`, and
//! neither `chat.rs` nor `freeform_planner.rs` has to import the other for
//! these helpers. `tool_policy.rs` does not use it. They were relocated verbatim from `chat.rs` and

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: the header comment re-wrap left a ragged line ("...tool_policy.rs does not use it. They were relocated verbatim from chat.rs and"); please reflow.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Reflowed the header comment to normal wrapping, no wording change (5339c70).

Comment thread apps/rocm/src/chat.rs
render_services_text, validate_service_id,
render_services_text,
};
// The tool-call policy lives in `tool_policy.rs`. `chat_tool_approval_request`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This comment says ChatRocmCommandAction and rocm_chat_tool_requested_args are read through chat:: only by other modules' tests, but they are re-exported ungated while the two truly test-only items are #[cfg(test)]. Please make the comment and the gating agree.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The comment was wrong and the gating was right: chat.rs calls ChatRocmCommandAction and rocm_chat_tool_requested_args in its own non-test code, so that import cannot be #[cfg(test)]. It is pub(crate) only because the tests in driver_plan.rs and system_packages.rs read both through chat::, while the two truly test-only items stay #[cfg(test)]. I corrected the comment and left the code alone (5339c70).

architecture.md: validate_provider_planner_tool_call reads
serve_args_request_cpu_device in its own serve branch; an install sdk call
goes to tool_policy::validate_chat_rocm_command_safety. The sentence had the
two swapped.

chat.rs: ChatRocmCommandAction and rocm_chat_tool_requested_args are used by
chat.rs itself, so their import is ungated by necessity; only
chat_install_driver_action and chat_rocm_command_action_from_args are
test-only and cfg(test). Make the comment say so.

prompt_text.rs: reflow the module header. Comments and docs only.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
main changed code this branch moved out of chat.rs and freeform_planner.rs,
so the conflicts are resolved by applying main's change at the new home:

- tool_policy.rs takes the control-character screening of tool names and
  arguments (reject_control_characters_in_tool_arguments and its helpers),
  the wider check in normalized_chat_rocm_command_args, and the quoted
  --prefix paths in the three command-preview tests that moved with it.
- rocm_argv.rs takes the shell_quote based format_structured_tool_call and
  drops quote_tool_arg, which main removed. It now depends on the leaf
  shell_quote.rs, and its header and architecture.md entry say so.
- freeform_planner.rs keeps its imports of prompt_text, rocm_argv and
  tool_policy and adds main's has_terminal_control_character.
- chat.rs keeps its slimmer form plus main's command-preview property
  tests, which import has_terminal_control_character themselves.
- quoting.rs and the architecture.md providers entries take main's text.

Signed-off-by: Jussi Elo <jussi.elo@amd.com>
Signed-off-by: Jussi Elo <jussi.elo@amd.com>
@jussielo-amd

Copy link
Copy Markdown
Collaborator Author

Pushed b070434: one commit for the three inline findings (5339c70) and two merges of main (a7290e8, b070434; plain merge commits, no rebase or force-push), because main moved under this PR (#563 command-preview quoting, the providers split, the rocm-core split). Your reviewed commits are unchanged.

On your other notes. Nothing in tool_policy's dependencies imports chat: I followed the crate:: module imports transitively from tool_policy with test modules excluded, over the merged tree, and the only chat:: references in that set are in the test modules of driver_plan.rs and system_packages.rs. The scan follows module paths, not items reached through main.rs re-imports. Nothing enforces "planner never imports chat" at module level today (xtask/src/crate_edges.rs is crate-level), and I did not add a new guard in this PR; the xtask source-scan contracts would be the pattern to follow if you want one as a follow-up.

Merge resolution worth a look: main's control-character screening of tool arguments now lives in tool_policy.rs (reject_control_characters_in_tool_arguments and the wider check in normalized_chat_rocm_command_args, with the three quoted --prefix expectations in the moved tests), since the functions it changed moved there. rocm_argv.rs now quotes through shell_quote as quote_tool_arg was removed on main, and the architecture entries say so. cargo test -p rocm --bin rocm passes (1499 passed) and the leaf test-name list matches origin/main.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

No active review issues are identified for the shared-helper refactor.

No open issues 🎉

Review effort: Balanced

@juhovainio juhovainio left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed at b070434 after the merges from main. I compared the removed and added lines across the .rs files: the moves are verbatim apart from imports, call-site path rewrites, rustfmt of the widened signatures and docs. No logic line differs.

  • Test count is conserved (37 moved to tool_policy.rs, 1 to prompt_text.rs).
  • The new leaf modules import nothing from chat, so the cycle is broken as described.
  • Visibility widening is limited to items with a cross-module caller, and the test-only re-exports are gated on cfg(test).
  • architecture.md, the module docs and the main.rs comment match the code.

Non-blocking:

  • The history has two "Merge origin/main" commits; a rebase before merge would match the repo's preference.
  • chat.rs remains a re-export hub. That is documented and fine here, but follow-up splits could repoint callers at the leaf modules and drop the re-exports.
  • The rocm_command_call test helper is duplicated, as the PR body notes.

This is based on a read of the diff, not a local build. clippy and the build/test jobs were still pending when I looked, so please confirm they come back green before merging.

@jussielo-amd
jussielo-amd added this pull request to the merge queue Nov 5, 2026
Merged via the queue into ROCm:main with commit c3df31d Nov 5, 2026
30 checks passed
@jussielo-amd
jussielo-amd deleted the refactor-assistant-break-chat-planner-cycle branch November 5, 2026 15:38
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.

4 participants