docs(specs): hapus facade prepare(), panggil modul context langsung
Facade cuma dipakai generically oleh satu caller (Action::Compact); auto-loop tetap butuh kontrol per-stage sendiri. Selaras dengan prinsip "No DI" di CLAUDE.md. dedup::collapse juga diubah mengembalikan (Vec<ChatMessage>, bool) supaya caller tahu ada perubahan tanpa perlu ChatMessage: PartialEq. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
da39033e81
commit
c4e1d7c2b2
@@ -86,7 +86,7 @@ Replace `src/app/runtime/shortsend.rs` with `src/app/runtime/context/`:
|
||||
|
||||
```
|
||||
context/
|
||||
mod.rs — prepare() facade: dedup → count → maybe-shape
|
||||
mod.rs — module registration only, no facade (see below)
|
||||
tokens.rs — unified token counting (tiktoken-rs)
|
||||
dedup.rs — cross-call tool-result deduplication
|
||||
squash.rs — per-result compression (log/json/generic), applied at
|
||||
@@ -109,9 +109,14 @@ internal `/3`, the auto-loop's `/4` (`actions/mod.rs` ~1146), and `status.rs:68`
|
||||
### `dedup.rs`
|
||||
|
||||
```rust
|
||||
pub fn collapse(messages: &[ChatMessage]) -> Vec<ChatMessage>
|
||||
pub fn collapse(messages: &[ChatMessage]) -> (Vec<ChatMessage>, bool)
|
||||
```
|
||||
|
||||
The `bool` is `true` iff at least one message was replaced with a placeholder — callers use
|
||||
it to decide whether the result is worth persisting/announcing, without needing `ChatMessage`
|
||||
to implement `PartialEq` (it doesn't today, and adding it purely to diff whole message lists
|
||||
would be needless surface area for what `collapse` already knows precisely mid-walk).
|
||||
|
||||
Flow: walk messages, pair each `Role::Tool` message to its originating `ToolCall` via
|
||||
`tool_call_id`. Key = `(function.name, sha256(canonical_json(function.arguments)))` (`sha2`
|
||||
is already a dependency). Track the last index seen per key. For any earlier occurrence of a
|
||||
@@ -187,36 +192,31 @@ blocks in `Action::Compact`, `spawn_turn`, and `view/status.rs` (×2).
|
||||
|
||||
### `mod.rs`
|
||||
|
||||
```rust
|
||||
pub fn prepare(
|
||||
messages: &[ChatMessage],
|
||||
max_wire_tokens: usize,
|
||||
force: bool,
|
||||
client: Option<&LlmClient>,
|
||||
) -> Vec<ChatMessage>
|
||||
```
|
||||
|
||||
Orchestrates: `dedup::collapse` → per-message `squash::apply` is already baked into messages
|
||||
by the time they arrive here (applied earlier, at construction) → `tokens::count_message_tokens`
|
||||
sum → `shaping::should_shape` → conditionally `shaping::shape`. This is the single entry point
|
||||
both `run_agent_turn`'s auto-loop and `Action::Compact` call.
|
||||
No facade function — just `pub mod dedup; pub mod shaping; pub mod squash; pub mod tokens;
|
||||
pub mod window;`. `dedup`, `shaping`, and `tokens` are called directly from each call site
|
||||
(the auto-loop and `Action::Compact`), matching CLAUDE.md's "No DI — modules call ...
|
||||
directly" convention rather than introducing an orchestration layer that only one of the two
|
||||
callers would use generically (the auto-loop already needs per-stage control today — it
|
||||
inspects `should_shape` itself to decide whether to emit `TurnEvent::Compacted` — and would
|
||||
have to unpack a facade's result anyway).
|
||||
|
||||
## Data flow (per turn)
|
||||
|
||||
1. Tool executes → raw `output: String`.
|
||||
2. `squash::apply(tool_name, &output)` — compress if over the size floor.
|
||||
2. `squash::apply(tool_name, &output)` — compress if over the size floor (`read` exempted).
|
||||
3. Wrapped into `ChatMessage::tool_result(...)`, archived, pushed to `msgs`.
|
||||
4. Once per loop iteration: `context::prepare(&msgs, tc.context_window, force=false,
|
||||
Some(&tc.client))` — dedup pass always runs; shaping runs only if `should_shape` trips.
|
||||
5. Result pushed as `TurnEvent::Compacted` if shaping actually changed anything (same as
|
||||
today), consumed on the main thread to update `SessionRuntime.messages`.
|
||||
4. Once per loop iteration: `dedup::collapse(&msgs)` (always) → sum
|
||||
`tokens::count_message_tokens` over the result → `shaping::should_shape` → conditionally
|
||||
`shaping::shape_messages`.
|
||||
5. Result pushed as `TurnEvent::Compacted` if dedup changed anything or shaping triggered,
|
||||
consumed on the main thread to update `SessionRuntime.messages`.
|
||||
|
||||
## Fixing the manual/auto asymmetry
|
||||
|
||||
`Action::Compact` (`actions/mod.rs:547`) currently runs synchronously inside `apply_action`
|
||||
and can't block on an LLM call. Fix: make it spawn a background `std::thread::spawn` — the
|
||||
same pattern `spawn_turn` already uses (`actions/mod.rs:694`) — that calls
|
||||
`context::prepare(&rt.messages, max_wire_tokens, force=true, Some(&client))` and reports back
|
||||
same pattern `spawn_turn` already uses (`actions/mod.rs:694`) — that runs `dedup::collapse`
|
||||
then unconditionally `shaping::shape_messages(.., force=true, Some(&client))` and reports back
|
||||
via `TurnEvent::Compacted`, identical to the automatic path. The toast sequence becomes
|
||||
"Compacting…" immediately (optimistic, non-blocking) then "History compacted" when the
|
||||
`TurnEvent` arrives. This gives manual `/compact` real LLM summarization instead of always
|
||||
|
||||
Reference in New Issue
Block a user