Files
loki/docs/implementation/PHASE-1-STEP-8b-NOTES.md
2026-04-15 12:56:00 -06:00

12 KiB
Raw Blame History

Phase 1 Step 8b — Implementation Notes

Status

Done.

Plan reference

  • Plan: docs/PHASE-1-IMPLEMENTATION-PLAN.md
  • Section: "Step 8b: Finish Step 7's deferred mixed-method migrations"

Summary

Migrated 7 of the 9 Step 7 deferrals to RequestContext / AppConfig methods that take &AppConfig instead of &Config. Two methods (edit_role and update) remain deferred because they depend on use_role (Step 8d) and MCP registry manipulation (Step 8d) respectively. Four private helper functions in mod.rs were bumped to pub(super) to support the new repl_complete implementation.

What was changed

Files modified (3 files)

  • src/config/request_context.rs — added a fifth impl RequestContext block with 7 methods:

    • retrieve_role(&self, app: &AppConfig, name: &str) -> Result<Role> — loads a role by name, resolves its model via Model::retrieve_model(app, ...). Reads app.temperature and app.top_p for the no-model-id fallback branch.

    • set_model_on_role_like(&mut self, app: &AppConfig, model_id: &str) -> Result<bool> — resolves the model via Model::retrieve_model, sets it on the active role-like if present (returns true), or on ctx.model directly (returns false). The false case means the caller should also call AppConfig::set_model_id_default if they want the global default updated.

    • reload_current_model(&mut self, app: &AppConfig, model_id: &str) -> Result<()> — resolves a model by ID and assigns it to ctx.model. Used in tandem with AppConfig::ensure_default_model_id.

    • use_prompt(&mut self, _app: &AppConfig, prompt: &str) -> Result<()> — creates a TEMP_ROLE_NAME role with the prompt text, sets its model to current_model(), calls use_role_obj. The _app parameter is included for signature consistency; it's unused because use_prompt only reads runtime state.

    • set_rag_reranker_model(&mut self, app: &AppConfig, value: Option<String>) -> Result<bool> — validates the model ID via Model::retrieve_model(app, ...) if present, then clones-and-replaces the Arc<Rag> with the updated reranker model. Returns true if RAG was mutated, false if no RAG is active.

    • set_rag_top_k(&mut self, value: usize) -> Result<bool> — same clone-and-replace pattern on the active RAG. Returns true/false.

    • repl_complete(&self, app: &AppConfig, cmd: &str, args: &[&str], _line: &str) -> Vec<(String, Option<String>)> — full tab-completion handler. Reads app.* for serialized fields, self.* for runtime state, self.app.vault for vault completions. MCP configured-server completions are limited to app.mapping_mcp_servers keys during the bridge (no live McpRegistry on RequestContext; Step 8d's ToolScope will restore full MCP completions).

    Updated imports: added TEMP_ROLE_NAME, list_agents, ModelType, list_models, read_to_string, fuzzy_filter. Removed duplicate crate::utils import that had accumulated.

  • src/config/app_config.rs — added 4 methods to the existing set_*_default impl block:

    • set_rag_reranker_model_default(&mut self, value: Option<String>)
    • set_rag_top_k_default(&mut self, value: usize)
    • set_model_id_default(&mut self, model_id: String)
    • ensure_default_model_id(&mut self) -> Result<String> — picks the first available chat model if model_id is empty, updates self.model_id, returns the resolved ID.
  • src/config/mod.rs — bumped 4 private helper functions to pub(super):

    • parse_value — used by update when it migrates (Step 8f/8g)
    • complete_bool — used by repl_complete
    • complete_option_bool — used by repl_complete
    • map_completion_values — used by repl_complete

Files NOT changed

  • src/client/macros.rs, src/client/model.rs — untouched; Step 8a already migrated these.
  • All other source files — no changes. All existing Config methods stay intact.

Key decisions

1. Same bridge pattern as Steps 3-8a

New methods sit alongside originals. No caller migration. Config's retrieve_role, set_model, setup_model, use_prompt, set_rag_reranker_model, set_rag_top_k, repl_complete all stay on Config and continue working for every current caller.

2. set_model_on_role_like returns Result<bool> (not just bool)

Unlike the Step 7 set_temperature_on_role_like pattern that returns a plain bool, set_model_on_role_like returns Result<bool> because Model::retrieve_model can fail. The bool still signals whether a role-like was mutated. When false, the model was assigned to ctx.model directly (so the caller doesn't need to fall through to AppConfig — the "no role-like" case is handled in-method by assigning to ctx.model). This differs from the Step 7 pattern where false means "caller must call the _default."

3. setup_model split into two independent methods

Config::setup_model does three things:

  1. Picks a default model ID if empty (ensure_default_model_id)
  2. Calls set_model to resolve and assign the model
  3. Writes back model_id to config

The split:

  • AppConfig::ensure_default_model_id() handles #1 and #3
  • RequestContext::reload_current_model() handles #2

Step 8f will compose them: first call ensure_default_model_id on the app config, then call reload_current_model on the context with the returned ID.

4. repl_complete MCP completions are reduced during bridge

Config::repl_complete reads self.mcp_registry.list_configured_servers() for the enabled_mcp_servers completion values. RequestContext has no mcp_registry field. During the bridge window, the new repl_complete offers only mapping_mcp_servers keys (from AppConfig) as MCP completions. Step 8d's ToolScope will provide full MCP server completions.

This is acceptable because:

  • The new method isn't called by anyone yet (bridge pattern)
  • When Step 8d wires it up, ToolScope will be available

5. edit_role deferred to Step 8d

Config::edit_role calls self.use_role() as its last line. use_role is a scope-transition method that Step 8d will rewrite to use McpFactory::acquire(). Migrating edit_role without use_role would require either a stub or leaving it half-broken. Deferring it keeps the bridge clean.

6. update dispatcher deferred to Step 8f/8g

Config::update takes &GlobalConfig and has two branches that do heavy MCP registry manipulation (enabled_mcp_servers and mcp_server_support). These branches require Step 8d's McpFactory/ToolScope infrastructure. The remaining branches could be migrated individually, but splitting the dispatcher partially creates a confusing dual-path situation. Deferring the entire dispatcher keeps things clean.

7. RAG mutation uses clone-and-replace on Arc<Rag>

Config::set_rag_reranker_model uses the update_rag helper which takes &GlobalConfig, clones the Arc<Rag>, mutates the clone, and writes it back via config.write().rag = Some(Arc::new(rag)).

The new RequestContext methods do the same thing but without the GlobalConfig indirection: clone Arc<Rag> contents, mutate, wrap in a new Arc, assign to self.rag. Semantically identical.

Deviations from plan

2 methods deferred (not in plan's "done" scope for 8b)

Method Why deferred
edit_role Calls use_role which is Step 8d
update MCP registry branches require Step 8d's McpFactory/ToolScope

The plan's 8b description listed both as potential deferrals:

  • edit_role: "calls editor + upsert_role + use_role — use_role is still Step 8d, so edit_role may stay deferred"
  • update: "Once all the individual set_* methods exist on both types" — the MCP-touching set_* methods don't exist yet

set_model_on_role_like handles the no-role-like case internally

The plan said the split should be:

  • RequestContext::set_model_on_role_like → returns bool
  • AppConfig::set_model_default → sets global

But set_model doesn't just set model_id when no role-like is active — it also assigns the resolved Model struct to self.model (runtime). Since the Model struct lives on RequestContext, the no-role-like branch must also live on RequestContext. So set_model_on_role_like handles both cases (role-like mutation and ctx.model assignment) and returns false to signal that model_id on AppConfig may also need updating. AppConfig::set_model_id_default is the simpler companion.

Verification

Compilation

  • cargo check — clean, zero warnings, zero errors
  • cargo clippy — clean

Tests

  • cargo test63 passed, 0 failed (unchanged from Steps 18a)

No new tests added — this is a bridge-pattern step that adds methods alongside existing ones. The existing test suite confirms no regressions.

Handoff to next step

What Step 8c can rely on

Step 8c (extract McpFactory::acquire() from McpRegistry::init_server) can rely on:

  • All Step 8a guarantees still holdModel::retrieve_model, list_models, list_all_models, list_client_names all take &AppConfig
  • RequestContext now has 46 inherent methods across 5 impl blocks: 1 constructor + 13 reads + 12 writes + 14 mixed (Step 7) + 7 mixed (Step 8b) = 47 total (46 public + 1 private open_message_file)
  • AppConfig now has 21 methods: 7 reads + 4 writes + 10 setter-defaults (6 from Step 7 + 4 from Step 8b)

What Step 8c should watch for

Step 8c is independent of Step 8b. It extracts the MCP subprocess spawn logic from McpRegistry::init_server into a standalone function and implements McpFactory::acquire(). Step 8b provides no input to 8c.

What Step 8d should know about Step 8b's output

Step 8d (scope transitions) depends on both 8b and 8c. From 8b it gets:

  • RequestContext::retrieve_role(app, name) — needed by use_role
  • RequestContext::set_model_on_role_like(app, model_id) — may be useful inside scope transitions

What Step 8f/8g should know about Step 8b deferrals

  • edit_role — needs use_role from Step 8d. Once 8d ships, edit_role on RequestContext becomes: call app.editor(), call upsert_role(name), call self.use_role(app, name, abort_signal). The upsert_role method is still on Config and needs migrating (it calls self.editor() which is on AppConfig, and ensure_parent_exists which is a free function — straightforward).

  • update dispatcher — needs all set_* branches migrated. The non-MCP branches are ready now. The MCP branches need Step 8d's McpFactory/ToolScope.

  • use_role_safely / use_session_safely — still on Config. These wrappers exist only because Config::use_role is &mut self and the REPL holds Arc<RwLock<Config>>. Step 8g eliminates them when the REPL switches to holding RequestContext directly.

Bridge-window duplication count at end of Step 8b

Running tally:

  • AppConfig (Steps 3+4+7+8b): 21 methods
  • RequestContext (Steps 5+6+7+8b): 46 methods
  • paths module (Step 2): 33 free functions
  • Step 6.5 types: 4 new types on scaffolding
  • mod.rs visibility bumps: 4 helpers → pub(super)

Total: 67 methods + 33 paths + 4 types / ~1500 lines of parallel logic

All auto-delete in Step 10.

Files to re-read at the start of Step 8c

  • docs/PHASE-1-IMPLEMENTATION-PLAN.md — Step 8c section
  • src/mcp/mod.rsMcpRegistry::init_server method body (the spawn logic to extract)
  • src/config/mcp_factory.rs — current scaffolding from Step 6.5

References

  • Phase 1 plan: docs/PHASE-1-IMPLEMENTATION-PLAN.md
  • Step 8a notes: docs/implementation/PHASE-1-STEP-8a-NOTES.md
  • Step 7 notes: docs/implementation/PHASE-1-STEP-7-NOTES.md
  • Modified files:
    • src/config/request_context.rs (7 new methods, import updates)
    • src/config/app_config.rs (4 new set_*_default / ensure_* methods)
    • src/config/mod.rs (4 helper functions bumped to pub(super))