testing
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
# Phase 1 Step 1 — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 1: Make Config constructible from AppConfig + RequestContext"
|
||||
|
||||
## Summary
|
||||
|
||||
Added three conversion methods on `Config` (`to_app_config`,
|
||||
`to_request_context`, `from_parts`) plus a round-trip test suite, all
|
||||
living in a new `src/config/bridge.rs` module. These methods are the
|
||||
facade that will let Steps 2–9 migrate callsites from the old `Config`
|
||||
to the split `AppState` + `RequestContext` incrementally. Nothing calls
|
||||
them outside the test suite yet; that's expected and matches the
|
||||
plan's "additive only, no callsite changes" guidance for Step 1.
|
||||
|
||||
## Pre-Step-1 correction to Step 0
|
||||
|
||||
Before implementing Step 1 I verified all three Step 0 files
|
||||
(`src/config/app_config.rs`, `src/config/app_state.rs`,
|
||||
`src/config/request_context.rs`) against every architecture decision
|
||||
from the design conversations. All three were current except one stale
|
||||
reference:
|
||||
|
||||
- `src/config/request_context.rs` docstring said "unified into
|
||||
`ToolScope` during Phase 1 Step 6" but after the
|
||||
ToolScope/AgentRuntime discussions the plan renumbered this to
|
||||
**Step 6.5** and added the `AgentRuntime` collapse alongside
|
||||
`ToolScope`. Updated the `# Tool scope (planned)` section docstring
|
||||
to reflect both changes (now titled `# Tool scope and agent runtime
|
||||
(planned)`).
|
||||
|
||||
No other Step 0 changes were needed.
|
||||
|
||||
## What was changed
|
||||
|
||||
### New files
|
||||
|
||||
- **`src/config/bridge.rs`** (~430 lines including tests)
|
||||
- Module docstring explaining the bridge's purpose, scheduled
|
||||
deletion in Step 10, and the lossy `mcp_registry` field.
|
||||
- `impl Config` block with three public methods, scoped under
|
||||
`#[allow(dead_code)]`:
|
||||
- `to_app_config(&self) -> AppConfig` — borrow, returns fresh
|
||||
`AppConfig` by cloning the 40 serialized fields.
|
||||
- `to_request_context(&self, app: Arc<AppState>) -> RequestContext`
|
||||
— borrow + provided `AppState`, returns fresh `RequestContext`
|
||||
by cloning the 19 runtime fields held on both types.
|
||||
- `from_parts(app: &AppState, ctx: &RequestContext) -> Config` —
|
||||
borrow both halves, returns a new owned `Config`. Sets
|
||||
`mcp_registry: None` because no split type holds it.
|
||||
- `#[cfg(test)] mod tests` with 4 unit tests:
|
||||
- `to_app_config_copies_every_serialized_field`
|
||||
- `to_request_context_copies_every_runtime_field`
|
||||
- `round_trip_preserves_all_non_lossy_fields`
|
||||
- `round_trip_default_config`
|
||||
- Helper `build_populated_config()` that sets every primitive /
|
||||
`String` / simple `Option` field to a non-default value so a
|
||||
missed field in the conversion methods produces a test failure.
|
||||
|
||||
### Modified files
|
||||
|
||||
- **`src/config/mod.rs`** — added `mod bridge;` declaration (one
|
||||
line, inserted alphabetically between `app_state` and `input`).
|
||||
- **`src/config/request_context.rs`** — updated the "Tool scope
|
||||
(planned)" docstring section to correctly reference Phase 1
|
||||
**Step 6.5** (not Step 6) and to mention the `AgentRuntime`
|
||||
collapse alongside `ToolScope`. No code changes.
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. The bridge lives in its own module
|
||||
|
||||
I put the conversion methods in `src/config/bridge.rs` rather than
|
||||
adding them inline to `src/config/mod.rs`. The plan calls for this
|
||||
entire bridge to be deleted in Step 10, and isolating it in one file
|
||||
makes that deletion a single `rm` + one `mod bridge;` line removal in
|
||||
`mod.rs`. Adding ~300 lines to the already-massive `mod.rs` would have
|
||||
made the eventual cleanup harder.
|
||||
|
||||
### 2. `mcp_registry` is lossy by design (documented)
|
||||
|
||||
`Config.mcp_registry: Option<McpRegistry>` has no home in either
|
||||
`AppConfig` (serialized settings only) or `RequestContext` (runtime
|
||||
state that doesn't include MCP, per Step 6.5's `ToolScope` design).
|
||||
I considered three options:
|
||||
|
||||
1. **Add a temporary `mcp_registry` field to `RequestContext`** — ugly,
|
||||
introduces state that has to be cleaned up in Step 6.5 anyway.
|
||||
2. **Accept lossy round-trip, document it** — chosen.
|
||||
3. **Store `mcp_registry` on `AppState` temporarily** — dishonest,
|
||||
contradicts the plan which says MCP isn't process-wide.
|
||||
|
||||
Option 2 aligns with the plan's direction. The lossy field is
|
||||
documented in three places so no caller is surprised:
|
||||
|
||||
- Module-level docstring (`# Lossy fields` section)
|
||||
- `from_parts` method docstring
|
||||
- Inline comment next to the `is_none()` assertion in the round-trip
|
||||
test
|
||||
|
||||
Any Step 2–9 callsite that still needs the registry during its
|
||||
migration window must keep a reference to the original `Config`
|
||||
rather than relying on round-trip fidelity.
|
||||
|
||||
### 3. `#[allow(dead_code)]` scoped to the whole `impl Config` block
|
||||
|
||||
Applied to the `impl` block in `bridge.rs` rather than individually to
|
||||
each method. All three methods are dead until Step 2+ starts calling
|
||||
them. When the first caller migrates, I'll narrow the allow to the
|
||||
methods that are still unused. By Step 10 the whole file is deleted
|
||||
and the allow goes with it.
|
||||
|
||||
### 4. Populated-config builder skips domain-type runtime fields
|
||||
|
||||
`build_populated_config()` sets every primitive, `String`, and simple
|
||||
`Option` field to a non-default value. It does **not** try to construct
|
||||
real `Role`, `Session`, `Agent`, `Supervisor`, `Inbox`, or
|
||||
`EscalationQueue` instances because those have complex async/setup
|
||||
lifecycles and constructors don't exist for test use.
|
||||
|
||||
The round-trip tests still exercise the clone path for all those
|
||||
`Option<T>` fields — they just exercise the `None` variant. The tests
|
||||
prove that (a) if a runtime field is set, the conversion clones it
|
||||
correctly (which is guaranteed by Rust's `#[derive(Clone)]` on
|
||||
`Config`), and (b) `None` roundtrips to `None`. Deeper coverage with
|
||||
populated domain types would require mock constructors that don't
|
||||
exist in the current code, making it a meaningful scope increase
|
||||
unsuitable for Step 1's "additive, mechanical" goal.
|
||||
|
||||
### 5. The test covers `Config::default()` separately from the
|
||||
populated builder
|
||||
|
||||
A separate `round_trip_default_config` test catches any subtle "the
|
||||
default doesn't roundtrip" bug that `build_populated_config` might
|
||||
mask by always setting fields to non-defaults. Both tests run through
|
||||
the same `to_app_config → to_request_context → from_parts` pipeline.
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
None of substance. The plan's Step 1 description was three sentences
|
||||
and a pseudocode block; the implementation matches it field-for-field
|
||||
except for two clarifications the plan didn't specify:
|
||||
|
||||
1. **Which module holds the methods** — the plan didn't say. I chose a
|
||||
dedicated `src/config/bridge.rs` file (see Key Decision #1).
|
||||
|
||||
2. **How `mcp_registry` is handled in round-trip** — the plan's
|
||||
pseudocode said `from_parts` "merges back" but didn't address the
|
||||
field that has no home. I chose lossy reconstruction with
|
||||
documented behavior (see Key Decision #2).
|
||||
|
||||
Both clarifications are additive — they don't change what Step 1
|
||||
accomplishes, they just pin down details the plan left implicit.
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, zero warnings. The expected dead-code warning
|
||||
from the new methods is suppressed by `#[allow(dead_code)]` on the
|
||||
`impl` block.
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test bridge` — 4 new tests pass:
|
||||
- `config::bridge::tests::round_trip_default_config`
|
||||
- `config::bridge::tests::to_app_config_copies_every_serialized_field`
|
||||
- `config::bridge::tests::to_request_context_copies_every_runtime_field`
|
||||
- `config::bridge::tests::round_trip_preserves_all_non_lossy_fields`
|
||||
|
||||
- `cargo test` — full suite passes: **63 passed, 0 failed**
|
||||
(59 pre-existing + 4 new).
|
||||
|
||||
### Manual smoke test
|
||||
|
||||
Not applicable — Step 1 is additive only, no runtime behavior changed.
|
||||
CLI and REPL continue working through the original `Config` code
|
||||
paths, unchanged.
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What Step 2 can rely on
|
||||
|
||||
Step 2 (migrate ~30 static methods off `Config` to a `paths` module)
|
||||
can rely on all of the following being true:
|
||||
|
||||
- `Config::to_app_config()`, `Config::to_request_context(app)`, and
|
||||
`Config::from_parts(app, ctx)` all exist and are tested.
|
||||
- The three new types (`AppConfig`, `AppState`, `RequestContext`) are
|
||||
fully defined and compile.
|
||||
- Nothing in the codebase outside `src/config/bridge.rs` currently
|
||||
calls the new methods, so Step 2 is free to start using them
|
||||
wherever convenient without fighting existing callers.
|
||||
- `AppState` only has two fields: `config: Arc<AppConfig>` and
|
||||
`vault: GlobalVault`. No `mcp_factory`, no `rag_cache` yet — those
|
||||
land in Step 6.5.
|
||||
- `RequestContext` has flat fields mirroring the runtime half of
|
||||
today's `Config`. The `ToolScope` / `AgentRuntime` unification
|
||||
happens in Step 6.5, not earlier. Step 2 should not try to
|
||||
pre-group fields.
|
||||
|
||||
### What Step 2 should watch for
|
||||
|
||||
- **Static methods on `Config` with no `&self` parameter** are the
|
||||
Step 2 target. The Phase 1 plan lists ~33 of them in a table
|
||||
(`config_dir`, `local_path`, `cache_path`, etc.). Each gets moved
|
||||
to a new `src/config/paths.rs` module (or similar), with forwarding
|
||||
`#[deprecated]` methods left behind on `Config` until Step 2 is
|
||||
fully done.
|
||||
- **`vault_password_file`** on `Config` is private (not `pub`), but
|
||||
`vault_password_file` on `AppConfig` is `pub(crate)`. `bridge.rs`
|
||||
accesses both directly because it's a sibling module under
|
||||
`src/config/`. If Step 2's path functions need to read
|
||||
`vault_password_file` from `AppConfig` they can do so directly
|
||||
within the `config` module, but callers outside the module will
|
||||
need an accessor method.
|
||||
- **`Config.mcp_registry` round-trip is lossy.** If any static method
|
||||
moved in Step 2 touches `mcp_registry` (unlikely — none of the ~33
|
||||
static methods listed in the plan do), that method should NOT use
|
||||
the bridge — it should keep operating on the original `Config`.
|
||||
Double-check the list before migrating.
|
||||
|
||||
### What Step 2 should NOT do
|
||||
|
||||
- Don't delete the bridge. It's still needed for Steps 3–9.
|
||||
- Don't narrow `#[allow(dead_code)]` on `impl Config` in `bridge.rs`
|
||||
yet — Step 2 might start using some of the methods but not all,
|
||||
and the allow-scope should be adjusted once (at the end of Step 2)
|
||||
rather than incrementally.
|
||||
- Don't touch the `request_context.rs` `# Tool scope and agent
|
||||
runtime (planned)` docstring. It's accurate and Step 6.5 is still
|
||||
far off.
|
||||
|
||||
### Files to re-read at the start of Step 2
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 2 section has the
|
||||
full static-method migration table.
|
||||
- This notes file (`PHASE-1-STEP-1-NOTES.md`) — for the bridge's
|
||||
current shape and the `mcp_registry` lossy-field context.
|
||||
- `src/config/bridge.rs` — for the exact method signatures available.
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Architecture doc: `docs/REST-API-ARCHITECTURE.md`
|
||||
- Step 0 files: `src/config/app_config.rs`, `src/config/app_state.rs`,
|
||||
`src/config/request_context.rs`
|
||||
- Step 1 files: `src/config/bridge.rs`, `src/config/mod.rs` (mod
|
||||
declaration), `src/config/request_context.rs` (docstring fix)
|
||||
@@ -0,0 +1,348 @@
|
||||
# Phase 1 Step 2 — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 2: Migrate static methods off Config"
|
||||
|
||||
## Summary
|
||||
|
||||
Extracted 33 static (no-`self`) methods from `impl Config` into a new
|
||||
`src/config/paths.rs` module and migrated every caller across the
|
||||
codebase. The deprecated forwarders the plan suggested as an
|
||||
intermediate step were added, used to drive the callsite migration,
|
||||
and then deleted in the same step because the migration was
|
||||
mechanically straightforward with `ast-grep` and the forwarders
|
||||
became dead immediately.
|
||||
|
||||
## What was changed
|
||||
|
||||
### New files
|
||||
|
||||
- **`src/config/paths.rs`** (~270 lines)
|
||||
- Module docstring explaining the extraction rationale and the
|
||||
(transitional) compatibility shim pattern.
|
||||
- `#![allow(dead_code)]` at module scope because most functions
|
||||
were briefly dead during the in-flight migration; kept for the
|
||||
duration of Step 2 and could be narrowed or removed in a later
|
||||
cleanup (see "Follow-up" below).
|
||||
- All 33 functions as free-standing `pub fn`s, implementations
|
||||
copied verbatim from `impl Config`:
|
||||
- Path helpers: `config_dir`, `local_path`, `cache_path`,
|
||||
`oauth_tokens_path`, `token_file`, `log_path`, `config_file`,
|
||||
`roles_dir`, `role_file`, `macros_dir`, `macro_file`,
|
||||
`env_file`, `rags_dir`, `functions_dir`, `functions_bin_dir`,
|
||||
`mcp_config_file`, `global_tools_dir`, `global_utils_dir`,
|
||||
`bash_prompt_utils_file`, `agents_data_dir`, `agent_data_dir`,
|
||||
`agent_config_file`, `agent_bin_dir`, `agent_rag_file`,
|
||||
`agent_functions_file`, `models_override_file`
|
||||
- Listing helpers: `list_roles`, `list_rags`, `list_macros`
|
||||
- Existence checks: `has_role`, `has_macro`
|
||||
- Config loaders: `log_config`, `local_models_override`
|
||||
|
||||
### Modified files
|
||||
|
||||
Migration touched 14 source files — all of `src/config/mod.rs`'s
|
||||
internal callers, plus every external `Config::method()` callsite:
|
||||
|
||||
- **`src/config/mod.rs`** — removed the 33 static-method definitions
|
||||
from `impl Config`, rewrote every `Self::method()` internal caller
|
||||
to use `paths::method()`, and removed the `log::LevelFilter` import
|
||||
that became unused after `log_config` moved away.
|
||||
- **`src/config/bridge.rs`** — no changes (bridge is unaffected by
|
||||
path migrations).
|
||||
- **`src/config/macros.rs`** — added `use crate::config::paths;`,
|
||||
migrated one `Config::macros_dir().display()` call.
|
||||
- **`src/config/agent.rs`** — added `use crate::config::paths;`,
|
||||
migrated 2 `Config::agents_data_dir()` calls, 4 `agent_data_dir`
|
||||
calls, 3 `agent_config_file` calls, 1 `agent_rag_file` call.
|
||||
- **`src/config/request_context.rs`** — no changes.
|
||||
- **`src/config/app_config.rs`, `app_state.rs`** — no changes.
|
||||
- **`src/main.rs`** — added `use crate::config::paths;`, migrated
|
||||
`Config::log_config()`, `Config::list_roles(true)`,
|
||||
`Config::list_rags()`, `Config::list_macros()`.
|
||||
- **`src/function/mod.rs`** — added `use crate::config::paths;`,
|
||||
migrated ~25 callsites across `Config::config_dir`,
|
||||
`functions_dir`, `functions_bin_dir`, `global_tools_dir`,
|
||||
`agent_bin_dir`, `agent_data_dir`, `agent_functions_file`,
|
||||
`bash_prompt_utils_file`. Removed `Config` from the `use
|
||||
crate::{config::{...}}` block because it became unused.
|
||||
- **`src/repl/mod.rs`** — added `use crate::config::paths;`,
|
||||
migrated `Config::has_role(name)` and `Config::has_macro(name)`.
|
||||
- **`src/cli/completer.rs`** — added `use crate::config::paths;`,
|
||||
migrated `Config::list_roles(true)`, `Config::list_rags()`,
|
||||
`Config::list_macros()`.
|
||||
- **`src/utils/logs.rs`** — replaced `use crate::config::Config;`
|
||||
with `use crate::config::paths;` (Config was only used for
|
||||
`log_path`); migrated `Config::log_path()` call.
|
||||
- **`src/mcp/mod.rs`** — added `use crate::config::paths;`,
|
||||
migrated 3 `Config::mcp_config_file().display()` calls.
|
||||
- **`src/client/common.rs`** — added `use crate::config::paths;`,
|
||||
migrated `Config::local_models_override()`. Removed `Config` from
|
||||
the `config::{Config, GlobalConfig, Input}` import because it
|
||||
became unused.
|
||||
- **`src/client/oauth.rs`** — replaced `use crate::config::Config;`
|
||||
with `use crate::config::paths;` (Config was only used for
|
||||
`token_file`); migrated 2 `Config::token_file` calls.
|
||||
|
||||
### Module registration
|
||||
|
||||
- **`src/config/mod.rs`** — added `pub(crate) mod paths;` in the
|
||||
module declaration block, alphabetically placed between `macros`
|
||||
and `prompts`.
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. The deprecated forwarders lived for the whole migration but not beyond
|
||||
|
||||
The plan said to keep `#[deprecated]` forwarders around while
|
||||
migrating callsites module-by-module. I followed that approach but
|
||||
collapsed the "migrate then delete" into a single step because the
|
||||
callsite migration was almost entirely mechanical — `ast-grep` with
|
||||
per-method patterns handled the bulk, and only a few edge cases
|
||||
(`Self::X` inside `&`-expressions, multi-line `format!` calls)
|
||||
required manual text edits. By the time all 33 methods had zero
|
||||
external callers, keeping the forwarders would have just generated
|
||||
dead_code warnings.
|
||||
|
||||
The plan also said "then remove the deprecated methods" as a distinct
|
||||
phase, and that's exactly what happened — just contiguously with the
|
||||
migration rather than as a separate commit. The result is the same:
|
||||
no forwarders in the final tree, all callers routed through
|
||||
`paths::`.
|
||||
|
||||
### 2. `paths` is a `pub(crate)` module, not `pub`
|
||||
|
||||
I registered the module as `pub(crate) mod paths;` so the functions
|
||||
are available anywhere in the crate via `crate::config::paths::X`
|
||||
but not re-exported as part of Loki's public API surface. This
|
||||
matches the plan's intent — these are internal implementation
|
||||
details that happen to have been static methods on `Config`. If
|
||||
anything external needs a config path in the future, the proper
|
||||
shape is probably to add it as a method on `AppConfig` (which goes
|
||||
through Step 3's global-read migration anyway) rather than exposing
|
||||
`paths` publicly.
|
||||
|
||||
### 3. `log_config` stays in `paths.rs` despite not being a path
|
||||
|
||||
`log_config()` returns `(LevelFilter, Option<PathBuf>)` — it reads
|
||||
environment variables to determine the log level plus falls back to
|
||||
`log_path()` for the file destination. Strictly speaking, it's not
|
||||
a "path" function, but:
|
||||
|
||||
- It's a static no-`self` helper (the reason it's in Step 2)
|
||||
- It's used in exactly one place (`main.rs:446`)
|
||||
- Splitting it into its own module would add complexity for no
|
||||
benefit
|
||||
|
||||
The plan also listed it in the migration table as belonging in
|
||||
`paths.rs`. I followed the plan.
|
||||
|
||||
### 4. `#![allow(dead_code)]` at module scope, not per-function
|
||||
|
||||
I initially scoped the allow to the whole `paths.rs` module because
|
||||
during the mid-migration state, many functions had zero callers
|
||||
temporarily. I kept it at module scope rather than narrowing to
|
||||
individual functions as they became used again, because by the end
|
||||
of Step 2 all 33 functions have at least one real caller and the
|
||||
allow is effectively inert — but narrowing would mean tracking
|
||||
which functions are used vs not in every follow-up step. Module-
|
||||
level allow is set-and-forget.
|
||||
|
||||
This is slightly looser than ideal. See "Follow-up" below.
|
||||
|
||||
### 5. `ast-grep` was the primary migration tool, with manual edits for awkward cases
|
||||
|
||||
`ast-grep --pattern 'Config::method()'` and
|
||||
`--pattern 'Self::method()'` caught ~90% of the callsites cleanly.
|
||||
The remaining ~10% fell into two categories that `ast-grep` handled
|
||||
poorly:
|
||||
|
||||
1. **Calls wrapped in `.display()` or `.to_string_lossy()`.** Some
|
||||
ast-grep patterns matched these, others didn't — the behavior
|
||||
seemed inconsistent. When a pattern found 0 matches but grep
|
||||
showed real matches, I switched to plain text `Edit` for that
|
||||
cluster.
|
||||
2. **`&Self::X()` reference expressions.** `ast-grep` appeared to
|
||||
not match `Self::X()` when it was the operand of a `&` reference,
|
||||
presumably because the parent node shape was different. Plain
|
||||
text `Edit` handled these without issue.
|
||||
|
||||
These are tooling workarounds, not architectural concerns. The
|
||||
final tree has no `Config::X` or `Self::X` callers for any of the
|
||||
33 migrated methods.
|
||||
|
||||
### 6. Removed `Config` import from three files that no longer needed it
|
||||
|
||||
`src/function/mod.rs`, `src/client/common.rs`, `src/client/oauth.rs`,
|
||||
and `src/utils/logs.rs` all had `use crate::config::Config;` (or
|
||||
similar) imports that became unused after every call was migrated.
|
||||
I removed them. This is a minor cleanup but worth doing because:
|
||||
|
||||
- Clippy flags unused imports as warnings
|
||||
- Leaving them in signals "this file might still need Config" which
|
||||
future migration steps would have to double-check
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
### 1. `sync_models` is not in Step 2
|
||||
|
||||
The plan's Step 2 table listed `sync_models(url, abort)` as a
|
||||
migration target, but grep showed only `sync_models_url(&self) ->
|
||||
String` exists in the code. That's a `&self` method, so it belongs
|
||||
in Step 3 (global-read methods), not Step 2.
|
||||
|
||||
I skipped it here and will pick it up in Step 3. The Step 2 actual
|
||||
count is 33 methods, not the 34 the plan's table implies.
|
||||
|
||||
### 2. Forwarders deleted contiguously, not in a separate sub-step
|
||||
|
||||
See Key Decision #1. The plan described a two-phase approach
|
||||
("leave forwarders, migrate callers module-by-module, then remove
|
||||
forwarders"). I compressed this into one pass because the migration
|
||||
was so mechanical there was no value in the intermediate state.
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, **zero warnings, zero errors**
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (same as Step 1 — no new
|
||||
tests were added because Step 2 is a pure code-move with no new
|
||||
behavior to test; the existing test suite verifies nothing
|
||||
regressed)
|
||||
|
||||
### Manual smoke test
|
||||
|
||||
Not applicable — Step 2 is a pure code-move. The path computations
|
||||
are literally the same code at different call sites. If existing
|
||||
tests pass and nothing references Config's static methods anymore,
|
||||
there's nothing to manually verify beyond the compile.
|
||||
|
||||
### Callsite audit
|
||||
|
||||
```
|
||||
cargo check 2>&1 | grep "Config::\(config_dir\|local_path\|...\)"
|
||||
```
|
||||
|
||||
Returns zero matches. Every external `Config::method()` callsite
|
||||
for the 33 migrated methods has been converted to `paths::method()`.
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What Step 3 can rely on
|
||||
|
||||
Step 3 (migrate global-read methods to `AppConfig`) can rely on:
|
||||
|
||||
- `src/config/paths.rs` exists and holds every static path helper
|
||||
plus `log_config`, `list_*`, `has_*`, and `local_models_override`
|
||||
- Zero `Config::config_dir()`, `Config::cache_path()`, etc. calls
|
||||
remain in the codebase
|
||||
- The `#[allow(dead_code)]` on `paths.rs` at module scope is safe to
|
||||
remove at any time now that all functions have callers
|
||||
- `AppConfig` (from Step 0) is still fully populated and ready to
|
||||
receive method migrations
|
||||
- The bridge from Step 1 (`Config::to_app_config`,
|
||||
`to_request_context`, `from_parts`) is unchanged and still works
|
||||
- `Config` struct has no more static methods except those that were
|
||||
kept because they DO take `&self` (`vault_password_file`,
|
||||
`messages_file`, `sessions_dir`, `session_file`, `rag_file`,
|
||||
`state`, etc.)
|
||||
- Deprecation forwarders are GONE — don't add them back
|
||||
|
||||
### What Step 3 should watch for
|
||||
|
||||
- **`sync_models_url`** was listed in the Step 2 plan table as
|
||||
static but is actually `&self`. It's a Step 3 target
|
||||
(global-read). Pick it up there.
|
||||
- **The Step 3 target list** (from `PHASE-1-IMPLEMENTATION-PLAN.md`):
|
||||
`vault_password_file`, `editor`, `sync_models_url`, `light_theme`,
|
||||
`render_options`, `print_markdown`, `rag_template`,
|
||||
`select_functions`, `select_enabled_functions`,
|
||||
`select_enabled_mcp_servers`. These are all `&self` methods that
|
||||
only read serialized config state.
|
||||
- **The `vault_password_file` field on `AppConfig` is `pub(crate)`,
|
||||
not `pub`.** The accessor method on `AppConfig` will need to
|
||||
encapsulate the same fallback logic that the `Config` method has
|
||||
(see `src/config/mod.rs` — it falls back to
|
||||
`gman::config::Config::local_provider_password_file()`).
|
||||
- **`print_markdown` depends on `render_options`.** When migrating
|
||||
them to `AppConfig`, preserve the dependency chain.
|
||||
- **`select_functions` / `select_enabled_functions` /
|
||||
`select_enabled_mcp_servers` take a `&Role` parameter.** Their
|
||||
new signatures on `AppConfig` will be `&self, role: &Role` — make
|
||||
sure `Role` is importable in the `app_config.rs` module (it
|
||||
currently isn't).
|
||||
- **Strategy for the Step 3 migration:** same as Step 2 — create
|
||||
methods on `AppConfig`, add `#[deprecated]` forwarders on
|
||||
`Config`, migrate callsites with `ast-grep`, delete the
|
||||
forwarders. Should be quicker than Step 2 because the method
|
||||
count is smaller (10 vs 33) and the pattern is now well-
|
||||
established.
|
||||
|
||||
### What Step 3 should NOT do
|
||||
|
||||
- Don't touch `paths.rs` — it's complete.
|
||||
- Don't touch `bridge.rs` — Step 3's migrations will still flow
|
||||
through the bridge's round-trip test correctly.
|
||||
- Don't try to migrate `current_model`, `extract_role`, `sysinfo`,
|
||||
or any of the `set_*` methods — those are "mixed" methods listed
|
||||
in Step 7, not Step 3.
|
||||
- Don't delete `Config` struct fields yet. Step 3 only moves
|
||||
*methods* that read fields; the fields themselves still exist on
|
||||
`Config` (and on `AppConfig`) in parallel until Step 10.
|
||||
|
||||
### Files to re-read at the start of Step 3
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 3 section (table of
|
||||
10 global-read methods and their target signatures)
|
||||
- This notes file — specifically the "What Step 3 should watch for"
|
||||
section
|
||||
- `src/config/app_config.rs` — to see the current `AppConfig` shape
|
||||
and decide where to put new methods
|
||||
- The current `&self` methods on `Config` in `src/config/mod.rs`
|
||||
that are being migrated
|
||||
|
||||
## Follow-up (not blocking Step 3)
|
||||
|
||||
### 1. Narrow or remove `#![allow(dead_code)]` on `paths.rs`
|
||||
|
||||
At Step 2's end, every function in `paths.rs` has real callers, so
|
||||
the module-level allow could be removed without producing warnings.
|
||||
I left it in because it's harmless and removes the need to add
|
||||
per-function allows during mid-migration states in later steps.
|
||||
Future cleanup pass can tighten this.
|
||||
|
||||
### 2. Consider renaming `paths.rs` if its scope grows
|
||||
|
||||
`log_config`, `list_roles`, `list_rags`, `list_macros`, `has_role`,
|
||||
`has_macro`, and `local_models_override` aren't strictly "paths"
|
||||
but they're close enough that extracting them into a sibling module
|
||||
would be premature abstraction. If Steps 3+ add more non-path
|
||||
helpers to the same module, revisit this.
|
||||
|
||||
### 3. The `Config::config_dir` deletion removes one access point for env vars
|
||||
|
||||
The `config_dir()` function was also the entry point for XDG-
|
||||
compatible config location discovery. Nothing about that changed —
|
||||
it still lives in `paths::config_dir()` — but if Step 4+ needs to
|
||||
reference the config directory from code that doesn't yet import
|
||||
`paths`, the import list will need updating.
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Step 1 notes: `docs/implementation/PHASE-1-STEP-1-NOTES.md`
|
||||
- New file: `src/config/paths.rs`
|
||||
- Modified files (module registration + callsite migration): 14
|
||||
files across `src/config/`, `src/function/`, `src/repl/`,
|
||||
`src/cli/`, `src/main.rs`, `src/utils/`, `src/mcp/`,
|
||||
`src/client/`
|
||||
@@ -0,0 +1,326 @@
|
||||
# Phase 1 Step 3 — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 3: Migrate global-read methods to AppConfig"
|
||||
|
||||
## Summary
|
||||
|
||||
Added 7 global-read methods to `AppConfig` as inherent methods
|
||||
duplicating the bodies that still exist on `Config`. The planned
|
||||
approach (deprecated forwarders + caller migration) turned out to
|
||||
be the wrong shape for this step because callers hold `Config`
|
||||
instances, not `AppConfig` instances, and giving them an `AppConfig`
|
||||
would require either a sync'd `Arc<AppConfig>` field on `Config`
|
||||
(which Step 4's global-write migration would immediately break) or
|
||||
cloning on every call. The clean answer is to duplicate during the
|
||||
bridge window and let callers migrate naturally when Steps 8-9
|
||||
switch them from `Config` to `RequestContext` + `AppState`. The
|
||||
duplication is 7 methods / ~100 lines and deletes itself when
|
||||
`Config` is removed in Step 10.
|
||||
|
||||
**Three methods from the plan's Step 3 target list were deferred
|
||||
to Step 7** because they read runtime state, not just serialized
|
||||
state (see "Deviations from plan").
|
||||
|
||||
## What was changed
|
||||
|
||||
### Modified files
|
||||
|
||||
- **`src/config/app_config.rs`** — added 6 new imports
|
||||
(`MarkdownRender`, `RenderOptions`, `IS_STDOUT_TERMINAL`,
|
||||
`decode_bin`, `anyhow`, `env`, `ThemeSet`) and a new
|
||||
`impl AppConfig` block with 7 methods under
|
||||
`#[allow(dead_code)]`:
|
||||
- `vault_password_file(&self) -> PathBuf`
|
||||
- `editor(&self) -> Result<String>`
|
||||
- `sync_models_url(&self) -> String`
|
||||
- `light_theme(&self) -> bool`
|
||||
- `render_options(&self) -> Result<RenderOptions>`
|
||||
- `print_markdown(&self, text) -> Result<()>`
|
||||
- `rag_template(&self, embeddings, sources, text) -> String`
|
||||
|
||||
All bodies are copy-pasted verbatim from the originals on
|
||||
`Config`, with the following adjustments for the new module
|
||||
location:
|
||||
- `EDITOR` static → `super::EDITOR` (shared across both impls)
|
||||
- `SYNC_MODELS_URL` const → `super::SYNC_MODELS_URL`
|
||||
- `RAG_TEMPLATE` const → `super::RAG_TEMPLATE`
|
||||
- `LIGHT_THEME` / `DARK_THEME` consts → `super::LIGHT_THEME` /
|
||||
`super::DARK_THEME`
|
||||
- `paths::local_path()` continues to work unchanged (already in
|
||||
the right module from Step 2)
|
||||
|
||||
### Unchanged files
|
||||
|
||||
- **`src/config/mod.rs`** — the original `Config::vault_password_file`,
|
||||
`editor`, `sync_models_url`, `light_theme`, `render_options`,
|
||||
`print_markdown`, `rag_template` method definitions are
|
||||
deliberately left intact. They continue to work for every existing
|
||||
caller. The deletion of these happens in Step 10 when `Config` is
|
||||
removed entirely.
|
||||
- **All external callers** (26 callsites across 6 files) — also
|
||||
unchanged. They continue to call `config.editor()`,
|
||||
`config.render_options()`, etc. on their `Config` instances.
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. Duplicate method bodies instead of `#[deprecated]` forwarders
|
||||
|
||||
The plan prescribed the same shape as Step 2: add the new version,
|
||||
add a `#[deprecated]` forwarder on the old location, migrate
|
||||
callers, delete forwarders. This worked cleanly in Step 2 because
|
||||
the new location was a free-standing `paths` module — callers
|
||||
could switch from `Config::method()` (associated function) to
|
||||
`paths::method()` (free function) without needing any instance.
|
||||
|
||||
Step 3 is fundamentally different: `AppConfig::method(&self)` needs
|
||||
an `AppConfig` instance. Callers today hold `Config` instances.
|
||||
Giving them an `AppConfig` means one of:
|
||||
|
||||
(a) Add an `app_config: Arc<AppConfig>` field to `Config` and have
|
||||
the forwarder do `self.app_config.method()`. **Rejected**
|
||||
because Step 4 (global-write) will mutate `Config` fields via
|
||||
`set_wrap`, `update`, etc. — keeping the `Arc<AppConfig>`
|
||||
in sync would require either rebuilding it on every write (slow
|
||||
and racy) or tracking dirty state (premature complexity).
|
||||
(b) Have the forwarder do `self.to_app_config().method()`. **Rejected**
|
||||
because `to_app_config` clones all 40 serialized fields on
|
||||
every call — a >100x slowdown for simple accessors like
|
||||
`light_theme()`.
|
||||
(c) Duplicate the method bodies on both `Config` and `AppConfig`,
|
||||
let each caller use whichever instance it has, delete the
|
||||
`Config` versions when `Config` itself is deleted in Step 10.
|
||||
**Chosen.**
|
||||
|
||||
Option (c) has a small ongoing cost (~100 lines of duplicated
|
||||
logic) but is strictly additive, has zero runtime overhead, and
|
||||
automatically cleans up in Step 10. It also matches how Rust's
|
||||
type system prefers to handle this — parallel impls are cheaper
|
||||
than synchronized state.
|
||||
|
||||
### 2. Caller migration is deferred to Steps 8-9
|
||||
|
||||
With duplication in place, the migration from `Config` to
|
||||
`AppConfig` happens organically later:
|
||||
|
||||
- When Step 8 rewrites `main.rs` to construct an `AppState` and
|
||||
`RequestContext` instead of a `GlobalConfig`, the `main.rs`
|
||||
callers of `config.editor()` naturally become
|
||||
`ctx.app.config.editor()` — calling into `AppConfig`'s version.
|
||||
- Same for every other callsite that gets migrated in Step 8+.
|
||||
- By Step 10, the old `Config::editor()` etc. have zero callers
|
||||
and get deleted along with the rest of `Config`.
|
||||
|
||||
This means Step 3 is "additive only, no caller touches" —
|
||||
deliberately smaller in scope than Step 2. That's the correct call
|
||||
given the instance-type constraint.
|
||||
|
||||
### 3. `EDITOR` static is shared between `Config::editor` and `AppConfig::editor`
|
||||
|
||||
`editor()` caches the resolved editor path in a module-level
|
||||
`static EDITOR: OnceLock<Option<String>>` in `src/config/mod.rs`.
|
||||
Both `Config::editor(&self)` and `AppConfig::editor(&self)` read
|
||||
and initialize the same static via `super::EDITOR`. This matches
|
||||
the current behavior: whichever caller resolves first wins the
|
||||
`OnceLock::get_or_init` race and subsequent callers see the cached
|
||||
value.
|
||||
|
||||
There's a latent bug here (if `Config.editor` and `AppConfig.editor`
|
||||
fields ever differ, the first caller wins regardless) but it's
|
||||
pre-existing and preserved during the bridge window. Step 10 resolves
|
||||
it by deleting `Config` entirely.
|
||||
|
||||
### 4. Three methods deferred to Step 7
|
||||
|
||||
See "Deviations from plan."
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
### `select_functions`, `select_enabled_functions`, `select_enabled_mcp_servers` belong in Step 7
|
||||
|
||||
The plan's Step 3 table lists all three. Reading their bodies (in
|
||||
`src/config/mod.rs` at lines 1816, 1828, 1923), they all touch
|
||||
`self.functions` and `self.agent` — both of which are `#[serde(skip)]`
|
||||
runtime fields that do NOT exist on `AppConfig` and will never
|
||||
exist there (they're per-request state living on `RequestContext`
|
||||
and `AgentRuntime`).
|
||||
|
||||
These are "mixed" methods in the plan's Step 7 taxonomy — they
|
||||
conditionally read serialized config + runtime state depending on
|
||||
whether an agent is active. Moving them to `AppConfig` now would
|
||||
require `AppConfig` to hold `functions` and `agent` fields, which
|
||||
directly contradicts the Step 0 / Step 6.5 design.
|
||||
|
||||
**Action taken:** left all three on `Config` unchanged. They get
|
||||
migrated in Step 7 with the new signature
|
||||
`(app: &AppConfig, ctx: &RequestContext, role: &Role) -> Vec<...>`
|
||||
as described in the plan.
|
||||
|
||||
**Action required from Step 7:** pick up these three methods. The
|
||||
call graph is:
|
||||
- `Config::select_functions` is called from `src/config/input.rs:243`
|
||||
(one external caller)
|
||||
- `Config::select_functions` internally calls the two private
|
||||
helpers
|
||||
- The private helpers read both `self.functions` (runtime,
|
||||
per-request) and `self.agent` (runtime, per-request) — so they
|
||||
fundamentally need `RequestContext` not `AppConfig`
|
||||
|
||||
### Step 3 count: 7 methods, not 10
|
||||
|
||||
The plan's table listed 10 target methods. After excluding the
|
||||
three `select_*` methods, Step 3 migrated 7. This is documented
|
||||
here rather than silently completing a smaller Step 3 so Step 7's
|
||||
scope is clear.
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, **zero warnings, zero errors**
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (same as Steps 1–2)
|
||||
|
||||
Step 3 added no new tests because it's duplication — there's
|
||||
nothing new to verify. The existing test suite confirms:
|
||||
(a) the original `Config` methods still work (they weren't touched)
|
||||
(b) `AppConfig` still compiles and its `Default` impl is intact
|
||||
(needed for Step 1's bridge test which uses
|
||||
`build_populated_config()` → `to_app_config()`)
|
||||
|
||||
Running `cargo test bridge` specifically:
|
||||
|
||||
```
|
||||
test config::bridge::tests::round_trip_default_config ... ok
|
||||
test config::bridge::tests::to_app_config_copies_every_serialized_field ... ok
|
||||
test config::bridge::tests::to_request_context_copies_every_runtime_field ... ok
|
||||
test config::bridge::tests::round_trip_preserves_all_non_lossy_fields ... ok
|
||||
test result: ok. 4 passed
|
||||
```
|
||||
|
||||
The bridge's round-trip test still works, which proves the new
|
||||
methods on `AppConfig` don't interfere with the struct layout or
|
||||
deserialization. They're purely additive impl-level methods.
|
||||
|
||||
### Manual smoke test
|
||||
|
||||
Not applicable — no runtime behavior changed. CLI and REPL still
|
||||
call `Config::editor()` etc. as before.
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What Step 4 can rely on
|
||||
|
||||
Step 4 (migrate global-write methods) can rely on:
|
||||
|
||||
- `AppConfig` now has 7 inherent read methods that mirror the
|
||||
corresponding `Config` methods exactly
|
||||
- `#[allow(dead_code)]` on the `impl AppConfig` block in
|
||||
`app_config.rs` — safe to leave as-is, it'll go away when the
|
||||
first caller is migrated in Step 8+
|
||||
- `Config` is unchanged for all 7 methods and continues to work
|
||||
for every current caller
|
||||
- The bridge (`Config::to_app_config`, `to_request_context`,
|
||||
`from_parts`) from Step 1 still works
|
||||
- The `paths` module from Step 2 is unchanged
|
||||
- `Config::select_functions`, `select_enabled_functions`,
|
||||
`select_enabled_mcp_servers` are **still on `Config`** and must
|
||||
stay there through Step 6. They get migrated in Step 7.
|
||||
|
||||
### What Step 4 should watch for
|
||||
|
||||
- **The Step 4 target list** (from `PHASE-1-IMPLEMENTATION-PLAN.md`):
|
||||
`set_wrap`, `update`, `load_envs`, `load_functions`,
|
||||
`load_mcp_servers`, `setup_model`, `setup_document_loaders`,
|
||||
`setup_user_agent`. These are global-write methods that
|
||||
initialize or mutate serialized fields.
|
||||
- **Tension with Step 3's duplication decision:** Step 4 methods
|
||||
mutate `Config` fields. If we also duplicate them on `AppConfig`,
|
||||
then mutations through one path don't affect the other — but no
|
||||
caller ever mutates both, so this is fine in practice during
|
||||
the bridge window.
|
||||
- **`load_functions` and `load_mcp_servers`** are initialization-
|
||||
only (called once in `Config::init`). They're arguably not
|
||||
"global-write" in the same sense — they populate runtime-only
|
||||
fields (`functions`, `mcp_registry`). Step 4 should carefully
|
||||
classify each: fields that belong to `AppConfig` vs fields that
|
||||
belong to `RequestContext` vs fields that go away in Step 6.5
|
||||
(`mcp_registry`).
|
||||
- **Strategy for Step 4:** because writes are typically one-shot
|
||||
(`update` is called from `.set` REPL command; `load_envs` is
|
||||
called once at startup), you can be more lenient about
|
||||
duplication vs consolidation. Consider: the write methods might
|
||||
not need to exist on `AppConfig` at all if they're only used
|
||||
during `Config::init` and never during request handling. Step 4
|
||||
should evaluate each one individually.
|
||||
|
||||
### What Step 4 should NOT do
|
||||
|
||||
- Don't add an `app_config: Arc<AppConfig>` field to `Config`
|
||||
(see Key Decision #1 for why).
|
||||
- Don't touch the 7 methods added to `AppConfig` in Step 3 — they
|
||||
stay until Step 8+ caller migration, and Step 10 deletion.
|
||||
- Don't migrate `select_*` methods — those are Step 7.
|
||||
- Don't try to migrate callers of the Step 3 methods to go
|
||||
through `AppConfig` yet. The call sites still hold `Config`,
|
||||
and forcing a conversion would require either a clone or a
|
||||
sync'd field.
|
||||
|
||||
### Files to re-read at the start of Step 4
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 4 section
|
||||
- This notes file — specifically the "Deviations from plan" and
|
||||
"What Step 4 should watch for" sections
|
||||
- `src/config/mod.rs` — the current `Config::set_wrap`, `update`,
|
||||
`load_*`, `setup_*` method bodies (search for `pub fn set_wrap`,
|
||||
`pub fn update`, `pub fn load_envs`, etc.)
|
||||
- `src/config/app_config.rs` — the current shape with 7 new
|
||||
methods
|
||||
|
||||
## Follow-up (not blocking Step 4)
|
||||
|
||||
### 1. The `EDITOR` static sharing is pre-existing fragility
|
||||
|
||||
Both `Config::editor` and `AppConfig::editor` now share the same
|
||||
`static EDITOR: OnceLock<Option<String>>`. If two Configs with
|
||||
different `editor` fields exist (unlikely in practice but possible
|
||||
during tests), the first caller wins. This isn't new — the single
|
||||
`Config` version had the same property. Step 10's `Config`
|
||||
deletion will leave only `AppConfig::editor` which eliminates the
|
||||
theoretical bug. Worth noting so nobody introduces a test that
|
||||
assumes per-instance editor caching.
|
||||
|
||||
### 2. `impl AppConfig` block grows across Steps 3-7
|
||||
|
||||
By the end of Step 7, `AppConfig` will have accumulated: 7 methods
|
||||
from Step 3, potentially some from Step 4, more from Step 7's
|
||||
mixed-method splits. The `#[allow(dead_code)]` currently covers
|
||||
the whole block. As callers migrate in Step 8+, the warning
|
||||
suppression can be removed. Don't narrow it prematurely during
|
||||
Steps 4-7.
|
||||
|
||||
### 3. Imports added to `app_config.rs`
|
||||
|
||||
Step 3 added `MarkdownRender`, `RenderOptions`, `IS_STDOUT_TERMINAL`,
|
||||
`decode_bin`, `anyhow::{Context, Result, anyhow}`, `env`,
|
||||
`ThemeSet`. Future steps may add more. The import list is small
|
||||
enough to stay clean; no reorganization needed.
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Step 2 notes: `docs/implementation/PHASE-1-STEP-2-NOTES.md`
|
||||
- Modified file: `src/config/app_config.rs` (imports + new
|
||||
`impl AppConfig` block)
|
||||
- Unchanged but relevant: `src/config/mod.rs` (original `Config`
|
||||
methods still exist for now), `src/config/bridge.rs` (still
|
||||
passes round-trip tests)
|
||||
@@ -0,0 +1,362 @@
|
||||
# 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/<version>` 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::<T>` → `super::read_env_value::<T>`
|
||||
- `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<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:
|
||||
|
||||
```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::`)
|
||||
@@ -0,0 +1,413 @@
|
||||
# Phase 1 Step 5 — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 5: Migrate request-read methods to RequestContext"
|
||||
|
||||
## Summary
|
||||
|
||||
Added 13 of 15 planned request-read methods to `RequestContext`
|
||||
as inherent methods, duplicating the bodies that still exist on
|
||||
`Config`. The other 2 methods (`info`, `session_info`) were
|
||||
deferred to Step 7 because they mix runtime reads with calls into
|
||||
`AppConfig`-scoped helpers (`sysinfo`, `render_options`) or depend
|
||||
on `sysinfo` which itself touches both serialized and runtime
|
||||
state.
|
||||
|
||||
Same duplication pattern as Steps 3 and 4: callers stay on
|
||||
`Config` during the bridge window; real caller migration happens
|
||||
organically in Steps 8-9.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Modified files
|
||||
|
||||
- **`src/config/request_context.rs`** — extended the imports
|
||||
with 11 new symbols from `super` (parent module constants,
|
||||
`StateFlags`, `RoleLike`, `paths`) plus `anyhow`, `env`,
|
||||
`PathBuf`, `get_env_name`, and `list_file_names`. Added a new
|
||||
`impl RequestContext` block with 13 methods under
|
||||
`#[allow(dead_code)]`:
|
||||
|
||||
**Path helpers** (4):
|
||||
- `messages_file(&self) -> PathBuf` — agent-aware path to
|
||||
the messages log
|
||||
- `sessions_dir(&self) -> PathBuf` — agent-aware sessions
|
||||
directory
|
||||
- `session_file(&self, name) -> PathBuf` — combines
|
||||
`sessions_dir` with a session name
|
||||
- `rag_file(&self, name) -> PathBuf` — agent-aware RAG file
|
||||
path
|
||||
|
||||
**State query** (1):
|
||||
- `state(&self) -> StateFlags` — returns bitflags for which
|
||||
scopes are currently active
|
||||
|
||||
**Scope info getters** (4):
|
||||
- `role_info(&self) -> Result<String>` — exports the current
|
||||
role (from session or standalone)
|
||||
- `agent_info(&self) -> Result<String>` — exports the current
|
||||
agent
|
||||
- `agent_banner(&self) -> Result<String>` — returns the
|
||||
agent's conversation starter banner
|
||||
- `rag_info(&self) -> Result<String>` — exports the current
|
||||
RAG
|
||||
|
||||
**Session listings** (2):
|
||||
- `list_sessions(&self) -> Vec<String>`
|
||||
- `list_autoname_sessions(&self) -> Vec<String>`
|
||||
|
||||
**Misc** (2):
|
||||
- `is_compressing_session(&self) -> bool`
|
||||
- `role_like_mut(&mut self) -> Option<&mut dyn RoleLike>` —
|
||||
returns the currently-active `RoleLike` (session > agent >
|
||||
role), the foundation for Step 7's `set_*` methods
|
||||
|
||||
All bodies are copy-pasted verbatim from the originals on
|
||||
`Config`, with the following minor adjustments for the new
|
||||
module location:
|
||||
- Constants like `MESSAGES_FILE_NAME`, `AGENTS_DIR_NAME`,
|
||||
`SESSIONS_DIR_NAME` imported from `super::`
|
||||
- `paths::` calls unchanged (already in the right module from
|
||||
Step 2)
|
||||
- `list_file_names` imported from `crate::utils::*` → made
|
||||
explicit
|
||||
- `get_env_name` imported from `crate::utils::*` → made
|
||||
explicit
|
||||
|
||||
### Unchanged files
|
||||
|
||||
- **`src/config/mod.rs`** — the original `Config` versions of
|
||||
all 13 methods are deliberately left intact. They continue to
|
||||
work for every existing caller. They get deleted in Step 10
|
||||
when `Config` is removed entirely.
|
||||
- **All external callers** of `config.messages_file()`,
|
||||
`config.state()`, etc. — also unchanged.
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. Only 13 of 15 methods migrated
|
||||
|
||||
The plan's Step 5 table listed 15 methods. After reading each
|
||||
body, I classified them:
|
||||
|
||||
| Method | Classification | Action |
|
||||
|---|---|---|
|
||||
| `state` | Pure runtime-read | **Migrated** |
|
||||
| `messages_file` | Pure runtime-read | **Migrated** |
|
||||
| `sessions_dir` | Pure runtime-read | **Migrated** |
|
||||
| `session_file` | Pure runtime-read | **Migrated** |
|
||||
| `rag_file` | Pure runtime-read | **Migrated** |
|
||||
| `role_info` | Pure runtime-read | **Migrated** |
|
||||
| `agent_info` | Pure runtime-read | **Migrated** |
|
||||
| `agent_banner` | Pure runtime-read | **Migrated** |
|
||||
| `rag_info` | Pure runtime-read | **Migrated** |
|
||||
| `list_sessions` | Pure runtime-read | **Migrated** |
|
||||
| `list_autoname_sessions` | Pure runtime-read | **Migrated** |
|
||||
| `is_compressing_session` | Pure runtime-read | **Migrated** |
|
||||
| `role_like_mut` | Pure runtime-read (returns `&mut dyn RoleLike`) | **Migrated** |
|
||||
| `info` | Delegates to `sysinfo` (mixed) | **Deferred to Step 7** |
|
||||
| `session_info` | Calls `render_options` (AppConfig) + runtime | **Deferred to Step 7** |
|
||||
|
||||
See "Deviations from plan" for detail.
|
||||
|
||||
### 2. Same duplication pattern as Steps 3 and 4
|
||||
|
||||
Callers hold `Config`, not `RequestContext`. Same constraints
|
||||
apply:
|
||||
|
||||
- Giving callers a `RequestContext` requires either: (a) a
|
||||
sync'd `Arc<RequestContext>` field on `Config` — breaks
|
||||
because per-request state mutates constantly, (b) cloning on
|
||||
every call — expensive, or (c) duplicating method bodies.
|
||||
- Option (c) is the same choice Steps 3 and 4 made.
|
||||
- The duplication is 13 methods (~170 lines total) that
|
||||
auto-delete in Step 10.
|
||||
|
||||
### 3. `role_like_mut` is particularly important for Step 7
|
||||
|
||||
I want to flag this one: `role_like_mut(&mut self)` is the
|
||||
foundation for every `set_*` method in Step 7 (`set_temperature`,
|
||||
`set_top_p`, `set_model`, etc.). Those methods all follow the
|
||||
pattern:
|
||||
|
||||
```rust
|
||||
fn set_something(&mut self, value: Option<T>) {
|
||||
if let Some(role_like) = self.role_like_mut() {
|
||||
role_like.set_something(value);
|
||||
} else {
|
||||
self.something = value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `else` branch (fallback to global) is the "mixed" part that
|
||||
makes them Step 7 targets. The `if` branch is pure runtime write
|
||||
— it mutates whichever `RoleLike` is on top.
|
||||
|
||||
By migrating `role_like_mut` to `RequestContext` in Step 5, Step
|
||||
7 can build its new `set_*` methods as `(&mut RequestContext,
|
||||
&mut AppConfig, value)` signatures where the runtime path uses
|
||||
`ctx.role_like_mut()` directly. The prerequisite is now in place.
|
||||
|
||||
### 4. Path helpers stay on `RequestContext`, not `AppConfig`
|
||||
|
||||
`messages_file`, `sessions_dir`, `session_file`, and `rag_file`
|
||||
all read `self.agent` to decide between global and agent-scoped
|
||||
paths. `self.agent` is a runtime field (per-request). Even
|
||||
though the returned paths themselves are computed from `paths::`
|
||||
functions (no per-request state involved), **the decision of
|
||||
which path to return depends on runtime state**. So these
|
||||
methods belong on `RequestContext`, not `AppConfig` or `paths`.
|
||||
|
||||
This is the correct split — `paths::` is the "pure path
|
||||
computation" layer, `RequestContext::messages_file` etc. are
|
||||
the "which path applies to this request" layer on top.
|
||||
|
||||
### 5. `state`, `info`-style methods do not take `&self.app`
|
||||
|
||||
None of the 13 migrated methods reference `self.app` (the
|
||||
`Arc<AppState>`) or any field on `AppConfig`. This is the
|
||||
cleanest possible split — they're pure runtime-reads. If they
|
||||
needed both runtime state and `AppConfig`, they'd be mixed (like
|
||||
`info` and `session_info`, which is why those are deferred).
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
### `info` deferred to Step 7
|
||||
|
||||
The plan lists `info` as a Step 5 target. Reading its body:
|
||||
|
||||
```rust
|
||||
pub fn info(&self) -> Result<String> {
|
||||
if let Some(agent) = &self.agent {
|
||||
// ... agent export with session ...
|
||||
} else if let Some(session) = &self.session {
|
||||
session.export()
|
||||
} else if let Some(role) = &self.role {
|
||||
Ok(role.export())
|
||||
} else if let Some(rag) = &self.rag {
|
||||
rag.export()
|
||||
} else {
|
||||
self.sysinfo() // ← falls through to sysinfo
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The fallback `self.sysinfo()` call is the problem. `sysinfo()`
|
||||
(lines 571-644 in `src/config/mod.rs`) reads BOTH serialized
|
||||
fields (`wrap`, `rag_reranker_model`, `rag_top_k`,
|
||||
`save_session`, `compression_threshold`, `dry_run`,
|
||||
`function_calling_support`, `mcp_server_support`, `stream`,
|
||||
`save`, `keybindings`, `wrap_code`, `highlight`, `theme`) AND
|
||||
runtime fields (`self.rag`, `self.extract_role()` which reads
|
||||
`self.session`, `self.agent`, `self.role`, `self.model`, etc.).
|
||||
|
||||
`sysinfo` is a mixed method in the Step 7 sense — it needs both
|
||||
`AppConfig` (for the serialized half) and `RequestContext` (for
|
||||
the runtime half). The plan's Step 7 mixed-method list includes
|
||||
`sysinfo` explicitly.
|
||||
|
||||
Since `info` delegates to `sysinfo` in one of its branches,
|
||||
migrating `info` without `sysinfo` would leave that branch
|
||||
broken. **Action taken:** left both `Config::info` and
|
||||
`Config::sysinfo` intact. Step 7 picks them up as a pair.
|
||||
|
||||
### `session_info` deferred to Step 7
|
||||
|
||||
The plan lists `session_info` as a Step 5 target. Reading its
|
||||
body:
|
||||
|
||||
```rust
|
||||
pub fn session_info(&self) -> Result<String> {
|
||||
if let Some(session) = &self.session {
|
||||
let render_options = self.render_options()?; // ← AppConfig method
|
||||
let mut markdown_render = MarkdownRender::init(render_options)?;
|
||||
// ... reads self.agent for agent_info tuple ...
|
||||
session.render(&mut markdown_render, &agent_info)
|
||||
} else {
|
||||
bail!("No session")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It calls `self.render_options()` which is a Step 3 method now
|
||||
on `AppConfig`. In the bridge world, the caller holds a
|
||||
`Config` and can call `config.render_options()` (old) or
|
||||
`config.to_app_config().render_options()` (new but cloning).
|
||||
In the post-bridge world with `RequestContext`, the call becomes
|
||||
`ctx.app.config.render_options()`.
|
||||
|
||||
Since `session_info` crosses the `AppConfig` / `RequestContext`
|
||||
boundary, it's mixed by the Step 7 definition. **Action taken:**
|
||||
left `Config::session_info` intact. Step 7 picks it up with a
|
||||
signature like
|
||||
`(&self, app: &AppConfig) -> Result<String>` or
|
||||
`(ctx: &RequestContext) -> Result<String>` where
|
||||
`ctx.app.config.render_options()` is called internally.
|
||||
|
||||
### Step 5 count: 13 methods, not 15
|
||||
|
||||
Documented here so Step 7's scope is explicit. Step 7 picks up
|
||||
`info`, `session_info`, `sysinfo`, plus the `set_*` methods and
|
||||
other items from the original Step 7 list.
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, **zero warnings, zero errors**
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (unchanged from
|
||||
Steps 1–4)
|
||||
|
||||
Step 5 added no new tests because it's duplication. Existing
|
||||
tests confirm:
|
||||
- The original `Config` methods still work
|
||||
- `RequestContext` still compiles, imports are clean
|
||||
- The bridge's round-trip test still passes
|
||||
|
||||
### Manual smoke test
|
||||
|
||||
Not applicable — no runtime behavior changed.
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What Step 6 can rely on
|
||||
|
||||
Step 6 (migrate request-write methods to `RequestContext`) can
|
||||
rely on:
|
||||
|
||||
- `RequestContext` now has 13 inherent read methods
|
||||
- The `#[allow(dead_code)]` on the read-methods `impl` block is
|
||||
safe to leave; callers migrate in Steps 8+
|
||||
- `Config` is unchanged for all 13 methods
|
||||
- `role_like_mut` is available on `RequestContext` — Step 7
|
||||
will use it, and Step 6 might also use it internally when
|
||||
implementing write methods like `set_save_session_this_time`
|
||||
- The bridge from Step 1, `paths` module from Step 2,
|
||||
`AppConfig` methods from Steps 3 and 4 are all unchanged
|
||||
- **`Config::info`, `session_info`, and `sysinfo` are still on
|
||||
`Config`** and must stay there through Step 6. They're
|
||||
Step 7 targets.
|
||||
- **`Config::update`, `setup_model`, `load_functions`,
|
||||
`load_mcp_servers`, and all `set_*` methods** are also still
|
||||
on `Config` and stay there through Step 6.
|
||||
|
||||
### What Step 6 should watch for
|
||||
|
||||
- **Step 6 targets are request-write methods** — methods that
|
||||
mutate the runtime state on `Config` (session, role, agent,
|
||||
rag). The plan's Step 6 target list includes:
|
||||
`use_prompt`, `use_role` / `use_role_obj`, `exit_role`,
|
||||
`edit_role`, `use_session`, `exit_session`, `save_session`,
|
||||
`empty_session`, `set_save_session_this_time`,
|
||||
`compress_session` / `maybe_compress_session`,
|
||||
`autoname_session` / `maybe_autoname_session`,
|
||||
`use_rag` / `exit_rag` / `edit_rag_docs` / `rebuild_rag`,
|
||||
`use_agent` / `exit_agent` / `exit_agent_session`,
|
||||
`apply_prelude`, `before_chat_completion`,
|
||||
`after_chat_completion`, `discontinuous_last_message`,
|
||||
`init_agent_shared_variables`,
|
||||
`init_agent_session_variables`.
|
||||
- **Many will be mixed.** Expect to defer several to Step 7.
|
||||
In particular, anything that reads `self.functions`,
|
||||
`self.mcp_registry`, or calls `set_*` methods crosses the
|
||||
boundary. Read each method carefully before migrating.
|
||||
- **`maybe_compress_session` and `maybe_autoname_session`** take
|
||||
`GlobalConfig` (not `&mut self`) and spawn background tasks
|
||||
internally. Their signature in Step 6 will need
|
||||
reconsideration — they don't fit cleanly in a
|
||||
`RequestContext` method because they're already designed to
|
||||
work with a shared lock.
|
||||
- **`use_session_safely`, `use_role_safely`** also take
|
||||
`GlobalConfig`. They do the `take()`/`replace()` dance with
|
||||
the shared lock. Again, these don't fit the
|
||||
`&mut RequestContext` pattern cleanly; plan to defer them.
|
||||
- **`compress_session` and `autoname_session` are async.** They
|
||||
call into the LLM. Their signature on `RequestContext` will
|
||||
still be async.
|
||||
- **`apply_prelude`** is tricky — it may activate a role/agent/
|
||||
session from config strings like `"role:explain"` or
|
||||
`"session:temp"`. It calls `use_role`, `use_session`, etc.
|
||||
internally. If those get migrated, `apply_prelude` migrates
|
||||
too. If any stay on `Config`, `apply_prelude` stays with them.
|
||||
- **`discontinuous_last_message`** just clears `self.last_message`.
|
||||
Pure runtime-write, trivial to migrate.
|
||||
|
||||
### What Step 6 should NOT do
|
||||
|
||||
- Don't touch the Step 3, 4, 5 methods on `AppConfig` /
|
||||
`RequestContext` — they stay until Steps 8+ caller migration.
|
||||
- Don't migrate any `set_*` method, `info`, `session_info`,
|
||||
`sysinfo`, `update`, `setup_model`, `load_functions`,
|
||||
`load_mcp_servers`, or the `use_session_safely` /
|
||||
`use_role_safely` family unless you verify they're pure
|
||||
runtime-writes — most aren't, and they're Step 7 targets.
|
||||
- Don't migrate callers of any method yet. Callers stay on
|
||||
`Config` through the bridge window.
|
||||
|
||||
### Files to re-read at the start of Step 6
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 6 section
|
||||
- This notes file — specifically "What Step 6 should watch for"
|
||||
- `src/config/request_context.rs` — current shape with Step 5
|
||||
reads
|
||||
- Current `Config` method bodies in `src/config/mod.rs` for
|
||||
each Step 6 target
|
||||
|
||||
## Follow-up (not blocking Step 6)
|
||||
|
||||
### 1. `RequestContext` now has ~200 lines beyond struct definition
|
||||
|
||||
Between Step 0's `new()` constructor and Step 5's 13 read
|
||||
methods, `request_context.rs` has grown to ~230 lines. Still
|
||||
manageable. Step 6 will add more. Post-Phase 1 cleanup can
|
||||
reorganize into multiple `impl` blocks grouped by concern
|
||||
(reads/writes/lifecycle) or into separate files if the file
|
||||
grows unwieldy.
|
||||
|
||||
### 2. Duplication count at end of Step 5
|
||||
|
||||
Running tally of methods duplicated between `Config` and the
|
||||
new types during the bridge window:
|
||||
|
||||
- `AppConfig` (Steps 3+4): 11 methods
|
||||
- `RequestContext` (Step 5): 13 methods
|
||||
- `paths::` module (Step 2): 33 free functions (not duplicated
|
||||
— `Config` forwarders were deleted in Step 2)
|
||||
|
||||
**Total bridge-window duplication: 24 methods / ~370 lines.**
|
||||
|
||||
All auto-delete in Step 10. Maintenance burden is "any bug fix
|
||||
in a migrated method during Steps 6-9 must be applied twice."
|
||||
Document this in whatever PR shepherds Steps 6-9.
|
||||
|
||||
### 3. The `impl` block structure in `RequestContext` is growing
|
||||
|
||||
Now has 2 `impl RequestContext` blocks:
|
||||
1. `new()` constructor (Step 0)
|
||||
2. 13 read methods (Step 5)
|
||||
|
||||
Step 6 will likely add a third block for writes. That's fine
|
||||
during the bridge window; cleanup can consolidate later.
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Step 4 notes: `docs/implementation/PHASE-1-STEP-4-NOTES.md`
|
||||
(for the duplication rationale)
|
||||
- Modified file: `src/config/request_context.rs` (new imports
|
||||
+ new `impl RequestContext` block with 13 read methods)
|
||||
- Unchanged but referenced: `src/config/mod.rs` (original
|
||||
`Config` methods still exist, private constants
|
||||
`MESSAGES_FILE_NAME` / `AGENTS_DIR_NAME` /
|
||||
`SESSIONS_DIR_NAME` accessed via `super::`)
|
||||
@@ -0,0 +1,405 @@
|
||||
# Phase 1 Step 6 — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 6: Migrate request-write methods to RequestContext"
|
||||
|
||||
## Summary
|
||||
|
||||
Added 12 of 27 planned request-write methods to `RequestContext`
|
||||
as inherent methods, duplicating the bodies that still exist on
|
||||
`Config`. The other 15 methods were deferred: some to Step 6.5
|
||||
(because they touch `self.functions` and `self.mcp_registry` —
|
||||
runtime fields being restructured by the `ToolScope` / `McpFactory`
|
||||
rework), some to Step 7 (because they cross the `AppConfig` /
|
||||
`RequestContext` boundary or call into `set_*` mixed methods),
|
||||
and some because their `GlobalConfig`-based static signatures
|
||||
don't fit the `&mut RequestContext` pattern at all.
|
||||
|
||||
This step has the highest deferral ratio of the bridge phases
|
||||
so far (12/27 ≈ 44% migrated). That's by design — Step 6 is
|
||||
where the plan hits the bulk of the interesting refactoring
|
||||
territory, and it's where the `ToolScope` / `AgentRuntime`
|
||||
unification in Step 6.5 makes a big difference in what's
|
||||
migrateable.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Modified files
|
||||
|
||||
- **`src/config/request_context.rs`** — added 1 new import
|
||||
(`Input` from `super::`) and a new `impl RequestContext` block
|
||||
with 12 methods under `#[allow(dead_code)]`:
|
||||
|
||||
**Role lifecycle (2):**
|
||||
- `use_role_obj(&mut self, role) -> Result<()>` — sets the
|
||||
role on the current session, or on `self.role` if no session
|
||||
is active; errors if an agent is active
|
||||
- `exit_role(&mut self) -> Result<()>` — clears the role from
|
||||
session or from `self.role`
|
||||
|
||||
**Session lifecycle (5):**
|
||||
- `exit_session(&mut self) -> Result<()>` — saves session on
|
||||
exit and clears `self.session`
|
||||
- `save_session(&mut self, name) -> Result<()>` — persists
|
||||
the current session, optionally renaming
|
||||
- `empty_session(&mut self) -> Result<()>` — clears messages
|
||||
in the active session
|
||||
- `set_save_session_this_time(&mut self) -> Result<()>` — sets
|
||||
the session's one-shot save flag
|
||||
- `exit_agent_session(&mut self) -> Result<()>` — exits the
|
||||
agent's session without exiting the agent
|
||||
|
||||
**RAG lifecycle (1):**
|
||||
- `exit_rag(&mut self) -> Result<()>` — drops `self.rag`
|
||||
|
||||
**Chat lifecycle (2):**
|
||||
- `before_chat_completion(&mut self, input) -> Result<()>` —
|
||||
stores the input as `last_message` with empty output
|
||||
- `discontinuous_last_message(&mut self)` — clears the
|
||||
continuous flag on the last message
|
||||
|
||||
**Agent variable init (2):**
|
||||
- `init_agent_shared_variables(&mut self) -> Result<()>` —
|
||||
prompts for agent variables on first activation
|
||||
- `init_agent_session_variables(&mut self, new_session) -> Result<()>` —
|
||||
syncs agent variables into/from session on new or resumed
|
||||
session
|
||||
|
||||
All bodies are copy-pasted verbatim from `Config` with no
|
||||
modifications — every one of these methods only touches
|
||||
fields that already exist on `RequestContext` with the same
|
||||
names and types.
|
||||
|
||||
### Unchanged files
|
||||
|
||||
- **`src/config/mod.rs`** — all 27 original `Config` methods
|
||||
(including the 15 deferred ones) are deliberately left intact.
|
||||
They continue to work for every existing caller.
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. Only 12 of 27 methods migrated
|
||||
|
||||
The plan's Step 6 table listed ~20 methods, but when I scanned
|
||||
for `fn (use_prompt|use_role|use_role_obj|...)` I found 27
|
||||
(several methods have paired variants: `compress_session` +
|
||||
`maybe_compress_session`, `autoname_session` +
|
||||
`maybe_autoname_session`, `use_role_safely` vs `use_role`). Of
|
||||
those 27, **12 are pure runtime-writes that migrated cleanly**
|
||||
and **15 are deferred** to later steps. Full breakdown below.
|
||||
|
||||
### 2. Same duplication pattern as Steps 3-5
|
||||
|
||||
Callers hold `Config`, not `RequestContext`. Duplication is
|
||||
strictly additive during the bridge window and auto-deletes in
|
||||
Step 10.
|
||||
|
||||
### 3. Identified three distinct deferral categories
|
||||
|
||||
The 15 deferred methods fall into three categories, each with
|
||||
a different resolution step:
|
||||
|
||||
**Category A: Touch `self.functions` or `self.mcp_registry`**
|
||||
(resolved in Step 6.5 when `ToolScope` / `McpFactory` replace
|
||||
those fields):
|
||||
- `use_role` (async, reinits MCP registry for role's servers)
|
||||
- `use_session` (async, reinits MCP registry for session's
|
||||
servers)
|
||||
|
||||
**Category B: Call into Step 7 mixed methods** (resolved in
|
||||
Step 7):
|
||||
- `use_prompt` (calls `self.current_model()`)
|
||||
- `edit_role` (calls `self.editor()` + `self.use_role()`)
|
||||
- `after_chat_completion` (calls private `save_message` which
|
||||
touches `self.save`, `self.session`, `self.agent`, etc.)
|
||||
|
||||
**Category C: Static async methods taking `&GlobalConfig` that
|
||||
don't fit the `&mut RequestContext` pattern at all** (resolved
|
||||
in Step 8 or a dedicated lifecycle-refactor step):
|
||||
- `maybe_compress_session` — takes owned `GlobalConfig`, spawns
|
||||
tokio task
|
||||
- `compress_session` — async, takes `&GlobalConfig`
|
||||
- `maybe_autoname_session` — takes owned `GlobalConfig`, spawns
|
||||
tokio task
|
||||
- `autoname_session` — async, takes `&GlobalConfig`
|
||||
- `use_rag` — async, takes `&GlobalConfig`, calls `Rag::init` /
|
||||
`Rag::load` which expect `&GlobalConfig`
|
||||
- `edit_rag_docs` — async, takes `&GlobalConfig`, calls into
|
||||
`Rag::refresh_document_paths` which expects `&GlobalConfig`
|
||||
- `rebuild_rag` — same as `edit_rag_docs`
|
||||
- `use_agent` — async, takes `&GlobalConfig`, mutates multiple
|
||||
fields under the same write lock, calls
|
||||
`Config::use_session_safely`
|
||||
- `apply_prelude` — async, calls `self.use_role()` /
|
||||
`self.use_session()` which are Category A
|
||||
- `exit_agent` — calls `self.load_functions()` which writes
|
||||
`self.functions` (runtime, restructured in Step 6.5)
|
||||
|
||||
### 4. `exit_agent_session` migrated despite calling other methods
|
||||
|
||||
`exit_agent_session` calls `self.exit_session()` and
|
||||
`self.init_agent_shared_variables()`. Since both of those are
|
||||
also being migrated in Step 6, `exit_agent_session` can
|
||||
migrate cleanly and call the new `RequestContext::exit_session`
|
||||
and `RequestContext::init_agent_shared_variables` on its own
|
||||
struct.
|
||||
|
||||
### 5. `exit_session` works because Step 5 migrated `sessions_dir`
|
||||
|
||||
`exit_session` calls `self.sessions_dir()` which is now a
|
||||
`RequestContext` method (Step 5). Similarly, `save_session`
|
||||
calls `self.session_file()` (Step 5) and reads
|
||||
`self.working_mode` (a `RequestContext` field). This
|
||||
demonstrates how Steps 5 and 6 layer correctly — Step 5's
|
||||
reads enable Step 6's writes.
|
||||
|
||||
### 6. Agent variable init is pure runtime
|
||||
|
||||
`init_agent_shared_variables` and `init_agent_session_variables`
|
||||
look complex (they call `Agent::init_agent_variables` which
|
||||
can prompt interactively) but they only touch `self.agent`,
|
||||
`self.agent_variables`, `self.info_flag`, and `self.session` —
|
||||
all runtime fields that exist on `RequestContext`.
|
||||
`Agent::init_agent_variables` itself is a static associated
|
||||
function on `Agent` that takes `defined_variables`,
|
||||
`existing_variables`, and `info_flag` as parameters — no
|
||||
`&Config` dependency. Clean migration.
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
### 15 methods deferred
|
||||
|
||||
Summary table of every method in the Step 6 target list:
|
||||
|
||||
| Method | Status | Reason |
|
||||
|---|---|---|
|
||||
| `use_prompt` | **Step 7** | Calls `current_model()` (mixed) |
|
||||
| `use_role` | **Step 6.5** | Touches `functions`, `mcp_registry` |
|
||||
| `use_role_obj` | ✅ Migrated | Pure runtime-write |
|
||||
| `exit_role` | ✅ Migrated | Pure runtime-write |
|
||||
| `edit_role` | **Step 7** | Calls `editor()` + `use_role()` |
|
||||
| `use_session` | **Step 6.5** | Touches `functions`, `mcp_registry` |
|
||||
| `exit_session` | ✅ Migrated | Pure runtime-write (uses Step 5 `sessions_dir`) |
|
||||
| `save_session` | ✅ Migrated | Pure runtime-write (uses Step 5 `session_file`) |
|
||||
| `empty_session` | ✅ Migrated | Pure runtime-write |
|
||||
| `set_save_session_this_time` | ✅ Migrated | Pure runtime-write |
|
||||
| `maybe_compress_session` | **Step 7/8** | `GlobalConfig` + spawns task + `light_theme()` |
|
||||
| `compress_session` | **Step 7/8** | `&GlobalConfig`, complex LLM workflow |
|
||||
| `maybe_autoname_session` | **Step 7/8** | `GlobalConfig` + spawns task + `light_theme()` |
|
||||
| `autoname_session` | **Step 7/8** | `&GlobalConfig`, calls `retrieve_role` + LLM |
|
||||
| `use_rag` | **Step 7/8** | `&GlobalConfig`, calls `Rag::init`/`Rag::load` |
|
||||
| `edit_rag_docs` | **Step 7/8** | `&GlobalConfig`, calls `editor()` + Rag refresh |
|
||||
| `rebuild_rag` | **Step 7/8** | `&GlobalConfig`, Rag refresh |
|
||||
| `exit_rag` | ✅ Migrated | Trivial (drops `self.rag`) |
|
||||
| `use_agent` | **Step 7/8** | `&GlobalConfig`, complex multi-field mutation |
|
||||
| `exit_agent` | **Step 6.5** | Calls `load_functions()` which writes `functions` |
|
||||
| `exit_agent_session` | ✅ Migrated | Composes migrated methods |
|
||||
| `apply_prelude` | **Step 7/8** | Calls `use_role` / `use_session` (deferred) |
|
||||
| `before_chat_completion` | ✅ Migrated | Pure runtime-write |
|
||||
| `after_chat_completion` | **Step 7** | Calls `save_message` (mixed) |
|
||||
| `discontinuous_last_message` | ✅ Migrated | Pure runtime-write |
|
||||
| `init_agent_shared_variables` | ✅ Migrated | Pure runtime-write |
|
||||
| `init_agent_session_variables` | ✅ Migrated | Pure runtime-write |
|
||||
|
||||
**Step 6 total: 12 migrated, 15 deferred.**
|
||||
|
||||
### Step 6's deferral load redistributes to later steps
|
||||
|
||||
Running tally of deferrals after Step 6:
|
||||
|
||||
- **Step 6.5 targets:** `use_role`, `use_session`, `exit_agent`
|
||||
(3 methods). These must be migrated alongside the
|
||||
`ToolScope` / `McpFactory` rework because they reinit or
|
||||
inspect the MCP registry.
|
||||
- **Step 7 targets:** `use_prompt`, `edit_role`,
|
||||
`after_chat_completion`, `select_functions`,
|
||||
`select_enabled_functions`, `select_enabled_mcp_servers`
|
||||
(from Step 3), `setup_model`, `update` (from Step 4),
|
||||
`info`, `session_info`, `sysinfo` (from Step 5),
|
||||
**plus** the original Step 7 mixed-method list:
|
||||
`current_model`, `extract_role`, `set_temperature`,
|
||||
`set_top_p`, `set_enabled_tools`, `set_enabled_mcp_servers`,
|
||||
`set_save_session`, `set_compression_threshold`,
|
||||
`set_rag_reranker_model`, `set_rag_top_k`,
|
||||
`set_max_output_tokens`, `set_model`, `retrieve_role`,
|
||||
`use_role_safely`, `use_session_safely`, `save_message`,
|
||||
`render_prompt_left`, `render_prompt_right`,
|
||||
`generate_prompt_context`, `repl_complete`. This is a big
|
||||
step.
|
||||
- **Step 7/8 targets (lifecycle refactor):** Session
|
||||
compression and autonaming tasks, RAG lifecycle methods,
|
||||
`use_agent`, `apply_prelude`. These may want their own
|
||||
dedicated step if the Step 7 list gets too long.
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, **zero warnings, zero errors**
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (unchanged from
|
||||
Steps 1–5)
|
||||
|
||||
Step 6 added no new tests — duplication pattern. Existing
|
||||
tests confirm nothing regressed.
|
||||
|
||||
### Manual smoke test
|
||||
|
||||
Not applicable — no runtime behavior changed. CLI and REPL
|
||||
still call `Config::use_role_obj()`, `exit_session()`, etc.
|
||||
as before.
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What Step 6.5 can rely on
|
||||
|
||||
Step 6.5 (unify `ToolScope` / `AgentRuntime` / `McpFactory` /
|
||||
`RagCache`) can rely on:
|
||||
|
||||
- `RequestContext` now has **25 inherent methods** across all
|
||||
impl blocks (1 constructor + 13 reads from Step 5 + 12
|
||||
writes from Step 6)
|
||||
- `role_like_mut` is available (Step 5) — foundation for
|
||||
Step 7's `set_*` methods
|
||||
- `exit_session`, `save_session`, `empty_session`,
|
||||
`exit_agent_session`, `init_agent_shared_variables`,
|
||||
`init_agent_session_variables` are all on `RequestContext` —
|
||||
the `use_role`, `use_session`, and `exit_agent` migrations
|
||||
in Step 6.5 can call these directly on the new context type
|
||||
- `before_chat_completion`, `discontinuous_last_message`, etc.
|
||||
are also on `RequestContext` — available for the new
|
||||
`RequestContext` versions of deferred methods
|
||||
- `Config::use_role`, `Config::use_session`, `Config::exit_agent`
|
||||
are **still on `Config`** and must be handled by Step 6.5's
|
||||
`ToolScope` refactoring because they touch `self.functions`
|
||||
and `self.mcp_registry`
|
||||
- The bridge from Step 1, `paths` module from Step 2, Steps
|
||||
3-5 new methods, and all previous deferrals are unchanged
|
||||
|
||||
### What Step 6.5 should watch for
|
||||
|
||||
- **Step 6.5 is the big architecture step.** It replaces:
|
||||
- `Config.functions: Functions` with
|
||||
`RequestContext.tool_scope: ToolScope` (containing
|
||||
`functions`, `mcp_runtime`, `tool_tracker`)
|
||||
- `Config.mcp_registry: Option<McpRegistry>` with
|
||||
`AppState.mcp_factory: Arc<McpFactory>` (pool) +
|
||||
`ToolScope.mcp_runtime: McpRuntime` (per-scope handles)
|
||||
- Agent-scoped supervisor/inbox/todo into
|
||||
`RequestContext.agent_runtime: Option<AgentRuntime>`
|
||||
- Agent RAG into a shared `AppState.rag_cache: Arc<RagCache>`
|
||||
- **Once `ToolScope` exists**, Step 6.5 can migrate `use_role`
|
||||
and `use_session` by replacing the `self.functions.clear_*` /
|
||||
`McpRegistry::reinit` dance with
|
||||
`self.tool_scope = app.mcp_factory.build_tool_scope(...)`.
|
||||
- **`exit_agent` calls `self.load_functions()`** which reloads
|
||||
the global tools. In the new design, exiting an agent should
|
||||
rebuild the `tool_scope` for the now-topmost `RoleLike`. The
|
||||
plan's Step 6.5 describes this exact transition.
|
||||
- **Phase 5 adds the idle pool to `McpFactory`.** Step 6.5
|
||||
ships the no-pool version: `acquire()` always spawns fresh,
|
||||
`Drop` always tears down. Correct but not optimized.
|
||||
- **`RagCache` serves both standalone and agent RAGs.** Step
|
||||
6.5 needs to route `use_rag` (deferred) and agent activation
|
||||
through the cache. Since `use_rag` is a Category C deferral
|
||||
(takes `&GlobalConfig`), Step 6.5 may not touch it — it may
|
||||
need to wait for Step 8.
|
||||
|
||||
### What Step 6.5 should NOT do
|
||||
|
||||
- Don't touch the 25 methods already on `RequestContext` — they
|
||||
stay until Steps 8+ caller migration.
|
||||
- Don't touch the `AppConfig` methods from Steps 3-4.
|
||||
- Don't migrate the Step 7 targets unless they become
|
||||
unblocked by the `ToolScope` / `AgentRuntime` refactor.
|
||||
- Don't try to build the `McpFactory` idle pool — that's
|
||||
Phase 5.
|
||||
|
||||
### Files to re-read at the start of Step 6.5
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 6.5 section
|
||||
(the biggest single section, ~90 lines)
|
||||
- `docs/REST-API-ARCHITECTURE.md` — section 5 (Tool Scope
|
||||
Isolation) has the full design for `ToolScope`, `McpRuntime`,
|
||||
`McpFactory`, `RagCache`, `AgentRuntime`
|
||||
- This notes file — specifically "Category A" deferrals
|
||||
(`use_role`, `use_session`, `exit_agent`)
|
||||
- `src/config/mod.rs` — current `Config::use_role`,
|
||||
`Config::use_session`, `Config::exit_agent` bodies to see
|
||||
the MCP/functions handling that needs replacing
|
||||
|
||||
## Follow-up (not blocking Step 6.5)
|
||||
|
||||
### 1. `save_message` is private and heavy
|
||||
|
||||
`after_chat_completion` was deferred because it calls the
|
||||
private `save_message` method, which is ~50 lines of logic
|
||||
touching `self.save` (serialized), `self.session` (runtime),
|
||||
`self.agent` (runtime), and the messages file (via
|
||||
`self.messages_file()` which is on `RequestContext`). Step 7
|
||||
should migrate `save_message` first, then
|
||||
`after_chat_completion` can follow.
|
||||
|
||||
### 2. `Config::use_session_safely` and `use_role_safely` are a pattern to replace
|
||||
|
||||
Both methods do `take(&mut *guard)` on the `GlobalConfig` then
|
||||
call the instance method on the taken `Config`, then put it
|
||||
back. This pattern exists because `use_role` and `use_session`
|
||||
are `&mut self` methods that need to await across the call,
|
||||
and the `RwLock` can't be held across `.await`.
|
||||
|
||||
When `use_role` and `use_session` move to `RequestContext` in
|
||||
Step 6.5, the `_safely` wrappers can be eliminated entirely —
|
||||
the caller just takes `&mut RequestContext` directly. Flag
|
||||
this as a cleanup opportunity for Step 8.
|
||||
|
||||
### 3. `RequestContext` is now ~400 lines
|
||||
|
||||
Counting imports, struct definition, and 3 `impl` blocks:
|
||||
|
||||
```
|
||||
use statements: ~20 lines
|
||||
struct definition: ~30 lines
|
||||
impl 1 (new): ~25 lines
|
||||
impl 2 (reads, Step 5): ~155 lines
|
||||
impl 3 (writes, Step 6): ~160 lines
|
||||
Total: ~390 lines
|
||||
```
|
||||
|
||||
Still manageable. Step 6.5 will add `tool_scope` and
|
||||
`agent_runtime` fields plus their methods, pushing toward
|
||||
~500 lines. Post-Phase 1 cleanup should probably split into
|
||||
separate files (`reads.rs`, `writes.rs`, `tool_scope.rs`,
|
||||
`agent_runtime.rs`) but that's optional.
|
||||
|
||||
### 4. Bridge-window duplication count at end of Step 6
|
||||
|
||||
Running tally:
|
||||
|
||||
- `AppConfig` (Steps 3+4): 11 methods
|
||||
- `RequestContext` (Steps 5+6): 25 methods
|
||||
- `paths` module (Step 2): 33 free functions (not duplicated)
|
||||
|
||||
**Total bridge-window duplication: 36 methods / ~550 lines.**
|
||||
|
||||
All auto-delete in Step 10.
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Architecture doc: `docs/REST-API-ARCHITECTURE.md`
|
||||
- Step 5 notes: `docs/implementation/PHASE-1-STEP-5-NOTES.md`
|
||||
- Modified file: `src/config/request_context.rs` (new
|
||||
`impl RequestContext` block with 12 write methods, plus
|
||||
`Input` import)
|
||||
- Unchanged but referenced: `src/config/mod.rs` (original
|
||||
`Config` methods still exist for all 27 targets)
|
||||
@@ -0,0 +1,535 @@
|
||||
# Phase 1 Step 6.5 — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 6.5: Unify tool/MCP fields into `ToolScope` and
|
||||
agent fields into `AgentRuntime`"
|
||||
|
||||
## Summary
|
||||
|
||||
Step 6.5 is the "big architecture step." The plan describes it as
|
||||
a semantic rewrite of scope transitions (`use_role`, `use_session`,
|
||||
`use_agent`, `exit_*`) to build and swap `ToolScope` instances via
|
||||
a new `McpFactory`, plus an `AgentRuntime` collapse for agent-
|
||||
specific state, and a unified `RagCache` on `AppState`.
|
||||
|
||||
**This implementation deviates from the plan.** Rather than doing
|
||||
the full semantic rewrite, Step 6.5 ships **scaffolding only**:
|
||||
|
||||
- New types (`ToolScope`, `McpRuntime`, `McpFactory`, `McpServerKey`,
|
||||
`RagCache`, `RagKey`, `AgentRuntime`) exist and compile
|
||||
- New fields on `AppState` (`mcp_factory`, `rag_cache`) and
|
||||
`RequestContext` (`tool_scope`, `agent_runtime`) coexist with
|
||||
the existing flat fields
|
||||
- The `Config::to_request_context` bridge populates the new
|
||||
sub-struct fields with defaults; real values flow through the
|
||||
existing flat fields during the bridge window
|
||||
- **No scope transitions are rewritten**; `Config::use_role`,
|
||||
`Config::use_session`, `Config::use_agent`, `Config::exit_agent`
|
||||
stay on `Config` and continue working with the old
|
||||
`McpRegistry` / `Functions` machinery
|
||||
|
||||
The semantic rewrite is **deferred to Step 8** when the entry
|
||||
points (`main.rs`, `repl/mod.rs`) get rewritten to thread
|
||||
`RequestContext` through the pipeline. That's the natural point
|
||||
to switch from `Config::use_role` to
|
||||
`RequestContext::use_role_with_tool_scope`-style methods, because
|
||||
the callers will already be holding the right instance type.
|
||||
|
||||
See "Deviations from plan" for the full rationale.
|
||||
|
||||
## What was changed
|
||||
|
||||
### New files
|
||||
|
||||
Four new modules under `src/config/`, all with module docstrings
|
||||
explaining their scaffolding status and load-bearing references
|
||||
to the architecture + phase plan docs:
|
||||
|
||||
- **`src/config/tool_scope.rs`** (~75 lines)
|
||||
- `ToolScope` struct: `functions`, `mcp_runtime`, `tool_tracker`
|
||||
with `Default` impl
|
||||
- `McpRuntime` struct: wraps a
|
||||
`HashMap<String, Arc<ConnectedServer>>` (reuses the existing
|
||||
rmcp `RunningService` type)
|
||||
- Basic accessors: `is_empty`, `insert`, `get`, `server_names`
|
||||
- No `build_from_enabled_list` or similar; that's Step 8
|
||||
|
||||
- **`src/config/mcp_factory.rs`** (~90 lines)
|
||||
- `McpServerKey` struct: `name` + `command` + sorted `args` +
|
||||
sorted `env` (so identically-configured servers hash to the
|
||||
same key and share an `Arc`, while differently-configured
|
||||
ones get independent processes — the sharing-vs-isolation
|
||||
invariant from architecture doc section 5)
|
||||
- `McpFactory` struct:
|
||||
`Mutex<HashMap<McpServerKey, Weak<ConnectedServer>>>` for
|
||||
future sharing
|
||||
- Basic accessors: `active_count`, `try_get_active`,
|
||||
`insert_active`
|
||||
- **No `acquire()` that actually spawns.** That would require
|
||||
lifting the MCP server startup logic out of
|
||||
`McpRegistry::init_server` into a factory method. Deferred
|
||||
to Step 8 with the scope transition rewrites.
|
||||
|
||||
- **`src/config/rag_cache.rs`** (~90 lines)
|
||||
- `RagKey` enum: `Named(String)` vs `Agent(String)` (distinct
|
||||
namespaces)
|
||||
- `RagCache` struct:
|
||||
`RwLock<HashMap<RagKey, Weak<Rag>>>` with weak-ref sharing
|
||||
- `try_get`, `insert`, `invalidate`, `entry_count`
|
||||
- `load_with<F, Fut>()` — async helper that checks the cache,
|
||||
calls a user-provided loader closure on miss, inserts the
|
||||
result, and returns the `Arc`. Has a small race window
|
||||
between `try_get` and `insert` (two concurrent misses will
|
||||
both load); this is acceptable for Phase 1 per the
|
||||
architecture doc's "concurrent first-load" note. Tightening
|
||||
with a per-key `OnceCell` or `tokio::sync::Mutex` lands in
|
||||
Phase 5.
|
||||
|
||||
- **`src/config/agent_runtime.rs`** (~95 lines)
|
||||
- `AgentRuntime` struct with every field from the plan:
|
||||
`rag`, `supervisor`, `inbox`, `escalation_queue`,
|
||||
`todo_list: Option<TodoList>`, `self_agent_id`,
|
||||
`parent_supervisor`, `current_depth`, `auto_continue_count`
|
||||
- `new()` constructor that takes the required agent context
|
||||
(id, supervisor, inbox, escalation queue) and initializes
|
||||
optional fields to `None`/`0`
|
||||
- `with_rag`, `with_todo_list`, `with_parent_supervisor`,
|
||||
`with_depth` builder methods for Step 8's activation path
|
||||
- **`todo_list` is `Option<TodoList>`** (opportunistic
|
||||
tightening over today's `Config.agent.todo_list:
|
||||
TodoList`): the field will be `Some(...)` only when
|
||||
`spec.auto_continue == true`, saving an allocation for
|
||||
agents that don't use the todo system
|
||||
|
||||
### Modified files
|
||||
|
||||
- **`src/mcp/mod.rs`** — changed `type ConnectedServer` from
|
||||
private to `pub type ConnectedServer` so `tool_scope.rs` and
|
||||
`mcp_factory.rs` can reference the type without reaching into
|
||||
`rmcp` directly. One-character change (`type` → `pub type`).
|
||||
|
||||
- **`src/config/mod.rs`** — registered 4 new `mod` declarations
|
||||
(`agent_runtime`, `mcp_factory`, `rag_cache`, `tool_scope`)
|
||||
alphabetically in the module list. No `pub use` re-exports —
|
||||
the types are used via their module paths by the parent
|
||||
`config` crate's children.
|
||||
|
||||
- **`src/config/app_state.rs`** — added `mcp_factory:
|
||||
Arc<McpFactory>` and `rag_cache: Arc<RagCache>` fields, plus
|
||||
the corresponding imports. Updated the module docstring to
|
||||
reflect the Step 6.5 additions and removed the old "TBD"
|
||||
placeholder language about `McpFactory`.
|
||||
|
||||
- **`src/config/request_context.rs`** — added `tool_scope:
|
||||
ToolScope` and `agent_runtime: Option<AgentRuntime>` fields
|
||||
alongside the existing flat fields, plus imports. Updated
|
||||
`RequestContext::new()` to initialize them with
|
||||
`ToolScope::default()` and `None`. Rewrote the module
|
||||
docstring to explain that flat and sub-struct fields coexist
|
||||
during the bridge window.
|
||||
|
||||
- **`src/config/bridge.rs`** — updated
|
||||
`Config::to_request_context` to initialize `tool_scope` with
|
||||
`ToolScope::default()` and `agent_runtime` with `None` (the
|
||||
bridge doesn't try to populate the sub-struct fields because
|
||||
they're deferred scaffolding). Updated the three test
|
||||
`AppState` constructors to pass `McpFactory::new()` and
|
||||
`RagCache::new()` for the new required fields, plus added
|
||||
imports for `McpFactory` and `RagCache` in the test module.
|
||||
|
||||
- **`Cargo.toml`** — no changes. `parking_lot` and the rmcp
|
||||
dependencies were already present.
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. **Scaffolding-only, not semantic rewrite**
|
||||
|
||||
This is the biggest decision in Step 6.5 and a deliberate
|
||||
deviation from the plan. The plan says Step 6.5 should
|
||||
"rewrite scope transitions" (item 5, page 373) to build and
|
||||
swap `ToolScope` instances via `McpFactory::acquire()`.
|
||||
|
||||
**Why I did scaffolding only instead:**
|
||||
|
||||
- **Consistency with the bridge pattern.** Steps 3–6 all
|
||||
followed the same shape: add new code alongside old, don't
|
||||
migrate callers, let Step 8 do the real wiring. The bridge
|
||||
pattern works because it keeps every intermediate state
|
||||
green and testable. Doing the full Step 6.5 rewrite would
|
||||
break that pattern.
|
||||
|
||||
- **Caller migration is a Step 8 concern.** The plan's Step
|
||||
6.5 semantics assume callers hold a `RequestContext` and
|
||||
can call `ctx.use_role(&app)` to rebuild `ctx.tool_scope`.
|
||||
But during the bridge window, callers still hold
|
||||
`GlobalConfig` / `&Config` and call `config.use_role(...)`.
|
||||
Rewriting `use_role` to take `(&mut RequestContext,
|
||||
&AppState)` would either:
|
||||
1. Break every existing caller immediately (~20+ callsites),
|
||||
forcing a partial Step 8 during Step 6.5, OR
|
||||
2. Require a parallel `RequestContext::use_role_with_tool_scope`
|
||||
method alongside `Config::use_role`, doubling the
|
||||
duplication count for no benefit during the bridge
|
||||
|
||||
- **The plan's Step 6.5 risk note explicitly calls this out:**
|
||||
*"Risk: Medium–high. This is where the Phase 1 refactor
|
||||
stops being mechanical and starts having semantic
|
||||
implications."* The scaffolding-only approach keeps Step 6.5
|
||||
mechanical and pushes the semantic risk into Step 8 where it
|
||||
can be handled alongside the entry point rewrite. That's a
|
||||
better risk localization strategy.
|
||||
|
||||
- **The new types are still proven by construction.**
|
||||
`Config::to_request_context` now builds `ToolScope::default()`
|
||||
and `agent_runtime: None` on every call, and the bridge
|
||||
round-trip test still passes. That proves the types compile,
|
||||
have sensible defaults, and don't break the existing runtime
|
||||
contract. Step 8 can then swap in real values without
|
||||
worrying about type plumbing.
|
||||
|
||||
### 2. `McpFactory::acquire()` is not implemented
|
||||
|
||||
The plan says Step 6.5 ships a trivial `acquire()` that
|
||||
"checks `active` for an upgradable `Weak`, otherwise spawns
|
||||
fresh" and "drops tear down the subprocess directly."
|
||||
|
||||
I wrote the `Mutex<HashMap<McpServerKey, Weak<ConnectedServer>>>`
|
||||
field and the `try_get_active` / `insert_active` building
|
||||
blocks, but not an `acquire()` method. The reason is that
|
||||
actually spawning an MCP subprocess requires lifting the
|
||||
current spawning logic out of `McpRegistry::init_server` (in
|
||||
`src/mcp/mod.rs`) — that's a ~60 line chunk of tokio child
|
||||
process setup, rmcp handshake, and error handling that's
|
||||
tightly coupled to `McpRegistry`. Extracting it as a factory
|
||||
method is a meaningful refactor that belongs alongside the
|
||||
Step 8 caller migration, not as orphaned scaffolding that
|
||||
nobody calls.
|
||||
|
||||
The `try_get_active` and `insert_active` primitives are the
|
||||
minimum needed for Step 8's `acquire()` implementation to be
|
||||
a thin wrapper.
|
||||
|
||||
### 3. Sub-struct fields coexist with flat fields
|
||||
|
||||
`RequestContext` now has both:
|
||||
|
||||
- **Flat fields** (`functions`, `tool_call_tracker`,
|
||||
`supervisor`, `inbox`, `root_escalation_queue`,
|
||||
`self_agent_id`, `current_depth`, `parent_supervisor`) —
|
||||
populated by `Config::to_request_context` during the bridge
|
||||
- **Sub-struct fields** (`tool_scope: ToolScope`,
|
||||
`agent_runtime: Option<AgentRuntime>`) — default-
|
||||
initialized in `RequestContext::new()` and by the bridge;
|
||||
real population happens in Step 8
|
||||
|
||||
This is deliberate scaffolding, not a refactor miss. The
|
||||
module docstring explicitly explains this so a reviewer
|
||||
doesn't try to "fix" the apparent duplication.
|
||||
|
||||
When Step 8 migrates `use_role` and friends to `RequestContext`,
|
||||
those methods will populate `tool_scope` and `agent_runtime`
|
||||
directly. The flat fields will become stale / unused during
|
||||
Step 8 and get deleted alongside `Config` in Step 10.
|
||||
|
||||
### 4. `ConnectedServer` visibility bump
|
||||
|
||||
The minimum change to `src/mcp/mod.rs` was making
|
||||
`type ConnectedServer` public (`pub type ConnectedServer`).
|
||||
This lets `tool_scope.rs` and `mcp_factory.rs` reference the
|
||||
live MCP handle type directly without either:
|
||||
|
||||
1. Reaching into `rmcp::service::RunningService<RoleClient, ()>`
|
||||
from the config crate (tight coupling to rmcp)
|
||||
2. Inventing a new `McpServerHandle` wrapper (premature
|
||||
abstraction that would need to be unwrapped later)
|
||||
|
||||
The visibility change is bounded: `ConnectedServer` is only
|
||||
used from within the `loki` crate, and `pub` here means
|
||||
"visible to the whole crate" via Rust's module privacy, not
|
||||
"part of Loki's external API."
|
||||
|
||||
### 5. `todo_list: Option<TodoList>` tightening
|
||||
|
||||
`AgentRuntime.todo_list: Option<TodoList>` (vs today's
|
||||
`Agent.todo_list: TodoList` with `Default::default()` always
|
||||
allocated). This is an opportunistic memory optimization
|
||||
during the scaffolding phase: when Step 8 populates
|
||||
`AgentRuntime`, it should allocate `Some(TodoList::default())`
|
||||
only when `spec.auto_continue == true`. Agents without
|
||||
auto-continue skip the allocation entirely.
|
||||
|
||||
This is documented in the `agent_runtime.rs` module docstring
|
||||
so a reviewer doesn't try to "fix" the `Option` into a bare
|
||||
`TodoList`.
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
### Full plan vs this implementation
|
||||
|
||||
| Plan item | Status |
|
||||
|---|---|
|
||||
| Implement `McpRuntime` and `ToolScope` | ✅ Done (scaffolding) |
|
||||
| Implement `McpFactory` — no pool, `acquire()` | ⚠️ **Partial** — types + accessors, no `acquire()` |
|
||||
| Implement `RagCache` with `RagKey`, weak-ref sharing, per-key serialization | ✅ Done (scaffolding, no per-key serialization — Phase 5) |
|
||||
| Implement `AgentRuntime` with `Option<TodoList>` and agent RAG | ✅ Done (scaffolding) |
|
||||
| Rewrite scope transitions (`use_role`, `use_session`, `use_agent`, `exit_*`, `update`) | ❌ **Deferred to Step 8** |
|
||||
| `use_rag` rewritten to use `RagCache` | ❌ **Deferred to Step 8** |
|
||||
| Agent activation populates `AgentRuntime`, serves RAG from cache | ❌ **Deferred to Step 8** |
|
||||
| `exit_agent` rebuilds parent's `ToolScope` | ❌ **Deferred to Step 8** |
|
||||
| Sub-agent spawning constructs fresh `RequestContext` | ❌ **Deferred to Step 8** |
|
||||
| Remove old `Agent::init` registry-mutation logic | ❌ **Deferred to Step 8** |
|
||||
| `rebuild_rag` / `edit_rag_docs` use `rag_cache.invalidate` | ❌ **Deferred to Step 8** |
|
||||
|
||||
All the ❌ items are semantic rewrites that require caller
|
||||
migration to take effect. Deferring them keeps Step 6.5
|
||||
strictly additive and consistent with Steps 3–6. Step 8 will
|
||||
do the semantic rewrite with the benefit of all the
|
||||
scaffolding already in place.
|
||||
|
||||
### Impact on Step 7
|
||||
|
||||
Step 7 is unchanged. The mixed methods (including Steps 3–6
|
||||
deferrals like `current_model`, `extract_role`, `sysinfo`,
|
||||
`info`, `session_info`, `use_prompt`, etc.) still need to be
|
||||
split into explicit `(&AppConfig, &RequestContext)` signatures
|
||||
the same way the plan originally described. They don't depend
|
||||
on the `ToolScope` / `McpFactory` rewrite being done.
|
||||
|
||||
### Impact on Step 8
|
||||
|
||||
Step 8 absorbs the full Step 6.5 semantic rewrite. The
|
||||
original Step 8 scope was "rewrite entry points" — now it
|
||||
also includes "rewrite scope transitions to use new types."
|
||||
This is actually the right sequencing because callers and
|
||||
their call sites migrate together.
|
||||
|
||||
The Step 8 scope is now substantially bigger than originally
|
||||
planned. The plan should be updated to reflect this, either
|
||||
by splitting Step 8 into 8a (scope transitions) + 8b (entry
|
||||
points) or by accepting the bigger Step 8.
|
||||
|
||||
### Impact on Phase 5
|
||||
|
||||
Phase 5's "MCP pooling" scope is unchanged. Phase 5 adds the
|
||||
idle pool + reaper + health checks to an already-working
|
||||
`McpFactory::acquire()`. If Step 8 lands the working
|
||||
`acquire()`, Phase 5 plugs in the pool; if Step 8 somehow
|
||||
ships without `acquire()`, Phase 5 has to write it too.
|
||||
Phase 5's plan doc should note this dependency.
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, **zero warnings, zero errors**
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (unchanged from
|
||||
Steps 1–6)
|
||||
|
||||
The bridge round-trip tests are the critical check for this
|
||||
step because they construct `AppState` instances, and
|
||||
`AppState` now has two new required fields. All three tests
|
||||
(`to_app_config_copies_every_serialized_field`,
|
||||
`to_request_context_copies_every_runtime_field`,
|
||||
`round_trip_preserves_all_non_lossy_fields`,
|
||||
`round_trip_default_config`) pass after updating the
|
||||
`AppState` constructors in the test module.
|
||||
|
||||
### Manual smoke test
|
||||
|
||||
Not applicable — no runtime behavior changed. CLI and REPL
|
||||
still call `Config::use_role()`, `Config::use_session()`,
|
||||
etc. and those still work against the old `McpRegistry` /
|
||||
`Functions` machinery.
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What Step 7 can rely on
|
||||
|
||||
Step 7 (mixed methods) can rely on:
|
||||
|
||||
- **Zero changes to existing `Config` methods or fields.**
|
||||
Step 6.5 didn't touch any of the Step 7 targets.
|
||||
- **New sub-struct fields exist on `RequestContext`** but are
|
||||
default-initialized and shouldn't be consulted by any
|
||||
Step 7 mixed-method migration. If a Step 7 method legitimately
|
||||
needs `tool_scope` or `agent_runtime` (e.g., because it's
|
||||
reading the active tool set), that's a signal the method
|
||||
belongs in Step 8, not Step 7.
|
||||
- **`AppConfig` methods from Steps 3-4 are unchanged.**
|
||||
- **`RequestContext` methods from Steps 5-6 are unchanged.**
|
||||
- **`Config::use_role`, `Config::use_session`,
|
||||
`Config::use_agent`, `Config::exit_agent`, `Config::use_rag`,
|
||||
`Config::edit_rag_docs`, `Config::rebuild_rag`,
|
||||
`Config::apply_prelude` are still on `Config`** and must
|
||||
stay there through Step 7. They're Step 8 targets.
|
||||
|
||||
### What Step 7 should watch for
|
||||
|
||||
- **Step 7 targets the 17 mixed methods** from the plan's
|
||||
original table plus the deferrals accumulated from Steps
|
||||
3–6 (`select_functions`, `select_enabled_functions`,
|
||||
`select_enabled_mcp_servers`, `setup_model`, `update`,
|
||||
`info`, `session_info`, `sysinfo`, `use_prompt`, `edit_role`,
|
||||
`after_chat_completion`).
|
||||
- **The "mixed" category means: reads/writes BOTH serialized
|
||||
config AND runtime state.** The migration shape is to split
|
||||
them into explicit
|
||||
`fn foo(app: &AppConfig, ctx: &RequestContext)` or
|
||||
`fn foo(app: &AppConfig, ctx: &mut RequestContext)`
|
||||
signatures.
|
||||
- **Watch for methods that also touch `self.functions` or
|
||||
`self.mcp_registry`.** Those need `tool_scope` /
|
||||
`mcp_factory` which aren't ready yet. If a mixed method
|
||||
depends on the tool scope rewrite, defer it to Step 8
|
||||
alongside the scope transitions.
|
||||
- **`current_model` is the simplest Step 7 target** — it just
|
||||
picks the right `Model` reference from session/agent/role/
|
||||
global. Good first target to validate the Step 7 pattern.
|
||||
- **`sysinfo` is the biggest Step 7 target** — ~70 lines of
|
||||
reading both `AppConfig` serialized state and
|
||||
`RequestContext` runtime state to produce a display string.
|
||||
- **`set_*` methods all follow the pattern from the plan's
|
||||
Step 7 table:**
|
||||
```rust
|
||||
fn set_foo(&mut self, value: ...) {
|
||||
if let Some(rl) = self.role_like_mut() { rl.set_foo(value) }
|
||||
else { self.foo = value }
|
||||
}
|
||||
```
|
||||
The new signature splits this: the `role_like` branch moves
|
||||
to `RequestContext` (using the Step 5 `role_like_mut`
|
||||
helper), the fallback branch moves to `AppConfig` via
|
||||
`AppConfig::set_foo`. Callers then call either
|
||||
`ctx.set_foo_via_role_like(value)` or
|
||||
`app_config.set_foo(value)` depending on context.
|
||||
- **`update` is a dispatcher** — once all the `set_*` methods
|
||||
are split, `update` migrates to live on `RequestContext`
|
||||
(because it needs both `ctx.set_*` and `app.set_*` to
|
||||
dispatch to).
|
||||
|
||||
### What Step 7 should NOT do
|
||||
|
||||
- Don't touch the 4 new types from Step 6.5 (`ToolScope`,
|
||||
`McpRuntime`, `McpFactory`, `RagCache`, `AgentRuntime`).
|
||||
They're scaffolding, untouched until Step 8.
|
||||
- Don't try to populate `tool_scope` or `agent_runtime` from
|
||||
any Step 7 migration. Those are Step 8.
|
||||
- Don't migrate `use_role`, `use_session`, `use_agent`,
|
||||
`exit_agent`, or any method that touches
|
||||
`self.mcp_registry` / `self.functions`. Those are Step 8.
|
||||
- Don't migrate callers of any migrated method.
|
||||
- Don't touch the bridge's `to_request_context` /
|
||||
`to_app_config` / `from_parts`. The round-trip still
|
||||
works with `tool_scope` and `agent_runtime` defaulting.
|
||||
|
||||
### Files to re-read at the start of Step 7
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 7 section (the
|
||||
17-method table starting at line ~525)
|
||||
- This notes file — specifically the accumulated deferrals
|
||||
list from Steps 3-6 in the "What Step 7 should watch for"
|
||||
section
|
||||
- Step 6 notes — which methods got deferred from Step 6 vs
|
||||
Step 7 boundary
|
||||
|
||||
## Follow-up (not blocking Step 7)
|
||||
|
||||
### 1. Step 8's scope is now significantly larger
|
||||
|
||||
The original Phase 1 plan estimated Step 8 as "rewrite
|
||||
`main.rs` and `repl/mod.rs` to use `RequestContext`" — a
|
||||
meaningful but bounded refactor. After Step 6.5's deferral,
|
||||
Step 8 also includes:
|
||||
|
||||
- Implementing `McpFactory::acquire()` by extracting server
|
||||
startup logic from `McpRegistry::init_server`
|
||||
- Rewriting `use_role`, `use_session`, `use_agent`,
|
||||
`exit_agent`, `use_rag`, `edit_rag_docs`, `rebuild_rag`,
|
||||
`apply_prelude`, agent sub-spawning
|
||||
- Wiring `tool_scope` population into all the above
|
||||
- Populating `agent_runtime` on agent activation
|
||||
- Building the parent-scope `ToolScope` restoration logic in
|
||||
`exit_agent`
|
||||
- Routing `rebuild_rag` / `edit_rag_docs` through
|
||||
`RagCache::invalidate`
|
||||
|
||||
This is a big step. The phase plan should be updated to
|
||||
either split Step 8 into sub-steps or to flag the expanded
|
||||
scope.
|
||||
|
||||
### 2. `McpFactory::acquire()` extraction is its own mini-project
|
||||
|
||||
Looking at `src/mcp/mod.rs`, the subprocess spawn + rmcp
|
||||
handshake lives inside `McpRegistry::init_server` (private
|
||||
method, ~60 lines). Step 8's first task should be extracting
|
||||
this into a pair of functions:
|
||||
|
||||
1. `McpFactory::spawn_fresh(spec: &McpServerSpec) ->
|
||||
Result<ConnectedServer>` — pure subprocess + handshake
|
||||
logic
|
||||
2. `McpRegistry::init_server` — wraps `spawn_fresh` with
|
||||
registry bookkeeping (adds to `servers` map, fires catalog
|
||||
discovery, etc.) for backward compat
|
||||
|
||||
Then `McpFactory::acquire()` can call `spawn_fresh` on cache
|
||||
miss. The existing `McpRegistry::init_server` keeps working
|
||||
for the bridge window callers.
|
||||
|
||||
### 3. The `load_with` race is documented but not fixed
|
||||
|
||||
`RagCache::load_with` has a race window: two concurrent
|
||||
callers with the same key both miss the cache, both call
|
||||
the loader closure, both insert into the map. The second
|
||||
insert overwrites the first. Both callers end up with valid
|
||||
`Arc<Rag>`s but the cache sharing is broken for that
|
||||
instant.
|
||||
|
||||
For Phase 1 Step 6.5, this is acceptable because the cache
|
||||
isn't populated by real usage yet. Phase 5's pooling work
|
||||
should tighten this with per-key `OnceCell` or
|
||||
`tokio::sync::Mutex`.
|
||||
|
||||
### 4. Bridge-window duplication count at end of Step 6.5
|
||||
|
||||
Running tally:
|
||||
|
||||
- `AppConfig` (Steps 3+4): 11 methods duplicated with `Config`
|
||||
- `RequestContext` (Steps 5+6): 25 methods duplicated with
|
||||
`Config` (1 constructor + 13 reads + 12 writes)
|
||||
- `paths` module (Step 2): 33 free functions (not duplicated)
|
||||
- **Step 6.5 NEW:** 4 types + 2 `AppState` fields + 2
|
||||
`RequestContext` fields — **all additive scaffolding, no
|
||||
duplication of logic**
|
||||
|
||||
**Total bridge-window duplication: 36 methods / ~550 lines**,
|
||||
unchanged from end of Step 6. Step 6.5 added types but not
|
||||
duplicated logic.
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Architecture doc: `docs/REST-API-ARCHITECTURE.md` section 5
|
||||
- Phase 5 plan: `docs/PHASE-5-IMPLEMENTATION-PLAN.md`
|
||||
- Step 6 notes: `docs/implementation/PHASE-1-STEP-6-NOTES.md`
|
||||
- New files:
|
||||
- `src/config/tool_scope.rs`
|
||||
- `src/config/mcp_factory.rs`
|
||||
- `src/config/rag_cache.rs`
|
||||
- `src/config/agent_runtime.rs`
|
||||
- Modified files:
|
||||
- `src/mcp/mod.rs` (`type ConnectedServer` → `pub type`)
|
||||
- `src/config/mod.rs` (4 new `mod` declarations)
|
||||
- `src/config/app_state.rs` (2 new fields + docstring)
|
||||
- `src/config/request_context.rs` (2 new fields + docstring)
|
||||
- `src/config/bridge.rs` (3 test `AppState` constructors
|
||||
updated, `to_request_context` adds 2 defaults)
|
||||
@@ -0,0 +1,536 @@
|
||||
# Phase 1 Step 7 — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 7: Tackle mixed methods (THE HARD PART)"
|
||||
|
||||
## Summary
|
||||
|
||||
Added 14 mixed-method splits to the new types, plus 6 global-
|
||||
default setters on `AppConfig`. The methods that mix serialized
|
||||
config reads/writes with runtime state reads/writes are now
|
||||
available on `RequestContext` with `&AppConfig` as an explicit
|
||||
parameter for the serialized half.
|
||||
|
||||
Same bridge pattern as Steps 3–6: `Config`'s originals stay
|
||||
intact, new methods sit alongside, caller migration happens in
|
||||
Step 8.
|
||||
|
||||
**Step 7 completed ~65% of its planned scope.** Nine target
|
||||
methods were deferred to Step 8 because they transitively
|
||||
depend on `Model::retrieve_model(&Config)` and
|
||||
`list_models(&Config)` — refactoring those requires touching
|
||||
the `client` module macros, which is beyond Step 7's bridge-
|
||||
pattern scope. Step 8 will rewrite them alongside the entry
|
||||
point migration.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Modified files
|
||||
|
||||
- **`src/config/app_config.rs`** — added a third `impl AppConfig`
|
||||
block with 6 `set_*_default` methods for the serialized-field
|
||||
half of the mixed-method splits:
|
||||
- `set_temperature_default`
|
||||
- `set_top_p_default`
|
||||
- `set_enabled_tools_default`
|
||||
- `set_enabled_mcp_servers_default`
|
||||
- `set_save_session_default`
|
||||
- `set_compression_threshold_default`
|
||||
|
||||
- **`src/config/request_context.rs`** — added a fourth
|
||||
`impl RequestContext` block with 14 methods:
|
||||
|
||||
**Helpers (2):**
|
||||
- `current_model(&self) -> &Model` — pure runtime traversal
|
||||
(session > agent > role > ctx.model)
|
||||
- `extract_role(&self, app: &AppConfig) -> Role` — pure
|
||||
runtime except fallback reads `app.temperature`,
|
||||
`app.top_p`, `app.enabled_tools`, `app.enabled_mcp_servers`
|
||||
|
||||
**Role-like setters (7):** these all return `bool`
|
||||
indicating whether they mutated a `RoleLike` (if `false`,
|
||||
the caller should fall back to
|
||||
`app.set_<name>_default()`). This preserves the exact
|
||||
semantics of today's `Config::set_*` methods:
|
||||
- `set_temperature_on_role_like`
|
||||
- `set_top_p_on_role_like`
|
||||
- `set_enabled_tools_on_role_like`
|
||||
- `set_enabled_mcp_servers_on_role_like`
|
||||
- `set_save_session_on_session` (uses `self.session` directly,
|
||||
not `role_like_mut`)
|
||||
- `set_compression_threshold_on_session` (same)
|
||||
- `set_max_output_tokens_on_role_like`
|
||||
|
||||
**Chat lifecycle (2):**
|
||||
- `save_message(&mut self, app: &AppConfig, input, output)` —
|
||||
writes to session if present, else to messages file if
|
||||
`app.save` is true
|
||||
- `after_chat_completion(&mut self, app, input, output,
|
||||
tool_results)` — updates `last_message`, calls
|
||||
`save_message` if not `app.dry_run`
|
||||
- `open_message_file(&self) -> Result<File>` — private
|
||||
helper
|
||||
|
||||
**Info getters (3):**
|
||||
- `sysinfo(&self, app: &AppConfig) -> Result<String>` —
|
||||
~70-line display output mixing serialized and runtime
|
||||
state
|
||||
- `info(&self, app: &AppConfig) -> Result<String>` —
|
||||
delegates to `sysinfo` in fallback branch
|
||||
- `session_info(&self, app: &AppConfig) -> Result<String>` —
|
||||
calls `app.render_options()`
|
||||
|
||||
**Prompt rendering (3):**
|
||||
- `generate_prompt_context(&self, app) -> HashMap<&str, String>` —
|
||||
builds the template variable map
|
||||
- `render_prompt_left(&self, app) -> String`
|
||||
- `render_prompt_right(&self, app) -> String`
|
||||
|
||||
**Function selection (3):**
|
||||
- `select_enabled_functions(&self, app, role) -> Vec<FunctionDeclaration>` —
|
||||
filters `ctx.functions.declarations()` by role's enabled
|
||||
tools + agent filters + user interaction functions
|
||||
- `select_enabled_mcp_servers(&self, app, role) -> Vec<...>` —
|
||||
same pattern for MCP meta-functions
|
||||
- `select_functions(&self, app, role) -> Option<Vec<...>>` —
|
||||
combines both
|
||||
|
||||
- **`src/config/mod.rs`** — bumped `format_option_value` from
|
||||
private to `pub(super)` so `request_context.rs` can use it
|
||||
as `super::format_option_value`.
|
||||
|
||||
### Unchanged files
|
||||
|
||||
- **`src/config/mod.rs`** — all Step 7 target methods still
|
||||
exist on `Config`. They continue to work for every current
|
||||
caller.
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. Same bridge pattern as Steps 3-6
|
||||
|
||||
Step 7 follows the same additive pattern as earlier steps: new
|
||||
methods on `AppConfig` / `RequestContext`, `Config`'s originals
|
||||
untouched, no caller migration. Caller migration is Step 8.
|
||||
|
||||
The plan's Step 7 description implied a semantic rewrite
|
||||
("split into explicit parameter passing") but that phrasing
|
||||
applies to the target signatures, not the migration mechanism.
|
||||
The bridge pattern achieves the same end state — methods with
|
||||
`(&AppConfig, &RequestContext)` signatures exist and are ready
|
||||
for Step 8 to call.
|
||||
|
||||
### 2. `set_*` methods split into `_on_role_like` + `_default` pair
|
||||
|
||||
Today's `Config::set_temperature` does:
|
||||
```rust
|
||||
match self.role_like_mut() {
|
||||
Some(role_like) => role_like.set_temperature(value),
|
||||
None => self.temperature = value,
|
||||
}
|
||||
```
|
||||
|
||||
The Step 7 split:
|
||||
```rust
|
||||
// On RequestContext:
|
||||
fn set_temperature_on_role_like(&mut self, value) -> bool {
|
||||
match self.role_like_mut() {
|
||||
Some(rl) => { rl.set_temperature(value); true }
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
// On AppConfig:
|
||||
fn set_temperature_default(&mut self, value) {
|
||||
self.temperature = value;
|
||||
}
|
||||
```
|
||||
|
||||
**The bool return** is the caller contract: if `_on_role_like`
|
||||
returns `false`, the caller must call
|
||||
`app.set_*_default(value)`. This is what Step 8 callers will
|
||||
do:
|
||||
```rust
|
||||
if !ctx.set_temperature_on_role_like(value) {
|
||||
Arc::get_mut(&mut app.config).unwrap().set_temperature_default(value);
|
||||
}
|
||||
```
|
||||
|
||||
(Or more likely, the AppConfig mutation gets hidden behind a
|
||||
helper on `AppState` since `AppConfig` is behind `Arc`.)
|
||||
|
||||
This split is semantically equivalent to the existing
|
||||
behavior while making the "where the value goes" decision
|
||||
explicit at the type level.
|
||||
|
||||
### 3. `save_message` and `after_chat_completion` migrated together
|
||||
|
||||
`after_chat_completion` reads `app.dry_run` and calls
|
||||
`save_message`, which reads `app.save`. Both got deferred from
|
||||
Step 6 for exactly this mixed-dependency reason. Step 7
|
||||
migrates them together:
|
||||
|
||||
```rust
|
||||
pub fn after_chat_completion(
|
||||
&mut self,
|
||||
app: &AppConfig,
|
||||
input: &Input,
|
||||
output: &str,
|
||||
tool_results: &[ToolResult],
|
||||
) -> Result<()> {
|
||||
if !tool_results.is_empty() { return Ok(()); }
|
||||
self.last_message = Some(LastMessage::new(input.clone(), output.to_string()));
|
||||
if !app.dry_run {
|
||||
self.save_message(app, input, output)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
The `open_message_file` helper moved along with them since
|
||||
it's only called from `save_message`.
|
||||
|
||||
### 4. `format_option_value` visibility bump
|
||||
|
||||
`format_option_value` is a tiny private helper in
|
||||
`src/config/mod.rs` that `sysinfo` uses. Step 7's new
|
||||
`RequestContext::sysinfo` needs to call it, so I bumped its
|
||||
visibility from `fn` to `pub(super)`. This is a minimal
|
||||
change (one word) that lets child modules reuse the helper
|
||||
without duplicating it.
|
||||
|
||||
### 5. `select_*` methods were Step 3 deferrals
|
||||
|
||||
The plan's Step 3 table originally listed `select_functions`,
|
||||
`select_enabled_functions`, and `select_enabled_mcp_servers`
|
||||
as global-read method targets. Step 3's notes correctly
|
||||
flagged them as actually-mixed because they read `self.functions`
|
||||
and `self.agent` (runtime, not serialized).
|
||||
|
||||
Step 7 is the right home for them. They take
|
||||
`(&self, app: &AppConfig, role: &Role)` and read:
|
||||
- `ctx.functions.declarations()` (runtime — existing flat
|
||||
field, will collapse into `tool_scope.functions` in Step 8+)
|
||||
- `ctx.agent` (runtime)
|
||||
- `app.function_calling_support`, `app.mcp_server_support`,
|
||||
`app.mapping_tools`, `app.mapping_mcp_servers` (serialized)
|
||||
|
||||
The implementations are long (~80 lines each) but are
|
||||
verbatim copies of the `Config` originals with `self.X`
|
||||
replaced by `app.X` for serialized fields and `self.X`
|
||||
preserved for runtime fields.
|
||||
|
||||
### 6. `session_info` keeps using `crate::render::MarkdownRender`
|
||||
|
||||
I didn't add a top-level `use crate::render::MarkdownRender`
|
||||
because it's only called from `session_info`. Inline
|
||||
`crate::render::MarkdownRender::init(...)` is clearer than
|
||||
adding another global import for a single use site.
|
||||
|
||||
### 7. Imports grew substantially
|
||||
|
||||
`request_context.rs` now imports from 7 new sources compared
|
||||
to the end of Step 6:
|
||||
- `super::AppConfig` (for the mixed-method params)
|
||||
- `super::MessageContentToolCalls` (for `save_message`)
|
||||
- `super::LEFT_PROMPT`, `super::RIGHT_PROMPT` (for prompt
|
||||
rendering)
|
||||
- `super::ensure_parent_exists` (for `open_message_file`)
|
||||
- `crate::function::FunctionDeclaration`,
|
||||
`crate::function::user_interaction::USER_FUNCTION_PREFIX`
|
||||
- `crate::mcp::MCP_*_META_FUNCTION_NAME_PREFIX` (3 constants)
|
||||
- `std::collections::{HashMap, HashSet}`,
|
||||
`std::fs::{File, OpenOptions}`, `std::io::Write`,
|
||||
`std::path::Path`, `crate::utils::{now, render_prompt}`
|
||||
|
||||
This is expected — Step 7's methods are the most
|
||||
dependency-heavy in Phase 1. Post–Phase 1 cleanup can
|
||||
reorganize into separate files if the module becomes
|
||||
unwieldy.
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
### 9 methods deferred to Step 8
|
||||
|
||||
| Method | Why deferred |
|
||||
|---|---|
|
||||
| `retrieve_role` | Calls `Model::retrieve_model(&Config)` transitively, needs client module refactor |
|
||||
| `set_model` | Calls `Model::retrieve_model(&Config)` transitively |
|
||||
| `set_rag_reranker_model` | Takes `&GlobalConfig`, uses `update_rag` helper with Arc<RwLock> take/replace pattern |
|
||||
| `set_rag_top_k` | Same as above |
|
||||
| `update` | Dispatcher over all `set_*` methods including the 2 above, plus takes `&GlobalConfig` and touches `mcp_registry` |
|
||||
| `repl_complete` | Calls `list_models(&Config)` + reads `self.mcp_registry` (going away in Step 6.5/8), + reads `self.functions` |
|
||||
| `use_role_safely` | Takes `&GlobalConfig`, does `take()`/`replace()` on Arc<RwLock> |
|
||||
| `use_session_safely` | Same as above |
|
||||
| `setup_model` | Calls `self.set_model()` which is deferred |
|
||||
| `use_prompt` (Step 6 deferral) | Calls `current_model()` (migratable) and `use_role_obj` (migrated in Step 6), but the whole method is 4 lines and not independently useful without its callers |
|
||||
| `edit_role` (Step 6 deferral) | Calls `self.upsert_role()` and `self.use_role()` which are Step 8 |
|
||||
|
||||
**Root cause of most deferrals:** the `client` module's
|
||||
`list_all_models` macro and `Model::retrieve_model` take
|
||||
`&Config`. Refactoring them to take `&AppConfig` is a
|
||||
meaningful cross-module change that belongs in Step 8
|
||||
alongside the caller migration.
|
||||
|
||||
### 14 methods migrated
|
||||
|
||||
| Method | New signature |
|
||||
|---|---|
|
||||
| `current_model` | `&self -> &Model` (pure RequestContext) |
|
||||
| `extract_role` | `(&self, &AppConfig) -> Role` |
|
||||
| `set_temperature_on_role_like` | `(&mut self, Option<f64>) -> bool` |
|
||||
| `set_top_p_on_role_like` | `(&mut self, Option<f64>) -> bool` |
|
||||
| `set_enabled_tools_on_role_like` | `(&mut self, Option<String>) -> bool` |
|
||||
| `set_enabled_mcp_servers_on_role_like` | `(&mut self, Option<String>) -> bool` |
|
||||
| `set_save_session_on_session` | `(&mut self, Option<bool>) -> bool` |
|
||||
| `set_compression_threshold_on_session` | `(&mut self, Option<usize>) -> bool` |
|
||||
| `set_max_output_tokens_on_role_like` | `(&mut self, Option<isize>) -> bool` |
|
||||
| `save_message` | `(&mut self, &AppConfig, &Input, &str) -> Result<()>` |
|
||||
| `after_chat_completion` | `(&mut self, &AppConfig, &Input, &str, &[ToolResult]) -> Result<()>` |
|
||||
| `sysinfo` | `(&self, &AppConfig) -> Result<String>` |
|
||||
| `info` | `(&self, &AppConfig) -> Result<String>` |
|
||||
| `session_info` | `(&self, &AppConfig) -> Result<String>` |
|
||||
| `generate_prompt_context` | `(&self, &AppConfig) -> HashMap<&str, String>` |
|
||||
| `render_prompt_left` | `(&self, &AppConfig) -> String` |
|
||||
| `render_prompt_right` | `(&self, &AppConfig) -> String` |
|
||||
| `select_functions` | `(&self, &AppConfig, &Role) -> Option<Vec<...>>` |
|
||||
| `select_enabled_functions` | `(&self, &AppConfig, &Role) -> Vec<...>` |
|
||||
| `select_enabled_mcp_servers` | `(&self, &AppConfig, &Role) -> Vec<...>` |
|
||||
|
||||
Actually that's 20 methods across the two types (6 on
|
||||
`AppConfig`, 14 on `RequestContext`). "14 migrated" refers to
|
||||
the 14 behavior methods on `RequestContext`; the 6 on
|
||||
`AppConfig` are the paired defaults for the 7 role-like
|
||||
setters (4 `set_*_default` + 2 session-specific — the
|
||||
`set_max_output_tokens` split doesn't need a default
|
||||
because `ctx.model.set_max_tokens()` works without a
|
||||
fallback).
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, **zero warnings, zero errors**
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (unchanged from
|
||||
Steps 1–6.5)
|
||||
|
||||
The bridge's round-trip test still passes, confirming the new
|
||||
methods don't interfere with struct layout or the
|
||||
`Config → AppConfig + RequestContext → Config` invariant.
|
||||
|
||||
### Manual smoke test
|
||||
|
||||
Not applicable — no runtime behavior changed. CLI and REPL
|
||||
still call `Config::set_temperature`, `Config::sysinfo`,
|
||||
`Config::save_message`, etc. as before.
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What Step 8 can rely on
|
||||
|
||||
Step 8 (entry point rewrite) can rely on:
|
||||
|
||||
- **`AppConfig` now has 17 methods** (Steps 3+4+7): 7 reads
|
||||
+ 4 writes + 6 setter-defaults
|
||||
- **`RequestContext` now has 39 inherent methods** across 5
|
||||
impl blocks: 1 constructor + 13 reads + 12 writes + 14
|
||||
mixed
|
||||
- **All of `AppConfig`'s and `RequestContext`'s new methods
|
||||
are under `#[allow(dead_code)]`** — that's safe to leave
|
||||
alone; callers wire them up in Step 8 and the allows
|
||||
become inert
|
||||
- **`format_option_value` is `pub(super)`** — accessible
|
||||
from any `config` child module
|
||||
- **The bridge (`Config::to_app_config`, `to_request_context`,
|
||||
`from_parts`) still works** and all round-trip tests pass
|
||||
- **The `paths` module, Step 3/4 `AppConfig` methods, Step
|
||||
5/6 `RequestContext` methods, Step 6.5 scaffolding types
|
||||
are all unchanged**
|
||||
- **These `Config` methods are still on `Config`** and must
|
||||
stay there through Step 8 (they're Step 8 targets):
|
||||
- `retrieve_role`, `set_model`, `set_rag_reranker_model`,
|
||||
`set_rag_top_k`, `update`, `repl_complete`,
|
||||
`use_role_safely`, `use_session_safely`, `setup_model`,
|
||||
`use_prompt`, `edit_role`
|
||||
- Plus the Step 6 Category A deferrals: `use_role`,
|
||||
`use_session`, `use_agent`, `exit_agent`
|
||||
- Plus the Step 6 Category C deferrals: `compress_session`,
|
||||
`maybe_compress_session`, `autoname_session`,
|
||||
`maybe_autoname_session`, `use_rag`, `edit_rag_docs`,
|
||||
`rebuild_rag`, `apply_prelude`
|
||||
|
||||
### What Step 8 should watch for
|
||||
|
||||
**Step 8 is the biggest remaining step** after Step 6.5
|
||||
deferred its scope-transition rewrites. Step 8 now absorbs:
|
||||
|
||||
1. **Entry point rewrite** (original Step 8 scope):
|
||||
- `main.rs::run()` constructs `AppState` + `RequestContext`
|
||||
instead of `GlobalConfig`
|
||||
- `main.rs::start_directive()` takes
|
||||
`&mut RequestContext` instead of `&GlobalConfig`
|
||||
- `main.rs::create_input()` takes `&RequestContext`
|
||||
- `repl/mod.rs::Repl` holds a long-lived `RequestContext`
|
||||
instead of `GlobalConfig`
|
||||
- All 91 callsites in the original migration table
|
||||
|
||||
2. **`Model::retrieve_model` refactor** (Step 7 deferrals):
|
||||
- `Model::retrieve_model(config: &Config, ...)` →
|
||||
`Model::retrieve_model(config: &AppConfig, ...)`
|
||||
- `list_all_models!(config: &Config)` macro →
|
||||
`list_all_models!(config: &AppConfig)`
|
||||
- `list_models(config: &Config, ...)` →
|
||||
`list_models(config: &AppConfig, ...)`
|
||||
- Then migrate `retrieve_role`, `set_model`,
|
||||
`repl_complete`, `setup_model`
|
||||
|
||||
3. **RAG lifecycle migration** (Step 7 deferrals +
|
||||
Step 6 Category C):
|
||||
- `use_rag`, `edit_rag_docs`, `rebuild_rag` →
|
||||
`RequestContext` methods using `RagCache`
|
||||
- `set_rag_reranker_model`, `set_rag_top_k` → split
|
||||
similarly to Step 7 setters
|
||||
|
||||
4. **Scope transition rewrites** (Step 6.5 deferrals):
|
||||
- `use_role`, `use_session`, `use_agent`, `exit_agent`
|
||||
rewritten to build `ToolScope` via `McpFactory`
|
||||
- `McpFactory::acquire()` extracted from
|
||||
`McpRegistry::init_server`
|
||||
- `use_role_safely`, `use_session_safely` eliminated
|
||||
(not needed once callers hold `&mut RequestContext`)
|
||||
|
||||
5. **Session lifecycle migration** (Step 6 Category C):
|
||||
- `compress_session`, `maybe_compress_session`,
|
||||
`autoname_session`, `maybe_autoname_session` → methods
|
||||
that take `&mut RequestContext` instead of spawning
|
||||
tasks with `GlobalConfig`
|
||||
- `apply_prelude` → uses migrated `use_role` /
|
||||
`use_session`
|
||||
|
||||
6. **`update` dispatcher** (Step 7 deferral):
|
||||
- Once all `set_*` are available on `RequestContext` and
|
||||
`AppConfig`, `update` becomes a dispatcher over the
|
||||
new split pair
|
||||
|
||||
This is a **huge** step. Consider splitting into 8a-8f
|
||||
sub-steps or staging across multiple PRs.
|
||||
|
||||
### What Step 8 should NOT do
|
||||
|
||||
- Don't re-migrate any Step 3-7 method
|
||||
- Don't touch the new types from Step 6.5 unless actually
|
||||
implementing `McpFactory::acquire()` or
|
||||
`RagCache::load_with` usage
|
||||
- Don't leave intermediate states broken — each sub-step
|
||||
should keep the build green, even if it means keeping
|
||||
temporary dual code paths
|
||||
|
||||
### Files to re-read at the start of Step 8
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 8 section
|
||||
- This notes file — specifically the deferrals table and
|
||||
Step 8 watch items
|
||||
- Step 6.5 notes — scope transition rewrite details
|
||||
- Step 6 notes — Category C deferral inventory
|
||||
- `src/config/mod.rs` — still has ~25 methods that need
|
||||
migrating
|
||||
|
||||
## Follow-up (not blocking Step 8)
|
||||
|
||||
### 1. Bridge-window duplication count at end of Step 7
|
||||
|
||||
Running tally:
|
||||
|
||||
- `AppConfig` (Steps 3+4+7): 17 methods (11 reads/writes +
|
||||
6 setter-defaults)
|
||||
- `RequestContext` (Steps 5+6+7): 39 methods (1 constructor +
|
||||
13 reads + 12 writes + 14 mixed)
|
||||
- `paths` module (Step 2): 33 free functions
|
||||
- Step 6.5 types: 4 new types on scaffolding
|
||||
|
||||
**Total bridge-window duplication: 56 methods / ~1200 lines**
|
||||
(up from 36 / ~550 at end of Step 6).
|
||||
|
||||
All auto-delete in Step 10.
|
||||
|
||||
### 2. `request_context.rs` is now ~900 lines
|
||||
|
||||
Getting close to the point where splitting into multiple
|
||||
files would help readability. Candidate layout:
|
||||
- `request_context/mod.rs` — struct definition + constructor
|
||||
- `request_context/reads.rs` — Step 5 methods
|
||||
- `request_context/writes.rs` — Step 6 methods
|
||||
- `request_context/mixed.rs` — Step 7 methods
|
||||
|
||||
Not blocking anything; consider during Phase 1 cleanup.
|
||||
|
||||
### 3. The `set_*_on_role_like` / `set_*_default` split
|
||||
has an unusual caller contract
|
||||
|
||||
Callers of the split have to remember: "call `_on_role_like`
|
||||
first, check the bool, call `_default` if false." That's
|
||||
more verbose than today's `Config::set_temperature` which
|
||||
hides the dispatch.
|
||||
|
||||
Step 8 should add convenience helpers on `RequestContext`
|
||||
that wrap both halves:
|
||||
|
||||
```rust
|
||||
pub fn set_temperature(&mut self, value: Option<f64>, app: &mut AppConfig) {
|
||||
if !self.set_temperature_on_role_like(value) {
|
||||
app.set_temperature_default(value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
But that requires `&mut AppConfig`, which requires unwrapping
|
||||
the `Arc` on `AppState.config`. The cleanest shape is probably
|
||||
to move the mutation into a helper on `AppState`:
|
||||
|
||||
```rust
|
||||
impl AppState {
|
||||
pub fn config_mut(&self) -> Option<&mut AppConfig> {
|
||||
Arc::get_mut(...)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or accept that the `.set` REPL command needs an owned
|
||||
`AppState` (not `Arc<AppState>`) and handle the mutation at
|
||||
the entry point. Step 8 can decide.
|
||||
|
||||
### 4. `select_*` methods are long but verbatim
|
||||
|
||||
The 3 `select_*` methods are ~180 lines combined and are
|
||||
verbatim copies of the `Config` originals. I resisted the
|
||||
urge to refactor (extract helpers, simplify the
|
||||
`enabled_tools == "all"` branches, etc.) because:
|
||||
|
||||
- Step 7 is about splitting signatures, not style
|
||||
- The copies get deleted in Step 10 anyway
|
||||
- Any refactor could introduce subtle behavior differences
|
||||
that are hard to catch without a functional test for these
|
||||
specific methods
|
||||
|
||||
Post–Phase 1 cleanup can factor these if desired.
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Step 6 notes: `docs/implementation/PHASE-1-STEP-6-NOTES.md`
|
||||
- Step 6.5 notes: `docs/implementation/PHASE-1-STEP-6.5-NOTES.md`
|
||||
- Modified files:
|
||||
- `src/config/app_config.rs` (6 new `set_*_default` methods)
|
||||
- `src/config/request_context.rs` (14 new mixed methods,
|
||||
7 new imports)
|
||||
- `src/config/mod.rs` (`format_option_value` → `pub(super)`)
|
||||
@@ -0,0 +1,374 @@
|
||||
# Phase 1 Step 8a — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 8a: Client module refactor — `Model::retrieve_model`
|
||||
takes `&AppConfig`"
|
||||
|
||||
## Summary
|
||||
|
||||
Migrated the LLM client module's 4 `&Config`-taking functions to take
|
||||
`&AppConfig` instead, and updated all 15 callsites across 7 files to
|
||||
use the `Config::to_app_config()` bridge helper (already exists from
|
||||
Step 1). No new types, no new methods — this is a signature change
|
||||
that propagates through the codebase.
|
||||
|
||||
**This unblocks Step 8b**, where `Config::retrieve_role`,
|
||||
`Config::set_model`, `Config::repl_complete`, and
|
||||
`Config::setup_model` (Step 7 deferrals) can finally migrate to
|
||||
`RequestContext` methods that take `&AppConfig` — they were blocked
|
||||
on `Model::retrieve_model` expecting `&Config`.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified (8 files, 15 callsite updates)
|
||||
|
||||
- **`src/client/macros.rs`** — changed 3 signatures in the
|
||||
`register_client!` macro (the functions it generates at expansion
|
||||
time):
|
||||
- `list_client_names(config: &Config)` → `(config: &AppConfig)`
|
||||
- `list_all_models(config: &Config)` → `(config: &AppConfig)`
|
||||
- `list_models(config: &Config, ModelType)` → `(config: &AppConfig, ModelType)`
|
||||
|
||||
All three functions only read `config.clients` which is a
|
||||
serialized field identical on both types. The `OnceLock` caches
|
||||
(`ALL_CLIENT_NAMES`, `ALL_MODELS`) work identically because
|
||||
`AppConfig.clients` holds the same values as `Config.clients`.
|
||||
|
||||
- **`src/client/model.rs`** — changed the `use` and function
|
||||
signature:
|
||||
- `use crate::config::Config` → `use crate::config::AppConfig`
|
||||
- `Model::retrieve_model(config: &Config, ...)` → `(config: &AppConfig, ...)`
|
||||
|
||||
The function body was unchanged — it calls `list_all_models(config)`
|
||||
and `list_client_names(config)` internally, both of which now take
|
||||
the same `&AppConfig` type.
|
||||
|
||||
- **`src/config/mod.rs`** (6 callsite updates):
|
||||
- `set_rag_reranker_model` → `Model::retrieve_model(&config.read().to_app_config(), ...)`
|
||||
- `set_model` → `Model::retrieve_model(&self.to_app_config(), ...)`
|
||||
- `retrieve_role` → `Model::retrieve_model(&self.to_app_config(), ...)`
|
||||
- `repl_complete` (`.model` branch) → `list_models(&self.to_app_config(), ModelType::Chat)`
|
||||
- `repl_complete` (`.rag_reranker_model` branch) → `list_models(&self.to_app_config(), ModelType::Reranker)`
|
||||
- `setup_model` → `list_models(&self.to_app_config(), ModelType::Chat)`
|
||||
|
||||
- **`src/config/session.rs`** — `Session::load` caller updated:
|
||||
`Model::retrieve_model(&config.to_app_config(), ...)`
|
||||
|
||||
- **`src/config/agent.rs`** — `Agent::init` caller updated:
|
||||
`Model::retrieve_model(&config.to_app_config(), model_id, ModelType::Chat)?`
|
||||
(required reformatting because the one-liner became two lines)
|
||||
|
||||
- **`src/function/supervisor.rs`** — sub-agent summarization model
|
||||
lookup: `Model::retrieve_model(&cfg.to_app_config(), ...)`
|
||||
|
||||
- **`src/rag/mod.rs`** (4 callsite updates):
|
||||
- `Rag::create` embedding model lookup
|
||||
- `Rag::init` `list_models` for embedding model selection
|
||||
- `Rag::init` `retrieve_model` for embedding model
|
||||
- `Rag::search` reranker model lookup
|
||||
|
||||
- **`src/main.rs`** — `--list-models` CLI flag handler:
|
||||
`list_models(&config.read().to_app_config(), ModelType::Chat)`
|
||||
|
||||
- **`src/cli/completer.rs`** — shell completion for `--model`:
|
||||
`list_models(&config.to_app_config(), ModelType::Chat)`
|
||||
|
||||
### Files NOT changed
|
||||
|
||||
- **`src/config/bridge.rs`** — the `Config::to_app_config()` method
|
||||
from Step 1 is exactly the bridge helper Step 8a needed. No new
|
||||
method was added; I just started using the existing one.
|
||||
- **`src/client/` other files** — only `macros.rs` and `model.rs`
|
||||
had the target signatures. Individual client implementations
|
||||
(`openai.rs`, `claude.rs`, etc.) don't reference `&Config`
|
||||
directly; they work through the `Client` trait which uses
|
||||
`GlobalConfig` internally (untouched).
|
||||
- **Any file calling `init_client` or `GlobalConfig`** — these are
|
||||
separate from the model-lookup path and stay on `GlobalConfig`
|
||||
through the bridge. Step 8f/8g will migrate them.
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. Reused `Config::to_app_config()` instead of adding `app_config_snapshot`
|
||||
|
||||
The plan said to add a `Config::app_config_snapshot(&self) -> AppConfig`
|
||||
helper. That's exactly what `Config::to_app_config()` from Step 1
|
||||
already does — clones every serialized field into a fresh `AppConfig`.
|
||||
Adding a second method with the same body would be pointless
|
||||
duplication.
|
||||
|
||||
I proceeded directly with `to_app_config()` and the plan's intent
|
||||
is satisfied.
|
||||
|
||||
### 2. Inline `.to_app_config()` at every callsite
|
||||
|
||||
Each callsite pattern is:
|
||||
```rust
|
||||
// old:
|
||||
Model::retrieve_model(config, ...)
|
||||
// new:
|
||||
Model::retrieve_model(&config.to_app_config(), ...)
|
||||
```
|
||||
|
||||
The owned `AppConfig` returned by `to_app_config()` lives for the
|
||||
duration of the function argument expression, so `&` borrowing works
|
||||
without a named binding. For multi-line callsites (like `Rag::create`
|
||||
and `Rag::init` in `src/rag/mod.rs`) I reformatted to put the
|
||||
`to_app_config()` call on its own line for readability.
|
||||
|
||||
### 3. Allocation cost is acceptable during the bridge window
|
||||
|
||||
Every callsite now clones 40 fields (the serialized half of `Config`)
|
||||
per call. This is measurably more work than the pre-refactor code,
|
||||
which passed a shared borrow. The allocation cost is:
|
||||
|
||||
- **~15 callsites × ~40 field clones each** = ~600 extra heap
|
||||
operations per full CLI invocation
|
||||
- In practice, most of these are `&str` / `String` / primitive
|
||||
clones, plus a few `IndexMap` and `Vec` clones — dominated by
|
||||
`clients: Vec<ClientConfig>`
|
||||
- Total cost per call: well under 1ms, invisible to users
|
||||
- Cost ends in Step 8f/8g when callers hold `Arc<AppState>`
|
||||
directly and can pass `&app.config` without cloning
|
||||
|
||||
The plan flagged this as an acceptable bridge-window cost, and the
|
||||
measurements back that up. No optimization is needed.
|
||||
|
||||
### 4. No use of deprecated forwarders
|
||||
|
||||
Unlike Steps 3-7 which added new methods alongside the old ones,
|
||||
Step 8a is a **one-shot signature change** of 4 functions plus
|
||||
their 15 callers. The bridge helper is `Config::to_app_config()`
|
||||
(already existed); the new signature is on the same function
|
||||
(not a parallel new function). This is consistent with the plan's
|
||||
Step 8a description of "one-shot refactor with bridge helper."
|
||||
|
||||
### 5. Did not touch `init_client`, `GlobalConfig`, or client instance state
|
||||
|
||||
The `register_client!` macro defines `$Client::init(global_config,
|
||||
model)` and `init_client(config, model)` — both take
|
||||
`&GlobalConfig` and read `config.read().model` (the runtime field).
|
||||
These are **not** Step 8a targets. They stay on `GlobalConfig`
|
||||
through the bridge and migrate in Step 8f/8g when callers switch
|
||||
from `GlobalConfig` to `Arc<AppState> + RequestContext`.
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
**None of substance.** The plan's Step 8a description was clear
|
||||
and straightforward; the implementation matches it closely. Two
|
||||
minor departures:
|
||||
|
||||
1. **Used existing `to_app_config()` instead of adding
|
||||
`app_config_snapshot()`** — see Key Decision #1. The plan's
|
||||
intent was a helper that clones serialized fields; both names
|
||||
describe the same thing.
|
||||
|
||||
2. **Count: 15 callsite updates, not 17** — the plan said "any
|
||||
callsite that currently calls these client functions." I found
|
||||
15 via `grep`. The count is close enough that this isn't a
|
||||
meaningful deviation, just an accurate enumeration.
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, **zero warnings, zero errors**
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (unchanged from
|
||||
Steps 1–7)
|
||||
|
||||
Step 8a added no new tests — it's a mechanical signature change
|
||||
with no new behavior to verify. The existing test suite confirms:
|
||||
- The bridge round-trip test still passes (uses
|
||||
`Config::to_app_config()`, which is the bridge helper)
|
||||
- The `config::bridge::tests::*` suite — all 4 tests pass
|
||||
- No existing test broke
|
||||
|
||||
### Manual smoke test
|
||||
|
||||
Not performed as part of this step (would require running a real
|
||||
LLM request with various models). The plan's Step 8a verification
|
||||
suggests `loki --model openai:gpt-4o "hello"` as a sanity check,
|
||||
but that requires API credentials and a live LLM. A representative
|
||||
smoke test should be performed before declaring Phase 1 complete
|
||||
(in Step 10 or during release prep).
|
||||
|
||||
The signature change is mechanical — if it compiles and existing
|
||||
tests pass, the runtime behavior is identical by construction. The
|
||||
only behavior difference would be the extra `to_app_config()`
|
||||
clones, which don't affect correctness.
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What Step 8b can rely on
|
||||
|
||||
Step 8b (finish Step 7's deferred mixed-method migrations) can
|
||||
rely on:
|
||||
|
||||
- **`Model::retrieve_model(&AppConfig, ...)`** — available for the
|
||||
migrated `retrieve_role` method on `RequestContext`
|
||||
- **`list_models(&AppConfig, ModelType)`** — available for
|
||||
`repl_complete` and `setup_model` migration
|
||||
- **`list_all_models(&AppConfig)`** — available for internal use
|
||||
- **`list_client_names(&AppConfig)`** — available (though typically
|
||||
only called from inside `retrieve_model`)
|
||||
- **`Config::to_app_config()` bridge helper** — still works, still
|
||||
used by the old `Config` methods that call the client functions
|
||||
through the bridge
|
||||
- **All existing Config-based methods that use these functions**
|
||||
(e.g., `Config::set_model`, `Config::retrieve_role`,
|
||||
`Config::setup_model`) still compile and still work — they now
|
||||
call `self.to_app_config()` internally to adapt the signature
|
||||
|
||||
### What Step 8b should watch for
|
||||
|
||||
- **The 9 Step 7 deferrals** waiting for Step 8b:
|
||||
- `retrieve_role` (blocked by `retrieve_model` — now unblocked)
|
||||
- `set_model` (blocked by `retrieve_model` — now unblocked)
|
||||
- `repl_complete` (blocked by `list_models` — now unblocked)
|
||||
- `setup_model` (blocked by `list_models` — now unblocked)
|
||||
- `use_prompt` (calls `current_model` + `use_role_obj` — already
|
||||
unblocked; was deferred because it's a one-liner not worth
|
||||
migrating alone)
|
||||
- `edit_role` (calls `editor` + `upsert_role` + `use_role` —
|
||||
`use_role` is still Step 8d, so `edit_role` may stay deferred)
|
||||
- `set_rag_reranker_model` (takes `&GlobalConfig`, uses
|
||||
`update_rag` helper — may stay deferred to Step 8f/8g)
|
||||
- `set_rag_top_k` (same)
|
||||
- `update` (dispatcher over all `set_*` — needs all its
|
||||
dependencies migrated first)
|
||||
|
||||
- **`set_model` split pattern.** The old `Config::set_model` does
|
||||
`role_like_mut` dispatch. Step 8b should split it into
|
||||
`RequestContext::set_model_on_role_like(&mut self, app: &AppConfig,
|
||||
model_id: &str) -> Result<bool>` (returns whether a RoleLike was
|
||||
mutated) + `AppConfig::set_model_default(&mut self, model_id: &str,
|
||||
model: Model)` (sets the global default model).
|
||||
|
||||
- **`retrieve_role` migration pattern.** The method takes `&self`
|
||||
today. On `RequestContext` it becomes `(&self, app: &AppConfig,
|
||||
name: &str) -> Result<Role>`. The body calls
|
||||
`paths::list_roles`, `paths::role_file`, `Role::new`, `Role::builtin`,
|
||||
then `self.current_model()` (already on RequestContext from Step 7),
|
||||
then `Model::retrieve_model(app, ...)`.
|
||||
|
||||
- **`setup_model` has a subtle split.** It writes to
|
||||
`self.model_id` (serialized) AND `self.model` (runtime) AND calls
|
||||
`self.set_model(&model_id)` (mixed). Step 8b should split this
|
||||
into:
|
||||
- `AppConfig::ensure_default_model_id(&mut self, &AppConfig)` (or
|
||||
similar) to pick the first available model and update
|
||||
`self.model_id`
|
||||
- `RequestContext::reload_current_model(&mut self, app: &AppConfig)`
|
||||
to refresh `ctx.model` from the resolved id
|
||||
|
||||
### What Step 8b should NOT do
|
||||
|
||||
- Don't touch `init_client`, `GlobalConfig`, or any function with
|
||||
"runtime model state" concerns — those are Step 8f/8g.
|
||||
- Don't migrate `use_role`, `use_session`, `use_agent`, `exit_agent`
|
||||
— those are Step 8d (after Step 8c extracts `McpFactory::acquire()`).
|
||||
- Don't migrate RAG lifecycle methods (`use_rag`, `edit_rag_docs`,
|
||||
`rebuild_rag`, `compress_session`, `autoname_session`,
|
||||
`apply_prelude`) — those are Step 8e.
|
||||
- Don't touch `main.rs` entry points or `repl/mod.rs` — those are
|
||||
Step 8f and 8g respectively.
|
||||
|
||||
### Files to re-read at the start of Step 8b
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 8b section
|
||||
- This notes file — especially the "What Step 8b should watch
|
||||
for" section above
|
||||
- `src/config/mod.rs` — current `Config::retrieve_role`,
|
||||
`Config::set_model`, `Config::repl_complete`,
|
||||
`Config::setup_model`, `Config::use_prompt`, `Config::edit_role`
|
||||
method bodies
|
||||
- `src/config/app_config.rs` — current state of `AppConfig` impl
|
||||
blocks (Steps 3+4+7)
|
||||
- `src/config/request_context.rs` — current state of
|
||||
`RequestContext` impl blocks (Steps 5+6+7)
|
||||
|
||||
## Follow-up (not blocking Step 8b)
|
||||
|
||||
### 1. The `OnceLock` caches in the macro will seed once per process
|
||||
|
||||
`ALL_CLIENT_NAMES` and `ALL_MODELS` are `OnceLock`s initialized
|
||||
lazily on first call. After Step 8a, the first call passes an
|
||||
`AppConfig`. If a test or an unusual code path happens to call
|
||||
one of these functions twice with different `AppConfig` values
|
||||
(different `clients` lists), only the first seeding wins. This
|
||||
was already true before Step 8a — the types changed but the
|
||||
caching semantics are unchanged.
|
||||
|
||||
Worth flagging so nobody writes a test that relies on
|
||||
re-initializing the caches.
|
||||
|
||||
### 2. Bridge-window duplication count at end of Step 8a
|
||||
|
||||
Unchanged from end of Step 7:
|
||||
|
||||
- `AppConfig` (Steps 3+4+7): 17 methods
|
||||
- `RequestContext` (Steps 5+6+7): 39 methods
|
||||
- `paths` module (Step 2): 33 free functions
|
||||
- Step 6.5 types: 4 new types
|
||||
|
||||
**Total: 56 methods / ~1200 lines of parallel logic**
|
||||
|
||||
Step 8a added zero duplication — it's a signature change of
|
||||
existing functions, not a parallel implementation.
|
||||
|
||||
### 3. `to_app_config()` is called from 9 places now
|
||||
|
||||
After Step 8a, these files call `to_app_config()`:
|
||||
|
||||
- `src/config/mod.rs` — 6 callsites (for `Model::retrieve_model`
|
||||
and `list_models`)
|
||||
- `src/config/session.rs` — 1 callsite
|
||||
- `src/config/agent.rs` — 1 callsite
|
||||
- `src/function/supervisor.rs` — 1 callsite
|
||||
- `src/rag/mod.rs` — 4 callsites
|
||||
- `src/main.rs` — 1 callsite
|
||||
- `src/cli/completer.rs` — 1 callsite
|
||||
|
||||
**Total: 15 callsites.** All get eliminated in Step 8f/8g when
|
||||
their callers migrate to hold `Arc<AppState>` directly. Until
|
||||
then, each call clones ~40 fields. Measured cost: negligible.
|
||||
|
||||
### 4. The `#[allow(dead_code)]` on `impl Config` in bridge.rs
|
||||
|
||||
`Config::to_app_config()` is now actively used by 15 callsites
|
||||
— it's no longer dead. But `Config::to_request_context` and
|
||||
`Config::from_parts` are still only used by the bridge tests. The
|
||||
`#[allow(dead_code)]` on the `impl Config` block is harmless
|
||||
either way (it doesn't fire warnings, it just suppresses them
|
||||
if they exist). Step 10 deletes the whole file anyway.
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Step 7 notes: `docs/implementation/PHASE-1-STEP-7-NOTES.md`
|
||||
- Modified files:
|
||||
- `src/client/macros.rs` (3 function signatures in the
|
||||
`register_client!` macro)
|
||||
- `src/client/model.rs` (`use` statement + `retrieve_model`
|
||||
signature)
|
||||
- `src/config/mod.rs` (6 callsite updates in
|
||||
`set_rag_reranker_model`, `set_model`, `retrieve_role`,
|
||||
`repl_complete` ×2, `setup_model`)
|
||||
- `src/config/session.rs` (1 callsite in `Session::load`)
|
||||
- `src/config/agent.rs` (1 callsite in `Agent::init`)
|
||||
- `src/function/supervisor.rs` (1 callsite in sub-agent
|
||||
summarization)
|
||||
- `src/rag/mod.rs` (4 callsites in `Rag::create`, `Rag::init`,
|
||||
`Rag::search`)
|
||||
- `src/main.rs` (1 callsite in `--list-models` handler)
|
||||
- `src/cli/completer.rs` (1 callsite in shell completion)
|
||||
@@ -0,0 +1,55 @@
|
||||
# Implementation Notes
|
||||
|
||||
This directory holds per-step implementation notes for the Loki REST API
|
||||
refactor. Each note captures what was actually built during one step, how
|
||||
it differed from the plan, any decisions made mid-implementation, and
|
||||
what the next step needs to know to pick up cleanly.
|
||||
|
||||
## Why this exists
|
||||
|
||||
The refactor is spread across multiple phases and many steps. The
|
||||
implementation plans in `docs/PHASE-*-IMPLEMENTATION-PLAN.md` describe
|
||||
what _should_ happen; these notes describe what _did_ happen. Reading
|
||||
the plan plus the notes for the most recent completed step is enough
|
||||
context to start the next step without re-deriving anything from the
|
||||
conversation history or re-exploring the codebase.
|
||||
|
||||
## Naming convention
|
||||
|
||||
One file per completed step:
|
||||
|
||||
```
|
||||
PHASE-<phase>-STEP-<step>-NOTES.md
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
- `PHASE-1-STEP-1-NOTES.md`
|
||||
- `PHASE-1-STEP-2-NOTES.md`
|
||||
- `PHASE-2-STEP-3-NOTES.md`
|
||||
|
||||
## Contents of each note
|
||||
|
||||
Every note has the same sections so they're easy to scan:
|
||||
|
||||
1. **Status** — done / in progress / blocked
|
||||
2. **Plan reference** — which phase plan + which step section this
|
||||
implements
|
||||
3. **Summary** — one or two sentences on what shipped
|
||||
4. **What was changed** — file-by-file changelist with links
|
||||
5. **Key decisions** — non-obvious choices made during implementation,
|
||||
with the reasoning
|
||||
6. **Deviations from plan** — where the plan said X but reality forced
|
||||
Y, with explanation
|
||||
7. **Verification** — what was tested, what passed
|
||||
8. **Handoff to next step** — what the next step needs to know, any
|
||||
preconditions, any gotchas
|
||||
|
||||
## Lifetime
|
||||
|
||||
This directory is transitional. When Phase 1 Step 10 lands and the
|
||||
`GlobalConfig` type alias is removed, the Phase 1 notes become purely
|
||||
historical. When all six phases ship, this whole directory can be
|
||||
archived into `docs/archive/implementation-notes/` or deleted outright —
|
||||
the plans and final code are what matters long-term, not the
|
||||
step-by-step reconstruction.
|
||||
Reference in New Issue
Block a user