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, StandaloneSessionRuntimeAuthorities,
157    classify_runtime_lifecycle_state, classify_runtime_loop_queue_admission,
158    standalone_session_runtime_authorities, standalone_tool_visibility_owner,
159};
160pub use meerkat_machine_types::{
161    HydratedSessionLlmState, ImageOperationRoutingRequest, ImageOperationRoutingResult,
162    ModelRoutingApprovalDisposition, ModelRoutingRealtimePolicy, ResolvedSessionLlmReconfigure,
163    SessionLlmCapabilitySurface, SessionLlmCapabilitySurfaceStatus, SessionLlmReconfigureHost,
164    SessionLlmReconfigureReport, SessionLlmReconfigureRequest, SessionToolVisibilityDelta,
165};
166#[doc(hidden)]
167pub use meerkat_machine_types::{
168    MeerkatAdmittedInputSnapshot, MeerkatArchiveSnapshot, MeerkatBindingSnapshot,
169    MeerkatCompletionWaiterSnapshot, MeerkatCompletionWaitersSnapshot, MeerkatControlSnapshot,
170    MeerkatCursorSnapshot, MeerkatDrainSnapshot, MeerkatDriverKind, MeerkatInputsSnapshot,
171    MeerkatMachineCatalogInput, MeerkatMachineCommandClassification,
172    MeerkatMachineCommandClassificationRecord, MeerkatMachineCommandVariant,
173    MeerkatMachineFieldlessRuntimeInternalInput, MeerkatMachineRuntimeInternalClassificationRecord,
174    MeerkatMachineRuntimeInternalInput, MeerkatMachineRuntimeInternalReason,
175    MeerkatMachineShellMechanicReason, MeerkatMachineSpineSnapshot, MeerkatOpsSnapshot,
176    canonical_meerkat_machine_command_classifications,
177    canonical_meerkat_machine_command_input_variant_manifest,
178    canonical_meerkat_machine_command_manifest,
179    canonical_meerkat_machine_runtime_internal_classifications,
180    canonical_meerkat_machine_runtime_internal_fieldless_input_variant_manifest,
181    canonical_meerkat_machine_runtime_internal_input_variant_manifest,
182    canonical_meerkat_machine_runtime_internal_manifest,
183};
184pub use ops_lifecycle::{
185    OpsLifecycleConfig, OpsLifecyclePersistenceRequest, PersistedOpsSnapshot,
186    RuntimeOpsLifecycleRegistry,
187};
188
189#[cfg(all(not(target_arch = "wasm32"), any(test, feature = "test-support")))]
190#[doc(hidden)]
191pub fn test_peer_comms_handle() -> Arc<dyn meerkat_core::handles::PeerCommsHandle> {
192    test_peer_comms_handle_with_silent(std::iter::empty::<String>())
193}
194
195#[cfg(all(not(target_arch = "wasm32"), any(test, feature = "test-support")))]
196#[doc(hidden)]
197#[allow(clippy::expect_used)]
198pub fn test_peer_comms_handle_with_silent<I, S>(
199    silent_intents: I,
200) -> Arc<dyn meerkat_core::handles::PeerCommsHandle>
201where
202    I: IntoIterator<Item = S>,
203    S: Into<String>,
204{
205    let silent_intents = silent_intents
206        .into_iter()
207        .map(Into::into)
208        .collect::<Vec<_>>();
209    std::thread::spawn(move || {
210        let runtime = tokio::runtime::Builder::new_current_thread()
211            .enable_all()
212            .build()
213            .expect("test peer-comms runtime should build");
214        runtime.block_on(async move {
215            let machine = MeerkatMachine::ephemeral();
216            let session_id = meerkat_core::SessionId::new();
217            let bindings = machine
218                .prepare_bindings(session_id.clone())
219                .await
220                .expect("generated MeerkatMachine should prepare test peer-comms bindings");
221            if !silent_intents.is_empty() {
222                machine
223                    .set_session_silent_intents(&session_id, silent_intents)
224                    .await
225                    .expect("set silent intents");
226            }
227            Arc::clone(bindings.peer_comms())
228        })
229    })
230    .join()
231    .expect("test peer-comms authority thread should finish")
232}
233
234#[cfg(all(not(target_arch = "wasm32"), any(test, feature = "test-support")))]
235#[doc(hidden)]
236#[allow(clippy::expect_used)]
237pub fn test_peer_input_candidate_from_interaction(
238    interaction: meerkat_core::interaction::InboxInteraction,
239    peer_id: meerkat_core::comms::PeerId,
240) -> meerkat_core::interaction::PeerInputCandidate {
241    use meerkat_core::interaction::{
242        InteractionContent, InteractionId, PeerIngressEnvelopeFacts, PeerIngressEnvelopeKind,
243        PeerIngressFact, PeerIngressIdentity,
244    };
245
246    let handle = test_peer_comms_handle();
247    let facts = PeerIngressEnvelopeFacts {
248        item_id: interaction.id.to_string(),
249        from_peer: interaction.from.clone(),
250        from_peer_id: peer_id,
251        kind: match &interaction.content {
252            InteractionContent::Message { body, .. } => {
253                PeerIngressEnvelopeKind::Message { body: body.clone() }
254            }
255            InteractionContent::Request { intent, params, .. } => {
256                PeerIngressEnvelopeKind::Request {
257                    intent: intent.clone(),
258                    params: params.clone(),
259                }
260            }
261            InteractionContent::Response {
262                in_reply_to,
263                status,
264                result,
265                ..
266            } => PeerIngressEnvelopeKind::Response {
267                in_reply_to: in_reply_to.to_string(),
268                status: *status,
269                result: result.clone(),
270            },
271        },
272    };
273    let admission = handle
274        .classify_external_envelope(facts)
275        .expect("generated peer-comms authority should classify test interaction");
276    // R084: the admitted sender identity comes from the machine-echoed
277    // canonical peer id on the classification effect, not the local input.
278    let canonical_from_peer_id = admission
279        .from_peer_id
280        .expect("generated envelope classification should echo the canonical sender peer id");
281    let classification = admission.classification;
282    let convention = match &interaction.content {
283        InteractionContent::Message { .. } => meerkat_core::PeerIngressConvention::Message,
284        InteractionContent::Request { intent, .. } => {
285            if let Some(kind) = classification.lifecycle_kind {
286                let peer = admission
287                    .lifecycle_peer
288                    .clone()
289                    .expect("generated lifecycle classification should include a peer subject");
290                meerkat_core::PeerIngressConvention::Lifecycle { kind, peer }
291            } else {
292                let request_id = admission
293                    .request_id
294                    .clone()
295                    .expect("generated request classification should include request id");
296                meerkat_core::PeerIngressConvention::Request {
297                    request_id,
298                    intent: intent.clone(),
299                }
300            }
301        }
302        InteractionContent::Response { status, .. } => {
303            let in_reply_to = admission
304                .request_id
305                .as_deref()
306                .and_then(|id| uuid::Uuid::parse_str(id).ok())
307                .map(InteractionId)
308                .expect("generated response classification should include in-reply-to id");
309            meerkat_core::PeerIngressConvention::Response {
310                in_reply_to,
311                status: *status,
312            }
313        }
314    };
315    let ingress = PeerIngressFact::peer(
316        interaction.id,
317        classification.class,
318        classification.kind,
319        Some(classification.auth),
320        PeerIngressIdentity::new(canonical_from_peer_id, interaction.from.clone(), convention),
321    );
322    let mut candidate = meerkat_core::interaction::PeerInputCandidate::new(
323        interaction,
324        ingress,
325        admission.lifecycle_peer,
326    );
327    candidate.response_terminality = classification.response_terminality;
328    candidate
329}
330
331/// Stamp prompt turn metadata with the runtime-owned input semantics.
332///
333/// This helper exists for runtime-backed service-turn paths that already hold
334/// machine admission and must pass a runtime-classified prompt turn into the
335/// session layer. New prompt materialization should prefer `MeerkatMachine`
336/// input admission so the machine creates this metadata directly.
337pub fn runtime_stamped_prompt_turn_metadata(
338    metadata: Option<RuntimeStampedTurnMetadata>,
339) -> RuntimeStampedTurnMetadata {
340    let input = Input::Prompt(PromptInput::from_content_input(
341        meerkat_core::ContentInput::Text(String::new()),
342        metadata,
343    ));
344    let semantics = runtime_prompt_semantics_from_machine(&input);
345    runtime_loop::for_input(&input, semantics)
346}
347
348#[allow(clippy::expect_used)]
349fn runtime_prompt_semantics_from_machine(input: &Input) -> ingress_types::RuntimeInputSemantics {
350    let mut authority = meerkat_machine::dsl_authority::new_initialized_authority(
351        "generated runtime prompt machine authority must initialize",
352    );
353    let transition = meerkat_machine::dsl::MeerkatMachineMutator::apply(
354        &mut authority,
355        meerkat_machine::dsl::MeerkatMachineInput::ResolveAdmissionPlan {
356            input_id: input.id().to_string(),
357            input_kind: meerkat_machine::dsl::AdmissionInputKind::from(input.kind()),
358            requested_lane: input
359                .handling_mode()
360                .map(meerkat_machine::dsl::InputLane::from),
361            continuation_kind: meerkat_machine::dsl::AdmissionContinuationKind::from(
362                input.continuation_kind(),
363            ),
364            silent_intent_match: false,
365            existing_superseded_input_id: None,
366            runtime_running: false,
367            active_turn_boundary_available: false,
368            without_wake: false,
369        },
370    )
371    .expect("generated admission authority must accept runtime prompt metadata");
372
373    transition
374        .into_effects()
375        .into_iter()
376        .find_map(|effect| match effect {
377            meerkat_machine::dsl::MeerkatMachineEffect::AdmissionResolved {
378                runtime_boundary,
379                runtime_execution_kind,
380                runtime_peer_response_terminal_apply_intent,
381                live_interrupt_required,
382                ..
383            } => Some(ingress_types::RuntimeInputSemantics {
384                boundary: runtime_boundary.into(),
385                execution_kind: runtime_execution_kind.into(),
386                execution_handling_mode: None,
387                peer_response_terminal_apply_intent: runtime_peer_response_terminal_apply_intent
388                    .map(Into::into),
389                live_interrupt_required,
390            }),
391            _ => None,
392        })
393        .expect("generated admission authority must emit prompt runtime semantics")
394}
395
396#[cfg(test)]
397mod runtime_prompt_metadata_tests {
398    #[test]
399    fn runtime_stamped_prompt_turn_metadata_uses_generated_prompt_semantics() {
400        let metadata = super::runtime_stamped_prompt_turn_metadata(None);
401        assert_eq!(
402            metadata.execution_kind,
403            Some(meerkat_core::lifecycle::RuntimeExecutionKind::ContentTurn)
404        );
405        assert!(metadata.peer_response_terminal_apply_intent.is_none());
406    }
407}
408
409#[doc(hidden)]
410pub mod machine_schema_exports {
411    pub fn meerkat_machine_schema() -> meerkat_machine_schema::MachineSchema {
412        meerkat_machine_schema::catalog::dsl::meerkat_machine_schema_metadata()
413            .attach_to(crate::meerkat_machine::dsl::MeerkatMachineState::schema())
414    }
415
416    pub fn auth_machine_schema() -> meerkat_machine_schema::MachineSchema {
417        meerkat_machine_schema::catalog::dsl::auth_machine_schema_metadata()
418            .attach_to(crate::auth_machine::dsl::AuthMachineState::schema())
419    }
420}
421pub use interrupt_public_result::{
422    UserInterruptObservation, UserInterruptPublicResult, resolve_user_interrupt_public_result,
423};
424pub use peer_handling_mode::{PeerHandlingModeError, validate_peer_handling_mode};
425pub use policy::{
426    ApplyMode, ConsumePoint, DrainPolicy, PolicyDecision, QueueMode, RoutingDisposition, WakeMode,
427};
428pub use policy_table::{DefaultPolicyTable, generated_default_policy_version};
429pub use runtime_event::{
430    InputLifecycleEvent, RunLifecycleEvent, RuntimeEvent, RuntimeEventEnvelope,
431    RuntimeProjectionEvent, RuntimeStateChangeEvent, RuntimeTopologyEvent,
432};
433pub use runtime_state::{RuntimeState, RuntimeStateTransitionError};
434pub use service_ext::SessionServiceRuntimeExt;
435pub use store::{InMemoryRuntimeStore, RuntimeStore, RuntimeStoreError, SessionDelta};
436pub use traits::{
437    DestroyReport, RecoveryReport, RecycleReport, ResetReport, RetireReport, RuntimeControlPlane,
438    RuntimeControlPlaneError, RuntimeDriver, RuntimeDriverError,
439};