7.1 KiB
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
pub enum BusEvent {
UserMessage(ChatEvent),
AssistantResponse(ChatEvent),
CompactionDone(CompactionEvent),
}
Consumers match on the variant to decide what to handle:
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
// 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:
BusEvent::UserMessage— content of the user messageBusEvent::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 for the full flow.
Adding a Consumer
- Call
skald.event_bus.subscribe()inmain.rsafterSkald::new()completes. - Spawn a background task with a receive loop.
- Match on
BusEventvariants — ignore variants you don't care about. - On
RecvError::Lagged, log and continue — do not panic. - 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, orCompactionEvent - 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
pub enum SystemEvent {
ApiProviderRegistered { type_id: String },
ApiProviderUnregistered { type_id: String },
}
Wiring
- Created early in
main.rs(no dependencies), stored inskald.system_busandPluginContext::system_bus. - Producers:
ProviderRegistry::register_plugin()/unregister_plugin()emit automatically — plugins do not need to touch the bus directly. - Consumers:
TtsManagerandTranscribeManagersubscribe at construction time and callreload()onApiProviderRegistered/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
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
- Add the variant to
SystemEventincrates/core-api/src/system_bus.rs. - Emit it from the relevant producer.
- Update consumers that need to react.
- Update this file.