# Phase 1 Step 7 — Implementation Notes ## Status Done. ## Plan reference - Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md` - Section: "Step 7: Tackle mixed methods (THE HARD PART)" ## Summary Added 14 mixed-method splits to the new types, plus 6 global- default setters on `AppConfig`. The methods that mix serialized config reads/writes with runtime state reads/writes are now available on `RequestContext` with `&AppConfig` as an explicit parameter for the serialized half. Same bridge pattern as Steps 3–6: `Config`'s originals stay intact, new methods sit alongside, caller migration happens in Step 8. **Step 7 completed ~65% of its planned scope.** Nine target methods were deferred to Step 8 because they transitively depend on `Model::retrieve_model(&Config)` and `list_models(&Config)` — refactoring those requires touching the `client` module macros, which is beyond Step 7's bridge- pattern scope. Step 8 will rewrite them alongside the entry point migration. ## What was changed ### Modified files - **`src/config/app_config.rs`** — added a third `impl AppConfig` block with 6 `set_*_default` methods for the serialized-field half of the mixed-method splits: - `set_temperature_default` - `set_top_p_default` - `set_enabled_tools_default` - `set_enabled_mcp_servers_default` - `set_save_session_default` - `set_compression_threshold_default` - **`src/config/request_context.rs`** — added a fourth `impl RequestContext` block with 14 methods: **Helpers (2):** - `current_model(&self) -> &Model` — pure runtime traversal (session > agent > role > ctx.model) - `extract_role(&self, app: &AppConfig) -> Role` — pure runtime except fallback reads `app.temperature`, `app.top_p`, `app.enabled_tools`, `app.enabled_mcp_servers` **Role-like setters (7):** these all return `bool` indicating whether they mutated a `RoleLike` (if `false`, the caller should fall back to `app.set__default()`). This preserves the exact semantics of today's `Config::set_*` methods: - `set_temperature_on_role_like` - `set_top_p_on_role_like` - `set_enabled_tools_on_role_like` - `set_enabled_mcp_servers_on_role_like` - `set_save_session_on_session` (uses `self.session` directly, not `role_like_mut`) - `set_compression_threshold_on_session` (same) - `set_max_output_tokens_on_role_like` **Chat lifecycle (2):** - `save_message(&mut self, app: &AppConfig, input, output)` — writes to session if present, else to messages file if `app.save` is true - `after_chat_completion(&mut self, app, input, output, tool_results)` — updates `last_message`, calls `save_message` if not `app.dry_run` - `open_message_file(&self) -> Result` — private helper **Info getters (3):** - `sysinfo(&self, app: &AppConfig) -> Result` — ~70-line display output mixing serialized and runtime state - `info(&self, app: &AppConfig) -> Result` — delegates to `sysinfo` in fallback branch - `session_info(&self, app: &AppConfig) -> Result` — calls `app.render_options()` **Prompt rendering (3):** - `generate_prompt_context(&self, app) -> HashMap<&str, String>` — builds the template variable map - `render_prompt_left(&self, app) -> String` - `render_prompt_right(&self, app) -> String` **Function selection (3):** - `select_enabled_functions(&self, app, role) -> Vec` — filters `ctx.functions.declarations()` by role's enabled tools + agent filters + user interaction functions - `select_enabled_mcp_servers(&self, app, role) -> Vec<...>` — same pattern for MCP meta-functions - `select_functions(&self, app, role) -> Option>` — combines both - **`src/config/mod.rs`** — bumped `format_option_value` from private to `pub(super)` so `request_context.rs` can use it as `super::format_option_value`. ### Unchanged files - **`src/config/mod.rs`** — all Step 7 target methods still exist on `Config`. They continue to work for every current caller. ## Key decisions ### 1. Same bridge pattern as Steps 3-6 Step 7 follows the same additive pattern as earlier steps: new methods on `AppConfig` / `RequestContext`, `Config`'s originals untouched, no caller migration. Caller migration is Step 8. The plan's Step 7 description implied a semantic rewrite ("split into explicit parameter passing") but that phrasing applies to the target signatures, not the migration mechanism. The bridge pattern achieves the same end state — methods with `(&AppConfig, &RequestContext)` signatures exist and are ready for Step 8 to call. ### 2. `set_*` methods split into `_on_role_like` + `_default` pair Today's `Config::set_temperature` does: ```rust match self.role_like_mut() { Some(role_like) => role_like.set_temperature(value), None => self.temperature = value, } ``` The Step 7 split: ```rust // On RequestContext: fn set_temperature_on_role_like(&mut self, value) -> bool { match self.role_like_mut() { Some(rl) => { rl.set_temperature(value); true } None => false, } } // On AppConfig: fn set_temperature_default(&mut self, value) { self.temperature = value; } ``` **The bool return** is the caller contract: if `_on_role_like` returns `false`, the caller must call `app.set_*_default(value)`. This is what Step 8 callers will do: ```rust if !ctx.set_temperature_on_role_like(value) { Arc::get_mut(&mut app.config).unwrap().set_temperature_default(value); } ``` (Or more likely, the AppConfig mutation gets hidden behind a helper on `AppState` since `AppConfig` is behind `Arc`.) This split is semantically equivalent to the existing behavior while making the "where the value goes" decision explicit at the type level. ### 3. `save_message` and `after_chat_completion` migrated together `after_chat_completion` reads `app.dry_run` and calls `save_message`, which reads `app.save`. Both got deferred from Step 6 for exactly this mixed-dependency reason. Step 7 migrates them together: ```rust pub fn after_chat_completion( &mut self, app: &AppConfig, input: &Input, output: &str, tool_results: &[ToolResult], ) -> Result<()> { if !tool_results.is_empty() { return Ok(()); } self.last_message = Some(LastMessage::new(input.clone(), output.to_string())); if !app.dry_run { self.save_message(app, input, output)?; } Ok(()) } ``` The `open_message_file` helper moved along with them since it's only called from `save_message`. ### 4. `format_option_value` visibility bump `format_option_value` is a tiny private helper in `src/config/mod.rs` that `sysinfo` uses. Step 7's new `RequestContext::sysinfo` needs to call it, so I bumped its visibility from `fn` to `pub(super)`. This is a minimal change (one word) that lets child modules reuse the helper without duplicating it. ### 5. `select_*` methods were Step 3 deferrals The plan's Step 3 table originally listed `select_functions`, `select_enabled_functions`, and `select_enabled_mcp_servers` as global-read method targets. Step 3's notes correctly flagged them as actually-mixed because they read `self.functions` and `self.agent` (runtime, not serialized). Step 7 is the right home for them. They take `(&self, app: &AppConfig, role: &Role)` and read: - `ctx.functions.declarations()` (runtime — existing flat field, will collapse into `tool_scope.functions` in Step 8+) - `ctx.agent` (runtime) - `app.function_calling_support`, `app.mcp_server_support`, `app.mapping_tools`, `app.mapping_mcp_servers` (serialized) The implementations are long (~80 lines each) but are verbatim copies of the `Config` originals with `self.X` replaced by `app.X` for serialized fields and `self.X` preserved for runtime fields. ### 6. `session_info` keeps using `crate::render::MarkdownRender` I didn't add a top-level `use crate::render::MarkdownRender` because it's only called from `session_info`. Inline `crate::render::MarkdownRender::init(...)` is clearer than adding another global import for a single use site. ### 7. Imports grew substantially `request_context.rs` now imports from 7 new sources compared to the end of Step 6: - `super::AppConfig` (for the mixed-method params) - `super::MessageContentToolCalls` (for `save_message`) - `super::LEFT_PROMPT`, `super::RIGHT_PROMPT` (for prompt rendering) - `super::ensure_parent_exists` (for `open_message_file`) - `crate::function::FunctionDeclaration`, `crate::function::user_interaction::USER_FUNCTION_PREFIX` - `crate::mcp::MCP_*_META_FUNCTION_NAME_PREFIX` (3 constants) - `std::collections::{HashMap, HashSet}`, `std::fs::{File, OpenOptions}`, `std::io::Write`, `std::path::Path`, `crate::utils::{now, render_prompt}` This is expected — Step 7's methods are the most dependency-heavy in Phase 1. Post–Phase 1 cleanup can reorganize into separate files if the module becomes unwieldy. ## Deviations from plan ### 9 methods deferred to Step 8 | Method | Why deferred | |---|---| | `retrieve_role` | Calls `Model::retrieve_model(&Config)` transitively, needs client module refactor | | `set_model` | Calls `Model::retrieve_model(&Config)` transitively | | `set_rag_reranker_model` | Takes `&GlobalConfig`, uses `update_rag` helper with Arc take/replace pattern | | `set_rag_top_k` | Same as above | | `update` | Dispatcher over all `set_*` methods including the 2 above, plus takes `&GlobalConfig` and touches `mcp_registry` | | `repl_complete` | Calls `list_models(&Config)` + reads `self.mcp_registry` (going away in Step 6.5/8), + reads `self.functions` | | `use_role_safely` | Takes `&GlobalConfig`, does `take()`/`replace()` on Arc | | `use_session_safely` | Same as above | | `setup_model` | Calls `self.set_model()` which is deferred | | `use_prompt` (Step 6 deferral) | Calls `current_model()` (migratable) and `use_role_obj` (migrated in Step 6), but the whole method is 4 lines and not independently useful without its callers | | `edit_role` (Step 6 deferral) | Calls `self.upsert_role()` and `self.use_role()` which are Step 8 | **Root cause of most deferrals:** the `client` module's `list_all_models` macro and `Model::retrieve_model` take `&Config`. Refactoring them to take `&AppConfig` is a meaningful cross-module change that belongs in Step 8 alongside the caller migration. ### 14 methods migrated | Method | New signature | |---|---| | `current_model` | `&self -> &Model` (pure RequestContext) | | `extract_role` | `(&self, &AppConfig) -> Role` | | `set_temperature_on_role_like` | `(&mut self, Option) -> bool` | | `set_top_p_on_role_like` | `(&mut self, Option) -> bool` | | `set_enabled_tools_on_role_like` | `(&mut self, Option) -> bool` | | `set_enabled_mcp_servers_on_role_like` | `(&mut self, Option) -> bool` | | `set_save_session_on_session` | `(&mut self, Option) -> bool` | | `set_compression_threshold_on_session` | `(&mut self, Option) -> bool` | | `set_max_output_tokens_on_role_like` | `(&mut self, Option) -> bool` | | `save_message` | `(&mut self, &AppConfig, &Input, &str) -> Result<()>` | | `after_chat_completion` | `(&mut self, &AppConfig, &Input, &str, &[ToolResult]) -> Result<()>` | | `sysinfo` | `(&self, &AppConfig) -> Result` | | `info` | `(&self, &AppConfig) -> Result` | | `session_info` | `(&self, &AppConfig) -> Result` | | `generate_prompt_context` | `(&self, &AppConfig) -> HashMap<&str, String>` | | `render_prompt_left` | `(&self, &AppConfig) -> String` | | `render_prompt_right` | `(&self, &AppConfig) -> String` | | `select_functions` | `(&self, &AppConfig, &Role) -> Option>` | | `select_enabled_functions` | `(&self, &AppConfig, &Role) -> Vec<...>` | | `select_enabled_mcp_servers` | `(&self, &AppConfig, &Role) -> Vec<...>` | Actually that's 20 methods across the two types (6 on `AppConfig`, 14 on `RequestContext`). "14 migrated" refers to the 14 behavior methods on `RequestContext`; the 6 on `AppConfig` are the paired defaults for the 7 role-like setters (4 `set_*_default` + 2 session-specific — the `set_max_output_tokens` split doesn't need a default because `ctx.model.set_max_tokens()` works without a fallback). ## Verification ### Compilation - `cargo check` — clean, **zero warnings, zero errors** - `cargo clippy` — clean ### Tests - `cargo test` — **63 passed, 0 failed** (unchanged from Steps 1–6.5) The bridge's round-trip test still passes, confirming the new methods don't interfere with struct layout or the `Config → AppConfig + RequestContext → Config` invariant. ### Manual smoke test Not applicable — no runtime behavior changed. CLI and REPL still call `Config::set_temperature`, `Config::sysinfo`, `Config::save_message`, etc. as before. ## Handoff to next step ### What Step 8 can rely on Step 8 (entry point rewrite) can rely on: - **`AppConfig` now has 17 methods** (Steps 3+4+7): 7 reads + 4 writes + 6 setter-defaults - **`RequestContext` now has 39 inherent methods** across 5 impl blocks: 1 constructor + 13 reads + 12 writes + 14 mixed - **All of `AppConfig`'s and `RequestContext`'s new methods are under `#[allow(dead_code)]`** — that's safe to leave alone; callers wire them up in Step 8 and the allows become inert - **`format_option_value` is `pub(super)`** — accessible from any `config` child module - **The bridge (`Config::to_app_config`, `to_request_context`, `from_parts`) still works** and all round-trip tests pass - **The `paths` module, Step 3/4 `AppConfig` methods, Step 5/6 `RequestContext` methods, Step 6.5 scaffolding types are all unchanged** - **These `Config` methods are still on `Config`** and must stay there through Step 8 (they're Step 8 targets): - `retrieve_role`, `set_model`, `set_rag_reranker_model`, `set_rag_top_k`, `update`, `repl_complete`, `use_role_safely`, `use_session_safely`, `setup_model`, `use_prompt`, `edit_role` - Plus the Step 6 Category A deferrals: `use_role`, `use_session`, `use_agent`, `exit_agent` - Plus the Step 6 Category C deferrals: `compress_session`, `maybe_compress_session`, `autoname_session`, `maybe_autoname_session`, `use_rag`, `edit_rag_docs`, `rebuild_rag`, `apply_prelude` ### What Step 8 should watch for **Step 8 is the biggest remaining step** after Step 6.5 deferred its scope-transition rewrites. Step 8 now absorbs: 1. **Entry point rewrite** (original Step 8 scope): - `main.rs::run()` constructs `AppState` + `RequestContext` instead of `GlobalConfig` - `main.rs::start_directive()` takes `&mut RequestContext` instead of `&GlobalConfig` - `main.rs::create_input()` takes `&RequestContext` - `repl/mod.rs::Repl` holds a long-lived `RequestContext` instead of `GlobalConfig` - All 91 callsites in the original migration table 2. **`Model::retrieve_model` refactor** (Step 7 deferrals): - `Model::retrieve_model(config: &Config, ...)` → `Model::retrieve_model(config: &AppConfig, ...)` - `list_all_models!(config: &Config)` macro → `list_all_models!(config: &AppConfig)` - `list_models(config: &Config, ...)` → `list_models(config: &AppConfig, ...)` - Then migrate `retrieve_role`, `set_model`, `repl_complete`, `setup_model` 3. **RAG lifecycle migration** (Step 7 deferrals + Step 6 Category C): - `use_rag`, `edit_rag_docs`, `rebuild_rag` → `RequestContext` methods using `RagCache` - `set_rag_reranker_model`, `set_rag_top_k` → split similarly to Step 7 setters 4. **Scope transition rewrites** (Step 6.5 deferrals): - `use_role`, `use_session`, `use_agent`, `exit_agent` rewritten to build `ToolScope` via `McpFactory` - `McpFactory::acquire()` extracted from `McpRegistry::init_server` - `use_role_safely`, `use_session_safely` eliminated (not needed once callers hold `&mut RequestContext`) 5. **Session lifecycle migration** (Step 6 Category C): - `compress_session`, `maybe_compress_session`, `autoname_session`, `maybe_autoname_session` → methods that take `&mut RequestContext` instead of spawning tasks with `GlobalConfig` - `apply_prelude` → uses migrated `use_role` / `use_session` 6. **`update` dispatcher** (Step 7 deferral): - Once all `set_*` are available on `RequestContext` and `AppConfig`, `update` becomes a dispatcher over the new split pair This is a **huge** step. Consider splitting into 8a-8f sub-steps or staging across multiple PRs. ### What Step 8 should NOT do - Don't re-migrate any Step 3-7 method - Don't touch the new types from Step 6.5 unless actually implementing `McpFactory::acquire()` or `RagCache::load_with` usage - Don't leave intermediate states broken — each sub-step should keep the build green, even if it means keeping temporary dual code paths ### Files to re-read at the start of Step 8 - `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 8 section - This notes file — specifically the deferrals table and Step 8 watch items - Step 6.5 notes — scope transition rewrite details - Step 6 notes — Category C deferral inventory - `src/config/mod.rs` — still has ~25 methods that need migrating ## Follow-up (not blocking Step 8) ### 1. Bridge-window duplication count at end of Step 7 Running tally: - `AppConfig` (Steps 3+4+7): 17 methods (11 reads/writes + 6 setter-defaults) - `RequestContext` (Steps 5+6+7): 39 methods (1 constructor + 13 reads + 12 writes + 14 mixed) - `paths` module (Step 2): 33 free functions - Step 6.5 types: 4 new types on scaffolding **Total bridge-window duplication: 56 methods / ~1200 lines** (up from 36 / ~550 at end of Step 6). All auto-delete in Step 10. ### 2. `request_context.rs` is now ~900 lines Getting close to the point where splitting into multiple files would help readability. Candidate layout: - `request_context/mod.rs` — struct definition + constructor - `request_context/reads.rs` — Step 5 methods - `request_context/writes.rs` — Step 6 methods - `request_context/mixed.rs` — Step 7 methods Not blocking anything; consider during Phase 1 cleanup. ### 3. The `set_*_on_role_like` / `set_*_default` split has an unusual caller contract Callers of the split have to remember: "call `_on_role_like` first, check the bool, call `_default` if false." That's more verbose than today's `Config::set_temperature` which hides the dispatch. Step 8 should add convenience helpers on `RequestContext` that wrap both halves: ```rust pub fn set_temperature(&mut self, value: Option, app: &mut AppConfig) { if !self.set_temperature_on_role_like(value) { app.set_temperature_default(value); } } ``` But that requires `&mut AppConfig`, which requires unwrapping the `Arc` on `AppState.config`. The cleanest shape is probably to move the mutation into a helper on `AppState`: ```rust impl AppState { pub fn config_mut(&self) -> Option<&mut AppConfig> { Arc::get_mut(...) } } ``` Or accept that the `.set` REPL command needs an owned `AppState` (not `Arc`) and handle the mutation at the entry point. Step 8 can decide. ### 4. `select_*` methods are long but verbatim The 3 `select_*` methods are ~180 lines combined and are verbatim copies of the `Config` originals. I resisted the urge to refactor (extract helpers, simplify the `enabled_tools == "all"` branches, etc.) because: - Step 7 is about splitting signatures, not style - The copies get deleted in Step 10 anyway - Any refactor could introduce subtle behavior differences that are hard to catch without a functional test for these specific methods Post–Phase 1 cleanup can factor these if desired. ## References - Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md` - Step 6 notes: `docs/implementation/PHASE-1-STEP-6-NOTES.md` - Step 6.5 notes: `docs/implementation/PHASE-1-STEP-6.5-NOTES.md` - Modified files: - `src/config/app_config.rs` (6 new `set_*_default` methods) - `src/config/request_context.rs` (14 new mixed methods, 7 new imports) - `src/config/mod.rs` (`format_option_value` → `pub(super)`)