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::{QueuedWorkDriver, QueuedWorkRunHandle, QueuedWorkRunRequest};
167pub use scenario_contracts::{RUNTIME_SCENARIO_CONTRACTS, ScenarioContractSpec};
168pub use session_manager::DirectCompletionClient;
169pub use state::RuntimeSessionState;
170use state::{
171 append_session_nodes_to_state_with_clock, apply_residency_on_load, apply_session_checkpoint,
172 apply_session_head, normalize_session_graph, open_agent_frame_in_state_with_clock,
173};
174pub use turn_control::{
175 TurnAddress, TurnAttach, TurnCancelOriginHint, TurnCancelOutcome, TurnCancelReceipt,
176 TurnCancelRequest, TurnCancellationEvidence, TurnTerminal, TurnWorkDriver,
177};
178pub use turn_input_ingress::{
179 PendingTurnInput, PendingTurnInputCancelOutcome, PendingTurnInputCancelResult,
180 PendingTurnInputCancelTarget, PendingTurnInputClaimDiagnostics, PendingTurnInputDraft,
181 PendingTurnInputSuffixCancelOutcome, QueuedCheckpointTurnInput, TurnInputCheckpointBoundary,
182 TurnInputClaim, TurnInputClaimMode, TurnInputCompletion, TurnInputIngress, TurnInputState,
183};
184pub use turn_loop::ensure_durable_effect_input;
185pub use turn_queue::{
186 DeliveryPolicy, MergeKey, QueuedCheckpointWork, QueuedTurnWork, QueuedWorkBatch,
187 QueuedWorkBatchDraft, QueuedWorkClaim, QueuedWorkClaimBoundary, QueuedWorkClass,
188 QueuedWorkCompletion, QueuedWorkItem, QueuedWorkPayload, SessionCommand, SessionCommandReceipt,
189 SlotPolicy, process_wake_batch_draft,
190};
191pub use usage::{
192 SessionUsageReport, TokenLedgerEntry, UsageReportRow, UsageTotals, diff_token_ledger,
193 diff_usage_reports,
194};
195use usage::{merge_ledger_entry, merge_usage_delta_entries, normalize_prompt_usage};
196
197#[doc(hidden)]
198#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
199pub enum RuntimeTurnPhase {
200 ContextTransform,
201 BeforeTurnHooks,
202 PromptBuild,
203 EffectLoop,
204 FinalizeTurn,
205 PersistTurn,
206 FinalCommit,
207 PostPersistHooks,
208}
209
210#[doc(hidden)]
211pub trait RuntimeTurnPhaseProbe: Send + Sync {
212 fn begin(&self, phase: RuntimeTurnPhase);
213 fn end(&self, phase: RuntimeTurnPhase);
214 fn begin_named(&self, _phase: &str) {}
215 fn end_named(&self, _phase: &str) {}
216}
217
218#[doc(hidden)]
219#[derive(Clone, Default)]
220pub struct RuntimeTurnPhaseProbeSlot {
221 probes: Arc<StdMutex<HashMap<crate::SessionScopeId, Arc<dyn RuntimeTurnPhaseProbe>>>>,
222}
223
224impl RuntimeTurnPhaseProbeSlot {
225 pub fn set_for_session(
226 &self,
227 session_id: impl Into<String>,
228 probe: Arc<dyn RuntimeTurnPhaseProbe>,
229 ) {
230 self.set_for_scope(&crate::SessionScope::new(session_id), probe);
231 }
232
233 pub fn set_for_scope(
234 &self,
235 scope: &crate::SessionScope,
236 probe: Arc<dyn RuntimeTurnPhaseProbe>,
237 ) {
238 self.probes
239 .lock()
240 .expect("runtime phase probe slot")
241 .insert(scope.id(), probe);
242 }
243
244 pub fn get_for_scope(
245 &self,
246 scope: &crate::SessionScope,
247 ) -> Option<Arc<dyn RuntimeTurnPhaseProbe>> {
248 let probes = self.probes.lock().expect("runtime phase probe slot");
249 probes.get(&scope.id()).cloned().or_else(|| {
250 probes
251 .get(&crate::SessionScope::new(&scope.session_id).id())
252 .cloned()
253 })
254 }
255}
256
257#[doc(hidden)]
258pub struct RuntimeNamedPhase {
259 probe: Option<Arc<dyn RuntimeTurnPhaseProbe>>,
260 phase: &'static str,
261}
262
263impl RuntimeNamedPhase {
264 pub fn begin(
265 probe: Option<Arc<dyn RuntimeTurnPhaseProbe>>,
266 phase: &'static str,
267 ) -> RuntimeNamedPhase {
268 if let Some(probe) = probe.as_ref() {
269 probe.begin_named(phase);
270 }
271 RuntimeNamedPhase { probe, phase }
272 }
273}
274
275impl Drop for RuntimeNamedPhase {
276 fn drop(&mut self) {
277 if let Some(probe) = self.probe.as_ref() {
278 probe.end_named(self.phase);
279 }
280 }
281}
282
283#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
285#[serde(tag = "type", rename_all = "snake_case")]
286pub enum InputItem {
287 Text { text: String },
288 Attachment { source: crate::AttachmentSource },
289}
290
291impl InputItem {
292 pub fn text(text: impl Into<String>) -> Self {
293 Self::Text { text: text.into() }
294 }
295
296 pub fn attachment(source: crate::AttachmentSource) -> Self {
297 Self::Attachment { source }
298 }
299}
300
301#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
303pub struct TurnInput {
304 pub items: Vec<InputItem>,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub protocol_turn_options: Option<crate::ProtocolTurnOptions>,
308 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub trace_turn_id: Option<String>,
312 #[serde(skip)]
313 pub protocol_extension: Option<ProtocolTurnExtensionHandle>,
314 #[serde(skip)]
315 pub turn_context: TurnContext,
316}
317
318impl TurnInput {
319 pub fn empty() -> Self {
320 Self::items(std::iter::empty())
321 }
322
323 pub fn text(text: impl Into<String>) -> Self {
324 Self::items([InputItem::text(text)])
325 }
326
327 pub fn items(items: impl IntoIterator<Item = InputItem>) -> Self {
328 Self {
329 items: items.into_iter().collect(),
330 protocol_turn_options: None,
331 trace_turn_id: None,
332 protocol_extension: None,
333 turn_context: TurnContext::default(),
334 }
335 }
336
337 pub fn with_attachment(mut self, source: crate::AttachmentSource) -> Self {
338 self.items.push(InputItem::attachment(source));
339 self
340 }
341
342 pub fn with_protocol_turn_options(mut self, options: crate::ProtocolTurnOptions) -> Self {
343 self.protocol_turn_options = Some(options);
344 self
345 }
346
347 pub fn with_trace_turn_id(mut self, trace_turn_id: impl Into<String>) -> Self {
348 self.trace_turn_id = Some(trace_turn_id.into());
349 self
350 }
351}
352
353#[derive(Clone, Default)]
363pub struct LiveTurnInputs {
364 inputs: HashMap<&'static str, Arc<dyn Any + Send + Sync>>,
365}
366
367impl LiveTurnInputs {
368 fn insert<T>(&mut self, plugin_id: &'static str, input: T)
369 where
370 T: Send + Sync + 'static,
371 {
372 self.inputs.insert(plugin_id, Arc::new(input));
373 }
374
375 fn get<T>(&self, plugin_id: &'static str) -> Option<&T>
376 where
377 T: 'static,
378 {
379 self.inputs
380 .get(plugin_id)
381 .and_then(|input| input.downcast_ref::<T>())
382 }
383
384 fn contains(&self, plugin_id: &'static str) -> bool {
385 self.inputs.contains_key(plugin_id)
386 }
387
388 pub fn plugin_ids(&self) -> Vec<&'static str> {
389 self.inputs.keys().copied().collect()
390 }
391
392 pub(crate) fn durable_effect_rejection(&self) -> Result<(), RuntimeError> {
395 if self.inputs.is_empty() {
396 return Ok(());
397 }
398 Err(RuntimeError::new(
399 RuntimeErrorCode::DurableEffectLivePluginInput,
400 "durable effect hosts do not support live TurnContext plugin inputs; encode replayable data in protocol_turn_options or persisted plugin state",
401 ))
402 }
403}
404
405#[derive(Clone, Default)]
406pub struct TurnContext {
407 plugin_inputs: LiveTurnInputs,
408 provider: Option<crate::ProviderHandle>,
409 prompt: crate::PromptLayer,
410 local_cancel_origin: TurnCancelOriginHint,
411}
412
413impl TurnContext {
414 pub fn new() -> Self {
415 Self::default()
416 }
417
418 pub fn insert_plugin_input<T>(&mut self, plugin_id: &'static str, input: T)
419 where
420 T: Send + Sync + 'static,
421 {
422 self.plugin_inputs.insert(plugin_id, input);
423 }
424
425 pub fn set_provider(&mut self, provider: crate::ProviderHandle) {
426 self.provider = Some(provider);
427 }
428
429 pub fn provider(&self) -> Option<&crate::ProviderHandle> {
430 self.provider.as_ref()
431 }
432
433 #[doc(hidden)]
434 pub fn set_local_cancel_origin_hint(&mut self, hint: TurnCancelOriginHint) {
435 self.local_cancel_origin = hint;
436 }
437
438 pub(crate) fn local_cancel_origin_hint(&self) -> TurnCancelOriginHint {
439 self.local_cancel_origin.clone()
440 }
441
442 pub fn plugin_input<T>(&self, plugin_id: &'static str) -> Option<&T>
443 where
444 T: 'static,
445 {
446 self.plugin_inputs.get(plugin_id)
447 }
448
449 pub fn has_plugin_input(&self, plugin_id: &'static str) -> bool {
450 self.plugin_inputs.contains(plugin_id)
451 }
452
453 pub fn has_live_plugin_inputs(&self) -> bool {
454 !self.plugin_inputs.inputs.is_empty()
455 }
456
457 pub fn live_plugin_input_ids(&self) -> Vec<&'static str> {
458 self.plugin_inputs.plugin_ids()
459 }
460
461 pub(crate) fn live_plugin_inputs(&self) -> &LiveTurnInputs {
464 &self.plugin_inputs
465 }
466
467 pub fn set_prompt_template(&mut self, template: crate::PromptTemplate) {
468 self.prompt.template = Some(template);
469 }
470
471 pub fn add_prompt_contribution(&mut self, contribution: crate::PromptContribution) {
472 self.prompt.add_contribution(contribution);
473 }
474
475 pub fn replace_prompt_slot(
476 &mut self,
477 slot: crate::PromptSlot,
478 contributions: impl IntoIterator<Item = crate::PromptContribution>,
479 ) {
480 self.prompt.replace_slot(slot, contributions);
481 }
482
483 pub fn clear_prompt_slot(&mut self, slot: crate::PromptSlot) {
484 self.prompt.clear_slot(slot);
485 }
486
487 pub fn set_prompt_layer(&mut self, prompt: crate::PromptLayer) {
488 self.prompt = prompt;
489 }
490
491 pub fn prompt_layer(&self) -> &crate::PromptLayer {
492 &self.prompt
493 }
494}
495
496impl fmt::Debug for TurnContext {
497 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
498 f.debug_struct("TurnContext")
499 .field("plugin_inputs", &self.plugin_inputs.plugin_ids())
500 .field("has_provider", &self.provider.is_some())
501 .field("has_prompt_layer", &(!self.prompt.is_empty()))
502 .finish()
503 }
504}
505
506#[derive(Clone)]
507pub struct ProtocolTurnExtensionHandle(Arc<dyn ProtocolTurnExtension>);
508
509impl ProtocolTurnExtensionHandle {
510 pub fn new(extension: impl ProtocolTurnExtension + 'static) -> Self {
511 Self(Arc::new(extension))
512 }
513
514 pub fn as_any(&self) -> &dyn Any {
515 self.0.as_any()
516 }
517
518 pub fn prompt_contributions(&self) -> Vec<crate::PromptContribution> {
519 self.0.prompt_contributions()
520 }
521}
522
523impl fmt::Debug for ProtocolTurnExtensionHandle {
524 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
525 f.write_str("ProtocolTurnExtensionHandle(..)")
526 }
527}
528
529pub trait ProtocolTurnExtension: Send + Sync {
530 fn as_any(&self) -> &dyn Any;
531
532 fn prompt_contributions(&self) -> Vec<crate::PromptContribution> {
533 Vec::new()
534 }
535}
536
537#[derive(Clone)]
538pub struct ProtocolSessionExtensionHandle(Arc<dyn ProtocolSessionExtension>);
539
540impl ProtocolSessionExtensionHandle {
541 pub fn new(extension: impl ProtocolSessionExtension + 'static) -> Self {
542 Self(Arc::new(extension))
543 }
544
545 pub fn as_any(&self) -> &dyn Any {
546 self.0.as_any()
547 }
548}
549
550impl fmt::Debug for ProtocolSessionExtensionHandle {
551 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
552 f.write_str("ProtocolSessionExtensionHandle(..)")
553 }
554}
555
556pub trait ProtocolSessionExtension: Send + Sync {
557 fn as_any(&self) -> &dyn Any;
558}
559
560#[derive(Clone, Debug)]
561pub(super) enum NormalizedItem {
562 Text(String),
563 Attachment(crate::AttachmentSource),
564}
565
566#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
568pub struct AssistantOutput {
569 pub safe_text: String,
570 pub raw_text: String,
571 pub state: OutputState,
572}
573
574#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
576#[serde(rename_all = "snake_case")]
577pub enum OutputState {
578 Usable,
579 EmptyOutput,
580 TracebackOnly,
581 RecoveredFromError,
582}
583
584#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
586pub struct CodeOutputRecord {
587 pub output: String,
588 #[serde(default, skip_serializing_if = "Option::is_none")]
589 pub error: Option<String>,
590}
591
592#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
594pub struct ExecutionSummary {
595 #[serde(default)]
596 pub had_tool_calls: bool,
597 #[serde(default)]
598 pub had_code_execution: bool,
599 #[serde(default)]
605 pub started_at_ms: u64,
606 #[serde(default)]
610 pub duration_ms: u64,
611}
612
613#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
615pub struct TurnIssue {
616 pub kind: String,
617 #[serde(default, skip_serializing_if = "Option::is_none")]
618 pub code: Option<String>,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
620 pub terminal_reason: Option<crate::LlmTerminalReason>,
621 pub message: String,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
623 pub raw: Option<String>,
624 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub retryable: Option<bool>,
630 #[serde(default, skip_serializing_if = "Option::is_none")]
633 pub provider_failure_kind: Option<crate::ProviderFailureKind>,
634}
635
636#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
638pub struct AssembledTurn {
639 pub state: SessionSnapshot,
640 pub outcome: crate::TurnOutcome,
641 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub cancellation: Option<TurnCancellationEvidence>,
644 pub assistant_output: AssistantOutput,
645 pub execution: ExecutionSummary,
646 #[serde(default)]
647 pub token_usage: TokenUsage,
648 #[serde(default)]
653 pub children_usage: Vec<TokenLedgerEntry>,
654 #[serde(default)]
657 pub llm_calls: Vec<crate::LlmCallRecord>,
658 #[serde(default)]
659 pub tool_calls: Vec<ToolCallRecord>,
660 #[serde(default)]
661 pub errors: Vec<TurnIssue>,
662}
663
664#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
670pub struct AgentFrameRun {
671 pub turns: Vec<AssembledTurn>,
672}
673
674impl AgentFrameRun {
675 pub fn final_turn(&self) -> Option<&AssembledTurn> {
676 self.turns.last()
677 }
678
679 pub fn into_final_turn(mut self) -> Option<AssembledTurn> {
680 self.turns.pop()
681 }
682
683 pub fn frame_switch_count(&self) -> usize {
684 self.turns
685 .iter()
686 .filter(|turn| matches!(turn.outcome, crate::TurnOutcome::AgentFrameSwitch { .. }))
687 .count()
688 }
689}
690
691#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
693pub struct TerminationPolicy {
694 #[serde(default)]
695 pub treat_missing_done_as_failure: bool,
696}
697
698impl Default for TerminationPolicy {
699 fn default() -> Self {
700 Self {
701 treat_missing_done_as_failure: true,
702 }
703 }
704}
705
706#[async_trait::async_trait]
709pub trait EventSink: Send + Sync {
710 fn is_noop(&self) -> bool {
711 false
712 }
713
714 async fn emit(&self, event: SessionStreamEvent);
715}
716
717pub struct NoopEventSink;
719
720pub static NOOP_EVENT_SINK: NoopEventSink = NoopEventSink;
722
723#[async_trait::async_trait]
724impl EventSink for NoopEventSink {
725 fn is_noop(&self) -> bool {
726 true
727 }
728
729 async fn emit(&self, _event: SessionStreamEvent) {}
730}
731
732#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
734#[serde(transparent)]
735pub struct TurnActivityId(pub Arc<str>);
736
737impl TurnActivityId {
738 pub fn new(id: impl Into<Arc<str>>) -> Self {
739 Self(id.into())
740 }
741
742 pub fn fresh() -> Self {
743 Self(Arc::from(uuid::Uuid::new_v4().to_string()))
744 }
745}
746
747#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
753pub struct TurnActivity {
754 pub id: TurnActivityId,
755 pub correlation_id: TurnActivityId,
756 #[serde(flatten)]
757 pub event: TurnEvent,
758}
759
760impl TurnActivity {
761 pub fn new(correlation_id: TurnActivityId, event: TurnEvent) -> Self {
762 Self {
763 id: TurnActivityId::fresh(),
764 correlation_id,
765 event,
766 }
767 }
768
769 pub fn independent(event: TurnEvent) -> Self {
770 let correlation_id = TurnActivityId::fresh();
771 Self::new(correlation_id, event)
772 }
773}
774
775#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
781#[serde(tag = "type", rename_all = "snake_case")]
782#[allow(clippy::large_enum_variant)]
784pub enum TurnEvent {
785 QueuedWorkStarted {
786 boundary: crate::QueuedWorkClaimBoundary,
787 batch_ids: Vec<String>,
788 causes: Vec<crate::TurnCause>,
789 },
790 ModelRequestStarted {
791 protocol_iteration: usize,
792 },
793 AssistantProseDelta {
794 text: Arc<str>,
795 },
796 ReasoningDelta {
797 text: Arc<str>,
798 },
799 ModelAttemptReset {
805 assistant_prose_correlation_ids: Vec<TurnActivityId>,
806 reasoning_correlation_ids: Vec<TurnActivityId>,
807 },
808 CodeBlockStarted {
809 language: String,
810 code: String,
811 #[serde(default, skip_serializing_if = "Option::is_none")]
812 graph_key: Option<String>,
813 },
814 CodeBlockCompleted {
815 language: String,
816 output: String,
817 #[serde(default, skip_serializing_if = "Option::is_none")]
818 error: Option<String>,
819 success: bool,
820 duration_ms: u64,
821 tool_call_ids: Vec<String>,
822 #[serde(default, skip_serializing_if = "Option::is_none")]
823 graph_key: Option<String>,
824 },
825 ToolCallStarted {
826 #[serde(default, skip_serializing_if = "Option::is_none")]
827 call_id: Option<String>,
828 name: String,
829 args: serde_json::Value,
830 #[serde(default, skip_serializing_if = "Option::is_none")]
833 graph_key: Option<String>,
834 #[serde(default, skip_serializing_if = "Option::is_none")]
837 parent_call_id: Option<String>,
838 },
839 ToolCallCompleted {
840 #[serde(default, skip_serializing_if = "Option::is_none")]
841 call_id: Option<String>,
842 name: String,
843 args: serde_json::Value,
844 output: crate::ToolCallOutput,
845 duration_ms: u64,
846 #[serde(default, skip_serializing_if = "Option::is_none")]
849 graph_key: Option<String>,
850 #[serde(default, skip_serializing_if = "Option::is_none")]
853 parent_call_id: Option<String>,
854 },
855 FinalValue {
856 value: serde_json::Value,
857 },
858 ToolValue {
859 tool_name: String,
860 value: serde_json::Value,
861 },
862 Usage {
863 protocol_iteration: usize,
864 usage: TokenUsage,
865 cumulative: TokenUsage,
866 },
867 ChildUsage {
868 session_id: String,
869 source: String,
870 model: String,
871 protocol_iteration: usize,
872 usage: TokenUsage,
873 cumulative: TokenUsage,
874 },
875 RetryStatus {
876 wait_seconds: u64,
877 attempt: usize,
878 max_attempts: usize,
879 reason: String,
880 },
881 PluginRuntime {
882 plugin_id: String,
883 event: crate::PluginRuntimeEvent,
884 },
885 QueuedInputAccepted {
886 checkpoint: crate::CheckpointKind,
887 inputs: Vec<crate::AcceptedInjectedTurnInput>,
888 },
889 QueuedMessagesCommitted {
890 messages: Vec<crate::PluginMessage>,
891 checkpoint: crate::CheckpointKind,
892 },
893 Error {
894 message: String,
895 },
896}
897
898#[async_trait::async_trait]
899pub trait TurnActivitySink: Send + Sync {
900 fn is_noop(&self) -> bool {
901 false
902 }
903
904 async fn emit(&self, activity: TurnActivity);
905}
906
907pub struct NoopTurnActivitySink;
908
909pub static NOOP_TURN_ACTIVITY_SINK: NoopTurnActivitySink = NoopTurnActivitySink;
911
912#[async_trait::async_trait]
913impl TurnActivitySink for NoopTurnActivitySink {
914 fn is_noop(&self) -> bool {
915 true
916 }
917
918 async fn emit(&self, _activity: TurnActivity) {}
919}
920
921pub struct TurnOptions<'a> {
929 events: Option<&'a dyn EventSink>,
930 turn_events: Option<&'a dyn TurnActivitySink>,
931 scoped_effect_controller: ScopedEffectController<'a>,
932 cancel: CancellationToken,
933 local_cancel_origin: Option<TurnCancelOriginHint>,
934}
935
936impl<'a> TurnOptions<'a> {
937 pub fn new(
938 cancel: CancellationToken,
939 scoped_effect_controller: ScopedEffectController<'a>,
940 ) -> Self {
941 Self {
942 events: None,
943 turn_events: None,
944 scoped_effect_controller,
945 cancel,
946 local_cancel_origin: None,
947 }
948 }
949
950 pub fn with_events(mut self, events: &'a dyn EventSink) -> Self {
951 self.events = Some(events);
952 self
953 }
954
955 pub fn with_turn_events(mut self, turn_events: &'a dyn TurnActivitySink) -> Self {
956 self.turn_events = Some(turn_events);
957 self
958 }
959
960 #[doc(hidden)]
961 pub fn with_local_cancel_origin_hint(mut self, hint: TurnCancelOriginHint) -> Self {
962 self.local_cancel_origin = Some(hint);
963 self
964 }
965
966 pub(crate) fn local_cancel_origin_hint(&self) -> Option<TurnCancelOriginHint> {
967 self.local_cancel_origin.clone()
968 }
969
970 pub(crate) fn events_or_noop(&self) -> &'a dyn EventSink {
971 self.events.unwrap_or(&NOOP_EVENT_SINK)
972 }
973
974 pub(crate) fn turn_events_or_noop(&self) -> &'a dyn TurnActivitySink {
975 self.turn_events.unwrap_or(&NOOP_TURN_ACTIVITY_SINK)
976 }
977
978 pub(crate) fn execution_scope_id(&self) -> &str {
979 self.scoped_effect_controller.scope_id()
980 }
981
982 pub(crate) fn scoped_effect_controller(&self) -> ScopedEffectController<'a> {
983 self.scoped_effect_controller.clone()
984 }
985}
986
987enum RuntimeStreamEvent {
988 Session(SessionStreamEvent),
989 Turn(TurnActivity),
990}
991
992#[derive(Clone)]
993pub struct SessionStoreCreateRequest {
994 pub session_id: String,
995 pub relation: SessionRelation,
996 pub policy: SessionPolicy,
997}
998
999impl SessionStoreCreateRequest {
1000 pub fn parent_session_id(&self) -> Option<&str> {
1001 self.relation.parent_session_id()
1002 }
1003}
1004
1005#[async_trait::async_trait]
1006pub trait SessionStoreFactory: Send + Sync {
1007 fn durability_tier(&self) -> crate::DurabilityTier {
1010 crate::DurabilityTier::Inline
1011 }
1012
1013 async fn create_store(
1014 &self,
1015 request: &SessionStoreCreateRequest,
1016 ) -> Result<Arc<dyn crate::store::RuntimePersistence>, String>;
1017
1018 async fn open_existing_store(
1019 &self,
1020 _request: &SessionStoreCreateRequest,
1021 ) -> Result<Option<Arc<dyn crate::store::RuntimePersistence>>, String> {
1022 Ok(None)
1023 }
1024
1025 async fn delete_session(&self, session_id: &str) -> Result<(), String>;
1026
1027 async fn live_attachment_refs(
1035 &self,
1036 intent_grace_cutoff_epoch_ms: u64,
1037 ) -> Result<std::collections::BTreeSet<crate::AttachmentId>, crate::store::StoreError> {
1038 let _ = intent_grace_cutoff_epoch_ms;
1039 Ok(std::collections::BTreeSet::new())
1040 }
1041
1042 async fn has_live_attachment_ref(
1056 &self,
1057 attachment_id: &crate::AttachmentId,
1058 intent_grace_cutoff_epoch_ms: u64,
1059 ) -> Result<bool, crate::store::StoreError> {
1060 Ok(self
1061 .live_attachment_refs(intent_grace_cutoff_epoch_ms)
1062 .await?
1063 .contains(attachment_id))
1064 }
1065}
1066
1067pub struct LashRuntime {
1069 pub(in crate::runtime) session: Option<Session>,
1070 pub(in crate::runtime) policy: SessionPolicy,
1071 pub(in crate::runtime) host: RuntimeHost,
1072 pub(in crate::runtime) services: RuntimeServices,
1073 pub(in crate::runtime) state: RuntimeSessionState,
1074 pub(in crate::runtime) runtime_scope_id: Arc<str>,
1075 pub(in crate::runtime) runtime_lease_owner: crate::LeaseOwnerIdentity,
1076 pub(in crate::runtime) managed_sessions: Arc<Mutex<HashMap<String, RuntimeHandle>>>,
1077 pub(in crate::runtime) managed_turns: Arc<Mutex<HashMap<String, ManagedSessionTurn>>>,
1078 pub(in crate::runtime) protocol_turn_options: crate::ProtocolTurnOptions,
1080 pub(in crate::runtime) shared_token_ledger: Arc<std::sync::Mutex<Vec<TokenLedgerEntry>>>,
1085 pub(in crate::runtime) process_sync_needed: Arc<AtomicBool>,
1086 pub(in crate::runtime) turn_phase_probe: Option<Arc<dyn RuntimeTurnPhaseProbe>>,
1087 pub(in crate::runtime) last_committed_lease_continuity:
1091 Option<session_execution_lease::SessionExecutionLeaseContinuity>,
1092 pub(in crate::runtime) graph_loaded_from_store: bool,
1094 pub(in crate::runtime) residency: Residency,
1099}