mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-17 04:37:03 +00:00
During a long LLM turn the durable event stream was silent: between `agent.tool.completed` and the next `agent.message` nothing was emitted, so "the model is generating" and "the worker is wedged" were indistinguishable from the run store, SSE, or the UI. The signal already existed. `AssistantTextStart` fired at exactly the right point — after `build_request()`, after compaction, immediately before the stream opens — then was classified as streaming noise and thrown away. This promotes it rather than inventing a new one. Two events, each asserting only what is provable when it is emitted: - `agent.llm.started` carries the *requested* provider/model. No usage, no cost, no context window: none of it exists yet, and failover can re-target, so `agent.message` stays authoritative for what answered. - `agent.llm.first_output` is edge-triggered on the first output of an attempt and names what arrived. `ToolCall` is required, not optional: a turn that opens with a tool call produces no text or reasoning delta, so a latch keyed on those two would stay silent for exactly the tool-heavy rounds where liveness matters most. `agent.llm.retry` now also fires on the one previously invisible mid-turn path — a stream that ends without a finish event, which replays the turn and discards its output with nothing to show for it. Its `attempt` field was already fed by two independent counters, so an optional `phase` (open | consume) names which loop it counts. `StageProjection.inference` projects the open bracket. `Some` means "the event log contains an unclosed inference bracket", not "the model is computing now" — a SIGKILLed worker leaves it open, which is the truthful statement of what we know, and `watchdog.timeout` remains the authority on actually-stuck. The close is the subtle part. Terminal cancel and wall-clock timeout tear the session down through `discard_session` without emitting a message, error, or interrupt, so a session-lifecycle backstop is required. It has to be `agent.session.ended`, not `agent.session.deactivated`: deactivation is emitted by `lease.release()` *before* the forwarder drains queued agent events, so a queued `agent.llm.started` can arrive after it and re-open the bracket. But `agent.session.ended` carries no stage identity, so the close takes ordering from the event and identity from the projection, scanning for brackets the ending session opened. A normal stage lookup there finds no target and silently no-ops. Presentation states what the log proves and nothing more: no progress bar or ETA (no completion estimate exists), "reasoning" only when the provider sent reasoning output, elapsed counted since the request opened, and no live animation once the run is terminal. Scope is session-backed agent stages. One-shot completions call `client.complete` directly and never build a session; covering them means moving the emit point into `fabro-llm`, filed as a follow-up. `agent.output.start` was never persisted — it existed in a name map, an `unreachable!` arm, and docs — so the rename carries no migration risk. Corrects `events.md`, which documented it as a real emitted event, and the v2 proposal, which mapped it to `message.part.started` despite it firing before the request opens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| src | ||
| tests | ||
| build.rs | ||
| Cargo.toml | ||