Skip to main content

lash_core/
lib.rs

1//! Runtime kernel for Lash.
2//!
3//! The process kernel intentionally understands `ToolCall`, `SessionTurn`, and
4//! `External` because those inputs carry runtime mechanisms core must enforce:
5//! tool orchestration, child-session turns, and externally completed work. New
6//! process runtimes should use `ProcessInput::Engine { kind, payload }` unless
7//! core must understand their semantics to enforce a kernel mechanism.
8//!
9//! Protocols follow the same boundary: core owns the `HostTurnProtocol` state
10//! shape and the `ProtocolDriverPlugin` slot, while external protocol crates
11//! provide the driver implementation.
12
13pub mod attachments;
14pub mod chronological;
15pub mod direct;
16pub mod llm;
17mod model;
18pub mod plugin;
19mod plugin_stack;
20mod protocol_build;
21pub mod provider;
22pub mod runtime;
23pub mod session;
24pub mod session_graph;
25pub mod session_model;
26mod stable_hash;
27pub mod store;
28#[cfg(any(test, feature = "testing"))]
29pub mod testing;
30pub mod tool_dispatch;
31mod tool_provider;
32pub mod tool_registry;
33mod tool_result;
34mod trace;
35pub mod triggers;
36
37pub use lash_sansio::sansio;
38
39pub const VERSION: &str = env!("CARGO_PKG_VERSION");
40pub const SANSIO_VERSION: &str = lash_sansio::VERSION;
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum DurabilityTier {
44    Inline,
45    Durable,
46}
47
48// Re-exports
49pub use attachments::{
50    AttachmentReclamationReport, AttachmentRootSet, AttachmentStore, AttachmentStoreError,
51    AttachmentStorePersistence, FileAttachmentStore, InMemoryAttachmentStore,
52    NoopAttachmentManifest, SessionAttachmentStore, StoredAttachment, StoredBlobRef,
53    reclaim_unreferenced_attachments,
54};
55pub use chronological::{
56    BorrowedChronologicalEntry, BorrowedChronologicalMessage, BorrowedChronologicalPayload,
57    ChronologicalEntry, ChronologicalPayload, ChronologicalProjection, visit_turn_view,
58};
59pub use direct::{
60    DirectJsonSchema, DirectLlmClient, DirectLlmError, DirectLlmResult, DirectMessage,
61    DirectOutputSpec, DirectPart, DirectRequest, DirectRole,
62};
63pub use lash_sansio::llm::types::{
64    AttemptOutcome, AttemptRecord, ExecutionEvidence, GenerationOptions, LlmCallId, LlmCallRecord,
65    LlmOutputPart, LlmRequest, LlmRequestScope, LlmResponse, LlmTerminalReason, NormalizedError,
66    ProtocolPosition, RetryDecision,
67};
68pub use lash_sansio::{
69    AcceptedInjectedTurnInput, AttachmentCreateMeta, AttachmentId, AttachmentMeta, AttachmentRef,
70    BaseRenderCache, CheckpointDelivery, CheckpointKind, CompactToolContract, EffectId,
71    ErrorEnvelope, ExecImage, ExecResponse, ImageMediaType, LashSchema, LlmCallError, MediaType,
72    Message, MessageOrigin, MessageRole, MessageSequence, ModelToolReturn, ModelToolReturnPart,
73    Part, PartKind, PluginMessage, PluginRuntimeEvent, PreparedPrompt, ProjectionMode,
74    PromptBuildInput, PromptBuiltin, PromptContext, PromptContribution, PromptContributionGate,
75    PromptContributionSet, PromptFingerprint, PromptLayer, PromptSlot, PromptSlotLayer,
76    PromptTemplate, PromptTemplateEntry, PromptTemplateSection, ProviderSchemaCapabilities,
77    PruneState, RenderedPrompt, ResolvedPromptLayer, ResolvedSchema, Response, SchemaContract,
78    SchemaDialect, SchemaProjectionOverride, SchemaProjectionPolicy, SchemaPurpose,
79    SchemaResolutionError, SchemaResolutionRequest, SessionEvent, TextProjectionMetadata,
80    TokenUsage, ToolActivation, ToolArgumentProjectionPolicy, ToolCallOutcome, ToolCallOutput,
81    ToolCallRecord, ToolCallStatus, ToolCancellation, ToolCatalog, ToolCatalogBuildInput,
82    ToolCatalogEntry, ToolContract, ToolControl, ToolDefinition, ToolFailure, ToolFailureClass,
83    ToolFailureSource, ToolId, ToolManifest, ToolOutputContract, ToolRetryDisposition,
84    ToolRetryPolicy, ToolScheduling, ToolValue, TurnCause, TurnFinish, TurnLimitFinalMessage,
85    TurnOutcome, TurnStop, append_assistant_text_part, build_prompt, build_tool_catalog,
86    build_turn, default_prompt_template, head_tail_truncate, messages_are_prompt_resume_safe,
87    normalized_response_parts, project_anthropic_bedrock_schema, project_for_dialect,
88    prompt_template_fingerprint, prompt_text_fingerprint, prompt_tool_names_fingerprint,
89    reasoning_part, render_turn_causes_prompt, resolve_prompt_layers, resolve_schema, shared_parts,
90    validate_tool_input, visible_response_parts, visible_response_text_from_parts,
91};
92
93/// Project a successful tool control into its terminal turn outcome.
94///
95/// Agent-frame seeds are decoded here, at the protocol producer seam, so a
96/// terminal outcome can never advertise nodes that the commit materializer
97/// would have to drop.
98pub fn turn_outcome_from_tool_control(
99    tool_name: &str,
100    control: &ToolControl,
101) -> Option<TurnOutcome> {
102    match control {
103        ToolControl::SwitchAgentFrame {
104            frame_id,
105            initial_nodes,
106            task: Some(task),
107        } if !frame_id.trim().is_empty() && !task.trim().is_empty() => {
108            for (index, node) in initial_nodes.iter().enumerate() {
109                if let Err(err) = serde_json::from_value::<SessionAppendNode>(node.clone()) {
110                    return Some(TurnOutcome::Stopped(TurnStop::ToolError {
111                        tool_name: tool_name.to_string(),
112                        value: serde_json::json!({
113                            "error": format!("agent frame seed node {index} is invalid: {err}")
114                        }),
115                    }));
116                }
117            }
118            Some(TurnOutcome::AgentFrameSwitch {
119                frame_id: frame_id.clone(),
120                task: task.clone(),
121                initial_nodes: initial_nodes.clone(),
122            })
123        }
124        ToolControl::Finish { value } => Some(TurnOutcome::Finished(TurnFinish::ToolValue {
125            tool_name: tool_name.to_string(),
126            value: value.to_json_value(),
127        })),
128        ToolControl::Fail { failure } => Some(TurnOutcome::Stopped(TurnStop::ToolError {
129            tool_name: tool_name.to_string(),
130            value: failure.to_json_value(),
131        })),
132        ToolControl::SwitchAgentFrame { .. } => None,
133    }
134}
135pub use protocol_build::ProtocolBuildInput;
136pub use tool_registry::{
137    PLUGIN_TOOL_SOURCE_ID, ReconfigureError, ToolRegistry, ToolRestoreReport, ToolSourceHandle,
138    ToolState, ToolStateEntry,
139};
140pub use tool_result::{CancelHint, PendingCompletion, TimeoutBehavior, ToolResult};
141pub use triggers::{
142    InMemoryTriggerStore, TriggerDeliveryEmitOutcome, TriggerDeliveryEmitReport,
143    TriggerDeliveryReservation, TriggerDeliveryReservationStatus, TriggerEmitReport, TriggerEvent,
144    TriggerEventCatalog, TriggerEventKey, TriggerEventType, TriggerInputBinding,
145    TriggerOccurrenceFilter, TriggerOccurrenceRecord, TriggerOccurrenceRequest,
146    TriggerRegistration, TriggerRouter, TriggerStore, TriggerSubscriptionDraft,
147    TriggerSubscriptionFilter, TriggerSubscriptionRecord, TriggerTargetSummary,
148    default_trigger_source_key, deterministic_delivery_process_id, deterministic_occurrence_id,
149    empty_trigger_source_key, trigger_event_type, trigger_occurrence_request_hash,
150    validate_trigger_occurrence_request,
151};
152pub const PROTOCOL_TURN_OPTIONS_SCHEMA_VERSION: u32 = 1;
153
154#[derive(Clone, Debug, serde::Serialize)]
155pub struct ProtocolTurnOptions {
156    pub schema_version: u32,
157    pub payload: serde_json::Value,
158}
159
160fn empty_protocol_turn_payload() -> serde_json::Value {
161    serde_json::Value::Object(serde_json::Map::new())
162}
163
164#[derive(Debug, thiserror::Error)]
165pub enum ProtocolTurnOptionsError {
166    #[error(
167        "protocol turn options are missing schema_version and were written by unsupported pre-versioned state (expected {expected})"
168    )]
169    MissingSchemaVersion { expected: u32 },
170    #[error(
171        "protocol turn options schema_version {actual} is not supported by this binary (expected {expected})"
172    )]
173    UnsupportedSchemaVersion { actual: u32, expected: u32 },
174    #[error(
175        "protocol turn options schema_version {actual} is invalid (expected integer {expected})"
176    )]
177    InvalidSchemaVersion { actual: String, expected: u32 },
178    #[error("failed to decode protocol turn options payload: {0}")]
179    Decode(#[source] serde_json::Error),
180}
181
182fn parse_protocol_turn_options_schema_version(
183    value: Option<serde_json::Value>,
184) -> Result<u32, ProtocolTurnOptionsError> {
185    let expected = PROTOCOL_TURN_OPTIONS_SCHEMA_VERSION;
186    let Some(value) = value else {
187        return Err(ProtocolTurnOptionsError::MissingSchemaVersion { expected });
188    };
189    let Some(actual) = value
190        .as_u64()
191        .and_then(|version| u32::try_from(version).ok())
192    else {
193        return Err(ProtocolTurnOptionsError::InvalidSchemaVersion {
194            actual: value.to_string(),
195            expected,
196        });
197    };
198    ensure_protocol_turn_options_schema_version(actual)?;
199    Ok(actual)
200}
201
202fn ensure_protocol_turn_options_schema_version(
203    actual: u32,
204) -> Result<(), ProtocolTurnOptionsError> {
205    let expected = PROTOCOL_TURN_OPTIONS_SCHEMA_VERSION;
206    if actual == expected {
207        Ok(())
208    } else {
209        Err(ProtocolTurnOptionsError::UnsupportedSchemaVersion { actual, expected })
210    }
211}
212
213impl<'de> serde::Deserialize<'de> for ProtocolTurnOptions {
214    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
215    where
216        D: serde::Deserializer<'de>,
217    {
218        #[derive(serde::Deserialize)]
219        struct ProtocolTurnOptionsWire {
220            schema_version: Option<serde_json::Value>,
221            #[serde(default = "empty_protocol_turn_payload")]
222            payload: serde_json::Value,
223        }
224
225        let wire = ProtocolTurnOptionsWire::deserialize(deserializer)?;
226        let schema_version = parse_protocol_turn_options_schema_version(wire.schema_version)
227            .map_err(serde::de::Error::custom)?;
228        Ok(Self {
229            schema_version,
230            payload: wire.payload,
231        })
232    }
233}
234
235impl Default for ProtocolTurnOptions {
236    fn default() -> Self {
237        Self::empty()
238    }
239}
240
241impl ProtocolTurnOptions {
242    pub fn empty() -> Self {
243        Self {
244            schema_version: PROTOCOL_TURN_OPTIONS_SCHEMA_VERSION,
245            payload: serde_json::Value::Object(serde_json::Map::new()),
246        }
247    }
248
249    pub fn from_payload(payload: serde_json::Value) -> Self {
250        Self {
251            schema_version: PROTOCOL_TURN_OPTIONS_SCHEMA_VERSION,
252            payload,
253        }
254    }
255
256    pub fn is_empty(&self) -> bool {
257        match &self.payload {
258            serde_json::Value::Object(map) => map.is_empty(),
259            _ => false,
260        }
261    }
262
263    pub fn merged_with_override(&self, override_options: &Self) -> Self {
264        match (&self.payload, &override_options.payload) {
265            (serde_json::Value::Object(base), serde_json::Value::Object(overrides)) => {
266                let mut payload = base.clone();
267                payload.extend(overrides.clone());
268                Self {
269                    schema_version: PROTOCOL_TURN_OPTIONS_SCHEMA_VERSION,
270                    payload: serde_json::Value::Object(payload),
271                }
272            }
273            _ => override_options.clone(),
274        }
275    }
276
277    pub fn typed<T>(value: T) -> Result<Self, serde_json::Error>
278    where
279        T: serde::Serialize,
280    {
281        Ok(Self {
282            schema_version: PROTOCOL_TURN_OPTIONS_SCHEMA_VERSION,
283            payload: serde_json::to_value(value)?,
284        })
285    }
286
287    pub fn decode<T>(&self) -> Result<T, ProtocolTurnOptionsError>
288    where
289        T: serde::de::DeserializeOwned,
290    {
291        ensure_protocol_turn_options_schema_version(self.schema_version)?;
292        serde_json::from_value(self.payload.clone()).map_err(ProtocolTurnOptionsError::Decode)
293    }
294}
295
296#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
297pub struct ProtocolDriverState {
298    pub plugin_id: String,
299    pub payload: serde_json::Value,
300}
301
302impl ProtocolDriverState {
303    pub fn new(plugin_id: impl Into<String>, payload: serde_json::Value) -> Self {
304        Self {
305            plugin_id: plugin_id.into(),
306            payload,
307        }
308    }
309}
310
311#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
312pub struct HostTurnProtocol;
313
314impl lash_sansio::TurnProtocol for HostTurnProtocol {
315    type Event = crate::session_model::ProtocolEvent;
316    type Termination = ProtocolTurnOptions;
317    type DriverState = ProtocolDriverState;
318}
319
320pub type Effect = lash_sansio::Effect<HostTurnProtocol>;
321pub type DriverAction = lash_sansio::DriverAction<HostTurnProtocol>;
322pub type DriverContextView<'a> = lash_sansio::DriverContextView<'a, HostTurnProtocol>;
323pub type TurnDriverConfig = lash_sansio::TurnDriverConfig<HostTurnProtocol>;
324pub type TurnDriverPreamble = lash_sansio::TurnDriverPreamble<HostTurnProtocol>;
325pub type ProjectorContext<'a> = lash_sansio::ProjectorContext<'a, HostTurnProtocol>;
326pub type PreparedTurnMachine = lash_sansio::PreparedTurnMachine<HostTurnProtocol>;
327pub type SansIoTurnInput = lash_sansio::SansIoTurnInput<HostTurnProtocol>;
328pub type TurnMachine = lash_sansio::TurnMachine<HostTurnProtocol>;
329pub type TurnMachineConfig = lash_sansio::TurnMachineConfig<HostTurnProtocol>;
330#[cfg(feature = "otel-trace")]
331pub use lash_trace::otel::{OtelTraceOptions, OtelTraceSink};
332pub use lash_trace::{
333    JsonlTraceSink, TraceAttachment, TraceBranchSelection, TraceContentBlock, TraceContext,
334    TraceError, TraceEvent, TraceLabelMetadata, TraceLevel, TraceLlmMessage, TraceLlmRequest,
335    TraceLlmResponse, TracePromptComponent, TraceProviderStreamEvent, TraceRecord,
336    TraceRuntimeScope, TraceRuntimeStreamEvent, TraceRuntimeSubject, TraceSink, TraceSinkError,
337    TraceTokenUsage, TraceToolSpec,
338};
339pub use llm::transport::{LlmTransportError, ProviderFailure, ProviderFailureKind};
340pub use model::{ModelLimits, ModelSpec};
341pub use plugin::{
342    AgentFrameAssignment, AgentFrameId, AgentFrameReason, AgentFrameRecord, AgentFrameStatus,
343    AppendSessionNodesRequest, AppendSessionNodesResult, AssistantResponseHookContext,
344    AssistantResponseTransform, AssistantStreamHookContext, AssistantStreamTransform,
345    CheckpointHookContext, CompactionContext, ContextCompaction, ContextCompactor, ContextError,
346    ContextRegistrations, DirectCompletion, DirectLlmCompletion, OpenAgentFrameRequest,
347    OpenAgentFrameResult, PersistentRuntimeServices, PluginCommand, PluginCommandContext,
348    PluginCommandOutcome, PluginCommandReceipt, PluginDirective, PluginError,
349    PluginExtensionContribution, PluginExtensions, PluginFactory, PluginHost, PluginLifecycleEvent,
350    PluginLifecycleEventHook, PluginOperation, PluginOperationDef, PluginOperationFailure,
351    PluginOperationInvokeError, PluginOperationKind, PluginOptions, PluginOwned, PluginQuery,
352    PluginQueryContext, PluginRegistrar, PluginRuntimeDirective, PluginSession,
353    PluginSessionContext, PluginSessionSnapshot, PluginSnapshotArtifact, PluginSnapshotEntry,
354    PluginSnapshotMeta, PluginSpec, PluginSpecFactory, PluginTask, PluginTaskContext,
355    PluginTaskOutcome, PluginTaskReceipt, ProcessEngineContributionContext, PromptHookContext,
356    ProtocolBeforeLlmCallContext, ProtocolLlmCallAction, RuntimeServices, SessionAppendNode,
357    SessionConfigChangedContext, SessionContextOverlay, SessionCreateRequest, SessionGraphService,
358    SessionHandle, SessionLifecycleService, SessionParam, SessionPlugin, SessionPluginSource,
359    SessionReadView, SessionRelation, SessionSnapshot, SessionStartPoint,
360    SessionStateChangedContext, SessionStateService, SessionToolAccess, SessionTurnInput,
361    SessionTurnRequest, SnapshotReader, SnapshotWriter, SubagentSessionContext,
362    ToolCatalogContribution, ToolResultProjectionContext, ToolResultProjector,
363    TriggerEventRegistrations, TurnContextTransform, TurnHookContext, TurnResultHookContext,
364    TurnResultSummary, TurnTransformContext, plugin_operation_def,
365};
366pub use plugin_stack::PluginStack;
367pub use provider::{
368    CacheRetention, EmptyProviderResolver, LlmTimeouts, MapProviderResolver, ModelCapability,
369    ModelEffortValidationCategory, ModelEffortValidationError, Provider, ProviderBinding,
370    ProviderCompletion, ProviderCompletionError, ProviderComponents, ProviderFactory,
371    ProviderHandle, ProviderOptions, ProviderResolutionError, ProviderSpec, ReasoningCapability,
372    ReasoningDisableEncoding, ReasoningEncoding, ReasoningSelection, RequestTimeout,
373    RuntimeProviderResolver, SingleProviderResolver,
374};
375#[cfg(any(test, feature = "testing"))]
376pub use runtime::TestLocalProcessRegistry;
377pub use runtime::{
378    AbandonEvidence, AbandonRequest, AbandonWriter, AgentFrameRun, AssembledTurn, AssistantOutput,
379    AwaitEventKey, AwaitEventResolver, AwaitEventWaitIdentity, BoundaryReason, CausalRef, Clock,
380    CodeOutputRecord, DefaultProcessCancelAbility, DeliveryPolicy, DirectCompletionClient,
381    DurableProcessWorker, DurableProcessWorkerConfig, DurableStoreFacet, EffectHost,
382    EmbeddedRuntimeBuilder, EmbeddedRuntimeHost, EventSink, ExecutionScope, ExecutionSummary,
383    ExternalCompletionError, InMemoryLiveReplayStore, InMemoryLiveReplayStoreConfig,
384    InMemoryProcessExecutionEnvStore, InMemorySessionStore, InMemorySessionStoreFactory,
385    InlineEffectHost, InlineProcessRunHandle, InlineRuntimeEffectController, InputItem,
386    LashRuntime, LiveReplayGap, LiveReplayGapReason, LiveReplayResult, LiveReplayStore,
387    LiveReplayStoreError, LiveReplaySubscribeResult, LiveReplaySubscription, MergeKey,
388    NoopEventSink, NoopTurnActivitySink, ObservedProcess, ObservedProcessEvent, ObservedWorkItem,
389    OutputState, PROCESS_LEASE_SCHEMA_VERSION, ParkedSession, PendingTurnInput,
390    PendingTurnInputCancelOutcome, PendingTurnInputCancelResult, PendingTurnInputCancelTarget,
391    PendingTurnInputClaimDiagnostics, PendingTurnInputDraft, PendingTurnInputSuffixCancelOutcome,
392    PersistedSegmentHandover, ProcessAttach, ProcessAwaitOutput, ProcessAwaiter,
393    ProcessCancelAbility, ProcessCancelAllRequest, ProcessCancelRequest, ProcessCancelSource,
394    ProcessCancelSummary, ProcessChangeCursor, ProcessChangeHub, ProcessCompletionAuthority,
395    ProcessDrainReport, ProcessEngine, ProcessEngineRegistry, ProcessEngineRunContext,
396    ProcessEngineRunGuard, ProcessEngineRuntimeContext, ProcessEngineValidationContext,
397    ProcessEvent, ProcessEventAppendPlan, ProcessEventAppendRequest, ProcessEventAppendResult,
398    ProcessEventSink, ProcessEventType, ProcessExecutionContext, ProcessExecutionEnvRef,
399    ProcessExecutionEnvSpec, ProcessExecutionEnvStore, ProcessExternalRef, ProcessHandleDescriptor,
400    ProcessHandleGrant, ProcessHandleSummary, ProcessId, ProcessIdentity, ProcessInput,
401    ProcessLease, ProcessLeaseClaimOutcome, ProcessLeaseCompletion, ProcessLifecycleStatus,
402    ProcessListFilter, ProcessListMode, ProcessLiveReferenceSummary, ProcessOpScope,
403    ProcessOriginator, ProcessProvenance, ProcessPruneReport, ProcessRecord, ProcessRegistration,
404    ProcessRegistry, ProcessRunHandle, ProcessRunOutcome, ProcessRuntimeHost, ProcessService,
405    ProcessSessionDeleteReport, ProcessSpawnProvenance, ProcessStartGrant, ProcessStartOptions,
406    ProcessStartRequest, ProcessStarted, ProcessStatus, ProcessStatusFilter,
407    ProcessTerminalSemantics, ProcessTerminalSpec, ProcessTerminalState, ProcessValueSelector,
408    ProcessWake, ProcessWakeDedupeKey, ProcessWakeDelivery, ProcessWakeDeliveryRequest,
409    ProcessWakeSpec, ProcessWorkDriver, ProcessWorkObserver, ProcessWorkSnapshot, PromptUsage,
410    ProtocolSessionExtension, ProtocolSessionExtensionHandle, ProtocolTurnExtension,
411    ProtocolTurnExtensionHandle, QueuedWorkDriver, QueuedWorkRunHandle, QueuedWorkRunRequest,
412    RecoveryDisposition, Residency, Resolution, ResolveOutcome, RuntimeEnvironment,
413    RuntimeEnvironmentBuilder, RuntimeError, RuntimeErrorCode, RuntimeHandle, RuntimeHostConfig,
414    RuntimeObservation, ScopedEffectController, SegmentHandover, SegmentProgress, SessionCommand,
415    SessionCommandReceipt, SessionCursor, SessionCursorError, SessionObservation,
416    SessionObservationEvent, SessionObservationEventPayload, SessionObservationSubscription,
417    SessionProcessEventKind, SessionQueueEventKind, SessionResume, SessionRevision, SessionScope,
418    SessionScopeId, SessionStoreCreateRequest, SessionStoreFactory, SessionUsageReport, SlotPolicy,
419    SystemClock, TerminationPolicy, TokenLedgerEntry, ToolCallLaunch, TurnActivity, TurnActivityId,
420    TurnActivitySink, TurnContext, TurnEvent, TurnInput, TurnInputCheckpointBoundary,
421    TurnInputClaim, TurnInputClaimMode, TurnInputCompletion, TurnInputIngress, TurnInputState,
422    TurnIssue, TurnOptions, UnavailableProcessService, UsageReportRow, UsageTotals, WaitKind,
423    WaitState, apply_process_status_projection, current_epoch_ms, diff_token_ledger,
424    diff_usage_reports, ensure_durable_effect_input, epoch_ms_from_system_time,
425    process_signal_event_type, process_signal_name_from_event_type, process_signal_wait_key,
426    process_wake_delivery, system_time_from_epoch_ms, terminal_append_request,
427    terminal_event_type_name, validate_process_signal_name, watch_process_registry,
428    watch_process_registry_with_sink,
429};
430#[allow(unused_imports)]
431pub(crate) use runtime::{
432    LlmAttachmentSpec, ProcessEventSemantics, QueuedCheckpointTurnInput, QueuedCheckpointWork,
433    QueuedTurnWork, QueuedWorkBatch, QueuedWorkBatchDraft, QueuedWorkClaim,
434    QueuedWorkClaimBoundary, QueuedWorkCompletion, QueuedWorkItem, QueuedWorkPayload,
435    RuntimeReplay, RuntimeScope, RuntimeSubject, load_process_execution_env,
436    materialize_process_event_semantics, persist_process_execution_env,
437    prepare_process_event_append, prepare_process_registration, process_event_invocation,
438    process_event_payload_hash, process_wake_batch_draft, process_wake_input_from_event_payload,
439    process_wake_turn_cause, process_wake_turn_text, require_event_replay,
440};
441pub use session_model::{
442    PLUGIN_RUNTIME_PROTOCOL_PLUGIN_ID, PersistedPluginRuntimeEvent,
443    plugin_runtime_event_from_protocol, plugin_runtime_protocol_event,
444};
445// Effect / process-control types consumed by external effect hosts (e.g.
446// lash-restate's workflows) and their integration tests. Kept on the public
447// surface; the rest of the runtime block above stays crate-internal.
448pub use runtime::{
449    LlmRequestSpec, ProcessCommand, ProcessEffectOutcome, ProcessEventSemanticsSpec,
450    RuntimeAwaitEventOptions, RuntimeEffectCommand, RuntimeEffectController,
451    RuntimeEffectControllerError, RuntimeEffectEnvelope, RuntimeEffectKind,
452    RuntimeEffectLocalExecutor, RuntimeEffectOutcome, RuntimeInvocation, RuntimeSessionState,
453    ToolAttemptEffectOutcome, ToolAttemptLaunch, ToolBatchEffectOutcome,
454};
455pub use schemars::JsonSchema;
456pub(crate) use session::RuntimeExecutionTracing;
457pub use session::{
458    ExecRequest, InjectedTurnInput, RuntimeExecutionContext, Session, SessionError, ToolInvocation,
459    ToolInvocationReply,
460};
461pub use session_graph::{
462    PersistedSessionConfig, PersistedTurnState, SessionGraph, SessionMessageTreeNode,
463    SessionNodePayload, SessionNodeRecord,
464};
465pub use session_model::context::PreparedContext;
466pub use session_model::{ConversationRecord, ProtocolEvent, SessionEventRecord};
467pub use session_model::{RuntimeSessionPolicy, SessionPolicy, SessionSpec};
468pub use store::{
469    AttachmentIntent, AttachmentManifest, AttachmentManifestEntry, BlobRef, GcReport,
470    LeaseOwnerIdentity, LeaseOwnerLiveness, LeaseTimings, LeaseTimingsError, QueuedWorkStore,
471    RuntimePersistence, SessionCommitStore, SessionExecutionLease,
472    SessionExecutionLeaseClaimOutcome, SessionExecutionLeaseCompletion, SessionExecutionLeaseFence,
473    SessionExecutionLeaseStore, SessionMeta, SessionPickerInfo, SessionReadScope, StoreError,
474    StoreMaintenance, TurnInputStore, VacuumReport,
475};
476#[allow(unused_imports)]
477pub(crate) use store::{
478    GraphCommitDelta, PersistedSessionRead, RuntimeCommitResult, SessionCheckpoint,
479    SessionHeadMeta, ensure_supported_schema_version, load_persisted_session_state,
480    load_persisted_session_state_active_path,
481};
482pub use store::{
483    HydratedSessionCheckpoint, RuntimeCommit, RuntimeTurnCommitStamp, SessionHead,
484    refresh_persisted_session_state,
485};
486pub use tool_provider::{
487    PreparedToolBatch, PreparedToolBatchCall, PreparedToolCall, ProgressSender, SandboxMessage,
488    ToolCall, ToolChildExecutionTraceHook, ToolChildProcessStarted, ToolContext,
489    ToolDurableEffects, ToolExecutionGrant, ToolPrepareCall, ToolPrepareContext, ToolProvider,
490    ToolSessionAdmin, ToolSessionModel, ToolSessionProcessAdmin, ToolTriggerClient,
491};
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    #[test]
498    fn invalid_agent_frame_seed_becomes_a_loud_tool_error() {
499        let outcome = turn_outcome_from_tool_control(
500            "continue_as",
501            &ToolControl::SwitchAgentFrame {
502                frame_id: "delegate".to_string(),
503                initial_nodes: vec![serde_json::json!({ "not": "a session append node" })],
504                task: Some("continue the work".to_string()),
505            },
506        )
507        .expect("complete switch control produces a terminal outcome");
508
509        let TurnOutcome::Stopped(TurnStop::ToolError { tool_name, value }) = outcome else {
510            panic!("invalid seed must stop as a tool error");
511        };
512        assert_eq!(tool_name, "continue_as");
513        assert!(
514            value["error"]
515                .as_str()
516                .is_some_and(|message| message.contains("agent frame seed node 0 is invalid"))
517        );
518    }
519
520    #[test]
521    fn protocol_turn_options_missing_payload_deserializes_to_empty_object() {
522        let options: ProtocolTurnOptions = serde_json::from_value(serde_json::json!({
523            "schema_version": PROTOCOL_TURN_OPTIONS_SCHEMA_VERSION
524        }))
525        .expect("deserialize options");
526
527        assert!(options.is_empty());
528        assert_eq!(options.schema_version, PROTOCOL_TURN_OPTIONS_SCHEMA_VERSION);
529        assert_eq!(options.payload, serde_json::json!({}));
530    }
531
532    #[test]
533    fn protocol_turn_options_explicit_null_is_not_empty() {
534        let options: ProtocolTurnOptions = serde_json::from_value(serde_json::json!({
535            "schema_version": PROTOCOL_TURN_OPTIONS_SCHEMA_VERSION,
536            "payload": null
537        }))
538        .expect("deserialize options");
539
540        assert!(!options.is_empty());
541        assert_eq!(options.payload, serde_json::Value::Null);
542    }
543
544    #[test]
545    fn protocol_turn_options_missing_schema_version_rejects_preversioned_state() {
546        let err =
547            serde_json::from_value::<ProtocolTurnOptions>(serde_json::json!({ "payload": {} }))
548                .expect_err("pre-versioned options should fail");
549
550        assert!(
551            err.to_string().contains(
552                "missing schema_version and were written by unsupported pre-versioned state"
553            ),
554            "{err}"
555        );
556    }
557
558    #[test]
559    fn protocol_turn_options_unsupported_schema_version_rejects_state() {
560        let err = serde_json::from_value::<ProtocolTurnOptions>(serde_json::json!({
561            "schema_version": PROTOCOL_TURN_OPTIONS_SCHEMA_VERSION + 1,
562            "payload": {}
563        }))
564        .expect_err("unsupported options version should fail");
565
566        assert!(
567            err.to_string().contains("is not supported by this binary"),
568            "{err}"
569        );
570    }
571
572    #[test]
573    fn root_exports_do_not_reintroduce_removed_session_state_shapes() {
574        let source = include_str!("lib.rs");
575        let removed_envelope = ["SessionState", "Envelope"].concat();
576        let removed_persisted = ["PersistedSession", "Snapshot"].concat();
577        let removed_history_rewriter = ["History", "Rewriter"].concat();
578        let removed_rewrite_trigger = ["Rewrite", "Trigger"].concat();
579        let removed_rewrite_context = ["Rewrite", "Context"].concat();
580        let removed_history_state = ["History", "State"].concat();
581        let removed_history_metadata = ["History", "Rewrite", "Metadata"].concat();
582
583        assert!(!source.contains(&removed_envelope));
584        assert!(!source.contains(&removed_persisted));
585        assert!(!source.contains(&removed_history_rewriter));
586        assert!(!source.contains(&removed_rewrite_trigger));
587        assert!(!source.contains(&removed_rewrite_context));
588        assert!(!source.contains(&removed_history_state));
589        assert!(!source.contains(&removed_history_metadata));
590    }
591
592    fn public_reexport_block(source: &str, module: &str) -> String {
593        let start = format!("pub use {module}::{{");
594        let mut block = String::new();
595        let mut collecting = false;
596        for line in source.lines() {
597            if line.trim_start().starts_with(&start) {
598                collecting = true;
599            }
600            if collecting {
601                block.push_str(line);
602                block.push('\n');
603                if line.trim_end() == "};" {
604                    break;
605                }
606            }
607        }
608        assert!(!block.is_empty(), "missing public {module} re-export block");
609        block
610    }
611
612    #[test]
613    fn root_runtime_exports_exclude_internal_runtime_records() {
614        let runtime_exports = public_reexport_block(include_str!("lib.rs"), "runtime");
615        for removed in [
616            "RuntimeEffectCommand",
617            "RuntimeEffectEnvelope",
618            "RuntimeEffectKind",
619            "RuntimeEffectOutcome",
620            "RuntimeInvocation",
621            "RuntimeScope",
622            "RuntimeSessionState",
623            "QueuedWorkBatch",
624            "QueuedWorkBatchDraft",
625            "QueuedWorkPayload",
626            "prepare_process_registration",
627            "process_wake_batch_draft",
628            "require_event_replay",
629        ] {
630            assert!(
631                !runtime_exports.contains(removed),
632                "runtime root export leaked {removed}"
633            );
634        }
635    }
636
637    #[test]
638    fn root_store_exports_exclude_wire_records() {
639        let store_exports = public_reexport_block(include_str!("lib.rs"), "store");
640        for removed in [
641            "SessionHead",
642            "SessionCheckpoint",
643            "RuntimeCommit",
644            "HydratedSessionCheckpoint",
645            "PersistedSessionRead",
646            "GraphCommitDelta",
647        ] {
648            assert!(
649                !store_exports.contains(removed),
650                "store root export leaked {removed}"
651            );
652        }
653    }
654
655    #[test]
656    fn removed_manager_and_host_trait_names_stay_removed() {
657        let removed_manager = ["Runtime", "Session", "Manager"].concat();
658        let removed_host = ["Runtime", "Session", "Host"].concat();
659        let sources = [
660            include_str!("runtime/session_manager/mod.rs"),
661            include_str!("plugin/runtime_host.rs"),
662            include_str!("tool_dispatch/context.rs"),
663            include_str!("tool_provider.rs"),
664        ];
665
666        for source in sources {
667            assert!(!source.contains(&removed_manager));
668            assert!(!source.contains(&removed_host));
669        }
670    }
671}