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