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