Skip to main content

meerkat_runtime/
lib.rs

1//! meerkat-runtime — v9 runtime control-plane for Meerkat agent lifecycle.
2//!
3//! This crate implements the runtime/control-plane layer of the v9 Canonical
4//! Lifecycle specification. It sits between surfaces (CLI, RPC, REST, MCP)
5//! and core (`meerkat-core`), managing:
6//!
7//! - Input acceptance, validation, and queueing
8//! - InputState lifecycle tracking
9//! - Policy resolution (what to do with each input)
10//! - Runtime state machine (Initializing ↔ Idle ↔ Attached ↔ Running ↔ Retired/Stopped/Destroyed)
11//! - Retire/recycle/reset lifecycle operations
12//! - RuntimeEvent observability
13//!
14//! Core-facing types (RunPrimitive, RunEvent, CoreExecutor, etc.) live in
15//! `meerkat-core::lifecycle`. This crate contains everything else.
16
17#![cfg_attr(
18    test,
19    allow(
20        dead_code,
21        unused_imports,
22        clippy::expect_used,
23        clippy::large_futures,
24        clippy::needless_borrow,
25        clippy::panic,
26        clippy::redundant_closure_for_method_calls,
27        clippy::redundant_clone,
28        clippy::type_complexity,
29        clippy::unnecessary_to_owned,
30        clippy::unwrap_used
31    )
32)]
33
34#[cfg(target_arch = "wasm32")]
35pub mod tokio {
36    pub use tokio_with_wasm::alias::*;
37}
38
39#[cfg(not(target_arch = "wasm32"))]
40pub use ::tokio;
41
42pub mod accept;
43pub mod auth_machine;
44pub mod coalescing;
45pub mod comms_bridge;
46pub mod comms_drain;
47pub mod comms_trust_reconcile;
48pub mod completion;
49pub mod composition;
50pub(crate) mod control_plane;
51pub mod driver;
52pub(crate) mod effect;
53#[doc(hidden)]
54pub mod generated;
55pub mod handles;
56pub mod identifiers;
57pub mod ingress_types;
58pub mod input;
59pub mod input_ledger;
60pub mod input_scope;
61pub mod input_state;
62pub mod interrupt_public_result;
63pub mod meerkat_machine;
64pub(crate) mod meerkat_machine_types;
65pub mod mob_adapter;
66pub mod mob_operator_authority;
67pub mod ops_lifecycle;
68pub mod peer_handling_mode;
69pub mod policy;
70pub mod policy_table;
71#[allow(unused_imports)]
72#[path = "generated/protocol_auth_lease_lifecycle_publication.rs"]
73pub mod protocol_auth_lease_lifecycle_publication;
74#[allow(unused_imports)]
75#[path = "generated/protocol_auth_release_oauth_flow_drain.rs"]
76pub mod protocol_auth_release_oauth_flow_drain;
77#[allow(unused_imports)]
78#[path = "generated/protocol_comms_trust_reconcile.rs"]
79pub mod protocol_comms_trust_reconcile;
80#[allow(unused_imports)]
81#[path = "generated/protocol_supervisor_trust_publish.rs"]
82pub mod protocol_supervisor_trust_publish;
83#[allow(unused_imports)]
84#[path = "generated/protocol_supervisor_trust_revoke.rs"]
85pub mod protocol_supervisor_trust_revoke;
86pub(crate) mod queue;
87pub mod runtime_event;
88pub(crate) mod runtime_loop;
89pub mod runtime_state;
90pub mod service_ext;
91pub(crate) mod silent_intent;
92pub mod store;
93pub mod terminal_status;
94pub mod traits;
95
96use meerkat_core::lifecycle::run_primitive::RuntimeTurnMetadata as RuntimeStampedTurnMetadata;
97use std::any::Any;
98use std::sync::Arc;
99
100struct SessionRuntimeBindingsAuthority;
101
102pub(crate) fn session_runtime_bindings_authority() -> Arc<dyn Any + Send + Sync> {
103    Arc::new(SessionRuntimeBindingsAuthority)
104}
105
106pub(crate) fn local_session_runtime_bindings_authority() -> Arc<dyn Any + Send + Sync> {
107    session_runtime_bindings_authority()
108}
109
110pub fn session_runtime_bindings_have_machine_authority(
111    bindings: &meerkat_core::SessionRuntimeBindings,
112) -> bool {
113    bindings
114        .__runtime_authority()
115        .is::<SessionRuntimeBindingsAuthority>()
116}
117
118// Re-exports for convenience
119pub use accept::{AcceptOutcome, RejectReason};
120pub use coalescing::{
121    AggregateDescriptor, CoalescingResult, SupersessionScope, check_supersession,
122    create_aggregate_input, is_coalescing_eligible,
123};
124pub use completion::{
125    CompletionCleanupObservation, CompletionHandle, CompletionOutcome, CompletionWaitError,
126};
127pub use driver::{EphemeralRuntimeDriver, PersistentRuntimeDriver, PostAdmissionSignal};
128pub use handles::{
129    HandleDslAuthority, RuntimeAuthLeaseHandle, RuntimeCommsDrainHandle,
130    RuntimeExternalToolSurfaceHandle, RuntimeInteractionStreamHandle,
131    RuntimeMcpServerLifecycleHandle, RuntimeModelRoutingHandle, RuntimePeerCommsHandle,
132    RuntimePeerInteractionHandle, RuntimeSessionAdmissionHandle, RuntimeSessionContextHandle,
133    RuntimeTurnStateHandle,
134};
135pub use identifiers::{
136    CausationId, ConversationId, CorrelationId, EventCodeId, IdempotencyKey, InputKind, KindId,
137    LogicalRuntimeId, PolicyVersion, ProjectionRuleId, RuntimeEventId, SchemaId, SupersessionKey,
138};
139pub use ingress_types::{ContentShape, RequestId, ReservationKey};
140pub use input::{
141    ContinuationInput, ContinuationKind, ExternalEventInput, FlowStepInput, Input, InputDurability,
142    InputHeader, InputOrigin, InputVisibility, OperationInput, PeerConvention, PeerInput,
143    PromptInput, ResponseProgressPhase, ResponseTerminalStatus, peer_response_terminal_input,
144    response_terminal_status_from_wire,
145};
146pub use input_ledger::InputLedger;
147pub use input_scope::InputScope;
148pub use input_state::{
149    InputAbandonReason, InputLifecycleState, InputState, InputStateEvent, InputStateHistoryEntry,
150    InputTerminalOutcome, PolicySnapshot, ReconstructionSource,
151};
152pub use meerkat_core::types::HandlingMode;
153pub use meerkat_machine::{
154    CommsDrainMode, CommsDrainPhase, DrainExitReason, MachineSessionControlAuthority,
155    MeerkatConsumerSurface, MeerkatMachine, PeerIngressOwner, RuntimeBindingsError,
156    RuntimeLifecycleFacts, RuntimeLoopQueueAdmissionPlan, classify_runtime_lifecycle_state,
157    classify_runtime_loop_queue_admission, standalone_tool_visibility_owner,
158};
159pub use meerkat_machine_types::{
160    HydratedSessionLlmState, ImageOperationRoutingRequest, ImageOperationRoutingResult,
161    ModelRoutingApprovalDisposition, ModelRoutingRealtimePolicy, ResolvedSessionLlmReconfigure,
162    SessionLlmCapabilitySurface, SessionLlmCapabilitySurfaceStatus, SessionLlmReconfigureHost,
163    SessionLlmReconfigureReport, SessionLlmReconfigureRequest, SessionToolVisibilityDelta,
164};
165#[doc(hidden)]
166pub use meerkat_machine_types::{
167    MeerkatAdmittedInputSnapshot, MeerkatArchiveSnapshot, MeerkatBindingSnapshot,
168    MeerkatCompletionWaiterSnapshot, MeerkatCompletionWaitersSnapshot, MeerkatControlSnapshot,
169    MeerkatCursorSnapshot, MeerkatDrainSnapshot, MeerkatDriverKind, MeerkatInputsSnapshot,
170    MeerkatMachineCatalogInput, MeerkatMachineCommandClassification,
171    MeerkatMachineCommandClassificationRecord, MeerkatMachineCommandVariant,
172    MeerkatMachineFieldlessRuntimeInternalInput, MeerkatMachineRuntimeInternalClassificationRecord,
173    MeerkatMachineRuntimeInternalInput, MeerkatMachineRuntimeInternalReason,
174    MeerkatMachineShellMechanicReason, MeerkatMachineSpineSnapshot, MeerkatOpsSnapshot,
175    canonical_meerkat_machine_command_classifications,
176    canonical_meerkat_machine_command_input_variant_manifest,
177    canonical_meerkat_machine_command_manifest,
178    canonical_meerkat_machine_runtime_internal_classifications,
179    canonical_meerkat_machine_runtime_internal_fieldless_input_variant_manifest,
180    canonical_meerkat_machine_runtime_internal_input_variant_manifest,
181    canonical_meerkat_machine_runtime_internal_manifest,
182};
183pub use ops_lifecycle::{
184    OpsLifecycleConfig, OpsLifecyclePersistenceRequest, PersistedOpsSnapshot,
185    RuntimeOpsLifecycleRegistry,
186};
187
188#[cfg(all(not(target_arch = "wasm32"), any(test, feature = "test-support")))]
189#[doc(hidden)]
190pub fn test_peer_comms_handle() -> Arc<dyn meerkat_core::handles::PeerCommsHandle> {
191    test_peer_comms_handle_with_silent(std::iter::empty::<String>())
192}
193
194#[cfg(all(not(target_arch = "wasm32"), any(test, feature = "test-support")))]
195#[doc(hidden)]
196#[allow(clippy::expect_used)]
197pub fn test_peer_comms_handle_with_silent<I, S>(
198    silent_intents: I,
199) -> Arc<dyn meerkat_core::handles::PeerCommsHandle>
200where
201    I: IntoIterator<Item = S>,
202    S: Into<String>,
203{
204    let silent_intents = silent_intents
205        .into_iter()
206        .map(Into::into)
207        .collect::<Vec<_>>();
208    std::thread::spawn(move || {
209        let runtime = tokio::runtime::Builder::new_current_thread()
210            .enable_all()
211            .build()
212            .expect("test peer-comms runtime should build");
213        runtime.block_on(async move {
214            let machine = MeerkatMachine::ephemeral();
215            let session_id = meerkat_core::SessionId::new();
216            let bindings = machine
217                .prepare_bindings(session_id.clone())
218                .await
219                .expect("generated MeerkatMachine should prepare test peer-comms bindings");
220            if !silent_intents.is_empty() {
221                machine
222                    .set_session_silent_intents(&session_id, silent_intents)
223                    .await
224                    .expect("set silent intents");
225            }
226            Arc::clone(bindings.peer_comms())
227        })
228    })
229    .join()
230    .expect("test peer-comms authority thread should finish")
231}
232
233#[cfg(all(not(target_arch = "wasm32"), any(test, feature = "test-support")))]
234#[doc(hidden)]
235#[allow(clippy::expect_used)]
236pub fn test_peer_input_candidate_from_interaction(
237    interaction: meerkat_core::interaction::InboxInteraction,
238    peer_id: meerkat_core::comms::PeerId,
239) -> meerkat_core::interaction::PeerInputCandidate {
240    use meerkat_core::interaction::{
241        InteractionContent, InteractionId, PeerIngressEnvelopeFacts, PeerIngressEnvelopeKind,
242        PeerIngressFact, PeerIngressIdentity,
243    };
244
245    let handle = test_peer_comms_handle();
246    let facts = PeerIngressEnvelopeFacts {
247        item_id: interaction.id.to_string(),
248        from_peer: interaction.from.clone(),
249        from_peer_id: peer_id,
250        kind: match &interaction.content {
251            InteractionContent::Message { body, .. } => {
252                PeerIngressEnvelopeKind::Message { body: body.clone() }
253            }
254            InteractionContent::Request { intent, params, .. } => {
255                PeerIngressEnvelopeKind::Request {
256                    intent: intent.clone(),
257                    params: params.clone(),
258                }
259            }
260            InteractionContent::Response {
261                in_reply_to,
262                status,
263                result,
264                ..
265            } => PeerIngressEnvelopeKind::Response {
266                in_reply_to: in_reply_to.to_string(),
267                status: *status,
268                result: result.clone(),
269            },
270        },
271    };
272    let admission = handle
273        .classify_external_envelope(facts)
274        .expect("generated peer-comms authority should classify test interaction");
275    // R084: the admitted sender identity comes from the machine-echoed
276    // canonical peer id on the classification effect, not the local input.
277    let canonical_from_peer_id = admission
278        .from_peer_id
279        .expect("generated envelope classification should echo the canonical sender peer id");
280    let classification = admission.classification;
281    let convention = match &interaction.content {
282        InteractionContent::Message { .. } => meerkat_core::PeerIngressConvention::Message,
283        InteractionContent::Request { intent, .. } => {
284            if let Some(kind) = classification.lifecycle_kind {
285                let peer = admission
286                    .lifecycle_peer
287                    .clone()
288                    .expect("generated lifecycle classification should include a peer subject");
289                meerkat_core::PeerIngressConvention::Lifecycle { kind, peer }
290            } else {
291                let request_id = admission
292                    .request_id
293                    .clone()
294                    .expect("generated request classification should include request id");
295                meerkat_core::PeerIngressConvention::Request {
296                    request_id,
297                    intent: intent.clone(),
298                }
299            }
300        }
301        InteractionContent::Response { status, .. } => {
302            let in_reply_to = admission
303                .request_id
304                .as_deref()
305                .and_then(|id| uuid::Uuid::parse_str(id).ok())
306                .map(InteractionId)
307                .expect("generated response classification should include in-reply-to id");
308            meerkat_core::PeerIngressConvention::Response {
309                in_reply_to,
310                status: *status,
311            }
312        }
313    };
314    let ingress = PeerIngressFact::peer(
315        interaction.id,
316        classification.class,
317        classification.kind,
318        Some(classification.auth),
319        PeerIngressIdentity::new(canonical_from_peer_id, interaction.from.clone(), convention),
320    );
321    let mut candidate = meerkat_core::interaction::PeerInputCandidate::new(
322        interaction,
323        ingress,
324        admission.lifecycle_peer,
325    );
326    candidate.response_terminality = classification.response_terminality;
327    candidate
328}
329
330/// Stamp prompt turn metadata with the runtime-owned input semantics.
331///
332/// This helper exists for runtime-backed service-turn paths that already hold
333/// machine admission and must pass a runtime-classified prompt turn into the
334/// session layer. New prompt materialization should prefer `MeerkatMachine`
335/// input admission so the machine creates this metadata directly.
336pub fn runtime_stamped_prompt_turn_metadata(
337    metadata: Option<RuntimeStampedTurnMetadata>,
338) -> RuntimeStampedTurnMetadata {
339    let input = Input::Prompt(PromptInput::from_content_input(
340        meerkat_core::ContentInput::Text(String::new()),
341        metadata,
342    ));
343    let semantics = runtime_prompt_semantics_from_machine(&input);
344    runtime_loop::for_input(&input, semantics)
345}
346
347#[allow(clippy::expect_used)]
348fn runtime_prompt_semantics_from_machine(input: &Input) -> ingress_types::RuntimeInputSemantics {
349    let mut authority = meerkat_machine::dsl_authority::new_initialized_authority(
350        "generated runtime prompt machine authority must initialize",
351    );
352    let transition = meerkat_machine::dsl::MeerkatMachineMutator::apply(
353        &mut authority,
354        meerkat_machine::dsl::MeerkatMachineInput::ResolveAdmissionPlan {
355            input_id: input.id().to_string(),
356            input_kind: meerkat_machine::dsl::AdmissionInputKind::from(input.kind()),
357            requested_lane: input
358                .handling_mode()
359                .map(meerkat_machine::dsl::InputLane::from),
360            continuation_kind: meerkat_machine::dsl::AdmissionContinuationKind::from(
361                input.continuation_kind(),
362            ),
363            silent_intent_match: false,
364            existing_superseded_input_id: None,
365            runtime_running: false,
366            active_turn_boundary_available: false,
367            without_wake: false,
368        },
369    )
370    .expect("generated admission authority must accept runtime prompt metadata");
371
372    transition
373        .into_effects()
374        .into_iter()
375        .find_map(|effect| match effect {
376            meerkat_machine::dsl::MeerkatMachineEffect::AdmissionResolved {
377                runtime_boundary,
378                runtime_execution_kind,
379                runtime_peer_response_terminal_apply_intent,
380                live_interrupt_required,
381                ..
382            } => Some(ingress_types::RuntimeInputSemantics {
383                boundary: runtime_boundary.into(),
384                execution_kind: runtime_execution_kind.into(),
385                execution_handling_mode: None,
386                peer_response_terminal_apply_intent: runtime_peer_response_terminal_apply_intent
387                    .map(Into::into),
388                live_interrupt_required,
389            }),
390            _ => None,
391        })
392        .expect("generated admission authority must emit prompt runtime semantics")
393}
394
395#[cfg(test)]
396mod runtime_prompt_metadata_tests {
397    #[test]
398    fn runtime_stamped_prompt_turn_metadata_uses_generated_prompt_semantics() {
399        let metadata = super::runtime_stamped_prompt_turn_metadata(None);
400        assert_eq!(
401            metadata.execution_kind,
402            Some(meerkat_core::lifecycle::RuntimeExecutionKind::ContentTurn)
403        );
404        assert!(metadata.peer_response_terminal_apply_intent.is_none());
405    }
406}
407
408#[doc(hidden)]
409pub mod machine_schema_exports {
410    pub fn meerkat_machine_schema() -> meerkat_machine_schema::MachineSchema {
411        meerkat_machine_schema::catalog::dsl::meerkat_machine_schema_metadata()
412            .attach_to(crate::meerkat_machine::dsl::MeerkatMachineState::schema())
413    }
414
415    pub fn auth_machine_schema() -> meerkat_machine_schema::MachineSchema {
416        meerkat_machine_schema::catalog::dsl::auth_machine_schema_metadata()
417            .attach_to(crate::auth_machine::dsl::AuthMachineState::schema())
418    }
419}
420pub use interrupt_public_result::{
421    UserInterruptObservation, UserInterruptPublicResult, resolve_user_interrupt_public_result,
422};
423pub use peer_handling_mode::{PeerHandlingModeError, validate_peer_handling_mode};
424pub use policy::{
425    ApplyMode, ConsumePoint, DrainPolicy, PolicyDecision, QueueMode, RoutingDisposition, WakeMode,
426};
427pub use policy_table::{DefaultPolicyTable, generated_default_policy_version};
428pub use runtime_event::{
429    InputLifecycleEvent, RunLifecycleEvent, RuntimeEvent, RuntimeEventEnvelope,
430    RuntimeProjectionEvent, RuntimeStateChangeEvent, RuntimeTopologyEvent,
431};
432pub use runtime_state::{RuntimeState, RuntimeStateTransitionError};
433pub use service_ext::SessionServiceRuntimeExt;
434pub use store::{InMemoryRuntimeStore, RuntimeStore, RuntimeStoreError, SessionDelta};
435pub use traits::{
436    DestroyReport, RecoveryReport, RecycleReport, ResetReport, RetireReport, RuntimeControlPlane,
437    RuntimeControlPlaneError, RuntimeDriver, RuntimeDriverError,
438};