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