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,111 @@
|
||||
# Phase 1 Step 10 — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done. Client chain migrated. `GlobalConfig` reduced to runtime-only
|
||||
usage (tool evaluation chain + REPL sync).
|
||||
|
||||
## Summary
|
||||
|
||||
Migrated the entire client chain away from `GlobalConfig`:
|
||||
- `Client` trait: `global_config()` → `app_config()`
|
||||
- Client structs: `GlobalConfig` → `Arc<AppConfig>`
|
||||
- `init_client`: `&GlobalConfig` → `&Arc<AppConfig>`
|
||||
- `Input` struct: removed `config: GlobalConfig` field entirely
|
||||
- `Rag`: deleted `build_temp_global_config` bridge
|
||||
- `render_stream`: `&GlobalConfig` → `&AppConfig`
|
||||
- `Config::search_rag`: `&GlobalConfig` → `&AppConfig`
|
||||
- `call_chat_completions*`: explicit `runtime: &GlobalConfig` parameter
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified (10 files)
|
||||
|
||||
- **`src/client/macros.rs`** — client structs hold `Arc<AppConfig>`,
|
||||
`init` takes `&Arc<AppConfig>`, `init_client` takes
|
||||
`&Arc<AppConfig>` + `Model`. Zero GlobalConfig in file.
|
||||
|
||||
- **`src/client/common.rs`** — `Client` trait: `app_config() -> &AppConfig`.
|
||||
`call_chat_completions*` take explicit `runtime: &GlobalConfig`.
|
||||
|
||||
- **`src/config/input.rs`** — removed `config: GlobalConfig` field.
|
||||
Added `rag: Option<Arc<Rag>>` captured at construction. Changed
|
||||
`set_regenerate` to take `current_role: Role` parameter. Zero
|
||||
`self.config` references.
|
||||
|
||||
- **`src/config/mod.rs`** — `search_rag` takes `&AppConfig`. Deleted
|
||||
dead `rag_template` method.
|
||||
|
||||
- **`src/render/mod.rs`** — `render_stream` takes `&AppConfig`. Zero
|
||||
GlobalConfig in file.
|
||||
|
||||
- **`src/rag/mod.rs`** — deleted `build_temp_global_config`. Creates
|
||||
clients via `init_client(&self.app_config, model)`. Zero
|
||||
GlobalConfig in file.
|
||||
|
||||
- **`src/main.rs`** — updated `call_chat_completions*` calls with
|
||||
explicit `runtime` parameter.
|
||||
|
||||
- **`src/repl/mod.rs`** — updated `call_chat_completions*` calls,
|
||||
`set_regenerate` call with `current_role` parameter.
|
||||
|
||||
- **`src/function/supervisor.rs`** — updated `call_chat_completions`
|
||||
call in `run_child_agent`.
|
||||
|
||||
- **`src/config/app_config.rs`** — no changes (already had all
|
||||
needed fields).
|
||||
|
||||
## Remaining GlobalConfig usage (71 references)
|
||||
|
||||
| Category | Files | Count | Why |
|
||||
|---|---|---|---|
|
||||
| Definition | `config/mod.rs` | 13 | Config struct, GlobalConfig alias, methods called by REPL |
|
||||
| Tool eval chain | `function/mod.rs` | 8 | `eval_tool_calls(&GlobalConfig)`, `ToolCall::eval(&GlobalConfig)` |
|
||||
| Tool handlers | `function/supervisor.rs` | 17 | All handler signatures |
|
||||
| Tool handlers | `function/todo.rs` | 2 | Todo handler signatures |
|
||||
| Tool handlers | `function/user_interaction.rs` | 3 | User interaction handler signatures |
|
||||
| Runtime param | `client/common.rs` | 3 | `call_chat_completions*(runtime: &GlobalConfig)` |
|
||||
| Input construction | `config/input.rs` | 4 | Constructor params + capture_input_config |
|
||||
| REPL | `repl/mod.rs` | 10 | Input construction, ask, sync helpers |
|
||||
| REPL components | `repl/completer.rs` | 3 | Holds GlobalConfig for reedline |
|
||||
| REPL components | `repl/prompt.rs` | 3 | Holds GlobalConfig for reedline |
|
||||
| REPL components | `repl/highlighter.rs` | 2 | Holds GlobalConfig for reedline |
|
||||
| Bridge | `config/request_context.rs` | 1 | `to_global_config()` |
|
||||
| Bridge | `config/macros.rs` | 2 | `macro_execute` takes &GlobalConfig |
|
||||
|
||||
The remaining GlobalConfig usage falls into 3 categories:
|
||||
1. **Tool evaluation chain** (30 refs) — `eval_tool_calls` and
|
||||
handlers read runtime state from GlobalConfig
|
||||
2. **REPL** (18 refs) — sync helpers, Input construction, reedline
|
||||
3. **Definition** (13 refs) — the Config struct itself
|
||||
|
||||
## Phase 1 final completion summary
|
||||
|
||||
Phase 1 is now complete. Every module that CAN be migrated HAS been
|
||||
migrated. The remaining GlobalConfig usage is the tool evaluation
|
||||
chain (which reads runtime state during active tool calls) and the
|
||||
REPL sync layer (which bridges RequestContext to GlobalConfig for
|
||||
the tool chain).
|
||||
|
||||
### Key achievements
|
||||
- `Input` no longer holds `GlobalConfig`
|
||||
- Client structs no longer hold `GlobalConfig`
|
||||
- `Rag` has zero `GlobalConfig` references
|
||||
- `render_stream` takes `&AppConfig`
|
||||
- `Agent::init` takes `&AppConfig` + `&AppState`
|
||||
- Both entry points thread `RequestContext`
|
||||
- 64+ methods on `RequestContext`, 21+ on `AppConfig`
|
||||
- Zero regressions: 63 tests, zero warnings, zero clippy issues
|
||||
|
||||
### What Phase 2 starts with
|
||||
Phase 2 can build REST API endpoints using `AppState` + `RequestContext`
|
||||
directly. The tool evaluation chain will need to be migrated from
|
||||
`&GlobalConfig` to `&mut RequestContext` when REST API tool calls
|
||||
are implemented — at that point, `Config` and `GlobalConfig` can
|
||||
be fully deleted.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo check` — zero warnings, zero errors
|
||||
- `cargo clippy` — zero warnings
|
||||
- `cargo test` — 63 passed, 0 failed
|
||||
@@ -0,0 +1,131 @@
|
||||
# Phase 1 Step 14 — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 14: Migrate `Input` constructors and REPL"
|
||||
|
||||
## Summary
|
||||
|
||||
Eliminated `GlobalConfig` from every file except `config/mod.rs`
|
||||
(where the type is defined). `Input` constructors take
|
||||
`&RequestContext`. REPL holds `Arc<RwLock<RequestContext>>` instead
|
||||
of `GlobalConfig`. Reedline components read from shared
|
||||
`RequestContext`. Sync helpers deleted. `to_global_config()` deleted.
|
||||
`macro_execute` takes `&mut RequestContext`. Implemented
|
||||
`RequestContext::use_agent`. Added MCP loading spinner, MCP server
|
||||
tab completions, and filtered internal tools from completions.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified
|
||||
|
||||
- **`src/config/input.rs`** — constructors take `&RequestContext`
|
||||
instead of `&GlobalConfig`. `capture_input_config` and
|
||||
`resolve_role` read from `RequestContext`/`AppConfig`.
|
||||
|
||||
- **`src/config/request_context.rs`** — added `use_agent()` method.
|
||||
Deleted `to_global_config()` and `sync_mcp_from_registry()`.
|
||||
Added MCP loading spinner in `rebuild_tool_scope`. Added
|
||||
configured MCP servers to `.set enabled_mcp_servers` completions.
|
||||
Filtered `user__*`, `mcp_*`, `todo__*`, `agent__*` from
|
||||
`.set enabled_tools` completions.
|
||||
|
||||
- **`src/repl/mod.rs`** — `Repl` struct holds
|
||||
`Arc<RwLock<RequestContext>>`, no `GlobalConfig` field. `ask` and
|
||||
`run_repl_command` take `&mut RequestContext` only. Deleted
|
||||
`sync_ctx_to_config`, `sync_config_to_ctx`,
|
||||
`sync_app_config_to_ctx`, `reinit_mcp_registry`.
|
||||
|
||||
- **`src/repl/completer.rs`** — holds
|
||||
`Arc<RwLock<RequestContext>>` instead of `GlobalConfig`.
|
||||
|
||||
- **`src/repl/prompt.rs`** — holds `Arc<RwLock<RequestContext>>`
|
||||
instead of `GlobalConfig`.
|
||||
|
||||
- **`src/repl/highlighter.rs`** — updated if it held `GlobalConfig`.
|
||||
|
||||
- **`src/config/macros.rs`** — `macro_execute` takes
|
||||
`&mut RequestContext` instead of `&GlobalConfig`.
|
||||
|
||||
- **`src/main.rs`** — all `to_global_config()` calls eliminated.
|
||||
Agent path uses `ctx.use_agent()`. Macro path passes
|
||||
`&mut ctx` directly.
|
||||
|
||||
### Methods added
|
||||
|
||||
- `RequestContext::use_agent(app, name, session, abort_signal)` —
|
||||
calls `Agent::init`, sets up MCP via `rebuild_tool_scope`,
|
||||
sets agent/rag/supervisor, starts session.
|
||||
|
||||
### Methods deleted
|
||||
|
||||
- `RequestContext::to_global_config()`
|
||||
- `RequestContext::sync_mcp_from_registry()`
|
||||
- REPL: `sync_ctx_to_config`, `sync_config_to_ctx`,
|
||||
`sync_app_config_to_ctx`, `reinit_mcp_registry`
|
||||
|
||||
### UX improvements
|
||||
|
||||
- MCP loading spinner restored in `rebuild_tool_scope`
|
||||
- `.set enabled_mcp_servers<TAB>` shows configured servers from
|
||||
`mcp.json` + mapping aliases
|
||||
- `.set enabled_tools<TAB>` hides internal tools (`user__*`,
|
||||
`mcp_*`, `todo__*`, `agent__*`)
|
||||
|
||||
## GlobalConfig remaining
|
||||
|
||||
Only `src/config/mod.rs` (13 references): type definition, legacy
|
||||
`Config::use_agent`, `Config::use_session_safely`,
|
||||
`Config::use_role_safely`, `Config::update`, `Config::delete` — all
|
||||
dead code. Step 15 deletes them.
|
||||
|
||||
## Post-implementation review (Oracle)
|
||||
|
||||
Oracle reviewed all REPL and CLI flows. Findings:
|
||||
|
||||
1. **AbortSignal not threaded through rebuild_tool_scope** —
|
||||
FIXED. `rebuild_tool_scope`, `bootstrap_tools`, `use_role`,
|
||||
`use_session`, `use_agent`, `update` now all thread the real
|
||||
`AbortSignal` through to the MCP loading spinner. Ctrl+C
|
||||
properly cancels MCP server loading.
|
||||
|
||||
2. **RwLock held across await in REPL** — KNOWN LIMITATION.
|
||||
`Repl::run` holds `ctx.write()` for the duration of
|
||||
`run_repl_command`. This is safe in the current design because
|
||||
reedline's prompt/completion is synchronous (runs between line
|
||||
reads, before the write lock is taken). Phase 2 should refactor
|
||||
to owned `RequestContext` + lightweight snapshot for reedline.
|
||||
|
||||
3. **MCP subprocess leaks** — NOT AN ISSUE. `rmcp::RunningService`
|
||||
has a `DropGuard` that cancels the tokio cancellation token on
|
||||
Drop. Servers are killed when their `Arc<ConnectedServer>`
|
||||
refcount hits zero.
|
||||
|
||||
4. **MCP duplication** — NOT AN ISSUE after Step 14. The
|
||||
`initial_global` sync was removed. MCP runtime is populated
|
||||
only by `rebuild_tool_scope` → `McpFactory::acquire`, which
|
||||
deduplicates via `Weak` references.
|
||||
|
||||
5. **Agent+session MCP override** — PRE-EXISTING behavior, not
|
||||
a regression. When an agent session has its own MCP config,
|
||||
it takes precedence. Supervisor child agents handle this
|
||||
explicitly via `populate_agent_mcp_runtime`.
|
||||
|
||||
6. **Stale Input in tool loop** — PRE-EXISTING design. Input
|
||||
captures state at construction time and uses `merge_tool_results`
|
||||
for continuations. Tools communicate results via tool results,
|
||||
not by mutating the session mid-turn. Not a regression.
|
||||
|
||||
7. **Auto-compression** — REPL does inline compression in `ask`.
|
||||
CLI directive path relies on session save which happens in
|
||||
`after_chat_completion`. Consistent with pre-migration behavior.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo check` — 6 dead-code warnings (legacy Config methods)
|
||||
- `cargo test` — 63 passed, 0 failed
|
||||
@@ -0,0 +1,138 @@
|
||||
# Phase 1 Step 15 — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done. Phase 1 complete.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 15: Delete `Config` struct and `GlobalConfig`"
|
||||
|
||||
## Summary
|
||||
|
||||
Deleted `GlobalConfig` type alias and all dead `Config` methods.
|
||||
Deleted `Config::from_parts` and bridge tests. Moved 8 flat
|
||||
runtime fields from `RequestContext` into `ToolScope` and
|
||||
`AgentRuntime`. `RequestContext` is now a clean composition of
|
||||
well-scoped state structs.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Dead code deletion
|
||||
|
||||
- `GlobalConfig` type alias — deleted
|
||||
- `Config::from_parts` — deleted
|
||||
- All bridge.rs tests — deleted
|
||||
- Dead `Config` methods — deleted (use_agent, use_session_safely,
|
||||
use_role_safely, update, delete, and associated helpers)
|
||||
- Dead `McpRegistry` methods (search_tools_server, describe,
|
||||
invoke) — deleted
|
||||
- Dead `Functions` methods — deleted
|
||||
- Unused imports cleaned across all files
|
||||
|
||||
### Field migrations
|
||||
|
||||
**From `RequestContext` to `ToolScope`:**
|
||||
- `functions: Functions` → `tool_scope.functions` (was duplicated)
|
||||
- `tool_call_tracker: Option<ToolCallTracker>` → `tool_scope.tool_tracker`
|
||||
|
||||
**From `RequestContext` to `AgentRuntime`:**
|
||||
- `supervisor: Option<Arc<RwLock<Supervisor>>>` → `agent_runtime.supervisor`
|
||||
- `parent_supervisor: Option<Arc<RwLock<Supervisor>>>` → `agent_runtime.parent_supervisor`
|
||||
- `self_agent_id: Option<String>` → `agent_runtime.self_agent_id`
|
||||
- `current_depth: usize` → `agent_runtime.current_depth`
|
||||
- `inbox: Option<Arc<Inbox>>` → `agent_runtime.inbox`
|
||||
- `root_escalation_queue: Option<Arc<EscalationQueue>>` → `agent_runtime.escalation_queue`
|
||||
|
||||
### RequestContext accessors added
|
||||
|
||||
Accessor methods on `RequestContext` provide the same API:
|
||||
- `current_depth()` → returns `agent_runtime.current_depth` or 0
|
||||
- `supervisor()` → returns `agent_runtime.supervisor` or None
|
||||
- `parent_supervisor()` → returns agent_runtime.parent_supervisor or None
|
||||
- `self_agent_id()` → returns agent_runtime.self_agent_id or None
|
||||
- `inbox()` → returns agent_runtime.inbox or None
|
||||
- `root_escalation_queue()` → returns agent_runtime.escalation_queue or None
|
||||
|
||||
### AgentRuntime changes
|
||||
|
||||
All fields made `Option` to support agents without spawning
|
||||
capability (no supervisor), root agents without inboxes, and
|
||||
lazy escalation queue creation.
|
||||
|
||||
### Files modified
|
||||
|
||||
- `src/config/request_context.rs` — removed 8 flat fields, added
|
||||
accessors, updated all internal methods
|
||||
- `src/config/tool_scope.rs` — removed `#![allow(dead_code)]`
|
||||
- `src/config/agent_runtime.rs` — made fields Optional, removed
|
||||
`#![allow(dead_code)]`, added `Default` impl
|
||||
- `src/config/bridge.rs` — deleted `from_parts`, tests; updated
|
||||
`to_request_context` to build `AgentRuntime`
|
||||
- `src/config/mod.rs` — deleted `GlobalConfig`, dead methods,
|
||||
dead runtime fields
|
||||
- `src/function/mod.rs` — `ctx.tool_scope.functions`,
|
||||
`ctx.tool_scope.tool_tracker`
|
||||
- `src/function/supervisor.rs` — agent_runtime construction,
|
||||
accessor methods
|
||||
- `src/function/user_interaction.rs` — accessor methods
|
||||
- `src/function/todo.rs` — agent_runtime access
|
||||
- `src/client/common.rs` — `ctx.tool_scope.tool_tracker`
|
||||
- `src/config/macros.rs` — agent_runtime construction
|
||||
- `src/repl/mod.rs` — tool_scope/agent_runtime access
|
||||
- `src/main.rs` — agent_runtime for startup path
|
||||
- `src/mcp/mod.rs` — deleted dead methods
|
||||
|
||||
## RequestContext final structure
|
||||
|
||||
```rust
|
||||
pub struct RequestContext {
|
||||
// Shared immutable state
|
||||
pub app: Arc<AppState>,
|
||||
|
||||
// Per-request identity
|
||||
pub macro_flag: bool,
|
||||
pub info_flag: bool,
|
||||
pub working_mode: WorkingMode,
|
||||
|
||||
// Current model
|
||||
pub model: Model,
|
||||
|
||||
// Active scope state
|
||||
pub role: Option<Role>,
|
||||
pub session: Option<Session>,
|
||||
pub rag: Option<Arc<Rag>>,
|
||||
pub agent: Option<Agent>,
|
||||
pub agent_variables: Option<AgentVariables>,
|
||||
pub last_message: Option<LastMessage>,
|
||||
|
||||
// Tool runtime (functions + MCP + tracker)
|
||||
pub tool_scope: ToolScope,
|
||||
|
||||
// Agent runtime (supervisor + inbox + escalation + depth)
|
||||
pub agent_runtime: Option<AgentRuntime>,
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo check` — zero warnings, zero errors
|
||||
- `cargo test` — 59 passed, 0 failed
|
||||
- `GlobalConfig` references — zero across entire codebase
|
||||
- Flat runtime fields on RequestContext — zero (all moved)
|
||||
|
||||
## Phase 1 complete
|
||||
|
||||
The monolithic `Config` god-state struct has been broken apart:
|
||||
|
||||
| Struct | Purpose | Lifetime |
|
||||
|---|---|---|
|
||||
| `AppConfig` | Serialized config from YAML | Immutable, shared |
|
||||
| `AppState` | Process-wide shared state (vault, MCP factory, RAG cache) | Immutable, shared via Arc |
|
||||
| `RequestContext` | Per-request mutable state | Owned per request |
|
||||
| `ToolScope` | Active tool declarations + MCP runtime + call tracker | Per scope transition |
|
||||
| `AgentRuntime` | Agent-specific wiring (supervisor, inbox, escalation) | Per agent activation |
|
||||
|
||||
The codebase is ready for Phase 2: REST API endpoints that create
|
||||
`RequestContext` per-request from shared `AppState`.
|
||||
@@ -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,296 @@
|
||||
# Phase 1 Step 8b — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 8b: Finish Step 7's deferred mixed-method migrations"
|
||||
|
||||
## Summary
|
||||
|
||||
Migrated 7 of the 9 Step 7 deferrals to `RequestContext` / `AppConfig`
|
||||
methods that take `&AppConfig` instead of `&Config`. Two methods
|
||||
(`edit_role` and `update`) remain deferred because they depend on
|
||||
`use_role` (Step 8d) and MCP registry manipulation (Step 8d)
|
||||
respectively. Four private helper functions in `mod.rs` were bumped
|
||||
to `pub(super)` to support the new `repl_complete` implementation.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified (3 files)
|
||||
|
||||
- **`src/config/request_context.rs`** — added a fifth `impl RequestContext`
|
||||
block with 7 methods:
|
||||
|
||||
- `retrieve_role(&self, app: &AppConfig, name: &str) -> Result<Role>` —
|
||||
loads a role by name, resolves its model via
|
||||
`Model::retrieve_model(app, ...)`. Reads `app.temperature` and
|
||||
`app.top_p` for the no-model-id fallback branch.
|
||||
|
||||
- `set_model_on_role_like(&mut self, app: &AppConfig, model_id: &str)
|
||||
-> Result<bool>` — resolves the model via `Model::retrieve_model`,
|
||||
sets it on the active role-like if present (returns `true`), or on
|
||||
`ctx.model` directly (returns `false`). The `false` case means the
|
||||
caller should also call `AppConfig::set_model_id_default` if they
|
||||
want the global default updated.
|
||||
|
||||
- `reload_current_model(&mut self, app: &AppConfig, model_id: &str)
|
||||
-> Result<()>` — resolves a model by ID and assigns it to
|
||||
`ctx.model`. Used in tandem with `AppConfig::ensure_default_model_id`.
|
||||
|
||||
- `use_prompt(&mut self, _app: &AppConfig, prompt: &str) -> Result<()>` —
|
||||
creates a `TEMP_ROLE_NAME` role with the prompt text, sets its model
|
||||
to `current_model()`, calls `use_role_obj`. The `_app` parameter is
|
||||
included for signature consistency; it's unused because `use_prompt`
|
||||
only reads runtime state.
|
||||
|
||||
- `set_rag_reranker_model(&mut self, app: &AppConfig,
|
||||
value: Option<String>) -> Result<bool>` — validates the model ID via
|
||||
`Model::retrieve_model(app, ...)` if present, then clones-and-replaces
|
||||
the `Arc<Rag>` with the updated reranker model. Returns `true` if RAG
|
||||
was mutated, `false` if no RAG is active.
|
||||
|
||||
- `set_rag_top_k(&mut self, value: usize) -> Result<bool>` — same
|
||||
clone-and-replace pattern on the active RAG. Returns `true`/`false`.
|
||||
|
||||
- `repl_complete(&self, app: &AppConfig, cmd: &str, args: &[&str],
|
||||
_line: &str) -> Vec<(String, Option<String>)>` — full tab-completion
|
||||
handler. Reads `app.*` for serialized fields, `self.*` for runtime
|
||||
state, `self.app.vault` for vault completions. MCP configured-server
|
||||
completions are limited to `app.mapping_mcp_servers` keys during the
|
||||
bridge (no live `McpRegistry` on `RequestContext`; Step 8d's
|
||||
`ToolScope` will restore full MCP completions).
|
||||
|
||||
Updated imports: added `TEMP_ROLE_NAME`, `list_agents`, `ModelType`,
|
||||
`list_models`, `read_to_string`, `fuzzy_filter`. Removed duplicate
|
||||
`crate::utils` import that had accumulated.
|
||||
|
||||
- **`src/config/app_config.rs`** — added 4 methods to the existing
|
||||
`set_*_default` impl block:
|
||||
|
||||
- `set_rag_reranker_model_default(&mut self, value: Option<String>)`
|
||||
- `set_rag_top_k_default(&mut self, value: usize)`
|
||||
- `set_model_id_default(&mut self, model_id: String)`
|
||||
- `ensure_default_model_id(&mut self) -> Result<String>` — picks the
|
||||
first available chat model if `model_id` is empty, updates
|
||||
`self.model_id`, returns the resolved ID.
|
||||
|
||||
- **`src/config/mod.rs`** — bumped 4 private helper functions to
|
||||
`pub(super)`:
|
||||
|
||||
- `parse_value` — used by `update` when it migrates (Step 8f/8g)
|
||||
- `complete_bool` — used by `repl_complete`
|
||||
- `complete_option_bool` — used by `repl_complete`
|
||||
- `map_completion_values` — used by `repl_complete`
|
||||
|
||||
### Files NOT changed
|
||||
|
||||
- **`src/client/macros.rs`**, **`src/client/model.rs`** — untouched;
|
||||
Step 8a already migrated these.
|
||||
- **All other source files** — no changes. All existing `Config` methods
|
||||
stay intact.
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. Same bridge pattern as Steps 3-8a
|
||||
|
||||
New methods sit alongside originals. No caller migration. `Config`'s
|
||||
`retrieve_role`, `set_model`, `setup_model`, `use_prompt`,
|
||||
`set_rag_reranker_model`, `set_rag_top_k`, `repl_complete` all stay
|
||||
on `Config` and continue working for every current caller.
|
||||
|
||||
### 2. `set_model_on_role_like` returns `Result<bool>` (not just `bool`)
|
||||
|
||||
Unlike the Step 7 `set_temperature_on_role_like` pattern that returns
|
||||
a plain `bool`, `set_model_on_role_like` returns `Result<bool>` because
|
||||
`Model::retrieve_model` can fail. The `bool` still signals whether a
|
||||
role-like was mutated. When `false`, the model was assigned to
|
||||
`ctx.model` directly (so the caller doesn't need to fall through to
|
||||
`AppConfig` — the "no role-like" case is handled in-method by assigning
|
||||
to `ctx.model`). This differs from the Step 7 pattern where `false`
|
||||
means "caller must call the `_default`."
|
||||
|
||||
### 3. `setup_model` split into two independent methods
|
||||
|
||||
`Config::setup_model` does three things:
|
||||
1. Picks a default model ID if empty (`ensure_default_model_id`)
|
||||
2. Calls `set_model` to resolve and assign the model
|
||||
3. Writes back `model_id` to config
|
||||
|
||||
The split:
|
||||
- `AppConfig::ensure_default_model_id()` handles #1 and #3
|
||||
- `RequestContext::reload_current_model()` handles #2
|
||||
|
||||
Step 8f will compose them: first call `ensure_default_model_id` on
|
||||
the app config, then call `reload_current_model` on the context
|
||||
with the returned ID.
|
||||
|
||||
### 4. `repl_complete` MCP completions are reduced during bridge
|
||||
|
||||
`Config::repl_complete` reads `self.mcp_registry.list_configured_servers()`
|
||||
for the `enabled_mcp_servers` completion values. `RequestContext` has no
|
||||
`mcp_registry` field. During the bridge window, the new `repl_complete`
|
||||
offers only `mapping_mcp_servers` keys (from `AppConfig`) as MCP
|
||||
completions. Step 8d's `ToolScope` will provide full MCP server
|
||||
completions.
|
||||
|
||||
This is acceptable because:
|
||||
- The new method isn't called by anyone yet (bridge pattern)
|
||||
- When Step 8d wires it up, `ToolScope` will be available
|
||||
|
||||
### 5. `edit_role` deferred to Step 8d
|
||||
|
||||
`Config::edit_role` calls `self.use_role()` as its last line.
|
||||
`use_role` is a scope-transition method that Step 8d will rewrite
|
||||
to use `McpFactory::acquire()`. Migrating `edit_role` without
|
||||
`use_role` would require either a stub or leaving it half-broken.
|
||||
Deferring it keeps the bridge clean.
|
||||
|
||||
### 6. `update` dispatcher deferred to Step 8f/8g
|
||||
|
||||
`Config::update` takes `&GlobalConfig` and has two branches that
|
||||
do heavy MCP registry manipulation (`enabled_mcp_servers` and
|
||||
`mcp_server_support`). These branches require Step 8d's
|
||||
`McpFactory`/`ToolScope` infrastructure. The remaining branches
|
||||
could be migrated individually, but splitting the dispatcher
|
||||
partially creates a confusing dual-path situation. Deferring the
|
||||
entire dispatcher keeps things clean.
|
||||
|
||||
### 7. RAG mutation uses clone-and-replace on `Arc<Rag>`
|
||||
|
||||
`Config::set_rag_reranker_model` uses the `update_rag` helper which
|
||||
takes `&GlobalConfig`, clones the `Arc<Rag>`, mutates the clone,
|
||||
and writes it back via `config.write().rag = Some(Arc::new(rag))`.
|
||||
|
||||
The new `RequestContext` methods do the same thing but without the
|
||||
`GlobalConfig` indirection: clone `Arc<Rag>` contents, mutate,
|
||||
wrap in a new `Arc`, assign to `self.rag`. Semantically identical.
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
### 2 methods deferred (not in plan's "done" scope for 8b)
|
||||
|
||||
| Method | Why deferred |
|
||||
|---|---|
|
||||
| `edit_role` | Calls `use_role` which is Step 8d |
|
||||
| `update` | MCP registry branches require Step 8d's `McpFactory`/`ToolScope` |
|
||||
|
||||
The plan's 8b description listed both as potential deferrals:
|
||||
- `edit_role`: "calls editor + upsert_role + use_role — use_role is
|
||||
still Step 8d, so edit_role may stay deferred"
|
||||
- `update`: "Once all the individual set_* methods exist on both types"
|
||||
— the MCP-touching set_* methods don't exist yet
|
||||
|
||||
### `set_model_on_role_like` handles the no-role-like case internally
|
||||
|
||||
The plan said the split should be:
|
||||
- `RequestContext::set_model_on_role_like` → returns `bool`
|
||||
- `AppConfig::set_model_default` → sets global
|
||||
|
||||
But `set_model` doesn't just set `model_id` when no role-like is
|
||||
active — it also assigns the resolved `Model` struct to `self.model`
|
||||
(runtime). Since the `Model` struct lives on `RequestContext`, the
|
||||
no-role-like branch must also live on `RequestContext`. So
|
||||
`set_model_on_role_like` handles both cases (role-like mutation and
|
||||
`ctx.model` assignment) and returns `false` to signal that `model_id`
|
||||
on `AppConfig` may also need updating. `AppConfig::set_model_id_default`
|
||||
is the simpler companion.
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, zero warnings, zero errors
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (unchanged from Steps 1–8a)
|
||||
|
||||
No new tests added — this is a bridge-pattern step that adds methods
|
||||
alongside existing ones. The existing test suite confirms no regressions.
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What Step 8c can rely on
|
||||
|
||||
Step 8c (extract `McpFactory::acquire()` from `McpRegistry::init_server`)
|
||||
can rely on:
|
||||
|
||||
- **All Step 8a guarantees still hold** — `Model::retrieve_model`,
|
||||
`list_models`, `list_all_models`, `list_client_names` all take
|
||||
`&AppConfig`
|
||||
- **`RequestContext` now has 46 inherent methods** across 5 impl blocks:
|
||||
1 constructor + 13 reads + 12 writes + 14 mixed (Step 7) + 7 mixed
|
||||
(Step 8b) = 47 total (46 public + 1 private `open_message_file`)
|
||||
- **`AppConfig` now has 21 methods**: 7 reads + 4 writes + 10
|
||||
setter-defaults (6 from Step 7 + 4 from Step 8b)
|
||||
|
||||
### What Step 8c should watch for
|
||||
|
||||
Step 8c is **independent of Step 8b**. It extracts the MCP subprocess
|
||||
spawn logic from `McpRegistry::init_server` into a standalone function
|
||||
and implements `McpFactory::acquire()`. Step 8b provides no input to
|
||||
8c.
|
||||
|
||||
### What Step 8d should know about Step 8b's output
|
||||
|
||||
Step 8d (scope transitions) depends on both 8b and 8c. From 8b it
|
||||
gets:
|
||||
|
||||
- `RequestContext::retrieve_role(app, name)` — needed by `use_role`
|
||||
- `RequestContext::set_model_on_role_like(app, model_id)` — may be
|
||||
useful inside scope transitions
|
||||
|
||||
### What Step 8f/8g should know about Step 8b deferrals
|
||||
|
||||
- **`edit_role`** — needs `use_role` from Step 8d. Once 8d ships,
|
||||
`edit_role` on `RequestContext` becomes: call `app.editor()`, call
|
||||
`upsert_role(name)`, call `self.use_role(app, name, abort_signal)`.
|
||||
The `upsert_role` method is still on `Config` and needs migrating
|
||||
(it calls `self.editor()` which is on `AppConfig`, and
|
||||
`ensure_parent_exists` which is a free function — straightforward).
|
||||
|
||||
- **`update` dispatcher** — needs all `set_*` branches migrated. The
|
||||
non-MCP branches are ready now. The MCP branches need Step 8d's
|
||||
`McpFactory`/`ToolScope`.
|
||||
|
||||
- **`use_role_safely` / `use_session_safely`** — still on `Config`.
|
||||
These wrappers exist only because `Config::use_role` is `&mut self`
|
||||
and the REPL holds `Arc<RwLock<Config>>`. Step 8g eliminates them
|
||||
when the REPL switches to holding `RequestContext` directly.
|
||||
|
||||
### Bridge-window duplication count at end of Step 8b
|
||||
|
||||
Running tally:
|
||||
|
||||
- `AppConfig` (Steps 3+4+7+8b): 21 methods
|
||||
- `RequestContext` (Steps 5+6+7+8b): 46 methods
|
||||
- `paths` module (Step 2): 33 free functions
|
||||
- Step 6.5 types: 4 new types on scaffolding
|
||||
- `mod.rs` visibility bumps: 4 helpers → `pub(super)`
|
||||
|
||||
**Total: 67 methods + 33 paths + 4 types / ~1500 lines of parallel logic**
|
||||
|
||||
All auto-delete in Step 10.
|
||||
|
||||
### Files to re-read at the start of Step 8c
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 8c section
|
||||
- `src/mcp/mod.rs` — `McpRegistry::init_server` method body (the
|
||||
spawn logic to extract)
|
||||
- `src/config/mcp_factory.rs` — current scaffolding from Step 6.5
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Step 8a notes: `docs/implementation/PHASE-1-STEP-8a-NOTES.md`
|
||||
- Step 7 notes: `docs/implementation/PHASE-1-STEP-7-NOTES.md`
|
||||
- Modified files:
|
||||
- `src/config/request_context.rs` (7 new methods, import updates)
|
||||
- `src/config/app_config.rs` (4 new `set_*_default` / `ensure_*`
|
||||
methods)
|
||||
- `src/config/mod.rs` (4 helper functions bumped to `pub(super)`)
|
||||
@@ -0,0 +1,226 @@
|
||||
# Phase 1 Step 8c — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 8c: Extract `McpFactory::acquire()` from
|
||||
`McpRegistry::init_server`"
|
||||
|
||||
## Summary
|
||||
|
||||
Extracted the MCP subprocess spawn + rmcp handshake logic from
|
||||
`McpRegistry::start_server` into a standalone `pub(crate) async fn
|
||||
spawn_mcp_server()` function. Rewrote `start_server` to call it.
|
||||
Implemented `McpFactory::acquire()` using the extracted function
|
||||
plus the existing `try_get_active` / `insert_active` scaffolding
|
||||
from Step 6.5. Three types in `mcp/mod.rs` were bumped to
|
||||
`pub(crate)` visibility for cross-module access.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified (2 files)
|
||||
|
||||
- **`src/mcp/mod.rs`** — 4 changes:
|
||||
|
||||
1. **Extracted `spawn_mcp_server`** (~40 lines) — standalone
|
||||
`pub(crate) async fn` that takes an `&McpServer` spec and
|
||||
optional log path, builds a `tokio::process::Command`, creates
|
||||
a `TokioChildProcess` transport (with optional stderr log
|
||||
redirect), calls `().serve(transport).await` for the rmcp
|
||||
handshake, and returns `Arc<ConnectedServer>`.
|
||||
|
||||
2. **Rewrote `McpRegistry::start_server`** — now looks up the
|
||||
`McpServer` spec from `self.config`, calls `spawn_mcp_server`,
|
||||
then does its own catalog building (tool listing, BM25 index
|
||||
construction). The spawn + handshake code that was previously
|
||||
inline is replaced by the one-liner
|
||||
`spawn_mcp_server(spec, self.log_path.as_deref()).await?`.
|
||||
|
||||
3. **Bumped 3 types to `pub(crate)`**: `McpServer`, `JsonField`,
|
||||
`McpServersConfig`. These were previously private to
|
||||
`mcp/mod.rs`. `McpFactory::acquire()` and
|
||||
`McpServerKey::from_spec()` need `McpServer` and `JsonField`
|
||||
to build the server key from a spec. `McpServersConfig` is
|
||||
bumped for completeness (Step 8d may need to access it when
|
||||
loading server specs during scope transitions).
|
||||
|
||||
- **`src/config/mcp_factory.rs`** — 3 changes:
|
||||
|
||||
1. **Added `McpServerKey::from_spec(name, &McpServer)`** — builds
|
||||
a key by extracting command, args (defaulting to empty vec),
|
||||
and env vars (converting `JsonField` variants to strings) from
|
||||
the spec. Args and env are sorted by the existing `new()`
|
||||
constructor to ensure identical specs produce identical keys.
|
||||
|
||||
2. **Added `McpFactory::acquire(name, &McpServer, log_path)`** —
|
||||
the core method. Builds an `McpServerKey` from the spec, checks
|
||||
`try_get_active` for an existing `Arc` (sharing path), otherwise
|
||||
calls `spawn_mcp_server` to start a fresh subprocess, inserts
|
||||
the result into `active` via `insert_active`, and returns the
|
||||
`Arc<ConnectedServer>`.
|
||||
|
||||
3. **Updated imports** — added `McpServer`, `spawn_mcp_server`,
|
||||
`Result`, `Path`.
|
||||
|
||||
### Files NOT changed
|
||||
|
||||
- **`src/config/tool_scope.rs`** — unchanged; Step 8d will use
|
||||
`McpFactory::acquire()` to populate `McpRuntime` instances.
|
||||
- **All caller code** — `McpRegistry::start_select_mcp_servers` and
|
||||
`McpRegistry::reinit` continue to call `self.start_server()` which
|
||||
internally uses the extracted function. No caller migration.
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. Spawn function does NOT list tools or build catalogs
|
||||
|
||||
The plan said to extract "the MCP subprocess spawn + rmcp handshake
|
||||
logic (~60 lines)." I interpreted this as: `Command` construction →
|
||||
transport creation → `serve()` handshake → `Arc` wrapping. The tool
|
||||
listing (`service.list_tools`) and catalog building (BM25 index) are
|
||||
`McpRegistry`-specific bookkeeping and stay in `start_server`.
|
||||
|
||||
`McpFactory::acquire()` returns a connected server handle ready to
|
||||
use. Callers (Step 8d's scope transitions) can list tools themselves
|
||||
if they need to build function declarations.
|
||||
|
||||
### 2. No `abort_signal` parameter on `spawn_mcp_server`
|
||||
|
||||
The plan suggested `abort_signal: &AbortSignal` as a parameter. The
|
||||
existing `start_server` doesn't use an abort signal — cancellation
|
||||
is handled at a higher level by `abortable_run_with_spinner` wrapping
|
||||
the entire batch of `start_select_mcp_servers`. Adding an abort signal
|
||||
to the individual spawn would require threading `tokio::select!` into
|
||||
the transport creation, which is a behavior change beyond Step 8c's
|
||||
scope. Step 8d can add cancellation when building `ToolScope` if
|
||||
needed.
|
||||
|
||||
### 3. `McpServerKey::from_spec` converts `JsonField` to strings
|
||||
|
||||
The `McpServer.env` field uses a `JsonField` enum (Str/Bool/Int) for
|
||||
JSON flexibility. The key needs string comparisons for hashing, so
|
||||
`from_spec` converts each variant to its string representation. This
|
||||
matches the conversion already done in the env-building code inside
|
||||
`spawn_mcp_server`.
|
||||
|
||||
### 4. `McpFactory::acquire` mutex contention is safe
|
||||
|
||||
The plan warned: "hold the lock only during HashMap mutation, never
|
||||
across subprocess spawn." The implementation achieves this by using
|
||||
the existing `try_get_active` and `insert_active` methods, which each
|
||||
acquire and release the mutex within their own scope. The `spawn_mcp_server`
|
||||
await happens between the two lock acquisitions with no lock held.
|
||||
|
||||
TOCTOU race: two concurrent callers could both miss in `try_get_active`,
|
||||
both spawn, and both insert. The second insert overwrites the first's
|
||||
`Weak`. This means one extra subprocess gets spawned and the first
|
||||
`Arc` has no `Weak` in the map (but stays alive via its holder's
|
||||
`Arc`). This is acceptable for Phase 1 — the worst case is a
|
||||
redundant spawn, not a crash or leak. Phase 5's pooling design
|
||||
(per-key `tokio::sync::Mutex`) will eliminate this race.
|
||||
|
||||
### 5. No integration tests for `acquire()`
|
||||
|
||||
The plan suggested writing integration tests for the factory's sharing
|
||||
behavior. Spawning a real MCP server requires a configured binary on
|
||||
the system PATH. A mock server would need a test binary that speaks
|
||||
the rmcp stdio protocol — this is substantial test infrastructure
|
||||
that doesn't exist yet. Rather than building it in Step 8c, I'm
|
||||
documenting that integration testing of `McpFactory::acquire()` should
|
||||
happen in Phase 5 when the pooling infrastructure provides natural
|
||||
test hooks (idle pool, reaper, health checks). The extraction itself
|
||||
is verified by the fact that existing MCP functionality (which goes
|
||||
through `McpRegistry::start_server` → `spawn_mcp_server`) still
|
||||
compiles and all 63 tests pass.
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
| Deviation | Rationale |
|
||||
|---|---|
|
||||
| No `abort_signal` parameter | Not used by existing code; adding it is a behavior change |
|
||||
| No integration tests | Requires MCP test infrastructure that doesn't exist |
|
||||
| Removed `get_server_spec` / `log_path` accessors from McpRegistry | Not needed; `acquire()` takes spec and log_path directly |
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, zero warnings, zero errors
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (unchanged from Steps 1–8b)
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What Step 8d can rely on
|
||||
|
||||
- **`spawn_mcp_server(&McpServer, Option<&Path>) -> Result<Arc<ConnectedServer>>`** —
|
||||
available from `crate::mcp::spawn_mcp_server`
|
||||
- **`McpFactory::acquire(name, &McpServer, log_path) -> Result<Arc<ConnectedServer>>`** —
|
||||
checks active map for sharing, spawns fresh if needed, inserts
|
||||
into active map
|
||||
- **`McpServerKey::from_spec(name, &McpServer) -> McpServerKey`** —
|
||||
builds a hashable key from a server spec
|
||||
- **`McpServer`, `McpServersConfig`, `JsonField`** — all `pub(crate)`
|
||||
and accessible from `src/config/`
|
||||
|
||||
### What Step 8d should do
|
||||
|
||||
Build real `ToolScope` instances during scope transitions:
|
||||
|
||||
1. Resolve the effective enabled-server list from the role/session/agent
|
||||
2. Look up each server's `McpServer` spec (from the MCP config)
|
||||
3. Call `app.mcp_factory.acquire(name, spec, log_path)` for each
|
||||
4. Populate an `McpRuntime` with the returned `Arc<ConnectedServer>`
|
||||
handles
|
||||
5. Construct a `ToolScope` with the runtime + resolved `Functions`
|
||||
6. Assign to `ctx.tool_scope`
|
||||
|
||||
### What Step 8d should watch for
|
||||
|
||||
- **Log path.** `McpRegistry` stores `log_path` during `init()`.
|
||||
Step 8d needs to decide where the log path comes from for
|
||||
factory-acquired servers. Options: store it on `AppState`,
|
||||
compute it from `paths::cache_path()`, or pass it through from
|
||||
the caller. The simplest is to store it on `McpFactory` at
|
||||
construction time.
|
||||
|
||||
- **MCP config loading.** `McpRegistry::init()` loads and parses
|
||||
`mcp.json`. Step 8d's scope transitions need access to the
|
||||
parsed `McpServersConfig` to look up server specs by name.
|
||||
Options: store the parsed config on `AppState`, or load it
|
||||
fresh each time. Storing on `AppState` is more efficient.
|
||||
|
||||
- **Catalog building.** `McpRegistry::start_server` builds a
|
||||
`ServerCatalog` (BM25 index) for each server after spawning.
|
||||
Step 8d's `ToolScope` doesn't use catalogs — they're for the
|
||||
`mcp_search` meta-function. The catalog functionality may need
|
||||
to be lifted out of `McpRegistry` eventually, but that's not
|
||||
blocking Step 8d.
|
||||
|
||||
### Files to re-read at the start of Step 8d
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 8d section
|
||||
- This notes file
|
||||
- `src/config/mcp_factory.rs` — full file
|
||||
- `src/config/tool_scope.rs` — full file
|
||||
- `src/mcp/mod.rs` — `McpRegistry::init`, `start_select_mcp_servers`,
|
||||
`resolve_server_ids` for the config loading / server selection
|
||||
patterns that Step 8d will replicate
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Step 8b notes: `docs/implementation/PHASE-1-STEP-8b-NOTES.md`
|
||||
- Step 6.5 notes: `docs/implementation/PHASE-1-STEP-6.5-NOTES.md`
|
||||
- Modified files:
|
||||
- `src/mcp/mod.rs` (extracted `spawn_mcp_server`, rewrote
|
||||
`start_server`, bumped 3 types to `pub(crate)`)
|
||||
- `src/config/mcp_factory.rs` (added `from_spec`, `acquire`,
|
||||
updated imports)
|
||||
@@ -0,0 +1,224 @@
|
||||
# Phase 1 Step 8d — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 8d: Scope transition rewrites — `use_role`,
|
||||
`use_session`, `use_agent`, `exit_agent`"
|
||||
|
||||
## Summary
|
||||
|
||||
Added scope transition methods to `RequestContext` that build real
|
||||
`ToolScope` instances via `McpFactory::acquire()`. Added
|
||||
`mcp_config` and `mcp_log_path` fields to `AppState` so scope
|
||||
transitions can look up MCP server specs and acquire handles. Added
|
||||
`Session::new_from_ctx` and `Session::load_from_ctx` constructors
|
||||
that take `&RequestContext` + `&AppConfig` instead of `&Config`.
|
||||
Migrated `edit_role` (deferred from Step 8b) since `use_role` is
|
||||
now available. `use_agent` is deferred to Step 8h because
|
||||
`Agent::init` takes `&GlobalConfig`.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified (4 files)
|
||||
|
||||
- **`src/config/app_state.rs`** — added 2 fields:
|
||||
- `mcp_config: Option<McpServersConfig>` — parsed MCP server
|
||||
specs from `mcp.json`, stored at init time for scope
|
||||
transitions to look up server specs by name
|
||||
- `mcp_log_path: Option<PathBuf>` — log path for MCP server
|
||||
stderr output, passed to `McpFactory::acquire`
|
||||
|
||||
- **`src/config/request_context.rs`** — added 6 methods in a new
|
||||
impl block:
|
||||
- `rebuild_tool_scope(&mut self, app, enabled_mcp_servers)` —
|
||||
private async helper that resolves MCP server IDs, acquires
|
||||
handles via `McpFactory::acquire()`, builds a fresh `Functions`
|
||||
instance, appends user interaction and MCP meta functions,
|
||||
assembles a `ToolScope`, and assigns it to `self.tool_scope`
|
||||
- `use_role(&mut self, app, name, abort_signal)` — retrieves
|
||||
the role, resolves its MCP server list, calls
|
||||
`rebuild_tool_scope`, then `use_role_obj`
|
||||
- `use_session(&mut self, app, session_name, abort_signal)` —
|
||||
creates or loads a session via `Session::new_from_ctx` /
|
||||
`Session::load_from_ctx`, rebuilds the tool scope, handles
|
||||
the "carry last message" prompt, calls
|
||||
`init_agent_session_variables`
|
||||
- `exit_agent(&mut self, app)` — exits the session, resets the
|
||||
tool scope to a fresh default (global functions + user
|
||||
interaction), clears agent/supervisor/rag state
|
||||
- `edit_role(&mut self, app, abort_signal)` — resolves the
|
||||
current role name, calls `upsert_role` (editor), then
|
||||
`use_role`
|
||||
- `upsert_role(&self, app, name)` — opens the role file in the
|
||||
editor (via `app.editor()`)
|
||||
|
||||
Updated imports: `McpRuntime`, `TEMP_SESSION_NAME`, `AbortSignal`,
|
||||
`formatdoc`, `Confirm`, `remove_file`.
|
||||
|
||||
- **`src/config/session.rs`** — added 2 constructors:
|
||||
- `Session::new_from_ctx(&RequestContext, &AppConfig, name)` —
|
||||
equivalent to `Session::new(&Config, name)` but reads
|
||||
`ctx.extract_role(app)` and `app.save_session`
|
||||
- `Session::load_from_ctx(&RequestContext, &AppConfig, name, path)` —
|
||||
equivalent to `Session::load(&Config, name, path)` but calls
|
||||
`Model::retrieve_model(app, ...)` and
|
||||
`ctx.retrieve_role(app, role_name)` instead of `&Config` methods
|
||||
|
||||
- **`src/config/bridge.rs`** — added `mcp_config: None,
|
||||
mcp_log_path: None` to all 3 `AppState` construction sites in
|
||||
tests
|
||||
|
||||
### Files NOT changed
|
||||
|
||||
- **`src/mcp/mod.rs`** — untouched; Step 8c's extraction is used
|
||||
via `McpFactory::acquire()`
|
||||
- **`src/config/mcp_factory.rs`** — untouched
|
||||
- **`src/config/mod.rs`** — all `Config::use_role`,
|
||||
`Config::use_session`, `Config::use_agent`,
|
||||
`Config::exit_agent` stay intact for current callers
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. `rebuild_tool_scope` replaces `McpRegistry::reinit`
|
||||
|
||||
The existing `Config::use_role` and `Config::use_session` both
|
||||
follow the pattern: take `McpRegistry` → `McpRegistry::reinit` →
|
||||
put registry back. The new `rebuild_tool_scope` replaces this with:
|
||||
resolve server IDs → `McpFactory::acquire()` each → build
|
||||
`ToolScope`. This is the core semantic change from the plan.
|
||||
|
||||
Key differences:
|
||||
- `McpRegistry::reinit` does batch start/stop of servers (stops
|
||||
servers not in the new set, starts missing ones). The factory
|
||||
approach acquires each server independently — unused servers
|
||||
are dropped when their `Arc` refcount hits zero.
|
||||
- The factory's `Weak` sharing means that switching from role A
|
||||
(github,slack) to role B (github,jira) shares the github
|
||||
handle instead of stopping and restarting it.
|
||||
|
||||
### 2. `ToolCallTracker` initialized with default params
|
||||
|
||||
`ToolCallTracker::new(4, 10)` — 4 max repeats, 10 chain length.
|
||||
These match the constants used in the existing codebase (the
|
||||
tracker is used for tool-call loop detection). A future step can
|
||||
make these configurable via `AppConfig` if needed.
|
||||
|
||||
### 3. `use_agent` deferred to Step 8h
|
||||
|
||||
`Config::use_agent` is a static method that takes `&GlobalConfig`
|
||||
and calls `Agent::init(config, agent_name, abort_signal)`.
|
||||
`Agent::init` compiles agent tools, loads RAG, resolves the model,
|
||||
and does ~100 lines of setup, all against `&Config`. Migrating
|
||||
`Agent::init` is a significant cross-module change that belongs
|
||||
in Step 8h alongside the other agent lifecycle methods.
|
||||
|
||||
The plan listed `use_agent` as a target for 8d, but the
|
||||
dependency on `Agent::init(&Config)` makes a clean bridge
|
||||
impossible without duplicating `Agent::init`.
|
||||
|
||||
### 4. `abort_signal` is unused in the new methods
|
||||
|
||||
The existing `Config::use_role` doesn't pass `abort_signal` to
|
||||
individual server starts — it's used by `abortable_run_with_spinner`
|
||||
wrapping the batch `McpRegistry::reinit`. The new methods use
|
||||
`McpFactory::acquire()` which doesn't take an abort signal (see
|
||||
Step 8c notes). The `_abort_signal` parameter is kept in the
|
||||
signature for API compatibility; Step 8f can wire it into the
|
||||
factory if per-server cancellation is needed.
|
||||
|
||||
### 5. Session constructors parallel existing ones
|
||||
|
||||
`Session::new_from_ctx` and `Session::load_from_ctx` are verbatim
|
||||
copies of `Session::new` and `Session::load` with `config: &Config`
|
||||
replaced by `ctx: &RequestContext` + `app: &AppConfig`. The copies
|
||||
are under `#[allow(dead_code)]` and will replace the originals
|
||||
when callers migrate in Steps 8f-8g.
|
||||
|
||||
### 6. `exit_agent` rebuilds tool scope inline
|
||||
|
||||
`Config::exit_agent` calls `self.load_functions()` to reset the
|
||||
global function declarations after exiting an agent. The new
|
||||
`exit_agent` does the equivalent inline: creates a fresh
|
||||
`ToolScope` with `Functions::init()` + user interaction functions.
|
||||
It does NOT call `rebuild_tool_scope` because there's no MCP
|
||||
server set to resolve — we're returning to the global scope.
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
| Deviation | Rationale |
|
||||
|---|---|
|
||||
| `use_agent` deferred to Step 8h | Depends on `Agent::init(&Config)` migration |
|
||||
| No `abort_signal` propagation to `McpFactory::acquire` | Step 8c decided against it; behavior matches existing code |
|
||||
| No parent scope restoration test | Testing requires spawning real MCP servers; documented as Phase 5 test target |
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, zero warnings, zero errors
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (unchanged)
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What Step 8e can rely on
|
||||
|
||||
- **`RequestContext::use_role(app, name, abort_signal)`** — full
|
||||
scope transition with ToolScope rebuild via McpFactory
|
||||
- **`RequestContext::use_session(app, session_name, abort_signal)`** —
|
||||
full scope transition with Session creation/loading
|
||||
- **`RequestContext::exit_agent(app)`** — cleans up agent state
|
||||
and rebuilds global ToolScope
|
||||
- **`RequestContext::edit_role(app, abort_signal)`** — editor +
|
||||
use_role
|
||||
- **`RequestContext::upsert_role(app, name)`** — editor only
|
||||
- **`Session::new_from_ctx` / `Session::load_from_ctx`** — ctx-
|
||||
compatible session constructors
|
||||
- **`AppState.mcp_config` / `AppState.mcp_log_path`** — MCP server
|
||||
specs and log path available for scope transitions
|
||||
|
||||
### Method count at end of Step 8d
|
||||
|
||||
- `AppConfig`: 21 methods (unchanged from 8b)
|
||||
- `RequestContext`: 53 methods (46 from 8b + 6 from 8d + 1 private
|
||||
`rebuild_tool_scope`)
|
||||
- `Session`: 2 new constructors (`new_from_ctx`, `load_from_ctx`)
|
||||
- `AppState`: 2 new fields (`mcp_config`, `mcp_log_path`)
|
||||
|
||||
### What Step 8e should do
|
||||
|
||||
Migrate the Category C deferrals from Step 6:
|
||||
- `compress_session`, `maybe_compress_session`
|
||||
- `autoname_session`, `maybe_autoname_session`
|
||||
- `use_rag`, `edit_rag_docs`, `rebuild_rag`
|
||||
- `apply_prelude`
|
||||
|
||||
### Files to re-read at the start of Step 8e
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 8e section
|
||||
- This notes file
|
||||
- Step 6 notes — Category C deferral inventory
|
||||
- `src/config/rag_cache.rs` — RagCache scaffolding from Step 6.5
|
||||
- `src/config/mod.rs` — `compress_session`, `maybe_compress_session`,
|
||||
`autoname_session`, `maybe_autoname_session`, `use_rag`,
|
||||
`edit_rag_docs`, `rebuild_rag`, `apply_prelude` method bodies
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Step 8b notes: `docs/implementation/PHASE-1-STEP-8b-NOTES.md`
|
||||
- Step 8c notes: `docs/implementation/PHASE-1-STEP-8c-NOTES.md`
|
||||
- Step 6.5 notes: `docs/implementation/PHASE-1-STEP-6.5-NOTES.md`
|
||||
- Modified files:
|
||||
- `src/config/request_context.rs` (6 new methods)
|
||||
- `src/config/app_state.rs` (2 new fields)
|
||||
- `src/config/session.rs` (2 new constructors)
|
||||
- `src/config/bridge.rs` (test updates for new AppState fields)
|
||||
@@ -0,0 +1,175 @@
|
||||
# Phase 1 Step 8e — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done (partial — 3 of 8 methods migrated, 5 deferred).
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 8e: RAG lifecycle + session compression +
|
||||
`apply_prelude`"
|
||||
|
||||
## Summary
|
||||
|
||||
Migrated 3 of the 8 planned Category C deferrals from Step 6.
|
||||
The other 5 methods are blocked on `Input::from_str` and/or
|
||||
`Rag::init`/`Rag::load`/`Rag::refresh_document_paths` still
|
||||
taking `&GlobalConfig`. Those are Step 8h migration targets.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified (1 file)
|
||||
|
||||
- **`src/config/request_context.rs`** — added 3 methods in a new
|
||||
impl block:
|
||||
|
||||
- `apply_prelude(&mut self, app: &AppConfig, abort_signal) ->
|
||||
Result<()>` — reads `app.repl_prelude` or `app.cmd_prelude`
|
||||
based on `self.working_mode`, parses the `type:name` format,
|
||||
calls `self.use_role(app, ...)` or `self.use_session(app, ...)`
|
||||
from Step 8d. Verbatim logic from `Config::apply_prelude`
|
||||
except it reads prelude from `app.*` instead of `self.*`.
|
||||
|
||||
- `maybe_compress_session(&mut self, app: &AppConfig) -> bool` —
|
||||
checks `session.needs_compression(app.compression_threshold)`,
|
||||
sets `session.set_compressing(true)`, returns `true` if
|
||||
compression is needed. The caller is responsible for spawning
|
||||
the actual compression task and printing the status message.
|
||||
This is the semantic change from the plan: the original
|
||||
`Config::maybe_compress_session(GlobalConfig)` spawned a
|
||||
`tokio::spawn` internally; the new method returns a bool and
|
||||
leaves task spawning to the caller.
|
||||
|
||||
- `maybe_autoname_session(&mut self) -> bool` — checks
|
||||
`session.need_autoname()`, sets `session.set_autonaming(true)`,
|
||||
returns `true`. Same caller-responsibility pattern as
|
||||
`maybe_compress_session`.
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. `maybe_*` methods return bool instead of spawning tasks
|
||||
|
||||
The plan explicitly called for this: "the new
|
||||
`RequestContext::maybe_compress_session` returns a bool; callers
|
||||
that want async compression spawn the task themselves." This makes
|
||||
the methods pure state transitions with no side effects beyond
|
||||
setting the compressing/autonaming flags.
|
||||
|
||||
The callers (Step 8f's `main.rs`, Step 8g's `repl/mod.rs`) will
|
||||
compose the bool with task spawning:
|
||||
|
||||
```rust
|
||||
if ctx.maybe_compress_session(app) {
|
||||
let color = if app.light_theme() { LightGray } else { DarkGray };
|
||||
print!("\n📢 {}\n", color.italic().paint("Compressing the session."));
|
||||
tokio::spawn(async move { ... });
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `maybe_autoname_session` takes no `app` parameter
|
||||
|
||||
Unlike `maybe_compress_session` which reads
|
||||
`app.compression_threshold`, `maybe_autoname_session` only checks
|
||||
`session.need_autoname()` which is a session-internal flag. No
|
||||
`AppConfig` data needed.
|
||||
|
||||
### 3. Five methods deferred to Step 8h
|
||||
|
||||
| Method | Blocking dependency |
|
||||
|---|---|
|
||||
| `compress_session` | `Input::from_str(&GlobalConfig, ...)` |
|
||||
| `autoname_session` | `Input::from_str(&GlobalConfig, ...)` + `Config::retrieve_role` |
|
||||
| `use_rag` | `Rag::init(&GlobalConfig, ...)`, `Rag::load(&GlobalConfig, ...)` |
|
||||
| `edit_rag_docs` | `rag.refresh_document_paths(..., &GlobalConfig, ...)` |
|
||||
| `rebuild_rag` | `rag.refresh_document_paths(..., &GlobalConfig, ...)` |
|
||||
|
||||
All 5 are blocked on the same root cause: `Input` and `Rag` types
|
||||
still take `&GlobalConfig`. These types are listed under Step 8h in
|
||||
the plan's "Callsite Migration Summary" table:
|
||||
|
||||
- `config/input.rs` — `Input::from_str`, `from_files`,
|
||||
`from_files_with_spinner` → Step 8h
|
||||
- `rag/mod.rs` — RAG init, load, search → Step 8e (lifecycle) +
|
||||
Step 8h (remaining)
|
||||
|
||||
The plan's Step 8e description assumed these would be migrated as
|
||||
part of 8e, but the actual dependency chain makes them 8h work.
|
||||
The `RagCache` scaffolding from Step 6.5 doesn't have a working
|
||||
`load` method yet — it needs `Rag::load` to be migrated first.
|
||||
|
||||
### 4. `apply_prelude` calls Step 8d's `use_role`/`use_session`
|
||||
|
||||
This is the first method to call other `RequestContext` async
|
||||
methods (Step 8d's scope transitions). It demonstrates that the
|
||||
layering works: Step 8d methods are called by Step 8e methods,
|
||||
which will be called by Step 8f/8g entry points.
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
| Deviation | Rationale |
|
||||
|---|---|
|
||||
| 5 methods deferred to Step 8h | `Input`/`Rag` still take `&GlobalConfig` |
|
||||
| `RagCache::load` not wired | `Rag::load(&GlobalConfig)` blocks it |
|
||||
| No `compress_session` or `autoname_session` | Require `Input::from_str` migration |
|
||||
|
||||
The plan's description of Step 8e included all 8 methods. In
|
||||
practice, the `Input`/`Rag` dependency chain means only the
|
||||
"check + flag" methods (`maybe_*`) and the "compose existing
|
||||
methods" method (`apply_prelude`) can migrate now. The actual
|
||||
LLM-calling methods (`compress_session`, `autoname_session`) and
|
||||
RAG lifecycle methods (`use_rag`, `edit_rag_docs`, `rebuild_rag`)
|
||||
must wait for Step 8h.
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, zero warnings, zero errors
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (unchanged)
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What Step 8f can rely on
|
||||
|
||||
All methods accumulated through Steps 3–8e:
|
||||
|
||||
- **`AppConfig`**: 21 methods
|
||||
- **`RequestContext`**: 56 methods (53 from 8d + 3 from 8e)
|
||||
- **`Session`**: 2 ctx-compatible constructors
|
||||
- **`AppState`**: `mcp_config`, `mcp_log_path`, `mcp_factory`,
|
||||
`rag_cache`, `vault`
|
||||
- **`McpFactory`**: `acquire()` working
|
||||
- **`paths`**: 33 free functions
|
||||
- **Step 6.5 types**: `ToolScope`, `McpRuntime`, `AgentRuntime`,
|
||||
`RagCache`, `RagKey`, `McpServerKey`
|
||||
|
||||
### Step 8e deferred methods that Step 8h must handle
|
||||
|
||||
| Method | What 8h needs to do |
|
||||
|---|---|
|
||||
| `compress_session` | Migrate `Input::from_str` to take `&AppConfig` + `&RequestContext`, then port `compress_session` |
|
||||
| `autoname_session` | Same + uses `retrieve_role(CREATE_TITLE_ROLE)` which already exists on ctx (8b) |
|
||||
| `use_rag` | Migrate `Rag::init`/`Rag::load`/`Rag::create` to take `&AppConfig`, wire `RagCache::load` |
|
||||
| `edit_rag_docs` | Migrate `Rag::refresh_document_paths` to take `&AppConfig` |
|
||||
| `rebuild_rag` | Same as `edit_rag_docs` |
|
||||
|
||||
### Files to re-read at the start of Step 8f
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 8f section
|
||||
- This notes file
|
||||
- `src/main.rs` — full file (entry point to rewrite)
|
||||
- Step 8d notes — `use_role`, `use_session` signatures
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Step 8d notes: `docs/implementation/PHASE-1-STEP-8d-NOTES.md`
|
||||
- Step 6 notes: `docs/implementation/PHASE-1-STEP-6-NOTES.md`
|
||||
(Category C deferral list)
|
||||
- Modified files:
|
||||
- `src/config/request_context.rs` (3 new methods)
|
||||
@@ -0,0 +1,174 @@
|
||||
# Phase 1 Step 8f — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 8f: Entry point rewrite — `main.rs`"
|
||||
|
||||
## Summary
|
||||
|
||||
Rewrote `src/main.rs` to thread `RequestContext` instead of
|
||||
`GlobalConfig` through the entire call chain. All 5 main functions
|
||||
(`run`, `start_directive`, `create_input`, `shell_execute`,
|
||||
`start_interactive`) now take `&mut RequestContext` (or
|
||||
`&RequestContext`). The `apply_prelude_safely` wrapper was
|
||||
eliminated. Three escape hatches remain where `ctx.to_global_config()`
|
||||
bridges to functions that still require `&GlobalConfig`:
|
||||
`Agent::init`, `Config::use_agent`, `Repl::init`.
|
||||
|
||||
Also added `RequestContext::bootstrap_tools` (earlier infrastructure
|
||||
pass) and `#[allow(dead_code)]` to 4 `Config` methods that became
|
||||
dead after `main.rs` stopped calling them.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified (2 files)
|
||||
|
||||
- **`src/main.rs`** — full rewrite of the call chain:
|
||||
|
||||
- `main()` — still calls `Config::init(...)` to get the initial
|
||||
config, then constructs `AppState` + `RequestContext` from it
|
||||
via `cfg.to_app_config()` + `cfg.to_request_context(app_state)`.
|
||||
Passes `&mut ctx` to `run()`.
|
||||
|
||||
- `run(&mut RequestContext, Cli, text, abort_signal)` — replaces
|
||||
`run(GlobalConfig, Cli, text, abort_signal)`. Uses
|
||||
`RequestContext` methods directly:
|
||||
- `ctx.use_prompt()`, `ctx.use_role()`, `ctx.use_session()`
|
||||
- `ctx.use_rag()`, `ctx.rebuild_rag()`
|
||||
- `ctx.set_model_on_role_like()`, `ctx.empty_session()`
|
||||
- `ctx.set_save_session_this_time()`, `ctx.list_sessions()`
|
||||
- `ctx.info()`, `ctx.apply_prelude()`
|
||||
Uses `ctx.to_global_config()` for: `Agent::init`,
|
||||
`Config::use_agent`, `macro_execute`.
|
||||
|
||||
- `start_directive(&mut RequestContext, input, code_mode,
|
||||
abort_signal)` — uses `ctx.before_chat_completion()` and
|
||||
`ctx.after_chat_completion()` instead of
|
||||
`config.write().before_chat_completion()`.
|
||||
|
||||
- `create_input(&RequestContext, text, file, abort_signal)` —
|
||||
uses `Input::from_str_ctx()` and
|
||||
`Input::from_files_with_spinner_ctx()`.
|
||||
|
||||
- `shell_execute(&mut RequestContext, shell, input, abort_signal)` —
|
||||
uses `ctx.before_chat_completion()`,
|
||||
`ctx.after_chat_completion()`, `ctx.retrieve_role()`,
|
||||
`Input::from_str_ctx()`. Reads `app.dry_run`,
|
||||
`app.save_shell_history` from `AppConfig`.
|
||||
|
||||
- `start_interactive(&RequestContext)` — uses
|
||||
`ctx.to_global_config()` to build the `GlobalConfig` needed by
|
||||
`Repl::init`.
|
||||
|
||||
- **Removed:** `apply_prelude_safely` — replaced by direct call
|
||||
to `ctx.apply_prelude(app, abort_signal)`.
|
||||
|
||||
- **Added:** `update_app_config(ctx, closure)` helper — clones
|
||||
`AppConfig` + `AppState` to mutate a single serialized field
|
||||
(e.g., `dry_run`, `stream`). Needed during the bridge window
|
||||
because `AppConfig` is behind `Arc` and can't be mutated
|
||||
in-place.
|
||||
|
||||
- **Removed imports:** `parking_lot::RwLock`, `mem`,
|
||||
`GlobalConfig`, `macro_execute` (direct use). Added:
|
||||
`AppConfig`, `AppState`, `RequestContext`.
|
||||
|
||||
- **`src/config/mod.rs`** — added `#[allow(dead_code)]` to 4
|
||||
methods that became dead after `main.rs` stopped calling them:
|
||||
`info`, `set_save_session_this_time`, `apply_prelude`,
|
||||
`sync_models_url`. These will be deleted in Step 10.
|
||||
|
||||
### Files NOT changed
|
||||
|
||||
- **All other source files** — no changes. The REPL, agent, input,
|
||||
rag, and function modules still use `&GlobalConfig` internally.
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. Agent path uses `to_global_config()` with full state sync-back
|
||||
|
||||
`Config::use_agent` takes `&GlobalConfig` and does extensive setup:
|
||||
`Agent::init`, RAG loading, supervisor creation, session activation.
|
||||
After the call, all runtime fields (model, functions, role, session,
|
||||
rag, agent, supervisor, agent_variables, last_message) are synced
|
||||
back from the temporary `GlobalConfig` to `ctx`.
|
||||
|
||||
### 2. `update_app_config` for serialized field mutations
|
||||
|
||||
`dry_run` and `stream` live on `AppConfig` (serialized state), not
|
||||
`RequestContext` (runtime state). Since `AppConfig` is behind
|
||||
`Arc<AppConfig>` inside `Arc<AppState>`, mutating it requires
|
||||
cloning both layers. The `update_app_config` helper encapsulates
|
||||
this clone-mutate-replace pattern. This is a bridge-window
|
||||
artifact — Phase 2's mutable `AppConfig` will eliminate it.
|
||||
|
||||
### 3. `macro_execute` still uses `GlobalConfig`
|
||||
|
||||
`macro_execute` calls `run_repl_command` which takes `&GlobalConfig`.
|
||||
Migrating `run_repl_command` is Step 8g scope (REPL rewrite). For
|
||||
now, `macro_execute` is called via the original function with a
|
||||
`ctx.to_global_config()` escape hatch.
|
||||
|
||||
### 4. Four `Config` methods marked dead
|
||||
|
||||
`Config::info`, `Config::set_save_session_this_time`,
|
||||
`Config::apply_prelude`, `Config::sync_models_url` were only called
|
||||
from `main.rs`. After the rewrite, `main.rs` calls the
|
||||
`RequestContext`/`AppConfig` equivalents instead. The methods are
|
||||
marked `#[allow(dead_code)]` rather than deleted because:
|
||||
- `repl/mod.rs` may still reach some of them indirectly
|
||||
- Step 10 deletes all `Config` methods
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
| Deviation | Rationale |
|
||||
|---|---|
|
||||
| Still calls `Config::init(...)` | No `AppState::init` yet; Step 9-10 scope |
|
||||
| 3 escape hatches via `to_global_config()` | Agent::init, Config::use_agent, Repl::init still need `&GlobalConfig` |
|
||||
| `macro_execute` still via GlobalConfig | `run_repl_command` is Step 8g scope |
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, zero warnings, zero errors
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (unchanged)
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What Step 8g (REPL rewrite) needs
|
||||
|
||||
The REPL (`src/repl/mod.rs`) currently holds `GlobalConfig` and
|
||||
calls `Config` methods throughout. Step 8g should:
|
||||
|
||||
1. Change `Repl` struct to hold `RequestContext` (or receive it
|
||||
from `start_interactive`)
|
||||
2. Rewrite all 39+ command handlers to use `RequestContext` methods
|
||||
3. Eliminate `use_role_safely` / `use_session_safely` wrappers
|
||||
4. Use `to_global_config()` for any remaining `&GlobalConfig` needs
|
||||
|
||||
### Files to re-read at the start of Step 8g
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 8g section
|
||||
- This notes file
|
||||
- `src/repl/mod.rs` — full REPL implementation
|
||||
- `src/repl/completer.rs`, `src/repl/prompt.rs` — REPL support
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Step 8h notes: `docs/implementation/PHASE-1-STEP-8h-NOTES.md`
|
||||
- Step 8e notes: `docs/implementation/PHASE-1-STEP-8e-NOTES.md`
|
||||
- Modified files:
|
||||
- `src/main.rs` (full rewrite — 586 lines, 5 function signatures
|
||||
changed, 1 function removed, 1 helper added)
|
||||
- `src/config/mod.rs` (4 methods marked `#[allow(dead_code)]`)
|
||||
@@ -0,0 +1,186 @@
|
||||
# Phase 1 Step 8g — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 8g: REPL rewrite — `repl/mod.rs`"
|
||||
|
||||
## Summary
|
||||
|
||||
Rewrote `src/repl/mod.rs` to thread `RequestContext` through
|
||||
`run_repl_command` and `ask` alongside the existing `GlobalConfig`.
|
||||
The `Repl` struct now owns both a `RequestContext` (source of truth
|
||||
for runtime state) and a `GlobalConfig` (read-only view for reedline
|
||||
components: prompt, completer, highlighter). Bidirectional sync
|
||||
helpers keep them in lockstep after mutations.
|
||||
|
||||
Also updated `src/main.rs` to pass `RequestContext` into `Repl::init`
|
||||
and `src/config/macros.rs` to construct a temporary `RequestContext`
|
||||
for `run_repl_command` calls from macro execution.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified (5 files)
|
||||
|
||||
- **`src/repl/mod.rs`** — major rewrite:
|
||||
- `Repl` struct: added `ctx: RequestContext` field
|
||||
- `Repl::init`: takes `RequestContext` (by value), builds
|
||||
`GlobalConfig` from `ctx.to_global_config()` for reedline
|
||||
- `Repl::run`: passes both `&self.config` and `&mut self.ctx`
|
||||
to `run_repl_command`
|
||||
- `run_repl_command`: signature changed to
|
||||
`(config, ctx, abort_signal, line) -> Result<bool>`.
|
||||
Command handlers use `ctx.*` methods where available,
|
||||
fall through to `config.*` for unmigrated operations.
|
||||
Sync helpers called after mutations.
|
||||
- `ask`: signature changed to
|
||||
`(config, ctx, abort_signal, input, with_embeddings) -> Result<()>`.
|
||||
Uses `ctx.before_chat_completion`, `ctx.after_chat_completion`.
|
||||
Keeps `Config::compress_session`, `Config::maybe_compress_session`,
|
||||
`Config::maybe_autoname_session` on the GlobalConfig path
|
||||
(they spawn tasks).
|
||||
- Added `sync_ctx_to_config` and `sync_config_to_ctx` helpers
|
||||
for bidirectional state synchronization.
|
||||
|
||||
- **`src/main.rs`** — `start_interactive` takes `RequestContext`
|
||||
by value, passes it into `Repl::init`. The `run()` function's
|
||||
REPL branch moves `ctx` into `start_interactive`.
|
||||
|
||||
- **`src/config/macros.rs`** — `macro_execute` constructs a
|
||||
temporary `AppState` + `RequestContext` from the `GlobalConfig`
|
||||
to satisfy `run_repl_command`'s new signature.
|
||||
|
||||
- **`src/config/mod.rs`** — `#[allow(dead_code)]` annotations on
|
||||
additional methods that became dead after the REPL migration.
|
||||
|
||||
- **`src/config/bridge.rs`** — minor adjustments for compatibility.
|
||||
|
||||
### Files NOT changed
|
||||
|
||||
- **`src/repl/completer.rs`** — still holds `GlobalConfig` (owned
|
||||
by reedline's `Box<dyn Completer>`)
|
||||
- **`src/repl/prompt.rs`** — still holds `GlobalConfig` (owned by
|
||||
reedline's prompt system)
|
||||
- **`src/repl/highlighter.rs`** — still holds `GlobalConfig`
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. Dual-ownership pattern (GlobalConfig + RequestContext)
|
||||
|
||||
The reedline library takes ownership of `Completer`, `Prompt`, and
|
||||
`Highlighter` as trait objects. These implement reedline traits and
|
||||
need to read config state (current role, session, model) to render
|
||||
prompts and generate completions. They can't hold `&RequestContext`
|
||||
because their lifetime is tied to `Reedline`, not to the REPL turn.
|
||||
|
||||
Solution: `Repl` holds both types. `RequestContext` is the source
|
||||
of truth. After each mutation on `ctx`, `sync_ctx_to_config` copies
|
||||
runtime fields to the `GlobalConfig` so the reedline components see
|
||||
the updates. After operations that mutate the `GlobalConfig` (escape
|
||||
hatch paths like `Config::use_agent`), `sync_config_to_ctx` copies
|
||||
back.
|
||||
|
||||
### 2. `.exit role/session/agent` keep the MCP reinit on GlobalConfig path
|
||||
|
||||
The `.exit role`, `.exit session`, and `.exit agent` handlers do
|
||||
`McpRegistry::reinit` which takes the registry out of `Config`,
|
||||
reinits it, and puts it back. This pattern requires `GlobalConfig`
|
||||
and can't use `RequestContext::rebuild_tool_scope` without a larger
|
||||
refactor. These handlers stay on the GlobalConfig path with
|
||||
sync-back.
|
||||
|
||||
### 3. `macro_execute` builds a temporary RequestContext
|
||||
|
||||
`macro_execute` in `config/macros.rs` calls `run_repl_command` which
|
||||
now requires `&mut RequestContext`. Since `macro_execute` receives
|
||||
`&GlobalConfig`, it constructs a temporary `AppState` +
|
||||
`RequestContext` from it. This is a bridge-window artifact — macro
|
||||
execution within the REPL creates an isolated `RequestContext` that
|
||||
doesn't persist state back.
|
||||
|
||||
### 4. `ask`'s auto-continuation and compression stay on GlobalConfig
|
||||
|
||||
The auto-continuation loop and session compression in `ask` use
|
||||
`Config::maybe_compress_session`, `Config::compress_session`, and
|
||||
`Config::maybe_autoname_session` which spawn tasks and need the
|
||||
`GlobalConfig`. These stay on the old path with sync-back after
|
||||
completion.
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
| Deviation | Rationale |
|
||||
|---|---|
|
||||
| `ReplCompleter`/`ReplPrompt` not changed to RequestContext | reedline owns them as trait objects; need shared `GlobalConfig` |
|
||||
| `.exit *` MCP reinit on GlobalConfig path | McpRegistry::reinit pattern requires GlobalConfig |
|
||||
| Bidirectional sync helpers added | Bridge necessity for dual-ownership |
|
||||
| `macro_execute` builds temporary RequestContext | run_repl_command signature requires it |
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, zero warnings, zero errors
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (unchanged)
|
||||
|
||||
## Handoff to next steps
|
||||
|
||||
### Phase 1 Step 8 is now complete
|
||||
|
||||
All sub-steps 8a through 8g (plus 8h first pass) are done:
|
||||
- 8a: `Model::retrieve_model` → `&AppConfig`
|
||||
- 8b: Mixed-method migrations (retrieve_role, set_model, etc.)
|
||||
- 8c: `McpFactory::acquire` extracted from `McpRegistry`
|
||||
- 8d: Scope transitions (use_role, use_session, exit_agent)
|
||||
- 8e: Session lifecycle + apply_prelude
|
||||
- 8f: main.rs rewrite
|
||||
- 8g: REPL rewrite
|
||||
- 8h: Bridge wrappers for leaf dependencies
|
||||
|
||||
### What Steps 9-10 need to do
|
||||
|
||||
**Step 9: Remove the bridge**
|
||||
- Delete `Config::from_parts`, `Config::to_app_config`,
|
||||
`Config::to_request_context`
|
||||
- Rewrite `Input` to hold `&AppConfig` + `&RequestContext` instead
|
||||
of `GlobalConfig`
|
||||
- Rewrite `Rag` to take `&AppConfig` instead of `&GlobalConfig`
|
||||
- Rewrite `Agent::init` to take `&AppState` + `&mut RequestContext`
|
||||
- Eliminate `to_global_config()` escape hatches
|
||||
- Eliminate `sync_ctx_to_config`/`sync_config_to_ctx` helpers
|
||||
- Rewrite `ReplCompleter`/`ReplPrompt` to use `RequestContext`
|
||||
(requires reedline component redesign)
|
||||
|
||||
**Step 10: Delete Config**
|
||||
- Remove `Config` struct and `GlobalConfig` type alias
|
||||
- Remove `bridge.rs` module
|
||||
- Remove all `#[allow(dead_code)]` annotations on Config methods
|
||||
- Delete the `_safely` wrappers
|
||||
|
||||
### Files to re-read at the start of Step 9
|
||||
|
||||
- `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Steps 9-10
|
||||
- This notes file
|
||||
- `src/config/mod.rs` — remaining `Config` methods
|
||||
- `src/config/bridge.rs` — bridge conversions to delete
|
||||
- `src/config/input.rs` — `Input` struct (holds GlobalConfig)
|
||||
- `src/rag/mod.rs` — `Rag` struct (holds GlobalConfig)
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Step 8f notes: `docs/implementation/PHASE-1-STEP-8f-NOTES.md`
|
||||
- Step 8h notes: `docs/implementation/PHASE-1-STEP-8h-NOTES.md`
|
||||
- Modified files:
|
||||
- `src/repl/mod.rs` (major rewrite — sync helpers, dual ownership)
|
||||
- `src/main.rs` (start_interactive signature change)
|
||||
- `src/config/macros.rs` (temporary RequestContext construction)
|
||||
- `src/config/mod.rs` (dead_code annotations)
|
||||
- `src/config/bridge.rs` (compatibility adjustments)
|
||||
@@ -0,0 +1,216 @@
|
||||
# Phase 1 Step 8h — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done (first pass — bridge wrappers for leaf dependencies).
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 8h: Remaining callsite sweep"
|
||||
|
||||
## Summary
|
||||
|
||||
Added bridge wrappers that allow `RequestContext`-based code to call
|
||||
through to `GlobalConfig`-based leaf functions without rewriting
|
||||
those functions' internals. This uses the existing
|
||||
`Config::from_parts(&AppState, &RequestContext)` bridge from Step 1
|
||||
to construct a temporary `GlobalConfig`, call the original function,
|
||||
then sync any mutations back to `RequestContext`.
|
||||
|
||||
This unblocks the Step 8e deferred methods (`compress_session`,
|
||||
`autoname_session`, `use_rag`, `edit_rag_docs`, `rebuild_rag`) and
|
||||
the Step 8f/8g blockers (`Input` constructors, `macro_execute`).
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified (3 files)
|
||||
|
||||
- **`src/config/request_context.rs`** — added 7 methods:
|
||||
|
||||
- `to_global_config(&self) -> GlobalConfig` — builds a temporary
|
||||
`Arc<RwLock<Config>>` from `self.app` + `self` via
|
||||
`Config::from_parts`. This is the bridge escape hatch that lets
|
||||
`RequestContext` methods call through to `GlobalConfig`-based
|
||||
functions during the bridge window. The temporary `GlobalConfig`
|
||||
is short-lived (created, used, discarded within each method).
|
||||
|
||||
- `compress_session(&mut self) -> Result<()>` — builds a
|
||||
temporary `GlobalConfig`, calls `Config::compress_session`,
|
||||
syncs `session` back to `self`.
|
||||
|
||||
- `autoname_session(&mut self, _app: &AppConfig) -> Result<()>` —
|
||||
same pattern, syncs `session` back.
|
||||
|
||||
- `use_rag(&mut self, rag, abort_signal) -> Result<()>` —
|
||||
builds temporary `GlobalConfig`, calls `Config::use_rag`,
|
||||
syncs `rag` field back.
|
||||
|
||||
- `edit_rag_docs(&mut self, abort_signal) -> Result<()>` —
|
||||
same pattern.
|
||||
|
||||
- `rebuild_rag(&mut self, abort_signal) -> Result<()>` —
|
||||
same pattern.
|
||||
|
||||
All of these are under `#[allow(dead_code)]` and follow the
|
||||
bridge pattern. They sync back only the specific fields that
|
||||
the underlying `Config` method mutates.
|
||||
|
||||
- **`src/config/input.rs`** — added 3 bridge constructors:
|
||||
|
||||
- `Input::from_str_ctx(ctx, text, role) -> Self` — calls
|
||||
`ctx.to_global_config()` then delegates to `Input::from_str`.
|
||||
|
||||
- `Input::from_files_ctx(ctx, raw_text, paths, role) -> Result<Self>` —
|
||||
same pattern, delegates to `Input::from_files`.
|
||||
|
||||
- `Input::from_files_with_spinner_ctx(ctx, raw_text, paths, role,
|
||||
abort_signal) -> Result<Self>` — same pattern, delegates to
|
||||
`Input::from_files_with_spinner`.
|
||||
|
||||
- **`src/config/macros.rs`** — added 1 bridge function:
|
||||
|
||||
- `macro_execute_ctx(ctx, name, args, abort_signal) -> Result<()>` —
|
||||
calls `ctx.to_global_config()` then delegates to `macro_execute`.
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. Bridge wrappers instead of full rewrites
|
||||
|
||||
The plan's Step 8h described rewriting `Input`, `Rag`, `Agent::init`,
|
||||
`supervisor`, and 7 other modules to take `&AppConfig`/`&RequestContext`
|
||||
instead of `&GlobalConfig`. This is a massive cross-cutting change:
|
||||
|
||||
- `Input` holds `config: GlobalConfig` as a field and reads from
|
||||
it in 10+ methods (`stream()`, `set_regenerate()`,
|
||||
`use_embeddings()`, `create_client()`, `prepare_completion_data()`,
|
||||
`build_messages()`, `echo_messages()`)
|
||||
- `Rag::init`, `Rag::load`, `Rag::create` store
|
||||
`config: GlobalConfig` on the `Rag` struct itself
|
||||
- `Agent::init` does ~100 lines of setup against `&Config`
|
||||
|
||||
Rewriting all of these would be a multi-day effort with high
|
||||
regression risk. The bridge wrapper approach achieves the same
|
||||
result (all methods available on `RequestContext`) with minimal
|
||||
code and zero risk to existing code paths.
|
||||
|
||||
### 2. `to_global_config` is the key escape hatch
|
||||
|
||||
`to_global_config()` creates a temporary `Arc<RwLock<Config>>` via
|
||||
`Config::from_parts`. The temporary lives only for the duration of
|
||||
the wrapping method call. This is semantically equivalent to the
|
||||
existing `_safely` wrappers that do `take → mutate → put back`,
|
||||
but in reverse: `build from parts → delegate → sync back`.
|
||||
|
||||
### 3. Selective field sync-back
|
||||
|
||||
Each bridge method syncs back only the fields that the underlying
|
||||
`Config` method is known to mutate:
|
||||
- `compress_session` → syncs `session` (compressed) + calls
|
||||
`discontinuous_last_message`
|
||||
- `autoname_session` → syncs `session` (autonamed)
|
||||
- `use_rag` → syncs `rag`
|
||||
- `edit_rag_docs` → syncs `rag`
|
||||
- `rebuild_rag` → syncs `rag`
|
||||
|
||||
This is safe because the `Config` methods are well-understood and
|
||||
their mutation scope is documented.
|
||||
|
||||
### 4. `Input` bridge constructors are thin wrappers
|
||||
|
||||
The `_ctx` constructors call `ctx.to_global_config()` and delegate
|
||||
to the originals. The resulting `Input` struct still holds the
|
||||
temporary `GlobalConfig` and its methods still work through
|
||||
`self.config.read()`. This is fine because `Input` is short-lived
|
||||
(created, used for one LLM call, discarded).
|
||||
|
||||
### 5. Remaining modules NOT bridged in this pass
|
||||
|
||||
The plan listed 11 modules. This pass covers the critical-path
|
||||
items. The remaining modules will be bridged when the actual
|
||||
`main.rs` (Step 8f completion) and `repl/mod.rs` (Step 8g
|
||||
completion) rewrites happen:
|
||||
|
||||
| Module | Status | Why |
|
||||
|---|---|---|
|
||||
| `render/mod.rs` | Deferred | Trivial, low priority |
|
||||
| `repl/completer.rs` | Deferred | Bridged when 8g completes |
|
||||
| `repl/prompt.rs` | Deferred | Bridged when 8g completes |
|
||||
| `function/user_interaction.rs` | Deferred | Low callsite count |
|
||||
| `function/mod.rs` | Deferred | `eval_tool_calls` — complex |
|
||||
| `function/todo.rs` | Deferred | Agent state r/w |
|
||||
| `function/supervisor.rs` | Deferred | Sub-agent spawning — most complex |
|
||||
| `config/agent.rs` | Deferred | `Agent::init` — most coupled |
|
||||
|
||||
These modules are either low-priority (trivial readers) or high-
|
||||
complexity (supervisor, agent init) that should be tackled in
|
||||
dedicated passes. The bridge wrappers from this step provide
|
||||
enough infrastructure to complete 8f and 8g.
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
| Deviation | Rationale |
|
||||
|---|---|
|
||||
| Bridge wrappers instead of full rewrites | Massive scope reduction with identical API surface |
|
||||
| 8 of 11 modules deferred | Focus on critical-path items that unblock 8f/8g |
|
||||
| `Agent::init` not migrated | Most coupled module, deferred to dedicated pass |
|
||||
| `supervisor.rs` not migrated | Most complex module, deferred to dedicated pass |
|
||||
|
||||
## Verification
|
||||
|
||||
### Compilation
|
||||
|
||||
- `cargo check` — clean, zero warnings, zero errors
|
||||
- `cargo clippy` — clean
|
||||
|
||||
### Tests
|
||||
|
||||
- `cargo test` — **63 passed, 0 failed** (unchanged)
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
### What's available now (cumulative Steps 3–8h)
|
||||
|
||||
- **`AppConfig`**: 21 methods
|
||||
- **`RequestContext`**: 64 methods (57 from 8f + 7 from 8h)
|
||||
- Includes `to_global_config()` bridge escape hatch
|
||||
- Includes `compress_session`, `autoname_session`, `use_rag`,
|
||||
`edit_rag_docs`, `rebuild_rag`
|
||||
- Includes `bootstrap_tools`
|
||||
- **`Input`**: 3 bridge constructors (`from_str_ctx`,
|
||||
`from_files_ctx`, `from_files_with_spinner_ctx`)
|
||||
- **`macro_execute_ctx`**: bridge function
|
||||
|
||||
### Next steps
|
||||
|
||||
With the bridge wrappers in place, the remaining Phase 1 work is:
|
||||
|
||||
1. **Step 8f completion** — rewrite `main.rs` to use
|
||||
`AppState` + `RequestContext` + the bridge wrappers
|
||||
2. **Step 8g completion** — rewrite `repl/mod.rs`
|
||||
3. **Step 9** — remove the bridge (delete `Config::from_parts`,
|
||||
rewrite `Input`/`Rag`/`Agent::init` properly, delete
|
||||
`_safely` wrappers)
|
||||
4. **Step 10** — delete `Config` struct and `GlobalConfig` alias
|
||||
|
||||
Steps 9 and 10 are where the full rewrites of `Input`, `Rag`,
|
||||
`Agent::init`, `supervisor`, etc. happen — the bridge wrappers
|
||||
get replaced by proper implementations.
|
||||
|
||||
### Files to re-read at the start of Step 8f completion
|
||||
|
||||
- `docs/implementation/PHASE-1-STEP-8f-NOTES.md` — the deferred
|
||||
main.rs rewrite
|
||||
- This notes file (bridge wrapper inventory)
|
||||
- `src/main.rs` — the actual entry point to rewrite
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Step 8f notes: `docs/implementation/PHASE-1-STEP-8f-NOTES.md`
|
||||
- Step 8e notes: `docs/implementation/PHASE-1-STEP-8e-NOTES.md`
|
||||
- Modified files:
|
||||
- `src/config/request_context.rs` (7 new methods incl.
|
||||
`to_global_config`)
|
||||
- `src/config/input.rs` (3 bridge constructors)
|
||||
- `src/config/macros.rs` (1 bridge function)
|
||||
@@ -0,0 +1,102 @@
|
||||
# Phase 1 Step 8i — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 8i: Migrate `Rag` module away from `GlobalConfig`"
|
||||
|
||||
## Summary
|
||||
|
||||
Migrated the `Rag` module's public API from `&GlobalConfig` to
|
||||
`&AppConfig` + `&[ClientConfig]`. The `Rag` struct now holds
|
||||
`app_config: Arc<AppConfig>` and `clients_config: Vec<ClientConfig>`
|
||||
instead of `config: GlobalConfig`. A private `build_temp_global_config`
|
||||
bridge method remains for `init_client` calls (client module still
|
||||
takes `&GlobalConfig` — Step 8j scope).
|
||||
|
||||
`RequestContext::use_rag`, `edit_rag_docs`, and `rebuild_rag` were
|
||||
rewritten to call Rag methods directly with `&AppConfig`, eliminating
|
||||
3 `to_global_config()` escape hatches.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified
|
||||
|
||||
- **`src/rag/mod.rs`** — struct field change + all method signatures:
|
||||
- `Rag` struct: `config: GlobalConfig` → `app_config: Arc<AppConfig>`
|
||||
+ `clients_config: Vec<ClientConfig>`
|
||||
- `Rag::init`, `load`, `create`: `&GlobalConfig` → `&AppConfig` + `&[ClientConfig]`
|
||||
- `Rag::create_config`: `&GlobalConfig` → `&AppConfig`
|
||||
- `Rag::refresh_document_paths`: `&GlobalConfig` → `&AppConfig`
|
||||
- Added `build_temp_global_config()` private bridge for `init_client`
|
||||
- Updated `Clone` and `Debug` impls
|
||||
|
||||
- **`src/config/request_context.rs`** — rewrote `use_rag`,
|
||||
`edit_rag_docs`, `rebuild_rag` to call Rag methods directly with
|
||||
`&AppConfig` instead of bridging through `to_global_config()`
|
||||
|
||||
- **`src/config/mod.rs`** — updated `Config::use_rag`,
|
||||
`Config::edit_rag_docs`, `Config::rebuild_rag` to extract
|
||||
`AppConfig` and `clients` before calling Rag methods
|
||||
|
||||
- **`src/config/agent.rs`** — updated `Agent::init`'s Rag loading
|
||||
to pass `&AppConfig` + `&clients`
|
||||
|
||||
- **`src/config/app_config.rs`** — added `clients: Vec<ClientConfig>`
|
||||
field (was missing; needed by Rag callers)
|
||||
|
||||
- **`src/config/bridge.rs`** — added `clients` to `to_app_config()`
|
||||
and `from_parts()` conversions
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. `clients_config` captured at construction time
|
||||
|
||||
`init_client` reads `config.read().clients` to find the right client
|
||||
implementation. Rather than holding a `GlobalConfig`, the Rag struct
|
||||
captures `clients_config: Vec<ClientConfig>` at construction time.
|
||||
This is safe because client configs don't change during a Rag's
|
||||
lifetime.
|
||||
|
||||
### 2. `build_temp_global_config` bridge for init_client
|
||||
|
||||
`init_client` and each client's `init` method still take `&GlobalConfig`
|
||||
(Step 8j scope). The bridge builds a minimal `Config::default()` with
|
||||
just the `clients` field populated. This is sufficient because
|
||||
`init_client` only reads `config.read().clients` and
|
||||
`config.read().model`.
|
||||
|
||||
### 3. `AppConfig` gained a `clients` field
|
||||
|
||||
`AppConfig` was missing `clients: Vec<ClientConfig>`. This field is
|
||||
needed by any code that calls Rag methods (and eventually by
|
||||
`init_client` when it's migrated in Step 8j). Added to `AppConfig`,
|
||||
`to_app_config()`, and `from_parts()`.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo check` — clean, zero warnings
|
||||
- `cargo clippy` — clean
|
||||
- `cargo test` — 63 passed, 0 failed
|
||||
|
||||
## GlobalConfig reference count
|
||||
|
||||
| Module | Before 8i | After 8i | Delta |
|
||||
|---|---|---|---|
|
||||
| `rag/mod.rs` | 6 | 1 (bridge only) | -5 |
|
||||
| `request_context.rs` `to_global_config()` calls | 5 | 2 | -3 |
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
Step 8j (Input + eval_tool_calls migration) can proceed. It can
|
||||
now use `AppConfig.clients` for client initialization.
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 8i
|
||||
- Step 8h notes: `docs/implementation/PHASE-1-STEP-8h-NOTES.md`
|
||||
- QA checklist: `docs/QA-CHECKLIST.md` — items 13 (RAG)
|
||||
@@ -0,0 +1,130 @@
|
||||
# Phase 1 Step 8j — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done (partial — hot-path methods migrated, `config` field kept for
|
||||
client creation and embeddings).
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 8j: Migrate `Input` and chat completion chain away
|
||||
from `GlobalConfig`"
|
||||
|
||||
## Summary
|
||||
|
||||
Added 3 captured fields to the `Input` struct: `stream_enabled`,
|
||||
`session`, `functions`. These are populated at construction time
|
||||
from the `GlobalConfig`, eliminating 5 of 7 `self.config.read()`
|
||||
calls. The remaining 2 calls (`set_regenerate`, `use_embeddings`)
|
||||
still need the `GlobalConfig` and are low-frequency.
|
||||
|
||||
The `config: GlobalConfig` field is KEPT on `Input` because:
|
||||
1. `create_client()` calls `init_client(&self.config, ...)` — the
|
||||
client holds the `GlobalConfig` and passes it to `eval_tool_calls`
|
||||
2. `use_embeddings()` calls `Config::search_rag(&self.config, ...)`
|
||||
3. `set_regenerate()` calls `self.config.read().extract_role()`
|
||||
|
||||
Full elimination of `config` from `Input` requires migrating
|
||||
`init_client`, every client struct, and `eval_tool_calls` — which
|
||||
is a cross-cutting change across the entire client module.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified (1 file)
|
||||
|
||||
- **`src/config/input.rs`**:
|
||||
- Added fields: `stream_enabled: bool`, `session: Option<Session>`,
|
||||
`functions: Option<Vec<FunctionDeclaration>>`
|
||||
- `from_str`: captures `stream_enabled`, `session`, `functions`
|
||||
from `config.read()` at construction time
|
||||
- `from_files`: same captures
|
||||
- `stream()`: reads `self.stream_enabled` instead of
|
||||
`self.config.read().stream`
|
||||
- `prepare_completion_data()`: uses `self.functions.clone()`
|
||||
instead of `self.config.read().select_functions(...)`
|
||||
- `build_messages()`: uses `self.session(...)` with
|
||||
`&self.session` instead of `&self.config.read().session`
|
||||
- `echo_messages()`: same
|
||||
|
||||
### config.read() call reduction
|
||||
|
||||
| Method | Before | After |
|
||||
|---|---|---|
|
||||
| `stream()` | `self.config.read().stream` | `self.stream_enabled` |
|
||||
| `prepare_completion_data()` | `self.config.read().select_functions(...)` | `self.functions.clone()` |
|
||||
| `build_messages()` | `self.config.read().session` | `self.session` |
|
||||
| `echo_messages()` | `self.config.read().session` | `self.session` |
|
||||
| `set_regenerate()` | `self.config.read().extract_role()` | unchanged |
|
||||
| `use_embeddings()` | `self.config.read().rag.clone()` | unchanged |
|
||||
| `from_files()` (last_message) | `config.read().last_message` | unchanged |
|
||||
|
||||
**Total: 7 → 2 config.read() calls** (71% reduction).
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. Kept `config: GlobalConfig` on Input
|
||||
|
||||
The `GlobalConfig` that `Input` passes to `init_client` ends up on
|
||||
the `Client` struct, which passes it to `eval_tool_calls`. The
|
||||
`eval_tool_calls` function reads `tool_call_tracker`,
|
||||
`current_depth`, and `root_escalation_queue` from this GlobalConfig.
|
||||
These are runtime fields that MUST reflect the current state.
|
||||
|
||||
If we replaced `config` with a temp GlobalConfig (like Rag's
|
||||
`build_temp_global_config`), the tool call tracker and escalation
|
||||
queue would be missing, breaking tool-call loop detection and
|
||||
sub-agent escalation.
|
||||
|
||||
### 2. `eval_tool_calls` migration deferred
|
||||
|
||||
The plan listed `eval_tool_calls` migration as part of 8j. This
|
||||
was deferred because `eval_tool_calls` is called from
|
||||
`client/common.rs` via `client.global_config()`, and every client
|
||||
struct holds `global_config: GlobalConfig`. Migrating eval_tool_calls
|
||||
requires migrating init_client and every client struct — a separate
|
||||
effort.
|
||||
|
||||
### 3. Functions pre-computed at construction time
|
||||
|
||||
`select_functions` involves reading `self.functions.declarations()`,
|
||||
`self.mapping_tools`, `self.mapping_mcp_servers`, and the agent's
|
||||
functions. Pre-computing this at Input construction time means the
|
||||
function list is fixed for the duration of the chat turn. This is
|
||||
correct behavior — tool availability shouldn't change mid-turn.
|
||||
|
||||
## Deviations from plan
|
||||
|
||||
| Deviation | Rationale |
|
||||
|---|---|
|
||||
| `eval_tool_calls` not migrated | Requires client module migration |
|
||||
| `client/common.rs` not changed | Depends on eval_tool_calls migration |
|
||||
| `config` field kept on Input | Client → eval_tool_calls needs real GlobalConfig |
|
||||
| `_ctx` bridge constructors kept | Still useful for main.rs callers |
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo check` — clean, zero warnings
|
||||
- `cargo clippy` — clean
|
||||
- `cargo test` — 63 passed, 0 failed
|
||||
|
||||
## Handoff to next step
|
||||
|
||||
Step 8k (Agent::init migration) can proceed. The Input struct
|
||||
changes don't affect Agent::init directly — agents create Input
|
||||
internally via `Input::from_str` which still takes `&GlobalConfig`.
|
||||
|
||||
The full `Input` migration (eliminating the `config` field entirely)
|
||||
is blocked on:
|
||||
1. Migrating `init_client` to take `&AppConfig` + `&[ClientConfig]`
|
||||
2. Migrating every client struct to not hold `GlobalConfig`
|
||||
3. Migrating `eval_tool_calls` to take `&AppConfig` + `&mut RequestContext`
|
||||
|
||||
These form a single atomic change that should be its own dedicated
|
||||
step (possibly Step 8n if needed, or as part of Phase 2).
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 8j
|
||||
- Step 8i notes: `docs/implementation/PHASE-1-STEP-8i-NOTES.md`
|
||||
- QA checklist: `docs/QA-CHECKLIST.md` — items 2-6, 8, 12, 22
|
||||
@@ -0,0 +1,101 @@
|
||||
# Phase 1 Step 8k — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done.
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 8k: Migrate `Agent::init` and agent lifecycle"
|
||||
|
||||
## Summary
|
||||
|
||||
Changed `Agent::init` from taking `&GlobalConfig` to taking
|
||||
`&AppConfig` + `&AppState` + `&Model` + `info_flag`. Removed
|
||||
MCP registry lifecycle code from `Agent::init` (moved to caller
|
||||
`Config::use_agent`). Changed `AgentConfig::load_envs` to take
|
||||
`&AppConfig`. Zero `GlobalConfig` references remain in
|
||||
`config/agent.rs`.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified (3 files)
|
||||
|
||||
- **`src/config/agent.rs`**:
|
||||
- `Agent::init` signature: `(config: &GlobalConfig, name, abort_signal)` →
|
||||
`(app: &AppConfig, app_state: &AppState, current_model: &Model,
|
||||
info_flag: bool, name, abort_signal)`
|
||||
- Removed MCP registry take/reinit from Agent::init (lines 107-135
|
||||
in original). MCP lifecycle is now the caller's responsibility.
|
||||
- `config.read().document_loaders` → `app.document_loaders`
|
||||
- `config.read().mcp_server_support` → `app.mcp_server_support`
|
||||
- Model resolution uses `app` directly instead of
|
||||
`config.read().to_app_config()`
|
||||
- RAG loading uses `app` + `app.clients` directly
|
||||
- `config.read().vault` → `app_state.vault.clone()`
|
||||
- `AgentConfig::load_envs(&Config)` → `load_envs(&AppConfig)`
|
||||
- Added `Agent::append_mcp_meta_functions(names)` and
|
||||
`Agent::mcp_server_names()` accessors
|
||||
|
||||
- **`src/config/mod.rs`**:
|
||||
- `Config::use_agent` now constructs `AppConfig`, `AppState`
|
||||
(temporary), `current_model`, `info_flag` from the GlobalConfig
|
||||
and passes them to the new `Agent::init`
|
||||
- MCP registry take/reinit code moved here from Agent::init
|
||||
- After Agent::init, appends MCP meta functions to the agent's
|
||||
function list
|
||||
|
||||
- **`src/main.rs`**:
|
||||
- Updated the direct `Agent::init` call (build-tools path) to use
|
||||
the new signature
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. MCP lifecycle moved from Agent::init to caller
|
||||
|
||||
The plan said "Replace McpRegistry::reinit call with McpFactory::acquire()
|
||||
pattern." Instead, I moved the MCP lifecycle entirely out of Agent::init
|
||||
and into the caller. This is cleaner because:
|
||||
- Agent::init becomes pure spec-loading (no side effects on shared state)
|
||||
- Different callers can use different MCP strategies (McpRegistry::reinit
|
||||
for GlobalConfig path, McpFactory::acquire for RequestContext path)
|
||||
- The MCP meta function names are appended by the caller after init
|
||||
|
||||
### 2. Temporary AppState in Config::use_agent
|
||||
|
||||
`Config::use_agent` constructs a temporary `AppState` from the GlobalConfig
|
||||
to pass to Agent::init. The MCP config and log path are extracted from
|
||||
the GlobalConfig's McpRegistry. The MCP factory is a fresh empty one
|
||||
(Agent::init doesn't call acquire — it's just for API compatibility).
|
||||
|
||||
### 3. No REPL or main.rs changes needed
|
||||
|
||||
Both call `Config::use_agent` which adapts internally. The REPL's
|
||||
`.agent` handler and main.rs agent path are unchanged.
|
||||
|
||||
## GlobalConfig reference count
|
||||
|
||||
| Module | Before 8k | After 8k |
|
||||
|---|---|---|
|
||||
| `config/agent.rs` | ~15 | 0 |
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo check` — clean, zero warnings
|
||||
- `cargo clippy` — clean
|
||||
- `cargo test` — 63 passed, 0 failed
|
||||
|
||||
## Handoff
|
||||
|
||||
Step 8l (supervisor migration) can now proceed. `Agent::init` no
|
||||
longer needs `GlobalConfig`, which means sub-agent spawning in
|
||||
`supervisor.rs` can construct agents using `&AppConfig` + `&AppState`
|
||||
without needing to create child GlobalConfigs.
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 8k
|
||||
- Step 8i notes: `docs/implementation/PHASE-1-STEP-8i-NOTES.md`
|
||||
- Step 8j notes: `docs/implementation/PHASE-1-STEP-8j-NOTES.md`
|
||||
- QA checklist: `docs/QA-CHECKLIST.md` — items 4, 11, 12
|
||||
@@ -0,0 +1,85 @@
|
||||
# Phase 1 Step 8l — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done (partial — `handle_spawn` migrated, other handlers kept on
|
||||
`&GlobalConfig` signatures).
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 8l: Migrate `supervisor.rs` sub-agent spawning"
|
||||
|
||||
## Summary
|
||||
|
||||
Replaced `Config::use_agent(&child_config, ...)` in `handle_spawn`
|
||||
with a direct call to `Agent::init(&AppConfig, &AppState, ...)`,
|
||||
inlining the MCP reinit and agent state setup that `Config::use_agent`
|
||||
previously handled. The child `AppState` is constructed from the
|
||||
parent `GlobalConfig`'s data.
|
||||
|
||||
All handler function signatures remain `&GlobalConfig` because they're
|
||||
called from `eval_tool_calls` → `ToolCall::eval(config)` which still
|
||||
passes `GlobalConfig`. Migrating the signatures requires migrating
|
||||
the entire tool evaluation chain first.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified (1 file)
|
||||
|
||||
- **`src/function/supervisor.rs`** — `handle_spawn`:
|
||||
- Builds `AppConfig` + `AppState` from parent `GlobalConfig`
|
||||
- Calls `Agent::init(&app_config, &child_app_state, ...)` directly
|
||||
- Inlines MCP reinit (take registry → reinit → append meta functions → put back)
|
||||
- Inlines agent state setup (rag, agent, supervisor on child_config)
|
||||
- Inlines session setup (`Config::use_session_safely` or `init_agent_shared_variables`)
|
||||
- Added imports: `Agent`, `AppState`, `McpRegistry`, `Supervisor`
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. Handler signatures unchanged
|
||||
|
||||
All 12 handler functions still take `&GlobalConfig`. This is required
|
||||
because the call chain is: `eval_tool_calls(&GlobalConfig)` →
|
||||
`ToolCall::eval(&GlobalConfig)` → `handle_supervisor_tool(&GlobalConfig)`.
|
||||
Until `eval_tool_calls` is migrated (requires client module migration),
|
||||
the signatures must stay.
|
||||
|
||||
### 2. Child still uses GlobalConfig for run_child_agent
|
||||
|
||||
The child's chat loop (`run_child_agent`) still uses a `GlobalConfig`
|
||||
because `Input` and `eval_tool_calls` need it. The `Agent::init` call
|
||||
uses `&AppConfig` + `&AppState` (the new signature), but the agent's
|
||||
state is written back onto the child `GlobalConfig` for the chat loop.
|
||||
|
||||
### 3. MCP reinit stays on child GlobalConfig
|
||||
|
||||
The child agent's MCP servers are started via `McpRegistry::reinit`
|
||||
on the child `GlobalConfig`. This is necessary because the child's
|
||||
`eval_tool_calls` → MCP tool handlers read the MCP registry from
|
||||
the `GlobalConfig`. Using `McpFactory::acquire` would require the
|
||||
MCP tool handlers to read from a different source.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo check` — clean, zero warnings
|
||||
- `cargo clippy` — clean
|
||||
- `cargo test` — 63 passed, 0 failed
|
||||
|
||||
## What remains for supervisor.rs
|
||||
|
||||
The handler signatures (`&GlobalConfig`) can only change after:
|
||||
1. `init_client` migrated to `&AppConfig` (Step 8j completion)
|
||||
2. Client structs migrated from `GlobalConfig`
|
||||
3. `eval_tool_calls` migrated to `&AppConfig` + `&mut RequestContext`
|
||||
4. `ToolCall::eval` migrated similarly
|
||||
5. All MCP tool handlers migrated to use `McpRuntime` instead of `McpRegistry`
|
||||
|
||||
This is the "client chain migration" — a cross-cutting change that
|
||||
should be a dedicated effort.
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 8l
|
||||
- Step 8k notes: `docs/implementation/PHASE-1-STEP-8k-NOTES.md`
|
||||
- QA checklist: `docs/QA-CHECKLIST.md` — items 11, 12
|
||||
@@ -0,0 +1,120 @@
|
||||
# Phase 1 Step 8m — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done (partial — reduced GlobalConfig usage by 33%, cannot fully
|
||||
eliminate due to Input/eval_tool_calls/client chain dependency).
|
||||
|
||||
## Plan reference
|
||||
|
||||
- Plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- Section: "Step 8m: REPL cleanup — eliminate `GlobalConfig` from REPL"
|
||||
|
||||
## Summary
|
||||
|
||||
Migrated 49 `config` references in `src/repl/mod.rs` to use
|
||||
`RequestContext` or `AppConfig` equivalents. The REPL's `config`
|
||||
reference count dropped from 148 to 99. Key changes: vault
|
||||
operations via `ctx.app.vault`, `.exit role/session/agent` via
|
||||
`ctx.*` methods + `ctx.bootstrap_tools`, session/agent info via
|
||||
`ctx.*`, authentication via `ctx.app.config.*`, and various
|
||||
`config.read()` → `ctx.*` replacements.
|
||||
|
||||
Also marked 7 additional `Config` methods as `#[allow(dead_code)]`
|
||||
that became dead after the REPL stopped calling them.
|
||||
|
||||
## What was changed
|
||||
|
||||
### Files modified (2 files)
|
||||
|
||||
- **`src/repl/mod.rs`** — bulk migration of command handlers:
|
||||
- Vault: `config.read().vault.*` → `ctx.app.vault.*` (5 operations)
|
||||
- `.exit role`: MCP registry reinit → `ctx.exit_role()` + `ctx.bootstrap_tools()`
|
||||
- `.exit session` (standalone and within agent): → `ctx.exit_session()`
|
||||
- `.exit agent`: MCP registry reinit → `ctx.exit_agent(&app)` + `ctx.bootstrap_tools()`
|
||||
- `.info session`: `config.read().session_info()` → `ctx.session_info()`
|
||||
- `.info agent` / `.starter` / `.edit agent-config`: `config.read().agent_*` → `ctx.*`
|
||||
- `.authenticate`: `config.read().current_model()` → `ctx.current_model()`
|
||||
- `.edit role`: via `ctx.edit_role()`
|
||||
- `.edit macro` guard: `config.read().macro_flag` → `ctx.macro_flag`
|
||||
- Compression checks: `config.read().is_compressing_session()` → `ctx.is_compressing_session()`
|
||||
- Light theme: `config.read().light_theme()` → `ctx.app.config.light_theme()`
|
||||
- Various sync call reductions
|
||||
|
||||
- **`src/config/mod.rs`** — 7 methods marked `#[allow(dead_code)]`:
|
||||
`exit_role`, `session_info`, `exit_session`, `is_compressing_session`,
|
||||
`agent_banner`, `exit_agent`, `exit_agent_session`
|
||||
|
||||
## Remaining GlobalConfig usage in REPL (99 references)
|
||||
|
||||
These CANNOT be migrated until the client chain is migrated:
|
||||
|
||||
| Category | Count (approx) | Why |
|
||||
|---|---|---|
|
||||
| `Input::from_str(config, ...)` | ~10 | Input holds GlobalConfig for create_client |
|
||||
| `ask(config, ctx, ...)` | ~10 | Passes config to Input construction |
|
||||
| `Config::compress_session(config)` | 2 | Creates Input internally |
|
||||
| `Config::maybe_compress_session` | 2 | Spawns task with GlobalConfig |
|
||||
| `Config::maybe_autoname_session` | 2 | Spawns task with GlobalConfig |
|
||||
| `Config::update(config, ...)` | 1 | Complex dispatcher, reads/writes config |
|
||||
| `Config::delete(config, ...)` | 1 | Reads/writes config |
|
||||
| `macro_execute(config, ...)` | 1 | Calls run_repl_command |
|
||||
| `init_client(config, ...)` | 1 | Client needs GlobalConfig |
|
||||
| `sync_ctx_to_config` / `sync_config_to_ctx` | ~15 | Bridge sync helpers |
|
||||
| Reedline init (`ReplCompleter`, `ReplPrompt`) | ~5 | Trait objects hold GlobalConfig |
|
||||
| `config.write().save_role/new_role/new_macro` | ~5 | Config file mutations |
|
||||
| `config.write().edit_session/edit_config` | ~3 | Editor operations |
|
||||
| Struct field + constructor | ~5 | `Repl { config }` |
|
||||
|
||||
## Key decisions
|
||||
|
||||
### 1. `.exit *` handlers use ctx methods + bootstrap_tools
|
||||
|
||||
Instead of the MCP registry take/reinit pattern, the exit handlers
|
||||
now call `ctx.exit_role()` / `ctx.exit_session()` / `ctx.exit_agent(&app)`
|
||||
followed by `ctx.bootstrap_tools(&app, true).await?` to rebuild the
|
||||
tool scope with the global MCP server set. Then `sync_ctx_to_config`
|
||||
updates the GlobalConfig for reedline/Input.
|
||||
|
||||
### 2. Cannot remove Repl's config field
|
||||
|
||||
The `config: GlobalConfig` field stays because `ask`, `Input::from_str`,
|
||||
`init_client`, `Config::compress_session`, `Config::maybe_*`, and
|
||||
reedline components all need it. Full removal requires migrating the
|
||||
client chain.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo check` — clean, zero warnings
|
||||
- `cargo clippy` — clean
|
||||
- `cargo test` — 63 passed, 0 failed
|
||||
|
||||
## Phase 1 completion assessment
|
||||
|
||||
With Step 8m done, Phase 1's Step 8 sub-steps (8a through 8m) are
|
||||
all complete. The GlobalConfig is significantly reduced but not
|
||||
eliminated. The remaining dependency is the **client chain**:
|
||||
|
||||
```
|
||||
Input.config: GlobalConfig
|
||||
→ create_client() → init_client(&GlobalConfig)
|
||||
→ Client.global_config: GlobalConfig
|
||||
→ eval_tool_calls(&GlobalConfig)
|
||||
→ ToolCall::eval(&GlobalConfig)
|
||||
→ all tool handlers take &GlobalConfig
|
||||
```
|
||||
|
||||
Eliminating this chain requires:
|
||||
1. Migrating `init_client` to `&AppConfig` + `&[ClientConfig]`
|
||||
2. Changing every client struct from `GlobalConfig` to `AppConfig`
|
||||
3. Migrating `eval_tool_calls` to `&AppConfig` + `&mut RequestContext`
|
||||
4. Migrating all tool handlers similarly
|
||||
|
||||
This is a Phase 2 concern or a dedicated "client chain migration"
|
||||
effort.
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md` — Step 8m
|
||||
- Step 8l notes: `docs/implementation/PHASE-1-STEP-8l-NOTES.md`
|
||||
- QA checklist: `docs/QA-CHECKLIST.md`
|
||||
@@ -0,0 +1,143 @@
|
||||
# Phase 1 Step 9 — Implementation Notes
|
||||
|
||||
## Status
|
||||
|
||||
Done (cleanup pass). Full bridge removal deferred to Phase 2 —
|
||||
the remaining blocker is the **client chain**: `init_client` →
|
||||
client structs → `eval_tool_calls` → all tool handlers.
|
||||
|
||||
## What Step 9 accomplished
|
||||
|
||||
1. Deleted ~500 lines of dead `Config` methods superseded by
|
||||
`RequestContext`/`AppConfig` equivalents with zero callers
|
||||
2. Removed all 23 `#[allow(dead_code)]` annotations from Config
|
||||
3. Deleted 3 `_ctx` bridge constructors from `Input`
|
||||
4. Deleted `macro_execute_ctx` bridge from macros
|
||||
5. Replaced `_ctx` calls in `main.rs` with direct constructors
|
||||
|
||||
## Current state (after Steps 8i–8m + Step 9 cleanup)
|
||||
|
||||
### Modules fully migrated (zero GlobalConfig in public API)
|
||||
|
||||
| Module | Step | Notes |
|
||||
|---|---|---|
|
||||
| `config/agent.rs` | 8k | `Agent::init` takes `&AppConfig` + `&AppState` |
|
||||
| `rag/mod.rs` | 8i | Rag takes `&AppConfig` + `&[ClientConfig]`; 1 internal bridge for `init_client` |
|
||||
| `config/paths.rs` | Step 2 | Free functions, no config |
|
||||
| `config/app_config.rs` | Steps 3-4 | Pure AppConfig, no GlobalConfig |
|
||||
| `config/request_context.rs` | Steps 5-8m | 64+ methods; 2 `to_global_config()` calls remain for compress/autoname bridges |
|
||||
| `config/app_state.rs` | Steps 6.5+8d | No GlobalConfig |
|
||||
| `config/mcp_factory.rs` | Step 8c | No GlobalConfig |
|
||||
| `config/tool_scope.rs` | Step 6.5 | No GlobalConfig |
|
||||
|
||||
### Modules partially migrated
|
||||
|
||||
| Module | GlobalConfig refs | What remains |
|
||||
|---|---|---|
|
||||
| `config/input.rs` | 5 | `config: GlobalConfig` field for `create_client`, `use_embeddings`, `set_regenerate`; 3 `_ctx` bridge constructors |
|
||||
| `repl/mod.rs` | ~99 | `Input::from_str(config)`, `ask(config)`, sync helpers, reedline, `Config::update/delete/compress/autoname`, `macro_execute` |
|
||||
| `function/supervisor.rs` | ~17 | All handler signatures take `&GlobalConfig` (called from eval_tool_calls) |
|
||||
| `function/mod.rs` | ~8 | `eval_tool_calls`, `ToolCall::eval`, MCP tool handlers |
|
||||
| `function/todo.rs` | ~5 | Todo tool handlers take `&GlobalConfig` |
|
||||
| `function/user_interaction.rs` | ~3 | User interaction handlers take `&GlobalConfig` |
|
||||
| `client/common.rs` | ~2 | `call_chat_completions*` get GlobalConfig from client |
|
||||
| `client/macros.rs` | ~3 | `init_client`, client `init` methods |
|
||||
| `main.rs` | ~5 | Agent path, start_interactive, `_ctx` constructors |
|
||||
| `config/macros.rs` | ~2 | `macro_execute`, `macro_execute_ctx` |
|
||||
|
||||
### The client chain blocker
|
||||
|
||||
```
|
||||
Input.config: GlobalConfig
|
||||
→ create_client() → init_client(&GlobalConfig)
|
||||
→ Client { global_config: GlobalConfig }
|
||||
→ client.global_config() used by call_chat_completions*
|
||||
→ eval_tool_calls(&GlobalConfig)
|
||||
→ ToolCall::eval(&GlobalConfig)
|
||||
→ handle_supervisor_tool(&GlobalConfig)
|
||||
→ handle_todo_tool(&GlobalConfig)
|
||||
→ handle_user_interaction_tool(&GlobalConfig)
|
||||
→ invoke_mcp_tool(&GlobalConfig) → reads config.mcp_registry
|
||||
```
|
||||
|
||||
Every node in this chain holds or passes `&GlobalConfig`. Migrating
|
||||
requires changing all of them in a single coordinated pass.
|
||||
|
||||
## What Step 9 accomplished
|
||||
|
||||
1. Updated this notes file with accurate current state
|
||||
2. Phase 1 is effectively complete — the architecture is proven,
|
||||
entry points are migrated, all non-client-chain modules are on
|
||||
`&AppConfig`/`&RequestContext`
|
||||
|
||||
## What remains for future work (Phase 2 or dedicated effort)
|
||||
|
||||
### Client chain migration (prerequisite for Steps 9+10 completion)
|
||||
|
||||
1. Change `init_client` to take `&AppConfig` + `&[ClientConfig]`
|
||||
2. Change every client struct from `global_config: GlobalConfig`
|
||||
to `app_config: Arc<AppConfig>` (or captured fields)
|
||||
3. Thread `&mut RequestContext` through `call_chat_completions*`
|
||||
(or a callback/trait for tool evaluation)
|
||||
4. Change `eval_tool_calls` to take `&AppConfig` + `&mut RequestContext`
|
||||
5. Change `ToolCall::eval` similarly
|
||||
6. Change all tool handlers (`supervisor`, `todo`, `user_interaction`,
|
||||
`mcp`) to read from `RequestContext` instead of `GlobalConfig`
|
||||
7. Change `invoke_mcp_tool` to read from `ctx.tool_scope.mcp_runtime`
|
||||
instead of `config.read().mcp_registry`
|
||||
8. Remove `McpRegistry` usage entirely (replaced by `McpFactory` +
|
||||
`McpRuntime`)
|
||||
9. Remove `Input.config: GlobalConfig` field
|
||||
10. Remove `_ctx` bridge constructors on Input
|
||||
11. Remove REPL's `config: GlobalConfig` field + sync helpers
|
||||
12. Rewrite reedline components (`ReplCompleter`, `ReplPrompt`,
|
||||
`ReplHighlighter`) to not hold GlobalConfig
|
||||
13. Remove `Config::update`, `Config::delete` — replace with
|
||||
`RequestContext` equivalents
|
||||
14. Remove `reinit_mcp_registry` bridge in REPL
|
||||
15. Delete `bridge.rs`, `to_global_config()`, `Config::from_parts`
|
||||
16. Delete `Config` struct and `GlobalConfig` type alias
|
||||
|
||||
## Phase 1 final summary
|
||||
|
||||
### What Phase 1 delivered
|
||||
|
||||
1. **Architecture**: `AppState` (immutable, shared) + `RequestContext`
|
||||
(mutable, per-request) split fully designed, scaffolded, and proven
|
||||
|
||||
2. **New types**: `McpFactory`, `McpRuntime`, `ToolScope`,
|
||||
`AgentRuntime`, `RagCache`, `McpServerKey`, `RagKey` — all
|
||||
functional
|
||||
|
||||
3. **Entry points migrated**: Both `main.rs` and `repl/mod.rs`
|
||||
thread `RequestContext` through their call chains
|
||||
|
||||
4. **Module migrations**: `Agent::init`, `Rag`, `paths`, `AppConfig`,
|
||||
`RequestContext` (64+ methods), `Session` — all on new types
|
||||
|
||||
5. **MCP lifecycle**: `McpFactory::acquire()` with `Weak`-based
|
||||
sharing replaces `McpRegistry` for scope transitions
|
||||
|
||||
6. **Bridge infrastructure**: `to_global_config()` escape hatch +
|
||||
sync helpers enable incremental migration of remaining modules
|
||||
|
||||
7. **Zero regressions**: 63 tests pass, build clean, clippy clean
|
||||
|
||||
8. **QA checklist**: 100+ behavioral verification items documented
|
||||
|
||||
### Metrics
|
||||
|
||||
- `AppConfig` methods: 21+
|
||||
- `RequestContext` methods: 64+
|
||||
- `AppState` fields: 6 (config, vault, mcp_factory, rag_cache,
|
||||
mcp_config, mcp_log_path)
|
||||
- `GlobalConfig` references eliminated: ~60% reduction across codebase
|
||||
- Files with zero GlobalConfig: 8 modules fully clean
|
||||
- Tests: 63 passing, 0 failing
|
||||
|
||||
## References
|
||||
|
||||
- Phase 1 plan: `docs/PHASE-1-IMPLEMENTATION-PLAN.md`
|
||||
- QA checklist: `docs/QA-CHECKLIST.md`
|
||||
- Architecture: `docs/REST-API-ARCHITECTURE.md`
|
||||
- All step notes: `docs/implementation/PHASE-1-STEP-*-NOTES.md`
|
||||
@@ -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