brainwires_stores/session/broker.rs
1//! Interactive surface for live session orchestration.
2//!
3//! [`SessionStore`](crate::SessionStore) is the **persistence** side of the
4//! session abstraction: load/save the messages of one session. [`SessionBroker`]
5//! is the **interactive** side: list peers, read history, push a message, or
6//! spawn a child session — all against a host-provided live session registry.
7//!
8//! These two sit in the same crate because both are about "a session", but
9//! they're paired interfaces, not a single one — a host can implement either
10//! independently.
11//!
12//! `brainwires-session` is a framework crate — it does not know about a
13//! specific gateway's per-user session map or any concrete agent type.
14//! Hosts (e.g., `brainwires-tools::SessionsTool` or
15//! `extras/brainclaw/gateway/src/sessions_broker.rs`) implement
16//! [`SessionBroker`] over their real registry and hand an
17//! `Arc<dyn SessionBroker>` to the consumer.
18//!
19//! Lives here so all session abstractions sit together and the
20//! `SessionId` type is shared between persistence and brokering.
21
22use async_trait::async_trait;
23use serde::{Deserialize, Serialize};
24
25use super::types::SessionId;
26
27/// Summary metadata for a single session, returned by `sessions_list`.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct SessionSummary {
30 /// The session identifier.
31 pub id: SessionId,
32 /// Originating channel (e.g. `"discord"`, `"web"`, `"internal"`).
33 pub channel: String,
34 /// Peer handle — user id on the channel, or `"spawned-by-<parent>"`.
35 pub peer: String,
36 /// When the session was first created.
37 pub created_at: chrono::DateTime<chrono::Utc>,
38 /// When the session last received or produced a message.
39 pub last_active: chrono::DateTime<chrono::Utc>,
40 /// Number of messages currently in the session's transcript.
41 pub message_count: usize,
42 /// Parent session that spawned this one, if any.
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub parent: Option<SessionId>,
45}
46
47/// A single message from a session's transcript.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct SessionMessage {
50 /// `"user"` | `"assistant"` | `"system"` | `"tool"`.
51 pub role: String,
52 /// Message text. Tool calls/results are stringified.
53 pub content: String,
54 /// When the message was recorded (may be approximate if the underlying
55 /// agent does not track per-message timestamps).
56 pub timestamp: chrono::DateTime<chrono::Utc>,
57}
58
59/// Parameters for [`SessionBroker::spawn`].
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct SpawnRequest {
62 /// Initial user message to seed the new session with.
63 pub prompt: String,
64 /// Optional provider/model override. `None` = inherit from parent.
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub model: Option<String>,
67 /// Optional system prompt override. `None` = inherit.
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub system: Option<String>,
70 /// Tools to allow in the spawned session. `None` = inherit parent's toolset.
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub tools: Option<Vec<String>>,
73 /// If `true`, block until the spawned session produces its first
74 /// assistant message (or [`Self::wait_timeout_secs`] elapses) and return
75 /// that in the tool result. Default: `false` — return immediately with
76 /// just the new session id.
77 #[serde(default)]
78 pub wait_for_first_reply: bool,
79 /// Seconds to wait when [`Self::wait_for_first_reply`] is `true`.
80 /// Default: `60`.
81 #[serde(default = "default_wait_timeout_secs")]
82 pub wait_timeout_secs: u64,
83}
84
85fn default_wait_timeout_secs() -> u64 {
86 60
87}
88
89impl Default for SpawnRequest {
90 fn default() -> Self {
91 Self {
92 prompt: String::new(),
93 model: None,
94 system: None,
95 tools: None,
96 wait_for_first_reply: false,
97 wait_timeout_secs: default_wait_timeout_secs(),
98 }
99 }
100}
101
102/// Result of [`SessionBroker::spawn`].
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct SpawnedSession {
105 /// The id of the newly-created session.
106 pub id: SessionId,
107 /// Set iff `wait_for_first_reply` was `true` and the first assistant
108 /// message arrived within the timeout.
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub first_reply: Option<SessionMessage>,
111}
112
113/// Host-provided bridge from session-control tools to the real session registry.
114///
115/// Implementations must be cheap to clone-via-`Arc` and safe to call from any
116/// async context.
117#[async_trait]
118pub trait SessionBroker: Send + Sync {
119 /// List every live session the host knows about.
120 async fn list(&self) -> anyhow::Result<Vec<SessionSummary>>;
121
122 /// Read a session's transcript, newest-last, capped at `limit` entries
123 /// (`None` = use the host's sensible default).
124 async fn history(
125 &self,
126 id: &SessionId,
127 limit: Option<usize>,
128 ) -> anyhow::Result<Vec<SessionMessage>>;
129
130 /// Inject a user-role message into `id`'s inbound queue. Fire-and-forget
131 /// — the target session processes it asynchronously.
132 async fn send(&self, id: &SessionId, text: String) -> anyhow::Result<()>;
133
134 /// Create a new session as a child of `parent`, seeded with `req.prompt`.
135 async fn spawn(&self, parent: &SessionId, req: SpawnRequest) -> anyhow::Result<SpawnedSession>;
136}