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