rustvani 0.3.0

Voice AI framework for Rust — real-time speech pipelines with STT, LLM, TTS, and Dhara conversation flows
# Changelog

## 0.3.0 — 2026-06-11

Rust-native rewrite of the agents module internals plus Pipecat-aligned
worker semantics (job routing, cancellation cleanup, ready-gating, frame
bridging via pipeline edges). **Breaking release** — all changes below are
in `rustvani::agents` unless noted.

### Bus (`agents::bus`)

- **Breaking:** `BusSubscriber::on_bus_message` now takes `Arc<BusMessage>`
  instead of `BusMessage`. Messages fan out as a single `Arc` — no deep
  clones per subscriber.
- **Breaking:** `BusMessage` gained a `seq: u64` field, stamped by the bus
  in `send()` before fan-out (total-order debugging across agents).
  Construct messages with the new `BusMessage::new(source, target, payload)`
  instead of a struct literal.
- `LocalAgentBus` internals rewritten: per-subscriber dispatch tasks built
  on an unbounded system channel + bounded data channel (default capacity
  256, configurable via `LocalAgentBus::with_capacity`), drained with a
  `biased tokio::select!`. System messages (End/Cancel/Activate/urgent task
  replies/registry) are always delivered before queued data messages and
  are never dropped; data messages are dropped (counted + rate-limited
  warn) when a subscriber's channel is full — control never drops, data
  never blocks.
- `subscribe()` now rejects duplicate names with an error (they previously
  coexisted silently).
- `unsubscribe()`/`stop()` let in-flight handlers finish (2 s grace, then
  abort) and deliver pending system messages before exiting.
- `send()` after `stop()` is a no-op. `send()` itself is lock-free
  (`arc-swap` subscriber snapshot).
- New: `LocalAgentBus::dropped_count(name)`, `DEFAULT_DATA_CAPACITY`.

### BaseAgent (`agents::base`)

- **Breaking:** `BaseAgent::new`'s `bridged` parameter changed from `bool`
  to `Option<Vec<String>>`: `None` = not bridged, `Some(vec![])` = accept
  bridged frames from any source, `Some(names)` = accept only from the
  listed peers. The `Agent::bridged() -> bool` accessor is unchanged
  (`true` when `Some`).
- Task routing implemented (Pipecat `BaseWorker` parity): register job
  handlers with `BaseAgent::on_task(name, handler)` /
  `on_task_default(handler)`. Handlers receive a `TaskRequestCtx` (new
  type) with `complete` / `stream_start` / `stream_data` / `stream_end`
  helpers; each handler runs in its own tokio task. Requests with no
  matching handler fail fast with a terminal `Failed` response.
- `TaskCancel` aborts the running handler and always sends the requester a
  terminal `Cancelled` response.
- Lifecycle cleanup: on `end()`/`cancel()`/`run()` return, all in-flight
  jobs are cancelled with terminal responses and all pending dispatched
  handles are failed (`TaskContext::fail_all_pending`) — a requester is
  never left hanging on a dead agent.
- `end()`/`cancel()` are now single-shot (repeat calls are no-ops) and
  cascade to child agents (see Runner below).
- New: `BaseAgent::bridged_pipeline(...)` convenience constructor,
  `with_output_edge`, `task_ctx()`, `pipeline()`, `active_flag()` accessors.
  New exported types: `TaskHandler`, `TaskRequestCtx`.
- Internals: bus/registry now `OnceCell` (set once in `setup()`, which now
  errors if called twice).

### TaskContext (`agents::task`)

- **Breaking:** `TaskContext::new(bus)` is now
  `TaskContext::new(bus, registry)`.
- `dispatch()` now ready-gates: if the target is not in the registry it
  waits (watch-based, no polling) up to 10 s
  (`DEFAULT_READY_TIMEOUT`) and errors on timeout instead of silently
  dropping the request. `dispatch_with(...)` exposes the timeout
  (`None` = send immediately without gating).
- New: `fail_all_pending(reason)`, `stream_start`, `stream_end`.
  `UpdateHandler` is now exported.
- `TaskHandle::await_completion` now skips non-terminal updates (stream
  chunks/progress) instead of erroring on them.

### Bus edges (`agents::edges` — new module)

- `BusOutputEdge`: a tail-of-pipeline `FrameProcessor` that forwards every
  frame through unchanged and publishes non-excluded frames to configured
  peer agents (empty = broadcast) as `BusPayload::Frame`. Lifecycle,
  task-control, processor-control, error, and heartbeat frames are never
  published. Publishing is gated by the owning agent's activation flag
  (inactive agents also skip injecting bridged input) — Pipecat-style
  handoff between several brains and one transport pipeline.

### Registry (`agents::registry`)

- New: `children_of(name)`, `mark_finished(name)`, `wait_finished(name)`
  (per-agent finished signal used by the End cascade).

### Runner (`agents::runner`)

- Shutdown now cascades through the agent tree: `End` goes to root agents
  only; an agent receiving `End` first forwards it to its children and
  waits (5 s per child, runner's 10 s join timeout as backstop) for them to
  finish before pushing its own `EndFrame`. `Cancel` propagates without
  waiting. The runner validates parent references at `run()` time.

### Frames (`crate::frames`)

- **Breaking (type change):** `AudioRawData.audio` is now `bytes::Bytes`
  instead of `Vec<u8>` — cloning an audio frame no longer copies the
  payload. `AudioRawData::new`, `Frame::input_audio`, `Frame::output_audio`
  accept `impl Into<Bytes>`, so existing `Vec<u8>` callers compile
  unchanged. Code that needs `Vec<u8>` back uses `.to_vec()` (explicit
  copy).

### Dependencies

- Added `arc-swap` (lock-free subscriber snapshot) and `bytes`
  (cheap-clone audio payloads). `tokio-util` (already a dependency) now
  also provides `CancellationToken`.

### Misc

- New example: `examples/bridged_agents.rs` — two bridged agents
  demonstrating dispatch, streaming reply, and cancel.
- Fixed stale assertions in the sixtydb STT config test and a
  non-exhaustive match in `examples/channel_pipeline.rs`.