Skip to main content

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}