14 KiB
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_nameviacrate::utils,terminal_colorsaurustypes) and a newimpl AppConfigblock with 4 methods under#[allow(dead_code)]:set_wrap(&mut self, value: &str) -> Result<()>— parses and setsself.wrapfor the.set wrapREPL commandsetup_document_loaders(&mut self)— seeds default PDF/DOCX loaders intoself.document_loadersif not already presentsetup_user_agent(&mut self)— expands"auto"intoloki/<version>inself.user_agentload_envs(&mut self)— ~140 lines of env-var overrides that populate all 30+ serialized fields fromLOKI_*environment variables
All bodies are copy-pasted verbatim from the originals on
Config, with references updated for the new module location:read_env_value::<T>→super::read_env_value::<T>read_env_bool→super::read_env_boolNO_COLOR,IS_STDOUT_TERMINAL,get_env_name,decode_bin→ imported fromcrate::utilsterminal_colorsaurus→ direct import
Unchanged files
src/config/mod.rs— the originalConfig::set_wrap,load_envs,setup_document_loaders,setup_user_agentdefinitions are deliberately left intact. They continue to work for every existing caller. They get deleted in Step 10 whenConfigis removed entirely.src/config/mod.rs— theread_env_valueandread_env_boolprivate helpers are unchanged and accessed viasuper::read_env_valuefromapp_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<AppConfig> 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:
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:
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 errorscargo 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
Configmethods still work (they weren't touched) AppConfigstill compiles, itsDefaultimpl is intact- The bridge's round-trip test still passes:
config::bridge::tests::round_trip_default_configconfig::bridge::tests::round_trip_preserves_all_non_lossy_fieldsconfig::bridge::tests::to_app_config_copies_every_serialized_fieldconfig::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:
AppConfignow has 11 methods total: 7 reads from Step 3, 4 writes from Step 4#[allow(dead_code)]on bothimpl AppConfigblocks — safe to leave as-is, goes away when callers migrate in Steps 8+Configis 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_serversare still onConfigand must stay there:setup_model→ migrates in Step 7 with theset_*methodsupdate→ migrates in Step 7 with theset_*methodsload_functions→ migrates toRequestContextin Step 5 or Step 6 (whichever handlesFunctions)load_mcp_servers→ deleted/transformed in Step 6.5
What Step 5 should watch for
- Step 5 targets are
&selfrequest-read methods that read runtime fields likeself.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, notAppConfig, because they read per-request state. - Same duplication pattern applies. Add methods to
RequestContext, leave originals onConfig, no caller migration. sessions_dirandmessages_filealready usepaths::functions internally (from Step 2's migration). They readself.agentto decide between the global and agent-scoped path. Those paths come from thepathsmodule.role_like_mutis interesting — it's the helper that returns a mutable reference to whichever of role/session/agent is on top. It's the foundation for everyset_*method in Step 7. Migrate it toRequestContextin Step 5 so Step 7 has it ready.list_sessionsandlist_autoname_sessionswrappaths::list_file_nameswith some filtering. They take&selfto 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, orload_mcp_servers— each has a specific later-step home. - Don't touch the
bridge.rsconversions — still needed. - Don't touch
paths.rs— still complete. - Don't migrate any caller of any method yet — callers stay on
Configthrough 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
Configmethod bodies insrc/config/mod.rsfor each Step 5 target (search forpub 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 + newimpl AppConfigblock with 4 write methods) - Unchanged but referenced:
src/config/mod.rs(originalConfigmethods still exist, private helpersread_env_value/read_env_boolaccessed viasuper::)