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