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