Files
loki/docs/implementation/PHASE-1-STEP-16a-NOTES.md

6.8 KiB

Phase 1 Step 16a — Implementation Notes

Status

Done.

Plan reference

  • Parent plan: docs/implementation/PHASE-1-STEP-16-NOTES.md
  • Sub-phase goal: "Build AppConfig::from_config absorbing env/wrap/docs/user-agent/model-resolution logic"

Summary

Introduced AppConfig::from_config(config: Config) -> Result<AppConfig> as the canonical constructor for a fully-initialized AppConfig. The new constructor chains the four mutation methods (load_envs, set_wrap, setup_document_loaders, setup_user_agent) plus a new resolve_model() method that picks a default model when model_id is empty.

The existing bridge (Config::init + Config::to_app_config) is untouched. from_config is currently dead code (gated with #[allow(dead_code)]) — it becomes the entry point in Step 16e when main.rs switches over. The methods it calls (load_envs, etc.) are no longer dead code because they're reachable via from_config, so their #[allow(dead_code)] gates were removed.

What was changed

src/config/app_config.rs

Added:

  • AppConfig::from_config(config) -> Result<Self> — canonical constructor that copies serde fields, applies env overrides, validates wrap, installs doc loaders, resolves user agent, and ensures a default model.
  • AppConfig::resolve_model(&mut self) -> Result<()> — if model_id is empty, picks the first available chat model. Errors if no models are available. Replaces the logic from Config::setup_model that belongs on AppConfig (the Model-resolution half of Config::setup_model stays in Config for now — that moves in 16e).
  • 8 unit tests covering field copying, doc loader insertion, user agent resolution, wrap validation (valid + invalid), and resolve_model error/happy paths.

Removed #[allow(dead_code)] from:

  • set_wrap
  • setup_document_loaders
  • setup_user_agent
  • load_envs

These are now reachable via from_config. They remain pub because REPL-mode mutations (via .set wrap <value> or similar) will go through them once RequestContext stops mutating Config.

Removed entirely:

  • AppConfig::ensure_default_model_id — redundant with the new resolve_model. Had no callers outside itself (confirmed via grep).

Behavior parity notes

  1. from_config is non-destructive: it consumes Config by value (not &Config) since post-bridge, Config is no longer needed. This matches the long-term design.

  2. from_config vs to_app_config + mutations: The methods called inside from_config are identical bodies to the ones currently called on Config inside Config::init. Env var reads, wrap validation, doc loader defaults, and user agent resolution all produce the same state.

  3. resolve_model vs Config::setup_model:

    • Config::setup_model does TWO things: (a) ensure model_id is non-empty (pick default if empty) (b) resolve the Model struct via Model::retrieve_model and store it in self.model
    • AppConfig::resolve_model only does (a).
    • (b) happens today in cfg.set_model(&model_id) inside Config::setup_model. In the new architecture, the Model struct lives on RequestContext.model, and Model::retrieve_model(&app_config, &app_config.model_id, ...) will be called inside RequestContext::new (or equivalent) once the bridge is removed in 16e.

Files modified

  • src/config/app_config.rs — 2 new methods, 4 #[allow(dead_code)] gates removed, 1 method deleted, 8 new tests.

Files NOT modified

  • src/config/mod.rsConfig::init still runs all mutations on Config; bridge still copies to AppConfig. Unchanged in 16a.
  • src/config/bridge.rs — Untouched. Used by from_config internally (config.to_app_config()).
  • src/main.rs — Still uses the bridge flow. Switch happens in 16e.

Assumptions made

  1. from_config consumes Config by value (not &Config) — aligns with the long-term design where Config is discarded after conversion. No current caller would benefit from keeping the Config around after conversion.

  2. resolve_model narrow scope: only responsible for ensuring model_id is non-empty. Does NOT resolve a Model struct — that's RequestContext's job. This matches the split between AppConfig (the configuration) and RequestContext (the resolved runtime handle).

  3. #[allow(dead_code)] on from_config and resolve_model: they're unused until 16e. The gate is explicit so grep-hunts can find them when 16e switches over.

  4. User agent prefix in tests: I assumed the user agent prefix is not critical to test literally (it depends on the crate name). The test checks for a non-"auto" value containing / rather than matching loki-ai/. Safer against crate rename.

Open questions (parked for later sub-phases)

  1. Should from_config also run secret interpolation? Currently Config::init does a two-pass YAML parse where the raw content gets secrets injected from the vault, then the Config is re-parsed. In the new architecture this belongs in main.rs or a separate helper (the Config comes in already-interpolated). Not a 16a concern.

  2. Test naming convention: Existing tests use fn test_name_returns_value_when_condition. New tests use fn from_config_does_thing. Both styles present in the file; kept consistent with new code.

  3. ensure_default_model_id deletion: confirmed via grep that it had no callers outside itself. Deleted cleanly. If a future sub-phase needs the Option return variant, it can be re-added.

Verification

  • cargo check — clean, zero warnings
  • cargo clippy — clean, zero warnings
  • cargo test — 122 passing (114 pre-16a + 8 new), zero failures

Remaining work for Step 16

  • 16b: Extract install_builtins() as top-level free function
  • 16c: Migrate Vault::init(&Config)Vault::init(&AppConfig)
  • 16d: Build AppState::init(app_config, ...).await
  • 16e: Switch main.rs and all 15 Config::init() callers to the new flow
  • 16f: Delete Config runtime fields, bridge.rs, Config::init, duplicated methods

Migration direction preserved

After 16a, no runtime behavior has changed. The new entry point exists but isn't wired in. The bridge flow continues as before:

YAML → Config::load_from_file
    → Config::init (unchanged, does all current mutations)
        - load_envs, set_wrap, setup_document_loaders, ...
        - setup_model, load_functions, load_mcp_servers
    → cfg.to_app_config() → AppConfig (via bridge)
    → cfg.to_request_context(AppState) → RequestContext

New entry point ready for 16e:

AppConfig::from_config(config) → AppConfig
    (internally: to_app_config, load_envs, set_wrap,
     setup_document_loaders, setup_user_agent, resolve_model)