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