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

180 lines
6.8 KiB
Markdown

# 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.rs``Config::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<String> 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)
```