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 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 traits;
94
95use meerkat_core::lifecycle::run_primitive::RuntimeTurnMetadata as RuntimeStampedTurnMetadata;
96use std::any::Any;
97use std::sync::Arc;
98
99struct SessionRuntimeBindingsAuthority;
100
101pub(crate) fn session_runtime_bindings_authority() -> Arc<dyn Any + Send + Sync> {
102    Arc::new(SessionRuntimeBindingsAuthority)
103}
104
105pub(crate) fn local_session_runtime_bindings_authority() -> Arc<dyn Any + Send + Sync> {
106    session_runtime_bindings_authority()
107}
108
109pub fn session_runtime_bindings_have_machine_authority(
110    bindings: &meerkat_core::SessionRuntimeBindings,
111) -> bool {
112    bindings
113        .__runtime_authority()
114        .is::<SessionRuntimeBindingsAuthority>()
115}
116
117// Re-exports for convenience
118pub use accept::{AcceptOutcome, RejectReason};
119pub use coalescing::{
120    AggregateDescriptor, CoalescingResult, SupersessionScope, check_supersession,
121    create_aggregate_input, is_coalescing_eligible,
122};
123pub use completion::{
124    CompletionCleanupObservation, CompletionHandle, CompletionOutcome, CompletionWaitError,
125};
126pub use driver::{EphemeralRuntimeDriver, PersistentRuntimeDriver, PostAdmissionSignal};
127pub use handles::{
128    HandleDslAuthority, RuntimeAuthLeaseHandle, RuntimeCommsDrainHandle,
129    RuntimeExternalToolSurfaceHandle, RuntimeInteractionStreamHandle,
130    RuntimeMcpServerLifecycleHandle, RuntimeModelRoutingHandle, RuntimePeerCommsHandle,
131    RuntimePeerInteractionHandle, RuntimeSessionAdmissionHandle, RuntimeSessionContextHandle,
132    RuntimeTurnStateHandle,
133};
134pub use identifiers::{
135    CausationId, ConversationId, CorrelationId, EventCodeId, IdempotencyKey, InputKind, KindId,
136    LogicalRuntimeId, PolicyVersion, ProjectionRuleId, RuntimeEventId, SchemaId, SupersessionKey,
137};
138pub use ingress_types::{ContentShape, RequestId, ReservationKey};
139pub use input::{
140    ContinuationInput, ContinuationKind, ExternalEventInput, FlowStepInput, Input, InputDurability,
141    InputHeader, InputOrigin, InputVisibility, OperationInput, PeerConvention, PeerInput,
142    PromptInput, ResponseProgressPhase, ResponseTerminalStatus, peer_response_terminal_input,
143    response_terminal_status_from_wire,
144};
145pub use input_ledger::InputLedger;
146pub use input_scope::InputScope;
147pub use input_state::{
148    InputAbandonReason, InputLifecycleState, InputState, InputStateEvent, InputStateHistoryEntry,
149    InputTerminalOutcome, PolicySnapshot, ReconstructionSource,
150};
151pub use meerkat_core::types::HandlingMode;
152pub use meerkat_machine::{
153    CommsDrainMode, CommsDrainPhase, DrainExitReason, MachineSessionControlAuthority,
154    MeerkatConsumerSurface, MeerkatMachine, PeerIngressOwner, RuntimeBindingsError,
155    RuntimeLifecycleFacts, RuntimeLoopQueueAdmissionPlan, classify_runtime_lifecycle_state,
156    classify_runtime_loop_queue_admission, standalone_tool_visibility_owner,
157};
158pub use meerkat_machine_types::{
159    HydratedSessionLlmState, ImageOperationRoutingRequest, ImageOperationRoutingResult,
160    ModelRoutingApprovalDisposition, ModelRoutingRealtimePolicy, ResolvedSessionLlmReconfigure,
161    SessionLlmCapabilitySurface, SessionLlmCapabilitySurfaceStatus, SessionLlmReconfigureHost,
162    SessionLlmReconfigureReport, SessionLlmReconfigureRequest, SessionToolVisibilityDelta,
163};
164#[doc(hidden)]
165pub use meerkat_machine_types::{
166    MeerkatAdmittedInputSnapshot, MeerkatArchiveSnapshot, MeerkatBindingSnapshot,
167    MeerkatCompletionWaiterSnapshot, MeerkatCompletionWaitersSnapshot, MeerkatControlSnapshot,
168    MeerkatCursorSnapshot, MeerkatDrainSnapshot, MeerkatDriverKind, MeerkatInputsSnapshot,
169    MeerkatMachineCatalogInput, MeerkatMachineCommandClassification,
170    MeerkatMachineCommandClassificationRecord, MeerkatMachineCommandVariant,
171    MeerkatMachineFieldlessRuntimeInternalInput, MeerkatMachineRuntimeInternalClassificationRecord,
172    MeerkatMachineRuntimeInternalInput, MeerkatMachineRuntimeInternalReason,
173    MeerkatMachineShellMechanicReason, MeerkatMachineSpineSnapshot, MeerkatOpsSnapshot,
174    canonical_meerkat_machine_command_classifications,
175    canonical_meerkat_machine_command_input_variant_manifest,
176    canonical_meerkat_machine_command_manifest,
177    canonical_meerkat_machine_runtime_internal_classifications,
178    canonical_meerkat_machine_runtime_internal_fieldless_input_variant_manifest,
179    canonical_meerkat_machine_runtime_internal_input_variant_manifest,
180    canonical_meerkat_machine_runtime_internal_manifest,
181};
182pub use ops_lifecycle::{
183    OpsLifecycleConfig, OpsLifecyclePersistenceRequest, PersistedOpsSnapshot,
184    RuntimeOpsLifecycleRegistry,
185};
186
187#[cfg(all(not(target_arch = "wasm32"), any(test, feature = "test-support")))]
188#[doc(hidden)]
189pub fn test_peer_comms_handle() -> Arc<dyn meerkat_core::handles::PeerCommsHandle> {
190    test_peer_comms_handle_with_silent(std::iter::empty::<String>())
191}
192
193#[cfg(all(not(target_arch = "wasm32"), any(test, feature = "test-support")))]
194#[doc(hidden)]
195#[allow(clippy::expect_used)]
196pub fn test_peer_comms_handle_with_silent<I, S>(
197    silent_intents: I,
198) -> Arc<dyn meerkat_core::handles::PeerCommsHandle>
199where
200    I: IntoIterator<Item = S>,
201    S: Into<String>,
202{
203    let silent_intents = silent_intents
204        .into_iter()
205        .map(Into::into)
206        .collect::<Vec<_>>();
207    std::thread::spawn(move || {
208        let runtime = tokio::runtime::Builder::new_current_thread()
209            .enable_all()
210            .build()
211            .expect("test peer-comms runtime should build");
212        runtime.block_on(async move {
213            let machine = MeerkatMachine::ephemeral();
214            let session_id = meerkat_core::SessionId::new();
215            let bindings = machine
216                .prepare_bindings(session_id.clone())
217                .await
218                .expect("generated MeerkatMachine should prepare test peer-comms bindings");
219            if !silent_intents.is_empty() {
220                machine
221                    .set_session_silent_intents(&session_id, silent_intents)
222                    .await
223                    .expect("set silent intents");
224            }
225            Arc::clone(bindings.peer_comms())
226        })
227    })
228    .join()
229    .expect("test peer-comms authority thread should finish")
230}
231
232#[cfg(all(not(target_arch = "wasm32"), any(test, feature = "test-support")))]
233#[doc(hidden)]
234#[allow(clippy::expect_used)]
235pub fn test_peer_input_candidate_from_interaction(
236    interaction: meerkat_core::interaction::InboxInteraction,
237    peer_id: meerkat_core::comms::PeerId,
238) -> meerkat_core::interaction::PeerInputCandidate {
239    use meerkat_core::interaction::{
240        InteractionContent, InteractionId, PeerIngressEnvelopeFacts, PeerIngressEnvelopeKind,
241        PeerIngressFact, PeerIngressIdentity,
242    };
243
244    let handle = test_peer_comms_handle();
245    let facts = PeerIngressEnvelopeFacts {
246        item_id: interaction.id.to_string(),
247        from_peer: interaction.from.clone(),
248        from_peer_id: peer_id,
249        kind: match &interaction.content {
250            InteractionContent::Message { body, .. } => {
251                PeerIngressEnvelopeKind::Message { body: body.clone() }
252            }
253            InteractionContent::Request { intent, params, .. } => {
254                PeerIngressEnvelopeKind::Request {
255                    intent: intent.clone(),
256                    params: params.clone(),
257                }
258            }
259            InteractionContent::Response {
260                in_reply_to,
261                status,
262                result,
263                ..
264            } => PeerIngressEnvelopeKind::Response {
265                in_reply_to: in_reply_to.to_string(),
266                status: *status,
267                result: result.clone(),
268            },
269        },
270    };
271    let admission = handle
272        .classify_external_envelope(facts)
273        .expect("generated peer-comms authority should classify test interaction");
274    // R084: the admitted sender identity comes from the machine-echoed
275    // canonical peer id on the classification effect, not the local input.
276    let canonical_from_peer_id = admission
277        .from_peer_id
278        .expect("generated envelope classification should echo the canonical sender peer id");
279    let classification = admission.classification;
280    let convention = match &interaction.content {
281        InteractionContent::Message { .. } => meerkat_core::PeerIngressConvention::Message,
282        InteractionContent::Request { intent, .. } => {
283            if let Some(kind) = classification.lifecycle_kind {
284                let peer = admission
285                    .lifecycle_peer
286                    .clone()
287                    .expect("generated lifecycle classification should include a peer subject");
288                meerkat_core::PeerIngressConvention::Lifecycle { kind, peer }
289            } else {
290                let request_id = admission
291                    .request_id
292                    .clone()
293                    .expect("generated request classification should include request id");
294                meerkat_core::PeerIngressConvention::Request {
295                    request_id,
296                    intent: intent.clone(),
297                }
298            }
299        }
300        InteractionContent::Response { status, .. } => {
301            let in_reply_to = admission
302                .request_id
303                .as_deref()
304                .and_then(|id| uuid::Uuid::parse_str(id).ok())
305                .map(InteractionId)
306                .expect("generated response classification should include in-reply-to id");
307            meerkat_core::PeerIngressConvention::Response {
308                in_reply_to,
309                status: *status,
310            }
311        }
312    };
313    let ingress = PeerIngressFact::peer(
314        interaction.id,
315        classification.class,
316        classification.kind,
317        Some(classification.auth),
318        PeerIngressIdentity::new(canonical_from_peer_id, interaction.from.clone(), convention),
319    );
320    let mut candidate = meerkat_core::interaction::PeerInputCandidate::new(
321        interaction,
322        ingress,
323        admission.lifecycle_peer,
324    );
325    candidate.response_terminality = classification.response_terminality;
326    candidate
327}
328
329/// Stamp prompt turn metadata with the runtime-owned input semantics.
330///
331/// This helper exists for runtime-backed service-turn paths that already hold
332/// machine admission and must pass a runtime-classified prompt turn into the
333/// session layer. New prompt materialization should prefer `MeerkatMachine`
334/// input admission so the machine creates this metadata directly.
335pub fn runtime_stamped_prompt_turn_metadata(
336    metadata: Option<RuntimeStampedTurnMetadata>,
337) -> RuntimeStampedTurnMetadata {
338    let input = Input::Prompt(PromptInput::from_content_input(
339        meerkat_core::ContentInput::Text(String::new()),
340        metadata,
341    ));
342    let semantics = runtime_prompt_semantics_from_machine(&input);
343    runtime_loop::for_input(&input, semantics)
344}
345
346#[allow(clippy::expect_used)]
347fn runtime_prompt_semantics_from_machine(input: &Input) -> ingress_types::RuntimeInputSemantics {
348    let mut authority = meerkat_machine::dsl_authority::new_initialized_authority(
349        "generated runtime prompt machine authority must initialize",
350    );
351    let transition = meerkat_machine::dsl::MeerkatMachineMutator::apply(
352        &mut authority,
353        meerkat_machine::dsl::MeerkatMachineInput::ResolveAdmissionPlan {
354            input_id: input.id().to_string(),
355            input_kind: meerkat_machine::dsl::AdmissionInputKind::from(input.kind()),
356            requested_lane: input
357                .handling_mode()
358                .map(meerkat_machine::dsl::InputLane::from),
359            continuation_kind: meerkat_machine::dsl::AdmissionContinuationKind::from(
360                input.continuation_kind(),
361            ),
362            silent_intent_match: false,
363            existing_superseded_input_id: None,
364            runtime_running: false,
365            active_turn_boundary_available: false,
366            without_wake: false,
367        },
368    )
369    .expect("generated admission authority must accept runtime prompt metadata");
370
371    transition
372        .into_effects()
373        .into_iter()
374        .find_map(|effect| match effect {
375            meerkat_machine::dsl::MeerkatMachineEffect::AdmissionResolved {
376                runtime_boundary,
377                runtime_execution_kind,
378                runtime_peer_response_terminal_apply_intent,
379                live_interrupt_required,
380                ..
381            } => Some(ingress_types::RuntimeInputSemantics {
382                boundary: runtime_boundary.into(),
383                execution_kind: runtime_execution_kind.into(),
384                execution_handling_mode: None,
385                peer_response_terminal_apply_intent: runtime_peer_response_terminal_apply_intent
386                    .map(Into::into),
387                live_interrupt_required,
388            }),
389            _ => None,
390        })
391        .expect("generated admission authority must emit prompt runtime semantics")
392}
393
394#[cfg(test)]
395mod runtime_prompt_metadata_tests {
396    #[test]
397    fn runtime_stamped_prompt_turn_metadata_uses_generated_prompt_semantics() {
398        let metadata = super::runtime_stamped_prompt_turn_metadata(None);
399        assert_eq!(
400            metadata.execution_kind,
401            Some(meerkat_core::lifecycle::RuntimeExecutionKind::ContentTurn)
402        );
403        assert!(metadata.peer_response_terminal_apply_intent.is_none());
404    }
405}
406
407#[doc(hidden)]
408pub mod machine_schema_exports {
409    pub fn meerkat_machine_schema() -> meerkat_machine_schema::MachineSchema {
410        meerkat_machine_schema::catalog::dsl::meerkat_machine_schema_metadata()
411            .attach_to(crate::meerkat_machine::dsl::MeerkatMachineState::schema())
412    }
413
414    pub fn auth_machine_schema() -> meerkat_machine_schema::MachineSchema {
415        meerkat_machine_schema::catalog::dsl::auth_machine_schema_metadata()
416            .attach_to(crate::auth_machine::dsl::AuthMachineState::schema())
417    }
418}
419pub use interrupt_public_result::{
420    UserInterruptObservation, UserInterruptPublicResult, resolve_user_interrupt_public_result,
421};
422pub use peer_handling_mode::{PeerHandlingModeError, validate_peer_handling_mode};
423pub use policy::{
424    ApplyMode, ConsumePoint, DrainPolicy, PolicyDecision, QueueMode, RoutingDisposition, WakeMode,
425};
426pub use policy_table::{DefaultPolicyTable, generated_default_policy_version};
427pub use queue::InputQueue;
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};