Add M2b plan: ten tasks, tests, recordings and the first system prompt
The tasks build the agent loop on M2a's client: channel messages and the usage record in proto, four config tables, the tool port and registry with find_tool and call_tool, the baseline and replay, the session store, the turn loop with its limits and the append-only property test, the channel server, loopd serve, bxctl chat, and the device checks including a four-turn conversation with a restart. Checked against a private reference implementation: the gate passes after every task in order, the new suites pass under CPU load, and the reference passes make verify-device on straylight with no cache loss. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# M2b task 09: `bxctl chat`
|
||||
|
||||
**Branch:** `m2b` (run `git switch m2b`; `git status --short` must be empty, otherwise stop)
|
||||
**Commit subject:** `Add bxctl chat`
|
||||
|
||||
## Goal
|
||||
|
||||
The owner's chat client: a line in, a turn over `loop.sock`, the stream shown as it arrives. It
|
||||
is also the scripting tool the device checks use (`--say`, `--json`).
|
||||
|
||||
## Files
|
||||
|
||||
- Copy: `crates/bxctl/tests/chat.rs`
|
||||
- Create: `crates/bxctl/src/chat.rs`
|
||||
- Modify: `crates/bxctl/Cargo.toml` (add `serde_json.workspace = true`), `crates/bxctl/src/lib.rs`
|
||||
(add `pub mod chat;`), `crates/bxctl/src/main.rs`, `docs/implementer-log.md`
|
||||
|
||||
## Interfaces
|
||||
|
||||
```rust
|
||||
// crates/bxctl/src/chat.rs
|
||||
pub enum ChatError { Connect(std::io::Error), Frame(FrameError), Refused(WireError), Protocol(String) } // Debug; Display; Error
|
||||
// Refused displays as "<code name>: <detail>", where the code name is lowercase words: "session full", "turn limit", "no such session", …
|
||||
|
||||
pub fn new_session_id() -> SessionId; // "chat-<unix seconds>-<nanos>"
|
||||
|
||||
/// Sends one turn and reads the reply. `on_event` sees every event frame as it arrives.
|
||||
pub fn run_turn(socket: &Path, session: &SessionId, content: &str, resume: bool, on_event: &mut dyn FnMut(&TurnEvent)) -> Result<TurnDone, ChatError>;
|
||||
|
||||
pub struct Printer { pub show_thinking: bool, pub json: bool, pub stream_content: bool, /* private: whether a dimmed block is open */ }
|
||||
impl Printer {
|
||||
pub fn new(show_thinking: bool, json: bool) -> Printer; // stream_content: true
|
||||
pub fn event(&mut self, out: &mut impl Write, event: &TurnEvent) -> std::io::Result<()>;
|
||||
pub fn end_reasoning(&mut self, out: &mut impl Write) -> std::io::Result<()>; // closes an open dimmed block
|
||||
}
|
||||
```
|
||||
|
||||
**`run_turn`:** connect; send `Turn { session, content, resume }` as an envelope with `id: 1`,
|
||||
`final: true`; then read frames. A frame whose `id` is not 1 is `Protocol`. `TurnEvent` with
|
||||
`final: false` goes to `on_event`; `TurnDone` with `final: true` is the result; `Error` with
|
||||
`final: true` is `Refused`; anything else is `Protocol`.
|
||||
|
||||
**`Printer::event`**, per event, when `json` is false:
|
||||
|
||||
| Event | Output |
|
||||
|---|---|
|
||||
| `Reasoning { text }` | if `show_thinking`: open a dimmed block with `\x1b[2m` on the first, then the text as it is |
|
||||
| `Content { text }` | close the dimmed block if open (`\x1b[0m` and a newline); then the text as it is, only if `stream_content` |
|
||||
| `ToolCallStarted { name }` | close the block if open; `[tool <name>]` and a newline |
|
||||
| `ToolResult { name, class, truncated }` | `[<name>: <class as Debug>]` or `[<name>: <class>, truncated]`, newline |
|
||||
| `Waiting { slot_busy }` | `[waiting: slot busy]` or `[waiting: slot idle]` |
|
||||
| `Retrying { attempt, after_ms, error }` | `[retrying: attempt <n> in <ms> ms: <error>]` |
|
||||
| `ThinkingCapped { tokens }` | `[thinking capped at <n> tokens]` |
|
||||
| `CacheLoss { expected, got }` | `[cache loss: <got> of <expected>]` |
|
||||
| `Queued`, `Progress` | nothing |
|
||||
|
||||
Flush after every event. When `json` is true: every event is one line of JSON (`serde_json`
|
||||
of the `TurnEvent`), nothing is skipped, no escape codes.
|
||||
|
||||
**`main`:** `bxctl chat [--socket <path>] [--session <id>] [--no-thinking] [--say <text>] [--json]`.
|
||||
The socket defaults to `$BOXMAKER_HOME/run/loop/loop.sock`, or `/var/lib/boxmaker/…` when the
|
||||
variable is unset. An invalid `--session`, an unknown option, or a first argument other than
|
||||
`chat` prints a usage line and exits 2.
|
||||
|
||||
- With `--session`, the first turn is sent with `resume: true`; if `loopd` answers
|
||||
`no_such_session`, it is sent again with `resume: false`. Without `--session`, a new id is made
|
||||
and the first turn creates it. Every later turn resumes.
|
||||
- `--say <text>`: one turn. Events go to **stderr**, with `stream_content` off; the answer goes to
|
||||
stdout with a newline; exit 0. On error: `bxctl: <error>` on stderr, nothing on stdout, exit 1.
|
||||
- Otherwise: print `session <id>`, then repeat: print `> `, read a line; EOF or `/quit` ends with
|
||||
exit 0; a blank line is skipped; anything else is a turn, with events and content on stdout, and
|
||||
a newline after the answer if it did not end with one. A failed turn prints `bxctl: <error>` and
|
||||
the loop goes on.
|
||||
- `--json` with `--say` also prints the `TurnDone` as one JSON line after the events.
|
||||
|
||||
## Steps
|
||||
|
||||
- [ ] **1. Copy.** `git switch m2b`, then
|
||||
`mkdir -p crates/bxctl/tests && cp docs/plans/M2b/files/crates/bxctl/tests/chat.rs crates/bxctl/tests/`
|
||||
- [ ] **2. See the test fail.** `cargo test -p bxctl --test chat`. Expected: it does not compile.
|
||||
- [ ] **3. Write `chat.rs`, `main.rs`, and the two one-line changes.** Run `cargo fmt --all`.
|
||||
- [ ] **4. See the tests pass.** `cargo test -p bxctl`. Expected: `11 passed`.
|
||||
- [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`.
|
||||
- [ ] **6. Log and commit.** `git add crates/bxctl docs/implementer-log.md && git commit`
|
||||
|
||||
## Done when
|
||||
|
||||
- `cargo test -p bxctl` reports 11 passed; `make gate` prints `gate: ok`.
|
||||
- `bxctl` depends on `proto` and `serde_json` only.
|
||||
|
||||
## Stop and report if
|
||||
|
||||
- A test in `chat.rs` needs the exact wording of an output line that this task does not give.
|
||||
Reference in New Issue
Block a user