1use crate::canonical::CanonicalUnitEvent;
4use crate::config::{InvocationConfig, SessionConfig};
5use crate::id::ToolId;
6use crate::id::{ChannelId, SessionId, SessionKey, TransactionId};
7use crate::input::CanonicalInput;
8use crate::safe::SafeDiagnostic;
9use crate::tool::ToolLifecycleEvent;
10use serde::{Deserialize, Serialize};
11use std::future::Future;
12use std::pin::Pin;
13use std::sync::Arc;
14use std::time::Duration;
15use thiserror::Error;
16
17pub type EventDelivery =
19 Pin<Box<dyn Future<Output = Result<(), EventDeliveryError>> + Send + 'static>>;
20
21pub trait TransactionEventSink: Send + Sync + 'static {
23 fn deliver(&self, event: TransactionEvent) -> EventDelivery;
25}
26
27pub type CompletionDelivery =
29 Pin<Box<dyn Future<Output = Result<(), CompletionDeliveryError>> + Send + 'static>>;
30
31pub trait CompletionCallback: Send + 'static {
33 fn call(self: Box<Self>, end: TransactionEnd) -> CompletionDelivery;
35}
36
37pub struct FnEventSink<F>(pub F);
39
40impl<F> TransactionEventSink for FnEventSink<F>
41where
42 F: Fn(TransactionEvent) -> EventDelivery + Send + Sync + 'static,
43{
44 fn deliver(&self, event: TransactionEvent) -> EventDelivery {
45 (self.0)(event)
46 }
47}
48
49pub struct FnCompletionCallback<F>(pub F);
51
52impl<F> CompletionCallback for FnCompletionCallback<F>
53where
54 F: FnOnce(TransactionEnd) -> CompletionDelivery + Send + 'static,
55{
56 fn call(self: Box<Self>, end: TransactionEnd) -> CompletionDelivery {
57 (self.0)(end)
58 }
59}
60
61pub struct TransactionRequest {
63 pub channel_id: ChannelId,
65 pub session_id: Option<SessionId>,
67 pub input: CanonicalInput,
69 pub session_config: Option<SessionConfig>,
71 pub invocation_config: InvocationConfig,
73 pub tools: Vec<ToolId>,
75 pub events: Arc<dyn TransactionEventSink>,
77 pub completion: Box<dyn CompletionCallback>,
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct AdmissionReceipt {
84 pub transaction_id: TransactionId,
86 pub session_id: Option<SessionId>,
88}
89
90#[derive(Clone, Debug, PartialEq, Eq, Hash)]
92pub enum TransactionSelector {
93 Transaction(TransactionId),
95 Session(SessionKey),
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
101pub enum TerminationMode {
102 Cancel {
104 reason: CancellationReason,
106 },
107 ForceTerminate {
109 reason: TerminationReason,
111 },
112}
113
114#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct CancellationReason {
117 pub code: CancellationReasonCode,
119 pub detail: Option<SafeDiagnostic>,
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
125pub enum CancellationReasonCode {
126 CallerRequested,
128 RuntimeShutdown,
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct TerminationReason {
135 pub code: TerminationReasonCode,
137 pub detail: Option<SafeDiagnostic>,
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
143pub enum TerminationReasonCode {
144 CallerRequested,
146 CancellationGraceExpired,
148 RuntimeShutdown,
150}
151
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154pub enum TerminationDisposition {
155 Accepted,
157 AlreadyRequested,
159 AlreadyTerminal,
161 NotFound,
163}
164
165pub type Shutdown = Pin<Box<dyn Future<Output = ShutdownDisposition> + Send + 'static>>;
167
168#[derive(Clone, Debug, PartialEq, Eq, Default)]
170pub struct ShutdownDisposition {
171 pub normally_finalized: u64,
173 pub supervisor_finalized: u64,
175 pub callback_failed: u64,
177 pub callback_aborted: u64,
179 pub invariant_failed: u64,
181}
182
183pub trait TransactionRuntime: Send + Sync {
185 fn submit(&self, request: TransactionRequest) -> Result<AdmissionReceipt, AdmissionError>;
187
188 fn terminate(
190 &self,
191 selector: TransactionSelector,
192 mode: TerminationMode,
193 ) -> TerminationDisposition;
194
195 fn shutdown(&self, deadline: Duration) -> Shutdown;
197}
198
199#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
201pub struct TransactionEvent {
202 pub transaction_id: TransactionId,
204 pub channel_id: ChannelId,
206 pub session_id: SessionId,
208 pub sequence: u64,
210 pub payload: TransactionEventPayload,
212}
213
214#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
216pub enum TransactionEventPayload {
217 SessionEstablished {
219 external_session_id: crate::id::ExternalSessionId,
221 },
222 CanonicalUnit(CanonicalUnitEvent),
224 ToolLifecycle(ToolLifecycleEvent),
226 Diagnostic(TransactionDiagnostic),
228 Ended(TransactionEnd),
230}
231
232#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
234pub struct TransactionDiagnostic {
235 pub diagnostic: SafeDiagnostic,
237}
238
239#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
241pub struct TransactionEnd {
242 pub transaction_id: TransactionId,
244 pub session_id: Option<SessionId>,
246 pub channel_id: ChannelId,
248 pub kind: TransactionEndKind,
250 pub prior_terminal_cause: Option<TransactionEndKind>,
252 pub event_delivery: EventDeliveryOutcome,
254 pub emitted_events: u64,
256 pub usage: TransactionUsage,
258 pub diagnostics: Vec<TransactionDiagnostic>,
260}
261
262#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
264pub enum TransactionEndKind {
265 Completed,
267 ContinuationRequired,
269 Cancelled,
271 Terminated,
273 RuntimeShutdown,
275 DeadlineExceeded,
277 ChannelOpenFailed,
279 EncodingFailed,
281 ConnectorFailed,
283 InterpretationFailed,
285 ToolExchangeFailed,
287 EventDeliveryFailed,
289 LimitExceeded,
291 InvariantFailed,
293}
294
295#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
297pub enum EventDeliveryOutcome {
298 Accepted,
300 Failed,
302}
303
304#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
306pub struct TransactionUsage {
307 pub provider_input_tokens: Option<u64>,
309 pub provider_output_tokens: Option<u64>,
311 pub provider_exchanges: u32,
313 pub tools_started: u32,
315 pub tools_completed: u32,
317}
318
319#[derive(Clone, Debug, Error, PartialEq, Eq)]
321pub enum EventDeliveryError {
322 #[error("event delivery failed")]
324 Failed,
325 #[error("event delivery deadline exceeded")]
327 DeadlineExceeded,
328}
329
330#[derive(Clone, Debug, Error, PartialEq, Eq)]
332pub enum CompletionDeliveryError {
333 #[error("completion callback failed")]
335 Failed,
336 #[error("completion callback deadline exceeded")]
338 DeadlineExceeded,
339}
340
341#[derive(Clone, Debug, Error, PartialEq, Eq)]
343#[error("{kind:?}: {message}")]
344pub struct AdmissionError {
345 pub kind: AdmissionErrorKind,
347 pub message: String,
349}
350
351impl AdmissionError {
352 pub fn new(kind: AdmissionErrorKind, message: impl Into<String>) -> Self {
354 Self {
355 kind,
356 message: message.into(),
357 }
358 }
359}
360
361#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
363pub enum AdmissionErrorKind {
364 RuntimeShuttingDown,
366 UnknownChannel,
368 SessionAlreadyActive,
370 UnknownTool,
372 DuplicateTool,
374 InvalidInput,
376 InvalidConfiguration,
378 CapabilityMismatch,
380 CapacityExceeded,
382 SpawnFailed,
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use crate::input::user_text_input;
390
391 #[test]
392 fn end_kind_round_trip() {
393 let kind = TransactionEndKind::Completed;
394 let json = serde_json::to_string(&kind).unwrap();
395 let back: TransactionEndKind = serde_json::from_str(&json).unwrap();
396 assert_eq!(kind, back);
397 }
398
399 #[tokio::test]
400 async fn sink_adapters_return_futures() {
401 let sink = FnEventSink(|_e| Box::pin(async { Ok(()) }) as EventDelivery);
402 let events: Arc<dyn TransactionEventSink> = Arc::new(sink);
403 let end = TransactionEnd {
404 transaction_id: TransactionId::generate(),
405 session_id: None,
406 channel_id: ChannelId::try_new("ch").unwrap(),
407 kind: TransactionEndKind::Completed,
408 prior_terminal_cause: None,
409 event_delivery: EventDeliveryOutcome::Accepted,
410 emitted_events: 1,
411 usage: TransactionUsage::default(),
412 diagnostics: vec![],
413 };
414 let ev = TransactionEvent {
415 transaction_id: end.transaction_id,
416 channel_id: end.channel_id.clone(),
417 session_id: SessionId::try_new("s").unwrap(),
418 sequence: 1,
419 payload: TransactionEventPayload::Ended(end.clone()),
420 };
421 events.deliver(ev).await.unwrap();
422
423 let cb: Box<dyn CompletionCallback> = Box::new(FnCompletionCallback(|_e| {
424 Box::pin(async { Ok(()) }) as CompletionDelivery
425 }));
426 cb.call(end).await.unwrap();
427
428 let _input = user_text_input("hello").unwrap();
429 }
430}