This commit is contained in:
2026-04-15 12:56:00 -06:00
parent ff3419a714
commit 63b6678e73
82 changed files with 14800 additions and 3310 deletions
+413
View File
@@ -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 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::`)