1use crate::error::AtmError;
4use crate::protocol::{FramePayload, RequestEnvelope, RequestId, ResponseEnvelope};
5pub use crate::protocol::{NotificationEvent, RuntimeStatusSnapshot};
6use crate::schema::AtmMessageId;
7use crate::types::{AgentName, PaneId, TaskId, TeamName};
8pub use atm_storage::contract::{AckTransition, Message, MessageKey, TaskState};
9pub use atm_storage::{
10 BuiltInNudgeTemplateKind, NudgeTemplateOverrideStore, TeamNudgeTemplateOverrideMode,
11 TeamNudgeTemplateOverrideRow,
12};
13
14#[doc(hidden)]
21pub mod sealed {
22 pub trait Sealed {}
23}
24
25mod mail;
26mod runtime;
27mod store;
28
29pub use mail::*;
33pub use runtime::*;
34pub use store::*;
35
36pub trait AtmProtocol: sealed::Sealed {
38 fn request_to_frame(
43 &self,
44 request_id: RequestId,
45 request: RequestEnvelope,
46 ) -> Result<FramePayload, AtmError>;
47
48 fn request_from_frame(
53 &self,
54 frame: FramePayload,
55 ) -> Result<(RequestId, RequestEnvelope), AtmError>;
56
57 fn response_to_frame(
62 &self,
63 request_id: RequestId,
64 response: ResponseEnvelope,
65 ) -> Result<FramePayload, AtmError>;
66
67 fn response_from_frame(
72 &self,
73 frame: FramePayload,
74 ) -> Result<(RequestId, ResponseEnvelope), AtmError>;
75}
76
77pub trait ClientTransport: sealed::Sealed + Send + Sync {
79 fn send(&self, request: RequestEnvelope) -> Result<ResponseEnvelope, AtmError>;
84}
85
86pub trait ServerTransport: sealed::Sealed {
88 fn serve(
93 &self,
94 dispatcher: std::sync::Arc<dyn RequestDispatcher + Send + Sync>,
95 ) -> Result<(), AtmError>;
96}
97
98pub trait RequestDispatcher: sealed::Sealed + Send + Sync {
100 fn dispatch(&self, request: RequestEnvelope) -> Result<ResponseEnvelope, AtmError>;
105}
106
107pub trait StatusSource: sealed::Sealed {
109 fn snapshot(&self) -> Result<RuntimeStatusSnapshot, AtmError>;
113}
114
115#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
116pub struct PostSendHookEvent {
117 pub sender: AgentName,
118 pub sender_team: TeamName,
119 pub recipient: AgentName,
120 pub recipient_team: TeamName,
121 pub message_id: AtmMessageId,
122 pub description: String,
123 pub requires_ack: bool,
124 pub is_ack: bool,
125 pub task_id: Option<TaskId>,
126 pub recipient_pane_id: Option<PaneId>,
127}
128
129pub fn built_in_nudge_template_kind_from_post_send_event(
130 event: &PostSendHookEvent,
131) -> BuiltInNudgeTemplateKind {
132 match (event.is_ack, event.task_id.is_some(), event.requires_ack) {
133 (true, true, _) => BuiltInNudgeTemplateKind::AcknowledgeTask,
134 (true, false, _) => BuiltInNudgeTemplateKind::Acknowledge,
135 (false, true, true) => BuiltInNudgeTemplateKind::DeliveryTaskAck,
136 (false, true, false) => BuiltInNudgeTemplateKind::DeliveryTask,
137 (false, false, true) => BuiltInNudgeTemplateKind::DeliveryAck,
138 (false, false, false) => BuiltInNudgeTemplateKind::Delivery,
139 }
140}
141
142#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
143pub struct ResolvedBuiltInNudgeTemplate {
144 pub kind: BuiltInNudgeTemplateKind,
145 pub body: Option<String>,
146}
147
148#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
149#[serde(rename_all = "snake_case")]
150pub enum BuiltInNudgeSinkTarget {
151 Tmux,
152 Graft,
153}
154
155#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
156pub struct InternalNudgeEnvelope {
157 pub event: PostSendHookEvent,
158 pub sink_target: BuiltInNudgeSinkTarget,
159 pub template: ResolvedBuiltInNudgeTemplate,
160}
161
162#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
163pub struct LocalTmuxNudgeTarget {
164 pub pane_id: PaneId,
165 pub rendered_nudge: String,
166}
167
168#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
169pub struct GraftNudgeTarget {
170 pub recipient: AgentName,
171 pub recipient_team: TeamName,
172}
173
174#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
175pub enum PostSendBuiltInTarget {
176 LocalTmux(LocalTmuxNudgeTarget),
177 Graft(GraftNudgeTarget),
178}
179
180#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
181pub struct BuiltInPostSendDispatch {
182 pub event: PostSendHookEvent,
183 pub target: PostSendBuiltInTarget,
184}
185
186#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
187#[serde(rename_all = "snake_case")]
188pub enum PostSendEmissionPath {
189 ExternalHook,
190 LocalTmux,
191 GraftPort,
192}
193
194#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
195pub struct HookExecutionSummary {
196 matched_rules: usize,
197 succeeded_rules: usize,
198 failed_rules: usize,
199}
200
201impl HookExecutionSummary {
202 pub fn new(
203 matched_rules: usize,
204 succeeded_rules: usize,
205 failed_rules: usize,
206 ) -> Result<Self, AtmError> {
207 if succeeded_rules + failed_rules > matched_rules {
208 return Err(AtmError::validation(format!(
209 "invalid post-send hook execution summary: succeeded ({succeeded_rules}) + failed ({failed_rules}) exceeds matched ({matched_rules})"
210 ))
211 .with_recovery(
212 "Count each matching post-send hook rule exactly once before constructing hook execution summary state.",
213 ));
214 }
215 Ok(Self {
216 matched_rules,
217 succeeded_rules,
218 failed_rules,
219 })
220 }
221
222 pub const fn matched_rules(&self) -> usize {
223 self.matched_rules
224 }
225
226 pub const fn succeeded_rules(&self) -> usize {
227 self.succeeded_rules
228 }
229
230 pub const fn failed_rules(&self) -> usize {
231 self.failed_rules
232 }
233}
234
235#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
236pub enum PostSendEmissionOutcome {
237 NoCapability {
238 hook_summary: HookExecutionSummary,
239 },
240 Delivered {
241 path: PostSendEmissionPath,
242 hook_summary: HookExecutionSummary,
243 },
244 Failed {
245 hook_summary: HookExecutionSummary,
246 warning: crate::send::WarningEntry,
247 },
248}
249pub trait PostSendHookEmitter: sealed::Sealed + Send + Sync {
251 fn emit_post_send(
256 &self,
257 dispatch: &BuiltInPostSendDispatch,
258 ) -> Result<PostSendEmissionPath, AtmError>;
259}
260
261pub trait GraftPostSendPort: sealed::Sealed + Send + Sync {
263 fn deliver_post_send(
268 &self,
269 event: &PostSendHookEvent,
270 target: &GraftNudgeTarget,
271 ) -> Result<(), AtmError>;
272}