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:
asepharyana
2026-07-16 07:49:43 +07:00
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