# Phase 1 Step 4 — Implementation Notes ## Status Done. ## Plan reference - Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md` - Section: "Step 4: Migrate global-write methods" ## Summary Added 4 of 8 planned global-write methods to `AppConfig` as inherent methods, duplicating the bodies that still exist on `Config`. The other 4 methods were deferred: 2 to Step 7 (mixed methods that call into `set_*` methods slated for Step 7), and 2 kept on `Config` because they populate runtime-only fields (`functions`, `mcp_registry`) that don't belong on `AppConfig`. Same duplication-no-caller-migration pattern as Step 3 — during the bridge window both `Config` and `AppConfig` have these methods; caller migration happens organically in Steps 8-9 when frontends switch from `GlobalConfig` to `AppState` + `RequestContext`. ## What was changed ### Modified files - **`src/config/app_config.rs`** — added 4 new imports (`NO_COLOR`, `get_env_name` via `crate::utils`, `terminal_colorsaurus` types) and a new `impl AppConfig` block with 4 methods under `#[allow(dead_code)]`: - `set_wrap(&mut self, value: &str) -> Result<()>` — parses and sets `self.wrap` for the `.set wrap` REPL command - `setup_document_loaders(&mut self)` — seeds default PDF/DOCX loaders into `self.document_loaders` if not already present - `setup_user_agent(&mut self)` — expands `"auto"` into `loki/` in `self.user_agent` - `load_envs(&mut self)` — ~140 lines of env-var overrides that populate all 30+ serialized fields from `LOKI_*` environment variables All bodies are copy-pasted verbatim from the originals on `Config`, with references updated for the new module location: - `read_env_value::` → `super::read_env_value::` - `read_env_bool` → `super::read_env_bool` - `NO_COLOR`, `IS_STDOUT_TERMINAL`, `get_env_name`, `decode_bin` → imported from `crate::utils` - `terminal_colorsaurus` → direct import ### Unchanged files - **`src/config/mod.rs`** — the original `Config::set_wrap`, `load_envs`, `setup_document_loaders`, `setup_user_agent` definitions are deliberately left intact. They continue to work for every existing caller. They get deleted in Step 10 when `Config` is removed entirely. - **`src/config/mod.rs`** — the `read_env_value` and `read_env_bool` private helpers are unchanged and accessed via `super::read_env_value` from `app_config.rs`. ## Key decisions ### 1. Only 4 of 8 methods migrated The plan's Step 4 table listed 8 methods. After reading each one carefully, I classified them: | Method | Classification | Action | |---|---|---| | `set_wrap` | Pure global-write | **Migrated** | | `load_envs` | Pure global-write | **Migrated** | | `setup_document_loaders` | Pure global-write | **Migrated** | | `setup_user_agent` | Pure global-write | **Migrated** | | `setup_model` | Calls `self.set_model()` (Step 7 mixed) | **Deferred to Step 7** | | `load_functions` | Writes runtime `self.functions` field | **Not migrated** (stays on `Config`) | | `load_mcp_servers` | Writes runtime `self.mcp_registry` field (going away in Step 6.5) | **Not migrated** (stays on `Config`) | | `update` | Dispatches to 10+ `set_*` methods, all Step 7 mixed | **Deferred to Step 7** | See "Deviations from plan" for detail on each deferral. ### 2. Same duplication-no-forwarder pattern as Step 3 Step 4's target callers are all `.write()` on a `GlobalConfig` / `Config` instance. Like Step 3, giving these callers an `AppConfig` instance would require either (a) a sync'd `Arc` field on `Config` (breaks because Step 4 itself mutates `Config`), (b) cloning on every call (expensive for `load_envs` which touches 30+ fields), or (c) duplicating the method bodies. Option (c) is the same choice Step 3 made and for the same reasons. The duplication is 4 methods (~180 lines total dominated by `load_envs`) that auto-delete in Step 10. ### 3. `load_envs` body copied verbatim despite being long `load_envs` is ~140 lines of repetitive `if let Some(v) = read_env_value(...) { self.X = v; }` blocks — one per serialized field. I considered refactoring it to reduce repetition (e.g., a macro or a data-driven table) but resisted that urge because: - The refactor would be a behavior change (even if subtle) during a mechanical code-move step - The verbatim copy is easy to audit for correctness (line-by-line diff against the original) - It gets deleted in Step 10 anyway, so the repetition is temporary - Any cleanup belongs in a dedicated tidying pass after Phase 1, not in the middle of a split ### 4. Methods stay in a separate `impl AppConfig` block Step 3 added its 7 read methods in one `impl AppConfig` block. Step 4 adds its 4 write methods in a second `impl AppConfig` block directly below it. Rust allows multiple `impl` blocks on the same type, and the visual separation makes it obvious which methods are reads vs writes during the bridge window. When Step 10 deletes `Config`, both blocks can be merged or left separate based on the cleanup maintainer's preference. ## Deviations from plan ### `setup_model` deferred to Step 7 The plan lists `setup_model` as a Step 4 target. Reading its body: ```rust fn setup_model(&mut self) -> Result<()> { let mut model_id = self.model_id.clone(); if model_id.is_empty() { let models = list_models(self, ModelType::Chat); // ... } self.set_model(&model_id)?; // ← this is Step 7 "mixed" self.model_id = model_id; Ok(()) } ``` It calls `self.set_model(&model_id)`, which the plan explicitly lists in **Step 7** ("mixed methods") because `set_model` conditionally writes to `role_like` (runtime) or `model_id` (serialized) depending on whether a role/session/agent is active. Since `setup_model` can't be migrated until `set_model` exists on `AppConfig` / `RequestContext`, it has to wait for Step 7. **Action:** left `Config::setup_model` intact. Step 7 picks it up. ### `update` deferred to Step 7 The plan lists `update` as a Step 4 target. Its body is a ~140 line dispatch over keys like `"temperature"`, `"top_p"`, `"enabled_tools"`, `"enabled_mcp_servers"`, `"max_output_tokens"`, `"save_session"`, `"compression_threshold"`, `"rag_reranker_model"`, `"rag_top_k"`, etc. — every branch calls into a `set_*` method on `Config` that the plan explicitly lists in **Step 7**: - `set_temperature` (Step 7) - `set_top_p` (Step 7) - `set_enabled_tools` (Step 7) - `set_enabled_mcp_servers` (Step 7) - `set_max_output_tokens` (Step 7) - `set_save_session` (Step 7) - `set_compression_threshold` (Step 7) - `set_rag_reranker_model` (Step 7) - `set_rag_top_k` (Step 7) Migrating `update` before those would mean `update` calls `Config::set_X` (old) from inside `AppConfig::update` (new) — which crosses the type boundary awkwardly and leaves `update`'s behavior split between the two types during the migration window. Not worth it. **Action:** left `Config::update` intact. Step 7 picks it up along with the `set_*` methods it dispatches to. At that point all 10 dependencies will be on `AppConfig`/`RequestContext` and `update` can be moved cleanly. ### `load_functions` not migrated (stays on Config) The plan lists `load_functions` as a Step 4 target. Its body: ```rust fn load_functions(&mut self) -> Result<()> { self.functions = Functions::init( self.visible_tools.as_ref().unwrap_or(&Vec::new()) )?; if self.working_mode.is_repl() { self.functions.append_user_interaction_functions(); } Ok(()) } ``` It writes to `self.functions` — a `#[serde(skip)]` runtime field that lives on `RequestContext` after Step 6 and inside `ToolScope` after Step 6.5. It also reads `self.working_mode`, another runtime field. This isn't a "global-write" method in the sense Step 4 targets — it's a runtime initialization method that will move to `RequestContext` when `functions` does. **Action:** left `Config::load_functions` intact. It gets handled in Step 5 or Step 6 when runtime fields start moving. Not Step 4, not Step 7. ### `load_mcp_servers` not migrated (stays on Config) Same story as `load_functions`. Its body writes `self.mcp_registry` (a field slated for deletion in Step 6.5 per the architecture plan) and `self.functions` (runtime, moving in Step 5/6). Nothing about this method belongs on `AppConfig`. **Action:** left `Config::load_mcp_servers` intact. It gets handled or deleted in Step 6.5 when `McpFactory` replaces the singleton registry entirely. ## Verification ### Compilation - `cargo check` — clean, **zero warnings, zero errors** - `cargo clippy` — clean ### Tests - `cargo test` — **63 passed, 0 failed** (unchanged from Steps 1–3) Step 4 added no new tests because it's duplication. The existing test suite confirms: - The original `Config` methods still work (they weren't touched) - `AppConfig` still compiles, its `Default` impl is intact - The bridge's round-trip test still passes: - `config::bridge::tests::round_trip_default_config` - `config::bridge::tests::round_trip_preserves_all_non_lossy_fields` - `config::bridge::tests::to_app_config_copies_every_serialized_field` - `config::bridge::tests::to_request_context_copies_every_runtime_field` ### Manual smoke test Not applicable — no runtime behavior changed. CLI and REPL still call `Config::set_wrap()`, `Config::update()`, `Config::load_envs()`, etc. unchanged. ## Handoff to next step ### What Step 5 can rely on Step 5 (migrate request-read methods to `RequestContext`) can rely on: - `AppConfig` now has **11 methods total**: 7 reads from Step 3, 4 writes from Step 4 - `#[allow(dead_code)]` on both `impl AppConfig` blocks — safe to leave as-is, goes away when callers migrate in Steps 8+ - `Config` is unchanged for all 11 methods — originals still work for all current callers - The bridge from Step 1, the paths module from Step 2, the read methods from Step 3 are all unchanged and still working - **`setup_model`, `update`, `load_functions`, `load_mcp_servers` are still on `Config`** and must stay there: - `setup_model` → migrates in Step 7 with the `set_*` methods - `update` → migrates in Step 7 with the `set_*` methods - `load_functions` → migrates to `RequestContext` in Step 5 or Step 6 (whichever handles `Functions`) - `load_mcp_servers` → deleted/transformed in Step 6.5 ### What Step 5 should watch for - **Step 5 targets are `&self` request-read methods** that read runtime fields like `self.session`, `self.role`, `self.agent`, `self.rag`, etc. The plan's Step 5 table lists: `state`, `messages_file`, `sessions_dir`, `session_file`, `rag_file`, `info`, `role_info`, `session_info`, `agent_info`, `agent_banner`, `rag_info`, `list_sessions`, `list_autoname_sessions`, `is_compressing_session`, `role_like_mut`. - **These migrate to `RequestContext`**, not `AppConfig`, because they read per-request state. - **Same duplication pattern applies.** Add methods to `RequestContext`, leave originals on `Config`, no caller migration. - **`sessions_dir` and `messages_file` already use `paths::` functions internally** (from Step 2's migration). They read `self.agent` to decide between the global and agent-scoped path. Those paths come from the `paths` module. - **`role_like_mut`** is interesting — it's the helper that returns a mutable reference to whichever of role/session/agent is on top. It's the foundation for every `set_*` method in Step 7. Migrate it to `RequestContext` in Step 5 so Step 7 has it ready. - **`list_sessions` and `list_autoname_sessions`** wrap `paths::list_file_names` with some filtering. They take `&self` to know the current agent context for path resolution. ### What Step 5 should NOT do - Don't touch the Step 3/4 methods on `AppConfig` — they stay until Steps 8+ caller migration. - Don't try to migrate `update`, `setup_model`, `load_functions`, or `load_mcp_servers` — each has a specific later-step home. - Don't touch the `bridge.rs` conversions — still needed. - Don't touch `paths.rs` — still complete. - Don't migrate any caller of any method yet — callers stay on `Config` through the bridge window. ### Files to re-read at the start of Step 5 - `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 5 section has the full request-read method table - This notes file — specifically "Deviations from plan" and "What Step 5 should watch for" - `src/config/request_context.rs` — to see the current shape that Step 5 will extend - Current `Config` method bodies in `src/config/mod.rs` for each Step 5 target (search for `pub fn state`, `pub fn messages_file`, etc.) ## Follow-up (not blocking Step 5) ### 1. `load_envs` is the biggest duplication so far At ~140 lines, `load_envs` is the largest single duplication in the bridge. It's acceptable because it's self-contained and auto-deletes in Step 10, but it's worth flagging that if Phase 1 stalls anywhere between now and Step 10, this method's duplication becomes a maintenance burden. Env var changes would need to be made twice. **Mitigation during the bridge window:** if someone adds a new env var during Steps 5-9, they MUST add it to both `Config::load_envs` and `AppConfig::load_envs`. Document this in the Step 5 notes if any env var changes ship during that interval. ### 2. `AppConfig` now has 11 methods across 2 `impl` blocks Fine during Phase 1. Post-Phase 1 cleanup can consider whether to merge them or keep the read/write split. Not a blocker. ### 3. The `read_env_value` / `read_env_bool` helpers are accessed via `super::` These are private module helpers in `src/config/mod.rs`. Step 4's migration means `app_config.rs` now calls them via `super::`, which works because `app_config.rs` is a sibling module. If Phase 2+ work moves these helpers anywhere else, the `super::` references in `app_config.rs` will need updating. ## References - Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md` - Step 3 notes: `docs/implementation/PHASE-1-STEP-3-NOTES.md` (for the duplication rationale) - Modified file: `src/config/app_config.rs` (new imports + new `impl AppConfig` block with 4 write methods) - Unchanged but referenced: `src/config/mod.rs` (original `Config` methods still exist, private helpers `read_env_value` / `read_env_bool` accessed via `super::`)