1mod assembly;
2mod builder;
3pub(crate) mod causal;
4mod clock;
5mod config_ops;
6mod effect;
7mod environment;
8mod error;
9mod host;
10mod in_memory_store;
11mod io;
12mod lifecycle;
13mod logical_turn;
14mod observation;
15mod process;
16mod process_work_driver;
17mod process_worker;
18mod queued_work_driver;
19pub mod scenario_contracts;
20mod session_api;
21mod session_execution_lease;
22mod session_manager;
23mod session_ops;
24mod state;
25#[cfg(test)]
26pub(crate) mod tests;
27mod turn_boundary;
28mod turn_commit_draft;
29pub(crate) mod turn_control;
30mod turn_driver;
31mod turn_graph_editor;
32mod turn_input_ingress;
33mod turn_loop;
34mod turn_queue;
35mod usage;
36
37use std::any::Any;
38use std::collections::HashMap;
39use std::fmt;
40use std::sync::Arc;
41use std::sync::Mutex as StdMutex;
42use std::sync::atomic::{AtomicBool, Ordering};
43
44use tokio::sync::{Mutex, mpsc};
45use tokio_util::sync::CancellationToken;
46
47use crate::llm::types::{
48 LlmOutputPart, LlmProviderTraceEvent, LlmProviderTraceSender, LlmRequest, LlmResponse,
49 LlmStreamEvent, LlmUsage,
50};
51use crate::plugin::{
52 CheckpointHookContext, PrepareTurnRequest, SessionConfigChangedContext, SessionRelation,
53};
54use crate::sansio::{LlmCallError, Response};
55use crate::session_model::{
56 Message, MessageRole, Part, PartKind, PruneState, RuntimeSessionPolicy, SessionPolicy,
57 SessionStreamEvent, TokenUsage, fresh_message_id, make_error_event, reassign_part_ids,
58 shared_parts, transport_stream_events,
59};
60use crate::{
61 CheckpointKind, PersistentRuntimeServices, PluginOperationInvokeError, PromptHookContext,
62 RuntimeServices, SandboxMessage, Session, SessionCreateRequest, SessionError, SessionHandle,
63 SessionSnapshot, SessionStartPoint, ToolCallRecord, TurnFinish, TurnOutcome, TurnStop,
64};
65use crate::{Effect, TurnMachine};
66
67use host::*;
68use session_execution_lease::*;
69use session_manager::*;
70use turn_boundary::*;
71use turn_commit_draft::*;
72use turn_driver::*;
73
74pub(super) fn runtime_error_from_store_commit(err: crate::store::StoreError) -> RuntimeError {
75 match err {
76 crate::store::StoreError::SessionExecutionLeaseExpired { session_id } => RuntimeError::new(
77 RuntimeErrorCode::SessionExecutionLeaseLost,
78 format!("session execution lease for session `{session_id}` was lost before commit"),
79 ),
80 err => RuntimeError::new(RuntimeErrorCode::StoreCommitFailed, err.to_string()),
81 }
82}
83
84pub use lash_sansio::PromptUsage;
86
87use assembly::{
88 LlmDebugText, LlmDebugToolCall, LlmStreamAccumulator, LlmStreamDebugState, LlmStreamEventLog,
89 LlmStreamState, LlmStreamSummary, TurnAssembler,
90};
91#[cfg(test)]
92#[allow(unused_imports)]
93use assembly::{classify_output_state, sanitize_assistant_output};
94pub use builder::EmbeddedRuntimeBuilder;
95pub use causal::process_event_invocation;
96pub(crate) use causal::tool_retry_sleep_invocation;
97pub use clock::{Clock, SystemClock};
98pub(crate) use effect::RuntimeEffectControllerHandle;
99pub use effect::{
100 AwaitEventKey, AwaitEventResolver, AwaitEventWaitIdentity, BoundaryReason, CausalRef,
101 EffectHost, ExecutionScope, ExternalCompletionError, InlineEffectHost,
102 InlineRuntimeEffectController, LlmAttachmentSpec, LlmRequestSpec, ProcessCommand,
103 ProcessEffectOutcome, Resolution, ResolveOutcome, RuntimeAwaitEventOptions,
104 RuntimeDirectLlmOutcome, RuntimeEffectCommand, RuntimeEffectController,
105 RuntimeEffectControllerError, RuntimeEffectEnvelope, RuntimeEffectKind,
106 RuntimeEffectLocalExecutor, RuntimeEffectOutcome, RuntimeInvocation, RuntimeLlmCallOutcome,
107 RuntimeReplay, RuntimeScope, RuntimeSubject, ScopedEffectController, SegmentProgress,
108 ToolAttemptEffectOutcome, ToolAttemptLaunch, ToolBatchEffectOutcome, ToolCallLaunch,
109};
110pub use environment::{ParkedSession, Residency, RuntimeEnvironment, RuntimeEnvironmentBuilder};
111pub use error::{DurableStoreFacet, RuntimeError, RuntimeErrorCode};
112pub use host::{EmbeddedRuntimeHost, ProcessRuntimeHost, RuntimeHostConfig};
113pub use in_memory_store::{InMemorySessionStore, InMemorySessionStoreFactory};
114use io::normalize_input_items;
115pub use observation::{
116 InMemoryLiveReplayStore, InMemoryLiveReplayStoreConfig, LiveReplayGap, LiveReplayGapReason,
117 LiveReplayResult, LiveReplayStore, LiveReplayStoreError, LiveReplaySubscribeResult,
118 LiveReplaySubscription, RuntimeHandle, RuntimeObservation, SessionCursor, SessionCursorError,
119 SessionObservation, SessionObservationEvent, SessionObservationEventPayload,
120 SessionObservationSubscription, SessionProcessEventKind, SessionQueueEventKind, SessionResume,
121 SessionRevision,
122};
123#[cfg(any(test, feature = "testing"))]
124pub use process::TestLocalProcessRegistry;
125pub use process::{
126 AbandonEvidence, AbandonRequest, AbandonWriter, DefaultProcessCancelAbility,
127 InMemoryProcessExecutionEnvStore, ObservedProcess, ObservedProcessEvent, ObservedWorkItem,
128 PROCESS_LEASE_SCHEMA_VERSION, PersistedSegmentHandover, ProcessAttach, ProcessAwaitOutput,
129 ProcessAwaiter, ProcessCancelAbility, ProcessCancelAllRequest, ProcessCancelRequest,
130 ProcessCancelSource, ProcessCancelSummary, ProcessChangeCursor, ProcessChangeHub,
131 ProcessCompletionAuthority, ProcessEngine, ProcessEngineRegistry, ProcessEngineRunContext,
132 ProcessEngineRunGuard, ProcessEngineRuntimeContext, ProcessEngineValidationContext,
133 ProcessEvent, ProcessEventAppendPlan, ProcessEventAppendRequest, ProcessEventAppendResult,
134 ProcessEventSemantics, ProcessEventSemanticsSpec, ProcessEventSink, ProcessEventType,
135 ProcessExecutionContext, ProcessExecutionEnvRef, ProcessExecutionEnvSpec,
136 ProcessExecutionEnvStore, ProcessExternalRef, ProcessHandleDescriptor, ProcessHandleGrant,
137 ProcessHandleGrantEntry, ProcessHandleSummary, ProcessId, ProcessIdentity, ProcessInput,
138 ProcessLease, ProcessLeaseClaimOutcome, ProcessLeaseCompletion, ProcessLifecycleStatus,
139 ProcessListFilter, ProcessListMode, ProcessLiveReferenceSummary, ProcessOpScope,
140 ProcessOriginator, ProcessProvenance, ProcessPruneReport, ProcessRecord, ProcessRegistration,
141 ProcessRegistry, ProcessRunOutcome, ProcessService, ProcessSessionDeleteReport,
142 ProcessSpawnProvenance, ProcessStartGrant, ProcessStartOptions, ProcessStartRequest,
143 ProcessStarted, ProcessStatus, ProcessStatusFilter, ProcessTerminalSemantics,
144 ProcessTerminalSpec, ProcessTerminalState, ProcessValueSelector, ProcessWake,
145 ProcessWakeDedupeKey, ProcessWakeDelivery, ProcessWakeDeliveryRequest, ProcessWakeSpec,
146 ProcessWorkObserver, ProcessWorkSnapshot, RecoveryDisposition, SegmentHandover, SessionScope,
147 SessionScopeId, UnavailableProcessService, WaitKind, WaitState,
148 apply_process_status_projection, current_epoch_ms, epoch_ms_from_system_time,
149 load_process_execution_env, materialize_process_event_semantics, persist_process_execution_env,
150 prepare_process_event_append, prepare_process_registration, process_event_payload_hash,
151 process_signal_event_type, process_signal_name_from_event_type, process_signal_wait_key,
152 process_wake_delivery, process_wake_input_from_event_payload, process_wake_turn_cause,
153 process_wake_turn_text, require_event_replay, system_time_from_epoch_ms,
154 terminal_append_request, terminal_event_type_name, validate_process_signal_name,
155 watch_process_registry, watch_process_registry_with_sink,
156};
157pub use process_work_driver::{InlineProcessRunHandle, ProcessRunHandle, ProcessWorkDriver};
158pub use process_worker::{DurableProcessWorker, DurableProcessWorkerConfig, ProcessDrainReport};
159pub use queued_work_driver::{QueuedWorkDriver, QueuedWorkRunHandle, QueuedWorkRunRequest};
160pub use scenario_contracts::{RUNTIME_SCENARIO_CONTRACTS, ScenarioContractSpec};
161pub use session_manager::DirectCompletionClient;
162pub use state::RuntimeSessionState;
163use state::{
164 append_session_nodes_to_state_with_clock, apply_residency_on_load, apply_session_checkpoint,
165 apply_session_head, normalize_session_graph, open_agent_frame_in_state_with_clock,
166};
167pub use turn_control::{
168 TurnAddress, TurnAttach, TurnCancelOriginHint, TurnCancelOutcome, TurnCancelReceipt,
169 TurnCancelRequest, TurnCancellationEvidence, TurnTerminal, TurnWorkDriver,
170};
171pub use turn_input_ingress::{
172 PendingTurnInput, PendingTurnInputCancelOutcome, PendingTurnInputCancelResult,
173 PendingTurnInputCancelTarget, PendingTurnInputClaimDiagnostics, PendingTurnInputDraft,
174 PendingTurnInputSuffixCancelOutcome, QueuedCheckpointTurnInput, TurnInputCheckpointBoundary,
175 TurnInputClaim, TurnInputClaimMode, TurnInputCompletion, TurnInputIngress, TurnInputState,
176};
177pub use turn_loop::ensure_durable_effect_input;
178pub use turn_queue::{
179 DeliveryPolicy, MergeKey, QueuedCheckpointWork, QueuedTurnWork, QueuedWorkBatch,
180 QueuedWorkBatchDraft, QueuedWorkClaim, QueuedWorkClaimBoundary, QueuedWorkClass,
181 QueuedWorkCompletion, QueuedWorkItem, QueuedWorkPayload, SessionCommand, SessionCommandReceipt,
182 SlotPolicy, process_wake_batch_draft,
183};
184pub use usage::{
185 SessionUsageReport, TokenLedgerEntry, UsageReportRow, UsageTotals, diff_token_ledger,
186 diff_usage_reports,
187};
188use usage::{merge_ledger_entry, merge_usage_delta_entries, normalize_prompt_usage};
189
190#[doc(hidden)]
191#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
192pub enum RuntimeTurnPhase {
193 ContextTransform,
194 BeforeTurnHooks,
195 PromptBuild,
196 EffectLoop,
197 FinalizeTurn,
198 PersistTurn,
199 FinalCommit,
200 PostPersistHooks,
201}
202
203#[doc(hidden)]
204pub trait RuntimeTurnPhaseProbe: Send + Sync {
205 fn begin(&self, phase: RuntimeTurnPhase);
206 fn end(&self, phase: RuntimeTurnPhase);
207 fn begin_named(&self, _phase: &str) {}
208 fn end_named(&self, _phase: &str) {}
209}
210
211#[doc(hidden)]
212#[derive(Clone, Default)]
213pub struct RuntimeTurnPhaseProbeSlot {
214 probes: Arc<StdMutex<HashMap<crate::SessionScopeId, Arc<dyn RuntimeTurnPhaseProbe>>>>,
215}
216
217impl RuntimeTurnPhaseProbeSlot {
218 pub fn set_for_session(
219 &self,
220 session_id: impl Into<String>,
221 probe: Arc<dyn RuntimeTurnPhaseProbe>,
222 ) {
223 self.set_for_scope(&crate::SessionScope::new(session_id), probe);
224 }
225
226 pub fn set_for_scope(
227 &self,
228 scope: &crate::SessionScope,
229 probe: Arc<dyn RuntimeTurnPhaseProbe>,
230 ) {
231 self.probes
232 .lock()
233 .expect("runtime phase probe slot")
234 .insert(scope.id(), probe);
235 }
236
237 pub fn get_for_scope(
238 &self,
239 scope: &crate::SessionScope,
240 ) -> Option<Arc<dyn RuntimeTurnPhaseProbe>> {
241 let probes = self.probes.lock().expect("runtime phase probe slot");
242 probes.get(&scope.id()).cloned().or_else(|| {
243 probes
244 .get(&crate::SessionScope::new(&scope.session_id).id())
245 .cloned()
246 })
247 }
248}
249
250#[doc(hidden)]
251pub struct RuntimeNamedPhase {
252 probe: Option<Arc<dyn RuntimeTurnPhaseProbe>>,
253 phase: &'static str,
254}
255
256impl RuntimeNamedPhase {
257 pub fn begin(
258 probe: Option<Arc<dyn RuntimeTurnPhaseProbe>>,
259 phase: &'static str,
260 ) -> RuntimeNamedPhase {
261 if let Some(probe) = probe.as_ref() {
262 probe.begin_named(phase);
263 }
264 RuntimeNamedPhase { probe, phase }
265 }
266}
267
268impl Drop for RuntimeNamedPhase {
269 fn drop(&mut self) {
270 if let Some(probe) = self.probe.as_ref() {
271 probe.end_named(self.phase);
272 }
273 }
274}
275
276#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
278#[serde(tag = "type", rename_all = "snake_case")]
279pub enum InputItem {
280 Text { text: String },
281 ImageRef { id: String },
282}
283
284impl InputItem {
285 pub fn text(text: impl Into<String>) -> Self {
286 Self::Text { text: text.into() }
287 }
288
289 pub fn image_ref(id: impl Into<String>) -> Self {
290 Self::ImageRef { id: id.into() }
291 }
292}
293
294#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
296pub struct TurnInput {
297 pub items: Vec<InputItem>,
298 #[serde(default)]
299 pub image_blobs: HashMap<String, Vec<u8>>,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub protocol_turn_options: Option<crate::ProtocolTurnOptions>,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub trace_turn_id: Option<String>,
307 #[serde(skip)]
308 pub protocol_extension: Option<ProtocolTurnExtensionHandle>,
309 #[serde(skip)]
310 pub turn_context: TurnContext,
311}
312
313impl TurnInput {
314 pub fn empty() -> Self {
315 Self::items(std::iter::empty())
316 }
317
318 pub fn text(text: impl Into<String>) -> Self {
319 Self::items([InputItem::text(text)])
320 }
321
322 pub fn items(items: impl IntoIterator<Item = InputItem>) -> Self {
323 Self {
324 items: items.into_iter().collect(),
325 image_blobs: HashMap::new(),
326 protocol_turn_options: None,
327 trace_turn_id: None,
328 protocol_extension: None,
329 turn_context: TurnContext::default(),
330 }
331 }
332
333 pub fn with_image_blob(mut self, id: impl Into<String>, bytes: Vec<u8>) -> Self {
334 self.image_blobs.insert(id.into(), bytes);
335 self
336 }
337
338 pub fn with_image_blobs<I, K>(mut self, image_blobs: I) -> Self
339 where
340 I: IntoIterator<Item = (K, Vec<u8>)>,
341 K: Into<String>,
342 {
343 self.image_blobs.extend(
344 image_blobs
345 .into_iter()
346 .map(|(id, bytes)| (id.into(), bytes)),
347 );
348 self
349 }
350
351 pub fn with_image_ref(mut self, id: impl Into<String>, bytes: Vec<u8>) -> Self {
352 let id = id.into();
353 self.items.push(InputItem::image_ref(id.clone()));
354 self.image_blobs.insert(id, bytes);
355 self
356 }
357
358 pub fn with_protocol_turn_options(mut self, options: crate::ProtocolTurnOptions) -> Self {
359 self.protocol_turn_options = Some(options);
360 self
361 }
362
363 pub fn with_trace_turn_id(mut self, trace_turn_id: impl Into<String>) -> Self {
364 self.trace_turn_id = Some(trace_turn_id.into());
365 self
366 }
367}
368
369#[derive(Clone, Default)]
379pub struct LiveTurnInputs {
380 inputs: HashMap<&'static str, Arc<dyn Any + Send + Sync>>,
381}
382
383impl LiveTurnInputs {
384 fn insert<T>(&mut self, plugin_id: &'static str, input: T)
385 where
386 T: Send + Sync + 'static,
387 {
388 self.inputs.insert(plugin_id, Arc::new(input));
389 }
390
391 fn get<T>(&self, plugin_id: &'static str) -> Option<&T>
392 where
393 T: 'static,
394 {
395 self.inputs
396 .get(plugin_id)
397 .and_then(|input| input.downcast_ref::<T>())
398 }
399
400 fn contains(&self, plugin_id: &'static str) -> bool {
401 self.inputs.contains_key(plugin_id)
402 }
403
404 pub fn plugin_ids(&self) -> Vec<&'static str> {
405 self.inputs.keys().copied().collect()
406 }
407
408 pub(crate) fn durable_effect_rejection(&self) -> Result<(), RuntimeError> {
411 if self.inputs.is_empty() {
412 return Ok(());
413 }
414 Err(RuntimeError::new(
415 RuntimeErrorCode::DurableEffectLivePluginInput,
416 "durable effect hosts do not support live TurnContext plugin inputs; encode replayable data in protocol_turn_options or persisted plugin state",
417 ))
418 }
419}
420
421#[derive(Clone, Default)]
422pub struct TurnContext {
423 plugin_inputs: LiveTurnInputs,
424 provider: Option<crate::ProviderHandle>,
425 prompt: crate::PromptLayer,
426 local_cancel_origin: TurnCancelOriginHint,
427}
428
429impl TurnContext {
430 pub fn new() -> Self {
431 Self::default()
432 }
433
434 pub fn insert_plugin_input<T>(&mut self, plugin_id: &'static str, input: T)
435 where
436 T: Send + Sync + 'static,
437 {
438 self.plugin_inputs.insert(plugin_id, input);
439 }
440
441 pub fn set_provider(&mut self, provider: crate::ProviderHandle) {
442 self.provider = Some(provider);
443 }
444
445 pub fn provider(&self) -> Option<&crate::ProviderHandle> {
446 self.provider.as_ref()
447 }
448
449 #[doc(hidden)]
450 pub fn set_local_cancel_origin_hint(&mut self, hint: TurnCancelOriginHint) {
451 self.local_cancel_origin = hint;
452 }
453
454 pub(crate) fn local_cancel_origin_hint(&self) -> TurnCancelOriginHint {
455 self.local_cancel_origin.clone()
456 }
457
458 pub fn plugin_input<T>(&self, plugin_id: &'static str) -> Option<&T>
459 where
460 T: 'static,
461 {
462 self.plugin_inputs.get(plugin_id)
463 }
464
465 pub fn has_plugin_input(&self, plugin_id: &'static str) -> bool {
466 self.plugin_inputs.contains(plugin_id)
467 }
468
469 pub fn has_live_plugin_inputs(&self) -> bool {
470 !self.plugin_inputs.inputs.is_empty()
471 }
472
473 pub fn live_plugin_input_ids(&self) -> Vec<&'static str> {
474 self.plugin_inputs.plugin_ids()
475 }
476
477 pub(crate) fn live_plugin_inputs(&self) -> &LiveTurnInputs {
480 &self.plugin_inputs
481 }
482
483 pub fn set_prompt_template(&mut self, template: crate::PromptTemplate) {
484 self.prompt.template = Some(template);
485 }
486
487 pub fn add_prompt_contribution(&mut self, contribution: crate::PromptContribution) {
488 self.prompt.add_contribution(contribution);
489 }
490
491 pub fn replace_prompt_slot(
492 &mut self,
493 slot: crate::PromptSlot,
494 contributions: impl IntoIterator<Item = crate::PromptContribution>,
495 ) {
496 self.prompt.replace_slot(slot, contributions);
497 }
498
499 pub fn clear_prompt_slot(&mut self, slot: crate::PromptSlot) {
500 self.prompt.clear_slot(slot);
501 }
502
503 pub fn set_prompt_layer(&mut self, prompt: crate::PromptLayer) {
504 self.prompt = prompt;
505 }
506
507 pub fn prompt_layer(&self) -> &crate::PromptLayer {
508 &self.prompt
509 }
510}
511
512impl fmt::Debug for TurnContext {
513 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514 f.debug_struct("TurnContext")
515 .field("plugin_inputs", &self.plugin_inputs.plugin_ids())
516 .field("has_provider", &self.provider.is_some())
517 .field("has_prompt_layer", &(!self.prompt.is_empty()))
518 .finish()
519 }
520}
521
522#[derive(Clone)]
523pub struct ProtocolTurnExtensionHandle(Arc<dyn ProtocolTurnExtension>);
524
525impl ProtocolTurnExtensionHandle {
526 pub fn new(extension: impl ProtocolTurnExtension + 'static) -> Self {
527 Self(Arc::new(extension))
528 }
529
530 pub fn as_any(&self) -> &dyn Any {
531 self.0.as_any()
532 }
533
534 pub fn prompt_contributions(&self) -> Vec<crate::PromptContribution> {
535 self.0.prompt_contributions()
536 }
537}
538
539impl fmt::Debug for ProtocolTurnExtensionHandle {
540 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
541 f.write_str("ProtocolTurnExtensionHandle(..)")
542 }
543}
544
545pub trait ProtocolTurnExtension: Send + Sync {
546 fn as_any(&self) -> &dyn Any;
547
548 fn prompt_contributions(&self) -> Vec<crate::PromptContribution> {
549 Vec::new()
550 }
551}
552
553#[derive(Clone)]
554pub struct ProtocolSessionExtensionHandle(Arc<dyn ProtocolSessionExtension>);
555
556impl ProtocolSessionExtensionHandle {
557 pub fn new(extension: impl ProtocolSessionExtension + 'static) -> Self {
558 Self(Arc::new(extension))
559 }
560
561 pub fn as_any(&self) -> &dyn Any {
562 self.0.as_any()
563 }
564}
565
566impl fmt::Debug for ProtocolSessionExtensionHandle {
567 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
568 f.write_str("ProtocolSessionExtensionHandle(..)")
569 }
570}
571
572pub trait ProtocolSessionExtension: Send + Sync {
573 fn as_any(&self) -> &dyn Any;
574}
575
576#[derive(Clone, Debug)]
577pub(super) enum NormalizedItem {
578 Text(String),
579 Image(crate::AttachmentRef),
580}
581
582#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
584pub struct AssistantOutput {
585 pub safe_text: String,
586 pub raw_text: String,
587 pub state: OutputState,
588}
589
590#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
592#[serde(rename_all = "snake_case")]
593pub enum OutputState {
594 Usable,
595 EmptyOutput,
596 TracebackOnly,
597 RecoveredFromError,
598}
599
600#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
602pub struct CodeOutputRecord {
603 pub output: String,
604 #[serde(default, skip_serializing_if = "Option::is_none")]
605 pub error: Option<String>,
606}
607
608#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
610pub struct ExecutionSummary {
611 #[serde(default)]
612 pub had_tool_calls: bool,
613 #[serde(default)]
614 pub had_code_execution: bool,
615 #[serde(default)]
621 pub started_at_ms: u64,
622 #[serde(default)]
626 pub duration_ms: u64,
627}
628
629#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
631pub struct TurnIssue {
632 pub kind: String,
633 #[serde(default, skip_serializing_if = "Option::is_none")]
634 pub code: Option<String>,
635 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub terminal_reason: Option<crate::LlmTerminalReason>,
637 pub message: String,
638 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub raw: Option<String>,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
645 pub retryable: Option<bool>,
646 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub provider_failure_kind: Option<crate::ProviderFailureKind>,
650}
651
652#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
654pub struct AssembledTurn {
655 pub state: SessionSnapshot,
656 pub outcome: crate::TurnOutcome,
657 #[serde(default, skip_serializing_if = "Option::is_none")]
659 pub cancellation: Option<TurnCancellationEvidence>,
660 pub assistant_output: AssistantOutput,
661 pub execution: ExecutionSummary,
662 #[serde(default)]
663 pub token_usage: TokenUsage,
664 #[serde(default)]
669 pub children_usage: Vec<TokenLedgerEntry>,
670 #[serde(default)]
673 pub llm_calls: Vec<crate::LlmCallRecord>,
674 #[serde(default)]
675 pub tool_calls: Vec<ToolCallRecord>,
676 #[serde(default)]
677 pub errors: Vec<TurnIssue>,
678}
679
680#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
686pub struct AgentFrameRun {
687 pub turns: Vec<AssembledTurn>,
688}
689
690impl AgentFrameRun {
691 pub fn final_turn(&self) -> Option<&AssembledTurn> {
692 self.turns.last()
693 }
694
695 pub fn into_final_turn(mut self) -> Option<AssembledTurn> {
696 self.turns.pop()
697 }
698
699 pub fn frame_switch_count(&self) -> usize {
700 self.turns
701 .iter()
702 .filter(|turn| matches!(turn.outcome, crate::TurnOutcome::AgentFrameSwitch { .. }))
703 .count()
704 }
705}
706
707#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
709pub struct TerminationPolicy {
710 #[serde(default)]
711 pub treat_missing_done_as_failure: bool,
712}
713
714impl Default for TerminationPolicy {
715 fn default() -> Self {
716 Self {
717 treat_missing_done_as_failure: true,
718 }
719 }
720}
721
722#[async_trait::async_trait]
725pub trait EventSink: Send + Sync {
726 fn is_noop(&self) -> bool {
727 false
728 }
729
730 async fn emit(&self, event: SessionStreamEvent);
731}
732
733pub struct NoopEventSink;
735
736pub static NOOP_EVENT_SINK: NoopEventSink = NoopEventSink;
738
739#[async_trait::async_trait]
740impl EventSink for NoopEventSink {
741 fn is_noop(&self) -> bool {
742 true
743 }
744
745 async fn emit(&self, _event: SessionStreamEvent) {}
746}
747
748#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
750#[serde(transparent)]
751pub struct TurnActivityId(pub String);
752
753impl TurnActivityId {
754 pub fn new(id: impl Into<String>) -> Self {
755 Self(id.into())
756 }
757
758 pub fn fresh() -> Self {
759 Self(uuid::Uuid::new_v4().to_string())
760 }
761}
762
763#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
769pub struct TurnActivity {
770 pub id: TurnActivityId,
771 pub correlation_id: TurnActivityId,
772 #[serde(flatten)]
773 pub event: TurnEvent,
774}
775
776impl TurnActivity {
777 pub fn new(correlation_id: TurnActivityId, event: TurnEvent) -> Self {
778 Self {
779 id: TurnActivityId::fresh(),
780 correlation_id,
781 event,
782 }
783 }
784
785 pub fn independent(event: TurnEvent) -> Self {
786 let correlation_id = TurnActivityId::fresh();
787 Self::new(correlation_id, event)
788 }
789}
790
791#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
797#[serde(tag = "type", rename_all = "snake_case")]
798#[allow(clippy::large_enum_variant)]
799pub enum TurnEvent {
800 QueuedWorkStarted {
801 boundary: crate::QueuedWorkClaimBoundary,
802 batch_ids: Vec<String>,
803 causes: Vec<crate::TurnCause>,
804 },
805 ModelRequestStarted {
806 protocol_iteration: usize,
807 },
808 AssistantProseDelta {
809 text: String,
810 },
811 ReasoningDelta {
812 text: String,
813 },
814 CodeBlockStarted {
815 language: String,
816 code: String,
817 #[serde(default, skip_serializing_if = "Option::is_none")]
818 graph_key: Option<String>,
819 },
820 CodeBlockCompleted {
821 language: String,
822 output: String,
823 #[serde(default, skip_serializing_if = "Option::is_none")]
824 error: Option<String>,
825 success: bool,
826 duration_ms: u64,
827 tool_call_ids: Vec<String>,
828 #[serde(default, skip_serializing_if = "Option::is_none")]
829 graph_key: Option<String>,
830 },
831 ToolCallStarted {
832 #[serde(default, skip_serializing_if = "Option::is_none")]
833 call_id: Option<String>,
834 name: String,
835 args: serde_json::Value,
836 #[serde(default, skip_serializing_if = "Option::is_none")]
839 graph_key: Option<String>,
840 #[serde(default, skip_serializing_if = "Option::is_none")]
843 parent_call_id: Option<String>,
844 },
845 ToolCallCompleted {
846 #[serde(default, skip_serializing_if = "Option::is_none")]
847 call_id: Option<String>,
848 name: String,
849 args: serde_json::Value,
850 output: crate::ToolCallOutput,
851 duration_ms: u64,
852 #[serde(default, skip_serializing_if = "Option::is_none")]
855 graph_key: Option<String>,
856 #[serde(default, skip_serializing_if = "Option::is_none")]
859 parent_call_id: Option<String>,
860 },
861 FinalValue {
862 value: serde_json::Value,
863 },
864 ToolValue {
865 tool_name: String,
866 value: serde_json::Value,
867 },
868 Usage {
869 protocol_iteration: usize,
870 usage: TokenUsage,
871 cumulative: TokenUsage,
872 },
873 ChildUsage {
874 session_id: String,
875 source: String,
876 model: String,
877 protocol_iteration: usize,
878 usage: TokenUsage,
879 cumulative: TokenUsage,
880 },
881 RetryStatus {
882 wait_seconds: u64,
883 attempt: usize,
884 max_attempts: usize,
885 reason: String,
886 },
887 PluginRuntime {
888 plugin_id: String,
889 event: crate::PluginRuntimeEvent,
890 },
891 QueuedInputAccepted {
892 checkpoint: crate::CheckpointKind,
893 inputs: Vec<crate::AcceptedInjectedTurnInput>,
894 },
895 QueuedMessagesCommitted {
896 messages: Vec<crate::PluginMessage>,
897 checkpoint: crate::CheckpointKind,
898 },
899 Error {
900 message: String,
901 },
902}
903
904#[async_trait::async_trait]
905pub trait TurnActivitySink: Send + Sync {
906 fn is_noop(&self) -> bool {
907 false
908 }
909
910 async fn emit(&self, activity: TurnActivity);
911}
912
913pub struct NoopTurnActivitySink;
914
915pub static NOOP_TURN_ACTIVITY_SINK: NoopTurnActivitySink = NoopTurnActivitySink;
917
918#[async_trait::async_trait]
919impl TurnActivitySink for NoopTurnActivitySink {
920 fn is_noop(&self) -> bool {
921 true
922 }
923
924 async fn emit(&self, _activity: TurnActivity) {}
925}
926
927pub struct TurnOptions<'a> {
935 events: Option<&'a dyn EventSink>,
936 turn_events: Option<&'a dyn TurnActivitySink>,
937 scoped_effect_controller: ScopedEffectController<'a>,
938 cancel: CancellationToken,
939 local_cancel_origin: Option<TurnCancelOriginHint>,
940}
941
942impl<'a> TurnOptions<'a> {
943 pub fn new(
944 cancel: CancellationToken,
945 scoped_effect_controller: ScopedEffectController<'a>,
946 ) -> Self {
947 Self {
948 events: None,
949 turn_events: None,
950 scoped_effect_controller,
951 cancel,
952 local_cancel_origin: None,
953 }
954 }
955
956 pub fn with_events(mut self, events: &'a dyn EventSink) -> Self {
957 self.events = Some(events);
958 self
959 }
960
961 pub fn with_turn_events(mut self, turn_events: &'a dyn TurnActivitySink) -> Self {
962 self.turn_events = Some(turn_events);
963 self
964 }
965
966 #[doc(hidden)]
967 pub fn with_local_cancel_origin_hint(mut self, hint: TurnCancelOriginHint) -> Self {
968 self.local_cancel_origin = Some(hint);
969 self
970 }
971
972 pub(crate) fn local_cancel_origin_hint(&self) -> Option<TurnCancelOriginHint> {
973 self.local_cancel_origin.clone()
974 }
975
976 pub(crate) fn events_or_noop(&self) -> &'a dyn EventSink {
977 self.events.unwrap_or(&NOOP_EVENT_SINK)
978 }
979
980 pub(crate) fn turn_events_or_noop(&self) -> &'a dyn TurnActivitySink {
981 self.turn_events.unwrap_or(&NOOP_TURN_ACTIVITY_SINK)
982 }
983
984 pub(crate) fn execution_scope_id(&self) -> &str {
985 self.scoped_effect_controller.scope_id()
986 }
987
988 pub(crate) fn scoped_effect_controller(&self) -> ScopedEffectController<'a> {
989 self.scoped_effect_controller.clone()
990 }
991}
992
993enum RuntimeStreamEvent {
994 Session(SessionStreamEvent),
995 Turn(TurnActivity),
996}
997
998#[derive(Clone)]
999pub struct SessionStoreCreateRequest {
1000 pub session_id: String,
1001 pub relation: SessionRelation,
1002 pub policy: SessionPolicy,
1003}
1004
1005impl SessionStoreCreateRequest {
1006 pub fn parent_session_id(&self) -> Option<&str> {
1007 self.relation.parent_session_id()
1008 }
1009}
1010
1011#[async_trait::async_trait]
1012pub trait SessionStoreFactory: Send + Sync {
1013 fn durability_tier(&self) -> crate::DurabilityTier {
1016 crate::DurabilityTier::Inline
1017 }
1018
1019 async fn create_store(
1020 &self,
1021 request: &SessionStoreCreateRequest,
1022 ) -> Result<Arc<dyn crate::store::RuntimePersistence>, String>;
1023
1024 async fn open_existing_store(
1025 &self,
1026 _request: &SessionStoreCreateRequest,
1027 ) -> Result<Option<Arc<dyn crate::store::RuntimePersistence>>, String> {
1028 Ok(None)
1029 }
1030
1031 async fn delete_session(&self, session_id: &str) -> Result<(), String>;
1032
1033 async fn live_attachment_refs(
1044 &self,
1045 intent_grace_cutoff_epoch_ms: u64,
1046 ) -> Result<std::collections::BTreeSet<crate::AttachmentId>, crate::store::StoreError> {
1047 let _ = intent_grace_cutoff_epoch_ms;
1048 Ok(std::collections::BTreeSet::new())
1049 }
1050
1051 async fn has_live_attachment_ref(
1065 &self,
1066 attachment_id: &crate::AttachmentId,
1067 intent_grace_cutoff_epoch_ms: u64,
1068 ) -> Result<bool, crate::store::StoreError> {
1069 Ok(self
1070 .live_attachment_refs(intent_grace_cutoff_epoch_ms)
1071 .await?
1072 .contains(attachment_id))
1073 }
1074}
1075
1076pub struct LashRuntime {
1078 pub(in crate::runtime) session: Option<Session>,
1079 pub(in crate::runtime) policy: SessionPolicy,
1080 pub(in crate::runtime) host: RuntimeHost,
1081 pub(in crate::runtime) services: RuntimeServices,
1082 pub(in crate::runtime) state: RuntimeSessionState,
1083 pub(in crate::runtime) runtime_scope_id: Arc<str>,
1084 pub(in crate::runtime) runtime_lease_owner: crate::LeaseOwnerIdentity,
1085 pub(in crate::runtime) managed_sessions: Arc<Mutex<HashMap<String, RuntimeHandle>>>,
1086 pub(in crate::runtime) managed_turns: Arc<Mutex<HashMap<String, ManagedSessionTurn>>>,
1087 pub(in crate::runtime) protocol_turn_options: crate::ProtocolTurnOptions,
1089 pub(in crate::runtime) shared_token_ledger: Arc<std::sync::Mutex<Vec<TokenLedgerEntry>>>,
1094 pub(in crate::runtime) process_sync_needed: Arc<AtomicBool>,
1095 pub(in crate::runtime) turn_phase_probe: Option<Arc<dyn RuntimeTurnPhaseProbe>>,
1096 pub(in crate::runtime) residency: Residency,
1101}