Skip to main content

beam_core/
api.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::{
5    ipc::{CliUsageLimitState, ScreenStatus},
6    session::{AdoptedFrom, AgentAttention, PendingResponseCardState, Session, SessionStatus},
7};
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub struct ApiHealth {
11    pub status: String,
12    pub pid: u32,
13    pub started_at: DateTime<Utc>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17pub struct DaemonRuntimeState {
18    pub pid: u32,
19    pub api_addr: String,
20    pub started_at: DateTime<Utc>,
21    pub log_path: String,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25pub struct CreateSessionRequest {
26    pub title: String,
27    pub cli_id: String,
28    pub cli_bin: String,
29    #[serde(default)]
30    pub cli_args: Vec<String>,
31    pub working_dir: String,
32    #[serde(default)]
33    pub prompt: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
37pub struct SessionInputRequest {
38    pub content: String,
39    #[serde(default)]
40    pub raw: bool,
41}
42
43/// A mention target: an open_id with an optional human-readable name.
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45pub struct MentionTarget {
46    pub open_id: String,
47    #[serde(default)]
48    pub name: Option<String>,
49}
50
51/// Structured request for beam send.
52///
53/// Only `content` is required; all other fields are optional with sensible defaults,
54/// providing backward compatibility with old `{ "content": "..." }` JSON payloads.
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
56pub struct FinalOutputRequest {
57    pub content: String,
58
59    // ---- mention / attention policy ----
60    /// Explicit mention targets (--mention <open_id[:name]>, repeatable).
61    #[serde(default)]
62    pub mentions: Vec<MentionTarget>,
63    /// Mention the sender who triggered the session (--mention-back).
64    #[serde(default)]
65    pub mention_back: bool,
66    /// Suppress all @-mentions in body and footer (--no-mention).
67    #[serde(default)]
68    pub no_mention: bool,
69
70    // ---- media attachments ----
71    /// Local file paths to upload and send as file messages (--files / --file, repeatable).
72    #[serde(default)]
73    pub files: Vec<String>,
74    /// Local image paths to upload and inline in an interactive card (--images / --image, repeatable).
75    #[serde(default)]
76    pub images: Vec<String>,
77
78    // ---- targeting / quoting ----
79    /// Force a top-level chat message instead of a reply (--top-level).
80    #[serde(default)]
81    pub top_level: bool,
82    /// Override target chat (--chat-id <oc_xxx>).
83    #[serde(default)]
84    pub chat_id: Option<String>,
85    /// Send into a specific thread (--into <message_id>).
86    #[serde(default)]
87    pub into: Option<String>,
88    /// Explicit quote target (--quote <message_id>).
89    #[serde(default)]
90    pub quote: Option<String>,
91    /// Disable automatic quoting (--no-quote).
92    #[serde(default)]
93    pub no_quote: bool,
94
95    // ---- voice ----
96    /// Request TTS/voice delivery (--voice).
97    #[serde(default)]
98    pub voice: bool,
99
100    // ---- attention (workflow) ----
101    /// Requesting attention with a specific kind (--attention[=kind]).
102    /// Valid: authz | decision | blocked | help. Default: blocked.
103    #[serde(default)]
104    pub attention: Option<String>,
105
106    // ---- compat flags ----
107    /// No-op (--card).
108    #[serde(default)]
109    pub card: bool,
110    /// No-op (--text).
111    #[serde(default)]
112    pub text: bool,
113    /// Passthrough (--anyway).
114    #[serde(default)]
115    pub anyway: bool,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119pub struct RestartSessionRequest {
120    #[serde(default)]
121    pub prompt: String,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
125pub struct ResumeSessionRequest {
126    #[serde(default)]
127    pub prompt: String,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131pub struct AttemptResumeRequest {
132    #[serde(default)]
133    pub reason: Option<String>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
137pub struct AttemptResumeStartResponse {
138    pub ok: bool,
139    pub resume_id: String,
140    pub run_id: String,
141    pub activity_id: String,
142    pub attempt_id: String,
143    pub session_id: String,
144    pub original_session_id: String,
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub cli_session_id: Option<String>,
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub web_port: Option<u16>,
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub write_token: Option<String>,
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub url: Option<String>,
153    pub already_running: bool,
154    pub started_at: u64,
155    pub log_path: String,
156    pub sidecar_path: String,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
160pub struct AttemptResumeEndResponse {
161    pub ok: bool,
162    pub resume_id: String,
163    pub status: String,
164    pub close_reason: String,
165    pub closed_at: u64,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
169pub struct TerminalInfo {
170    pub url: String,
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub read_only_token: Option<String>,
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub write_token: Option<String>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
178pub struct SessionSummary {
179    pub session_id: String,
180    pub title: String,
181    pub status: SessionStatus,
182    pub chat_type: Option<String>,
183    pub quote_target_id: Option<String>,
184    pub cli_id: Option<String>,
185    pub cli_bin: Option<String>,
186    pub cli_args: Vec<String>,
187    pub working_dir: Option<String>,
188    pub worker_pid: Option<u32>,
189    pub terminal_url: Option<String>,
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub read_only_token: Option<String>,
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub write_token: Option<String>,
194    pub created_at: DateTime<Utc>,
195    pub stream_card_nonce: Option<String>,
196    pub current_screen: Option<String>,
197    pub last_screen_status: Option<ScreenStatus>,
198    pub usage_limit: Option<CliUsageLimitState>,
199    pub current_image_key: Option<String>,
200    pub tui_prompt_card_id: Option<String>,
201    pub tui_prompt_options: Vec<crate::ipc::TuiPromptOption>,
202    pub tui_prompt_multi_select: Option<bool>,
203    pub tui_toggled_indices: Vec<usize>,
204    pub pending_response_card_id: Option<String>,
205    pub pending_response_card_state: Option<PendingResponseCardState>,
206    pub last_patched_response_card_id: Option<String>,
207    pub last_final_output_turn_id: Option<String>,
208    pub last_final_output: Option<String>,
209    pub adopted_from: Option<AdoptedFrom>,
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub agent_attention: Option<AgentAttention>,
212    /// Daemon-side liveness verdict from the worker heartbeat watchdog:
213    /// true when the worker stopped heartbeating while the session is active.
214    #[serde(default)]
215    pub worker_unresponsive: bool,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
219pub struct BotSummary {
220    pub lark_app_id: String,
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub name: Option<String>,
223    pub cli_id: String,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub model: Option<String>,
226    pub allowed_users: Vec<String>,
227    #[serde(skip_serializing_if = "Vec::is_empty")]
228    pub allowed_chat_groups: Vec<String>,
229    #[serde(skip_serializing_if = "Vec::is_empty")]
230    pub oncall_chats: Vec<String>,
231    pub private_card: bool,
232    pub active_sessions: usize,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
236pub struct SessionGroup {
237    pub chat_id: String,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub title: Option<String>,
240    pub sessions: Vec<SessionSummary>,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
244pub struct DaemonOverview {
245    pub pid: u32,
246    pub started_at: DateTime<Utc>,
247    pub session_count: usize,
248    pub active_session_count: usize,
249    pub closed_session_count: usize,
250    pub bot_count: usize,
251    pub worker_count: usize,
252    pub config_path: String,
253    pub data_dir: String,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
257pub struct SessionLocateInfo {
258    pub session_id: String,
259    pub terminal_url: Option<String>,
260    pub worker_pid: Option<u32>,
261}
262
263impl From<&Session> for SessionSummary {
264    fn from(value: &Session) -> Self {
265        Self {
266            session_id: value.session_id.clone(),
267            title: value.title.clone(),
268            status: value.status,
269            chat_type: value.chat_type.clone(),
270            quote_target_id: value.quote_target_id.clone(),
271            cli_id: value.cli_id.clone(),
272            cli_bin: value.cli_bin.clone(),
273            cli_args: value.cli_args.clone(),
274            working_dir: value.working_dir.clone(),
275            worker_pid: value.worker_pid,
276            terminal_url: value.terminal_url.clone(),
277            read_only_token: None,
278            write_token: None,
279            created_at: value.created_at,
280            stream_card_nonce: value.stream_card_nonce.clone(),
281            current_screen: value.current_screen.clone(),
282            last_screen_status: value.last_screen_status,
283            usage_limit: value.usage_limit.clone(),
284            current_image_key: value.current_image_key.clone(),
285            tui_prompt_card_id: value.tui_prompt_card_id.clone(),
286            tui_prompt_options: value.tui_prompt_options.clone(),
287            tui_prompt_multi_select: value.tui_prompt_multi_select,
288            tui_toggled_indices: value.tui_toggled_indices.clone(),
289            pending_response_card_id: value.pending_response_card_id.clone(),
290            pending_response_card_state: value.pending_response_card_state,
291            last_patched_response_card_id: value.last_patched_response_card_id.clone(),
292            last_final_output_turn_id: value.last_final_output_turn_id.clone(),
293            last_final_output: value.last_final_output.clone(),
294            adopted_from: value.adopted_from.clone(),
295            agent_attention: value.agent_attention.clone(),
296            worker_unresponsive: false,
297        }
298    }
299}
300
301/// Request body for `POST /api/attention` — aligned with botmux parity.
302/// Accepts both camelCase (sessionId) and snake_case (session_id).
303#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
304pub struct AttentionRequest {
305    #[serde(alias = "sessionId")]
306    pub session_id: String,
307    pub kind: String,
308    pub reason: String,
309}