Files
loki/docs/implementation/PHASE-1-STEP-5-NOTES.md
2026-04-10 15:45:51 -06:00

414 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 14)
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::`)