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