First Version

This commit is contained in:
2026-07-10 15:02:09 +01:00
commit 38494a85a9
562 changed files with 196313 additions and 0 deletions
+215
View File
@@ -0,0 +1,215 @@
# Event Buses
Two independent in-process buses:
| Bus | Type | Purpose |
| --- | ---- | ------- |
| **Chat** | `ChatEventBus` | Chat-turn events (user messages, assistant responses, compaction) |
| **System** | `SystemEventBus` | Infrastructure lifecycle events (provider registration, etc.) |
See below for details on each.
---
## Chat Event Bus
`src/core/chat_event_bus.rs` — thin re-export of `crates/core-api/src/bus.rs`.
All types (`ChatEventBus`, `BusEvent`, `ChatEvent`, `CompactionEvent`, etc.) are defined in `core-api` and re-exported here for backward compatibility.
## Purpose
Decouples producers (session handlers, compactor) from consumers (Honcho
memory, future analytics, etc.). Events are published **only after an operation
completes successfully** — messages are already persisted in the DB when the
event fires.
---
## Top-level Type: `BusEvent`
```rust
pub enum BusEvent {
UserMessage(ChatEvent),
AssistantResponse(ChatEvent),
CompactionDone(CompactionEvent),
}
```
Consumers match on the variant to decide what to handle:
```rust
match event {
BusEvent::UserMessage(e) | BusEvent::AssistantResponse(e) => { /* ... */ }
BusEvent::CompactionDone(e) => { /* optional */ }
}
```
---
## Sub-types
### `ChatEvent`
Published once per completed turn — one `UserMessage` and one
`AssistantResponse` in order.
| Field | Type | Description |
| --- | --- | --- |
| `session_id` | `i64` | `chat_sessions.id` |
| `stack_id` | `i64` | `chat_sessions_stack.id` |
| `message_id` | `i64` | `chat_history.id` for this message |
| `role` | `ChatEventRole` | `User`, `Assistant`, or `Agent` |
| `content` | `String` | Message text |
| `is_synthetic` | `bool` | `true` when the *message* is system-generated (TicManager ticks, ChatHub briefings) |
| `is_interactive` | `bool` | `true` when a real user participates (web, Telegram) |
| `is_ephemeral` | `bool` | `true` for short-lived automated sessions (cron, tic) |
| `tool_calls` | `Vec<ToolCallEvent>` | Non-empty only for `Assistant` events |
| `created_at` | `DateTime<Utc>` | Timestamp of publication |
### `ToolCallEvent`
| Field | Type | Description |
| --- | --- | --- |
| `name` | `String` | Tool name |
| `arguments` | `Option<String>` | JSON-serialized arguments |
| `result` | `Option<String>` | Tool output or error message |
| `status` | `String` | `"done"` or `"failed"` |
### `CompactionEvent`
Published by `ContextCompactor` after a summary is persisted to `chat_summaries`.
| Field | Type | Description |
| --- | --- | --- |
| `session_id` | `i64` | `chat_sessions.id` |
| `stack_id` | `i64` | `chat_sessions_stack.id` |
| `summary_id` | `i64` | `chat_summaries.id` of the new row |
| `covers_up_to_message_id` | `i64` | Boundary: messages with `id > this` are loaded raw from now on |
| `triggered_by_tokens` | `u32` | Input token count that triggered this compaction |
---
## ChatEventBus
```rust
// Instantiated once in main.rs, stored in `Skald` and ChatSessionManager.
let bus = Arc::new(ChatEventBus::new()); // default capacity: 256
// Publish (called internally):
bus.user_message(event); // from ChatSessionHandler
bus.assistant_response(event); // from ChatSessionHandler
bus.compaction_done(event); // from ContextCompactor
// Subscribe (call once at startup, loop in tokio::spawn):
let mut rx = bus.subscribe();
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(BusEvent::UserMessage(e)) => { /* process */ }
Ok(BusEvent::AssistantResponse(e)) => { /* process */ }
Ok(BusEvent::CompactionDone(e)) => { /* optional */ }
Err(RecvError::Lagged(n)) => warn!("lagged by {n} events"),
Err(RecvError::Closed) => break,
}
}
});
```
---
## Publication Rules
### Chat events
| Source | `is_synthetic` | Published? |
| --- | --- | --- |
| User via WebSocket / Telegram | `false` | ✅ on `TurnOutcome::Final` |
| Cron job | `false` | ✅ on `TurnOutcome::Final` |
| TicManager tick | `true` | ✅ on `TurnOutcome::Final` |
| ChatHub notification briefing | `true` (injected via `resume_turn`) | ❌ never (uses `resume_turn`, not `handle_message`) |
| `TurnOutcome::Cancelled` | — | ❌ never |
| `TurnOutcome::Exhausted` | — | ❌ never |
| Sub-agent turns (`dispatch_sub_agent`) | — | ❌ never (only root turns publish) |
Per each successful turn, **two events** are published in order:
1. `BusEvent::UserMessage` — content of the user message
2. `BusEvent::AssistantResponse` — content of the assistant response + tool calls
### Compaction events
`BusEvent::CompactionDone` is published by `ContextCompactor::try_compact()`
whenever a new summary is successfully persisted. It fires **before** the LLM
call that processes the user's message (Opzione C trigger). See
[compaction.md](compaction.md) for the full flow.
---
## Adding a Consumer
1. Call `skald.event_bus.subscribe()` in `main.rs` after `Skald::new()` completes.
2. Spawn a background task with a receive loop.
3. Match on `BusEvent` variants — ignore variants you don't care about.
4. On `RecvError::Lagged`, log and continue — do not panic.
5. Keep the consumer fast; if it does I/O (HTTP calls), buffer or batch internally.
---
## Channel Capacity
Default: **256 events** (`DEFAULT_CAPACITY` in `chat_event_bus.rs`). If a
consumer falls behind by more than 256 events it receives `Lagged` errors and
misses intermediate events. Tune via `ChatEventBus::with_capacity(n)`.
---
## When to Update This File
- New variants added to `BusEvent`
- New fields added to `ChatEvent`, `ToolCallEvent`, or `CompactionEvent`
- Publication rules change (new sources, new conditions)
- A new consumer is wired up in `main.rs`
---
## System Event Bus
`crates/core-api/src/system_bus.rs` — infrastructure lifecycle events, separate from chat-turn events.
### `SystemEvent`
```rust
pub enum SystemEvent {
ApiProviderRegistered { type_id: String },
ApiProviderUnregistered { type_id: String },
}
```
### Wiring
- **Created** early in `main.rs` (no dependencies), stored in `skald.system_bus` and `PluginContext::system_bus`.
- **Producers**: `ProviderRegistry::register_plugin()` / `unregister_plugin()` emit automatically — plugins do not need to touch the bus directly.
- **Consumers**: `TtsManager` and `TranscribeManager` subscribe at construction time and call `reload()` on `ApiProviderRegistered` / `ApiProviderUnregistered`. This ensures DB-backed models whose provider was not yet in the registry at startup are picked up as soon as the plugin starts.
### Adding a consumer
```rust
let mut rx = system_bus.subscribe();
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(SystemEvent::ApiProviderRegistered { type_id }) => { /* ... */ }
Ok(SystemEvent::ApiProviderUnregistered { type_id }) => { /* ... */ }
Err(RecvError::Lagged(n)) => warn!("system_bus lagged by {n}"),
Err(RecvError::Closed) => break,
}
}
});
```
### Adding a new `SystemEvent` variant
1. Add the variant to `SystemEvent` in `crates/core-api/src/system_bus.rs`.
2. Emit it from the relevant producer.
3. Update consumers that need to react.
4. Update this file.