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