Eight closed tasks for epochs and compaction, then ten for the scheduler, ToolSummary, and tiers. Given tests first. Gate 1456 → 1507 → 1572. Co-Authored-By: Grok 4.6 <noreply@x.ai>
214 lines
9.2 KiB
Markdown
214 lines
9.2 KiB
Markdown
# M5d-1 task 06: the summary call, and `session-compact-failed`
|
|
|
|
**Branch:** `m5d1`. Check it: `git branch --show-current` prints `m5d1` and `git status --short`
|
|
is empty; otherwise stop. Never switch branch.
|
|
**Commit subject:** `loopd: compact summary on the background slot`
|
|
**Log row:** the Task cell is `M5d1/06-loopd-compact-summary`.
|
|
|
|
## Goal
|
|
|
|
Compaction asks for a summary of the current epoch on the **background** slot, with **no tools**,
|
|
thinking on (spec 3.4). The last user message is `compact.md` (task 01's `compact_prompt`),
|
|
trimmed at the end. The completion is tag-neutralised and cut to `summary_chars`.
|
|
|
|
If that call is context-full (`exceed_context_size_error`), use task 05's `fallback_summary`.
|
|
If `compact.md` cannot be read, or the call fails for any other reason, return
|
|
`CompactError::Failed` whose `Display` ends with the runbook pointer
|
|
`see docs/runbook.md#session-compact-failed`. There is no fallback for a missing prompt.
|
|
|
|
This task does not write the next epoch. Task 07 does.
|
|
|
|
## Every place that changes
|
|
|
|
| Place | What happens there |
|
|
|---|---|
|
|
| `crates/loopd/src/compact.rs` | `CompactError`; `summarise` (edits E1, E2) |
|
|
| `docs/runbook.md` | append the given `session-compact-failed` entry |
|
|
| `crates/loopd/tests/compact_summary.rs` | new, given |
|
|
| everything else | **no change**: not `turn.rs`, not `channel.rs`, not `session.rs` |
|
|
|
|
## Files
|
|
|
|
- Copy, from `docs/plans/M5d1/files/` (the same path under it): the new test
|
|
`crates/loopd/tests/compact_summary.rs`.
|
|
- Append, from `docs/plans/M5d1/files/runbook-session-compact-failed.md`, to `docs/runbook.md`.
|
|
- Modify: `crates/loopd/src/compact.rs`, `docs/runbook.md`, `docs/implementer-log.md`.
|
|
- Do not touch: any other file. `config/compact.md` is already in the tree from task 05.
|
|
|
|
## Facts you need
|
|
|
|
- **The request.** `ChatRequest { slot: cfg.slots.background, messages, tools: Vec::new(),
|
|
thinking: true }`. Empty `tools` is omitted from the JSON (`skip_serializing_if` on the body).
|
|
The given test asserts `body.get("tools").is_none()` and `id_slot == 1` (the test config's
|
|
background slot).
|
|
- **The messages.** `messages_with_carry(session.baseline(), session.records(), session.carry())`,
|
|
then one `ChatMessage::User` whose content is `compact.md` after `trim_end`. Epoch 0 has no
|
|
carry; later epochs keep the carried summary in the prefix so the model summarises what the
|
|
session actually sends.
|
|
- **Read the prompt** with `std::fs::read_to_string(&cfg.compact_prompt())`. An I/O error is
|
|
`Failed { detail: format!("{}: {e}", path.display()) }` so the path (and `compact.md`) appears
|
|
in the message.
|
|
- **The call.** `client.chat_with_retry(&request, &mut |_| {})`. A 400 that is not context-full
|
|
is not retried (`is_retryable` is already false for it).
|
|
- **Context-full.** `crate::turn::is_context_full(&e)` then `fallback_summary(session.records(),
|
|
cfg.compact.summary_chars)`. Do not fall back for a missing file or any other `InferError`.
|
|
- **The text.** `completion.content.unwrap_or_default()`, then `bxcore::sanitize::neutralise`,
|
|
then `cap_chars(..., cfg.compact.summary_chars)`. Neutralise first so a tag is not split by
|
|
the cap.
|
|
- **`Display`.** `"{detail}; see docs/runbook.md#session-compact-failed"`. The given tests box
|
|
the error as `dyn Error` and check `to_string()`.
|
|
- **`compact.rs` stays ≤ 500 lines.** After this task it is about 120.
|
|
- The items the edits use. This table is complete.
|
|
|
|
| Item | What it does |
|
|
|---|---|
|
|
| `Config::compact_prompt` | task 01, `compact.md` beside `system.md` |
|
|
| `messages_with_carry` | task 03 |
|
|
| `Client::chat_with_retry` | same as a live turn |
|
|
| `turn::is_context_full` | the one 400 `exceed_context_size_error` |
|
|
| `bxcore::sanitize::neutralise` | tags to a visible look-alike |
|
|
| `fallback_summary`, `cap_chars` | task 05 |
|
|
| `ChatRequest.tools: Vec::new()` | omits `tools` on the wire |
|
|
|
|
## The edits, exactly
|
|
|
|
Nothing else in `compact.rs` changes. Each quoted "before" block occurs exactly once after
|
|
task 05. Keep the fields in the order shown.
|
|
|
|
### `crates/loopd/src/compact.rs`
|
|
|
|
**E1.** The module doc and the `use` line. The lines
|
|
|
|
```rust
|
|
//! Compaction: when a session starts a new epoch, the summary call, and writing the next files
|
|
//! (M5d spec, section 3). Later tasks fill the call and the write.
|
|
|
|
use bxcore::{LogRecord, Usage};
|
|
```
|
|
|
|
become
|
|
|
|
```rust
|
|
//! Compaction: when a session starts a new epoch, the summary call, and writing the next files
|
|
//! (M5d spec, section 3). Task 07 writes the next files; task 08 calls this after a turn.
|
|
|
|
use bxcore::sanitize::neutralise;
|
|
use bxcore::{LogRecord, Usage};
|
|
|
|
use crate::baseline::messages_with_carry;
|
|
use crate::config::Config;
|
|
use crate::llama::{ChatMessage, ChatRequest, Client};
|
|
use crate::session::Session;
|
|
use crate::turn::is_context_full;
|
|
|
|
/// Why a summary call or a next-epoch write failed. Every variant's `Display` ends with the
|
|
/// runbook pointer `session-compact-failed`.
|
|
#[derive(Debug)]
|
|
pub enum CompactError {
|
|
Failed { detail: String },
|
|
}
|
|
|
|
impl std::fmt::Display for CompactError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
CompactError::Failed { detail } => write!(
|
|
f,
|
|
"{detail}; see docs/runbook.md#session-compact-failed"
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for CompactError {}
|
|
```
|
|
|
|
**E2.** After `cap_chars` (the function that ends `s.chars().take(n).collect()` / `}`), add
|
|
`summarise`. Copy this function whole. There is no `todo!()`.
|
|
|
|
```rust
|
|
/// One completion on the background slot: no tools, thinking on, last user `compact.md`.
|
|
/// Context-full uses `fallback_summary`. A missing prompt, or any other infer error, is
|
|
/// `CompactError::Failed` with the runbook pointer. No fallback for a missing prompt.
|
|
pub fn summarise(cfg: &Config, client: &Client, session: &Session) -> Result<String, CompactError> {
|
|
let path = cfg.compact_prompt();
|
|
let prompt = std::fs::read_to_string(&path).map_err(|e| CompactError::Failed {
|
|
detail: format!("{}: {e}", path.display()),
|
|
})?;
|
|
let mut messages = messages_with_carry(session.baseline(), session.records(), session.carry());
|
|
messages.push(ChatMessage::User {
|
|
content: prompt.trim_end().to_string(),
|
|
});
|
|
let request = ChatRequest {
|
|
slot: cfg.slots.background,
|
|
messages,
|
|
tools: Vec::new(),
|
|
thinking: true,
|
|
};
|
|
match client.chat_with_retry(&request, &mut |_| {}) {
|
|
Ok(completion) => {
|
|
let text = completion.content.unwrap_or_default();
|
|
let text = neutralise(&text);
|
|
Ok(cap_chars(text.as_ref(), cfg.compact.summary_chars))
|
|
}
|
|
Err(e) if is_context_full(&e) => {
|
|
Ok(fallback_summary(session.records(), cfg.compact.summary_chars))
|
|
}
|
|
Err(e) => Err(CompactError::Failed {
|
|
detail: e.to_string(),
|
|
}),
|
|
}
|
|
}
|
|
```
|
|
|
|
## Steps
|
|
|
|
- [ ] **1. Copy the given test.**
|
|
|
|
```sh
|
|
cp docs/plans/M5d1/files/crates/loopd/tests/compact_summary.rs crates/loopd/tests/
|
|
```
|
|
|
|
- [ ] **2. See it fail.** `cargo build --tests --keep-going -p loopd`, timeout 600000 ms: test
|
|
target `compact_summary` does not compile (`E0432` unresolved import `loopd::compact::CompactError`
|
|
and/or `E0599` no function `summarise`). Every other target compiles. `compact_due` still
|
|
compiles.
|
|
- [ ] **3. The edits.** E1, E2. `cargo check -p loopd` prints no error and no warning.
|
|
`wc -l crates/loopd/src/compact.rs` prints a number ≤ 500.
|
|
- [ ] **4. The tests.** `cargo test -p loopd --test compact_summary --test compact_due`: every
|
|
test passes, 6 in `compact_summary` and 6 in `compact_due`.
|
|
- [ ] **5. The runbook.** From the repository root:
|
|
|
|
```sh
|
|
{ echo; cat docs/plans/M5d1/files/runbook-session-compact-failed.md; } >> docs/runbook.md
|
|
```
|
|
|
|
Then `grep -c '^## session-compact-failed$' docs/runbook.md` prints `1`.
|
|
- [ ] **6. Check.** `git diff --stat` lists `crates/loopd/src/compact.rs` and `docs/runbook.md`.
|
|
`git status --short` shows `compact_summary.rs` as new.
|
|
- [ ] **7. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line
|
|
`gate: ok`. Test count **1495** (1489 before, plus 6).
|
|
- [ ] **8. Log and commit.** Task cell `M5d1/06-loopd-compact-summary`. Stage with
|
|
`git add crates/loopd docs/runbook.md docs/implementer-log.md Cargo.lock`, then commit as
|
|
`AGENTS.md` says.
|
|
|
|
## Done when
|
|
|
|
- `make gate` prints `gate: ok` with 1495 tests.
|
|
- `git diff HEAD~1 --stat` lists only `crates/loopd/src/compact.rs`,
|
|
`crates/loopd/tests/compact_summary.rs`, `docs/runbook.md` and the log.
|
|
|
|
## Stop and report if
|
|
|
|
- The branch check fails; a "before" block is not found exactly once; step 2 differs; `cargo
|
|
check` warns; a test fails; the gate count is not 1495; a comment does not fit its code;
|
|
`compact.rs` exceeds 500 lines; you add a fallback for a missing `compact.md`; you send tools
|
|
on the summary request; you use `slots.main` for the summary.
|
|
|
|
## What the given tests pin
|
|
|
|
The summary request is `id_slot` of the background slot, omits `tools`, has thinking on, and its
|
|
last user is `compact.md` after `trim_end`; the completion is capped to `summary_chars`; a
|
|
context-full summary is the epoch's `User` texts; tags in the completion are neutralised; a
|
|
missing `compact.md` names the file and the runbook; a non-context-full infer error is
|
|
`CompactError::Failed` with the same pointer.
|