Skip to main content

meerkat_mobkit/identity_first/
types.rs

1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6// ---------------------------------------------------------------------------
7// Validation helpers
8// ---------------------------------------------------------------------------
9
10/// Error returned when an identity string fails validation.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct InvalidIdentity {
13    pub input: String,
14    pub reason: String,
15}
16
17impl fmt::Display for InvalidIdentity {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        write!(f, "invalid identity {:?}: {}", self.input, self.reason)
20    }
21}
22
23impl std::error::Error for InvalidIdentity {}
24
25fn validate_identity_string(s: &str) -> Result<(), InvalidIdentity> {
26    if s.is_empty() {
27        return Err(InvalidIdentity {
28            input: s.to_string(),
29            reason: "must not be empty".to_string(),
30        });
31    }
32    if s.contains(char::is_whitespace) {
33        return Err(InvalidIdentity {
34            input: s.to_string(),
35            reason: "must not contain whitespace".to_string(),
36        });
37    }
38    if s.contains('/') {
39        return Err(InvalidIdentity {
40            input: s.to_string(),
41            reason: "must not contain slashes".to_string(),
42        });
43    }
44    Ok(())
45}
46
47// ---------------------------------------------------------------------------
48// Macro for validated string newtypes (AgentIdentity, AgentRuntimeId)
49// ---------------------------------------------------------------------------
50
51macro_rules! validated_string_newtype {
52    ($(#[$meta:meta])* $name:ident) => {
53        $(#[$meta])*
54        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
55        pub struct $name(String);
56
57        impl $name {
58            /// Parse and validate a string into this type.
59            ///
60            /// # Errors
61            ///
62            /// Returns `InvalidIdentity` if the input is empty, contains whitespace,
63            /// or contains slashes.
64            pub fn parse(s: &str) -> Result<Self, InvalidIdentity> {
65                validate_identity_string(s)?;
66                Ok(Self(s.to_string()))
67            }
68
69            #[must_use]
70            pub fn as_str(&self) -> &str {
71                &self.0
72            }
73        }
74
75        impl fmt::Display for $name {
76            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77                f.write_str(&self.0)
78            }
79        }
80
81        impl Serialize for $name {
82            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
83            where
84                S: serde::Serializer,
85            {
86                serializer.serialize_str(&self.0)
87            }
88        }
89
90        impl<'de> Deserialize<'de> for $name {
91            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
92            where
93                D: serde::Deserializer<'de>,
94            {
95                let s = String::deserialize(deserializer)?;
96                Self::parse(&s).map_err(serde::de::Error::custom)
97            }
98        }
99    };
100}
101
102validated_string_newtype!(
103    /// The primary app-facing identity handle for all MobKit control-plane operations.
104    AgentIdentity
105);
106
107validated_string_newtype!(
108    /// Internal runtime-level ID minted at first-create.
109    AgentRuntimeId
110);
111
112// ---------------------------------------------------------------------------
113// AgentAddressability
114// ---------------------------------------------------------------------------
115
116/// Whether an agent accepts `send()` (addressable) or only `dispatch()` (internal-only).
117#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(rename_all = "snake_case")]
119pub enum AgentAddressability {
120    #[default]
121    Addressable,
122    InternalOnly,
123}
124
125// ---------------------------------------------------------------------------
126// DisplayName
127// ---------------------------------------------------------------------------
128
129/// Human-facing display name. Non-empty.
130#[derive(Debug, Clone, PartialEq, Eq, Hash)]
131pub struct DisplayName(String);
132
133impl DisplayName {
134    /// # Errors
135    ///
136    /// Returns `InvalidIdentity` if the input is empty.
137    pub fn parse(s: &str) -> Result<Self, InvalidIdentity> {
138        if s.is_empty() {
139            return Err(InvalidIdentity {
140                input: s.to_string(),
141                reason: "display name must not be empty".to_string(),
142            });
143        }
144        Ok(Self(s.to_string()))
145    }
146
147    #[must_use]
148    pub fn as_str(&self) -> &str {
149        &self.0
150    }
151}
152
153impl fmt::Display for DisplayName {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        f.write_str(&self.0)
156    }
157}
158
159impl Serialize for DisplayName {
160    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
161    where
162        S: serde::Serializer,
163    {
164        serializer.serialize_str(&self.0)
165    }
166}
167
168impl<'de> Deserialize<'de> for DisplayName {
169    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
170    where
171        D: serde::Deserializer<'de>,
172    {
173        let s = String::deserialize(deserializer)?;
174        Self::parse(&s).map_err(serde::de::Error::custom)
175    }
176}
177
178// ---------------------------------------------------------------------------
179// Monotonic u64 newtypes
180// ---------------------------------------------------------------------------
181
182macro_rules! monotonic_u64_newtype {
183    ($(#[$meta:meta])* $name:ident) => {
184        $(#[$meta])*
185        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
186        #[serde(transparent)]
187        pub struct $name(u64);
188
189        impl $name {
190            #[must_use]
191            pub const fn new(value: u64) -> Self {
192                Self(value)
193            }
194
195            #[must_use]
196            pub const fn get(self) -> u64 {
197                self.0
198            }
199        }
200
201        impl fmt::Display for $name {
202            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203                write!(f, "{}", self.0)
204            }
205        }
206    };
207}
208
209monotonic_u64_newtype!(
210    /// Monotonic generation counter for continuity. Starts at 0, incremented by `reset()`.
211    ContinuityGeneration
212);
213
214monotonic_u64_newtype!(
215    /// Monotonic checkpoint counter scoped to `(AgentIdentity, ContinuityGeneration)`.
216    CheckpointVersion
217);
218
219monotonic_u64_newtype!(
220    /// Monotonic ownership token issued by `LeaseProvider`.
221    FencingToken
222);
223
224// ---------------------------------------------------------------------------
225// Lightweight string newtypes (no validation beyond serde)
226// ---------------------------------------------------------------------------
227
228macro_rules! string_newtype {
229    ($(#[$meta:meta])* $name:ident) => {
230        $(#[$meta])*
231        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
232        #[serde(transparent)]
233        pub struct $name(String);
234
235        impl $name {
236            #[must_use]
237            pub fn new(s: impl Into<String>) -> Self {
238                Self(s.into())
239            }
240
241            #[must_use]
242            pub fn as_str(&self) -> &str {
243                &self.0
244            }
245        }
246
247        impl fmt::Display for $name {
248            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249                f.write_str(&self.0)
250            }
251        }
252    };
253}
254
255string_newtype!(
256    /// Correlation ID for dispatch tracing.
257    CorrelationId
258);
259
260string_newtype!(
261    /// Idempotency key for dispatch deduplication.
262    DispatchIdempotencyKey
263);
264
265// ---------------------------------------------------------------------------
266// ContinuityRecord
267// ---------------------------------------------------------------------------
268
269/// The authoritative continuity record for a durable agent identity.
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
271pub struct ContinuityRecord {
272    pub identity: AgentIdentity,
273    pub agent_runtime_id: AgentRuntimeId,
274    pub session_id: meerkat_core::types::SessionId,
275    pub generation: ContinuityGeneration,
276    pub checkpoint_version: CheckpointVersion,
277}
278
279// ---------------------------------------------------------------------------
280// ContinuityFailure + ContinuityFailureKind
281// ---------------------------------------------------------------------------
282
283/// Kind of continuity failure.
284#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case")]
286pub enum ContinuityFailureKind {
287    SnapshotMissing,
288    SnapshotCorrupted,
289    GenerationMismatch,
290    StoreUnavailable,
291}
292
293/// A typed failure payload for broken continuity.
294#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
295pub struct ContinuityFailure {
296    pub identity: AgentIdentity,
297    pub kind: ContinuityFailureKind,
298    pub record: Option<ContinuityRecord>,
299    pub detail: String,
300}
301
302// ---------------------------------------------------------------------------
303// ContinuityResolveState
304// ---------------------------------------------------------------------------
305
306/// The resolve result for a single identity from the continuity store.
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308#[serde(rename_all = "snake_case", tag = "state")]
309pub enum ContinuityResolveState {
310    Uninitialized,
311    Ready { record: ContinuityRecord },
312    Broken { failure: ContinuityFailure },
313}
314
315// ---------------------------------------------------------------------------
316// LeaseGrant
317// ---------------------------------------------------------------------------
318
319/// A lease grant returned by `LeaseProvider`.
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321pub struct LeaseGrant {
322    pub identity: AgentIdentity,
323    pub fencing_token: FencingToken,
324    #[serde(
325        serialize_with = "serde_duration_ms::serialize",
326        deserialize_with = "serde_duration_ms::deserialize"
327    )]
328    pub ttl: std::time::Duration,
329}
330
331/// Custom serde for `Duration` as integer milliseconds.
332mod serde_duration_ms {
333    use serde::{Deserialize, Deserializer, Serializer};
334    use std::time::Duration;
335
336    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
337    where
338        S: Serializer,
339    {
340        let ms = duration.as_millis();
341        // u128 -> u64 is safe for any reasonable TTL
342        serializer.serialize_u64(ms as u64)
343    }
344
345    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
346    where
347        D: Deserializer<'de>,
348    {
349        let ms = u64::deserialize(deserializer)?;
350        Ok(Duration::from_millis(ms))
351    }
352}
353
354// ---------------------------------------------------------------------------
355// LeaseAcquireResult + LeaseRenewResult
356// ---------------------------------------------------------------------------
357
358/// Result of a lease acquisition attempt.
359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
360#[serde(rename_all = "snake_case", tag = "result")]
361pub enum LeaseAcquireResult {
362    Acquired(LeaseGrant),
363    AlreadyHeld {
364        identity: AgentIdentity,
365        holder: String,
366    },
367}
368
369/// Result of a lease renewal attempt.
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
371#[serde(rename_all = "snake_case", tag = "result")]
372pub enum LeaseRenewResult {
373    Renewed(LeaseGrant),
374    Lost { identity: AgentIdentity },
375}
376
377// ---------------------------------------------------------------------------
378// DispatchOrigin + DispatchInput
379// ---------------------------------------------------------------------------
380
381/// Origin of a dispatch request.
382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
383#[serde(rename_all = "snake_case")]
384pub enum DispatchOrigin {
385    Connector,
386    Scheduler,
387    Policy,
388    Flow,
389    System,
390}
391
392/// Input for a dispatch operation.
393#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
394pub struct DispatchInput {
395    pub content: meerkat_core::ContentInput,
396    pub origin: DispatchOrigin,
397    pub correlation_id: Option<CorrelationId>,
398    pub idempotency_key: Option<DispatchIdempotencyKey>,
399}
400
401impl DispatchInput {
402    /// System-origin dispatch from plain text. The common case.
403    pub fn system(text: impl Into<String>) -> Self {
404        Self {
405            content: meerkat_core::ContentInput::Text(text.into()),
406            origin: DispatchOrigin::System,
407            correlation_id: None,
408            idempotency_key: None,
409        }
410    }
411
412    /// Dispatch with an explicit origin from plain text.
413    pub fn with_origin(text: impl Into<String>, origin: DispatchOrigin) -> Self {
414        Self {
415            content: meerkat_core::ContentInput::Text(text.into()),
416            origin,
417            correlation_id: None,
418            idempotency_key: None,
419        }
420    }
421
422    /// Attach a correlation ID (builder pattern).
423    pub fn with_correlation(mut self, id: impl Into<String>) -> Self {
424        self.correlation_id = Some(CorrelationId::new(id));
425        self
426    }
427
428    /// Attach an idempotency key (builder pattern).
429    pub fn with_idempotency(mut self, key: impl Into<String>) -> Self {
430        self.idempotency_key = Some(DispatchIdempotencyKey::new(key));
431        self
432    }
433}
434
435// ---------------------------------------------------------------------------
436// ManagedPeerEdge
437// ---------------------------------------------------------------------------
438
439/// Error returned when constructing an invalid `ManagedPeerEdge`.
440#[derive(Debug, Clone, PartialEq, Eq)]
441pub enum ManagedPeerEdgeError {
442    SelfEdge,
443}
444
445impl fmt::Display for ManagedPeerEdgeError {
446    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
447        match self {
448            Self::SelfEdge => write!(f, "self-edges are not allowed"),
449        }
450    }
451}
452
453impl std::error::Error for ManagedPeerEdgeError {}
454
455/// A managed dynamic topology edge between two agent identities.
456///
457/// Canonical ordering: `a < b`. Self-edges are rejected at construction time.
458/// Deserialization enforces the same invariant as `new()` via `TryFrom`.
459#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
460#[serde(into = "ManagedPeerEdgeRaw")]
461pub struct ManagedPeerEdge {
462    a: AgentIdentity,
463    b: AgentIdentity,
464}
465
466/// Raw deserialization target for `ManagedPeerEdge`.
467#[derive(Serialize, Deserialize)]
468struct ManagedPeerEdgeRaw {
469    a: AgentIdentity,
470    b: AgentIdentity,
471}
472
473impl From<ManagedPeerEdge> for ManagedPeerEdgeRaw {
474    fn from(edge: ManagedPeerEdge) -> Self {
475        Self {
476            a: edge.a,
477            b: edge.b,
478        }
479    }
480}
481
482impl TryFrom<ManagedPeerEdgeRaw> for ManagedPeerEdge {
483    type Error = ManagedPeerEdgeError;
484
485    fn try_from(raw: ManagedPeerEdgeRaw) -> Result<Self, Self::Error> {
486        Self::new(raw.a, raw.b)
487    }
488}
489
490impl<'de> Deserialize<'de> for ManagedPeerEdge {
491    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
492    where
493        D: serde::Deserializer<'de>,
494    {
495        let raw = ManagedPeerEdgeRaw::deserialize(deserializer)?;
496        Self::try_from(raw).map_err(serde::de::Error::custom)
497    }
498}
499
500impl ManagedPeerEdge {
501    /// Construct a managed peer edge with canonical ordering enforcement.
502    ///
503    /// # Errors
504    ///
505    /// Returns `ManagedPeerEdgeError::SelfEdge` if `a == b`.
506    pub fn new(a: AgentIdentity, b: AgentIdentity) -> Result<Self, ManagedPeerEdgeError> {
507        if a == b {
508            return Err(ManagedPeerEdgeError::SelfEdge);
509        }
510        if a < b {
511            Ok(Self { a, b })
512        } else {
513            Ok(Self { a: b, b: a })
514        }
515    }
516
517    #[must_use]
518    pub fn a(&self) -> &AgentIdentity {
519        &self.a
520    }
521
522    #[must_use]
523    pub fn b(&self) -> &AgentIdentity {
524        &self.b
525    }
526}
527
528// ---------------------------------------------------------------------------
529// NotAddressable error
530// ---------------------------------------------------------------------------
531
532/// Error returned when `send()` targets an `InternalOnly` agent.
533#[derive(Debug, Clone, PartialEq, Eq)]
534pub struct NotAddressable {
535    pub identity: AgentIdentity,
536    pub addressability: AgentAddressability,
537}
538
539impl fmt::Display for NotAddressable {
540    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
541        write!(
542            f,
543            "agent {:?} is not addressable (current: {:?})",
544            self.identity, self.addressability
545        )
546    }
547}
548
549impl std::error::Error for NotAddressable {}
550
551// ---------------------------------------------------------------------------
552// DurableAgentSpec
553// ---------------------------------------------------------------------------
554
555/// The preferred roster/spawn specification for identity-first continuity.
556#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
557pub struct DurableAgentSpec {
558    pub identity: AgentIdentity,
559    pub profile: meerkat_mob::ProfileName,
560    #[serde(default)]
561    pub addressability: AgentAddressability,
562    pub display_name: Option<DisplayName>,
563    #[serde(default)]
564    pub labels: std::collections::BTreeMap<String, String>,
565    pub context: Option<serde_json::Value>,
566    #[serde(default)]
567    pub additional_instructions: Vec<String>,
568    #[serde(default)]
569    pub initial_message: Option<meerkat_core::ContentInput>,
570    #[serde(default)]
571    pub runtime_mode_override: Option<meerkat_mob::MobRuntimeMode>,
572    #[serde(default)]
573    pub backend: Option<meerkat_mob::MobBackendKind>,
574    #[serde(default)]
575    pub binding: Option<meerkat_contracts::WireRuntimeBinding>,
576}
577
578// ---------------------------------------------------------------------------
579// IdentityStatus + supporting types
580// ---------------------------------------------------------------------------
581
582/// Lifecycle state of an identity.
583#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
584#[serde(rename_all = "snake_case")]
585pub enum IdentityLifecycleState {
586    /// Identity metadata is registered and addressable, but no concrete mob
587    /// member/session has been spawned or resumed in this runtime yet.
588    Dormant,
589    /// Identity has a concrete mob member/session in this runtime.
590    Active,
591    Retiring,
592    Suspended,
593    /// Continuity exists but cannot currently be materialized safely.
594    Broken,
595    Uninitialized,
596}
597
598impl IdentityLifecycleState {
599    /// Canonical wire vocabulary for identity lifecycle states.
600    ///
601    /// meerkat 0.7 moved the member rows (`mobkit/get_member`,
602    /// `list_members`, `ensure_member`, `find_members`) to lowercase state
603    /// strings — matching the published SDK constants
604    /// (`MEMBER_STATE_ACTIVE = "active"`) and the console vocabulary. The
605    /// identity-first status/inspect surfaces must speak the same casing so
606    /// the two member-state surfaces never disagree on the same wire.
607    pub fn wire_str(self) -> &'static str {
608        match self {
609            Self::Dormant => "dormant",
610            Self::Active => "active",
611            Self::Retiring => "retiring",
612            Self::Suspended => "suspended",
613            Self::Broken => "broken",
614            Self::Uninitialized => "uninitialized",
615        }
616    }
617}
618
619/// Information about a held lease.
620#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
621pub struct LeaseInfo {
622    pub fencing_token: FencingToken,
623    #[serde(
624        serialize_with = "serde_duration_ms::serialize",
625        deserialize_with = "serde_duration_ms::deserialize"
626    )]
627    pub ttl_remaining: std::time::Duration,
628    pub healthy: bool,
629}
630
631/// Durability policy declared by the continuity store.
632#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
633#[serde(rename_all = "snake_case", tag = "kind")]
634pub enum DurabilityPolicy {
635    SyncWriteThrough,
636    AsyncReplicated,
637    BufferedExport { max_loss_window_ms: u64 },
638}
639
640/// Health of the continuity store for an identity.
641#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
642pub struct ContinuityHealth {
643    pub store_reachable: bool,
644    pub durability_policy: DurabilityPolicy,
645    pub last_checkpoint_version: Option<CheckpointVersion>,
646}
647
648/// Full status response for an identity.
649#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
650pub struct IdentityStatus {
651    pub identity: AgentIdentity,
652    pub state: IdentityLifecycleState,
653    pub agent_runtime_id: Option<AgentRuntimeId>,
654    pub session_id: Option<meerkat_core::types::SessionId>,
655    pub profile: Option<meerkat_mob::ProfileName>,
656    pub runtime_mode: Option<meerkat_mob::MobRuntimeMode>,
657    pub addressability: AgentAddressability,
658    pub display_name: Option<DisplayName>,
659    #[serde(default)]
660    pub labels: std::collections::BTreeMap<String, String>,
661    pub generation: Option<ContinuityGeneration>,
662    pub checkpoint_version: Option<CheckpointVersion>,
663    pub lease: Option<LeaseInfo>,
664    pub continuity_health: Option<ContinuityHealth>,
665}
666
667// ---------------------------------------------------------------------------
668// AgentBuildContext + AgentBuildDraft + ExternalToolDef
669// ---------------------------------------------------------------------------
670
671#[derive(Clone, Default)]
672pub struct AgentRuntimeServices {
673    mob_handle: Option<meerkat_mob::MobHandle>,
674}
675
676impl AgentRuntimeServices {
677    pub fn new(mob_handle: meerkat_mob::MobHandle) -> Self {
678        Self {
679            mob_handle: Some(mob_handle),
680        }
681    }
682
683    pub fn empty() -> Self {
684        Self { mob_handle: None }
685    }
686
687    pub fn mob_handle(&self) -> Option<meerkat_mob::MobHandle> {
688        self.mob_handle.clone()
689    }
690
691    pub fn has_mob_handle(&self) -> bool {
692        self.mob_handle.is_some()
693    }
694}
695
696impl std::fmt::Debug for AgentRuntimeServices {
697    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
698        f.debug_struct("AgentRuntimeServices")
699            .field("mob_handle", &self.mob_handle.is_some())
700            .finish()
701    }
702}
703
704impl PartialEq for AgentRuntimeServices {
705    fn eq(&self, other: &Self) -> bool {
706        self.mob_handle.is_some() == other.mob_handle.is_some()
707    }
708}
709
710impl Eq for AgentRuntimeServices {}
711
712/// Read-only context provided to `AgentCustomizer` at build time.
713#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
714pub struct AgentBuildContext {
715    pub identity: AgentIdentity,
716    pub active_peers: Vec<AgentIdentity>,
717    pub managed_edges: Vec<ManagedPeerEdge>,
718    #[serde(default, skip)]
719    pub runtime_services: AgentRuntimeServices,
720}
721
722/// Tool definition for the customizer boundary.
723#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
724pub struct ExternalToolDef {
725    pub name: String,
726    pub description: String,
727    pub input_schema: serde_json::Value,
728}
729
730/// Mutable draft that `AgentCustomizer` modifies.
731#[derive(Clone, Default)]
732pub struct LocalExternalToolOverlay {
733    dispatcher: Option<Arc<dyn meerkat_core::agent::AgentToolDispatcher>>,
734}
735
736impl LocalExternalToolOverlay {
737    pub fn new(dispatcher: Arc<dyn meerkat_core::agent::AgentToolDispatcher>) -> Self {
738        Self {
739            dispatcher: Some(dispatcher),
740        }
741    }
742
743    pub fn empty() -> Self {
744        Self { dispatcher: None }
745    }
746
747    pub fn dispatcher(&self) -> Option<Arc<dyn meerkat_core::agent::AgentToolDispatcher>> {
748        self.dispatcher.clone()
749    }
750
751    pub fn is_some(&self) -> bool {
752        self.dispatcher.is_some()
753    }
754}
755
756impl std::fmt::Debug for LocalExternalToolOverlay {
757    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
758        f.debug_struct("LocalExternalToolOverlay")
759            .field("dispatcher", &self.dispatcher.is_some())
760            .finish()
761    }
762}
763
764impl PartialEq for LocalExternalToolOverlay {
765    fn eq(&self, other: &Self) -> bool {
766        self.dispatcher.is_some() == other.dispatcher.is_some()
767    }
768}
769
770impl Eq for LocalExternalToolOverlay {}
771
772/// Mutable draft that `AgentCustomizer` modifies.
773///
774/// `external_tools` remains the serializable SDK/gateway declaration surface.
775/// `local_external_tools` is intentionally skipped by serde and is the
776/// in-process Rust overlay for apps that can supply a real dispatcher.
777#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
778pub struct AgentBuildDraft {
779    pub model: Option<String>,
780    pub system_prompt: Option<String>,
781    #[serde(default)]
782    pub additional_instructions: Vec<String>,
783    #[serde(default)]
784    pub labels: std::collections::BTreeMap<String, String>,
785    pub app_context: Option<serde_json::Value>,
786    #[serde(default)]
787    pub external_tools: Vec<ExternalToolDef>,
788    #[serde(default, skip)]
789    pub local_external_tools: LocalExternalToolOverlay,
790}
791
792// ---------------------------------------------------------------------------
793// SessionSnapshot
794// ---------------------------------------------------------------------------
795
796/// Opaque wrapper around serialized Meerkat session state.
797///
798/// Stored and loaded by `ContinuityStore`.
799/// Wire format (JSON-RPC): `{ "data": "<base64 string>" }`.
800#[derive(Debug, Clone, PartialEq, Eq)]
801pub struct SessionSnapshot {
802    pub data: Vec<u8>,
803}
804
805impl Serialize for SessionSnapshot {
806    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
807    where
808        S: serde::Serializer,
809    {
810        use base64::Engine;
811        use serde::ser::SerializeStruct;
812        let encoded = base64::engine::general_purpose::STANDARD.encode(&self.data);
813        let mut s = serializer.serialize_struct("SessionSnapshot", 1)?;
814        s.serialize_field("data", &encoded)?;
815        s.end()
816    }
817}
818
819impl<'de> Deserialize<'de> for SessionSnapshot {
820    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
821    where
822        D: serde::Deserializer<'de>,
823    {
824        use base64::Engine;
825
826        #[derive(Deserialize)]
827        struct Wrapper {
828            data: String,
829        }
830
831        let wrapper = Wrapper::deserialize(deserializer)?;
832        let data = base64::engine::general_purpose::STANDARD
833            .decode(&wrapper.data)
834            .map_err(serde::de::Error::custom)?;
835        Ok(Self { data })
836    }
837}
838
839// ---------------------------------------------------------------------------
840// RosterContext + TopologyContext
841// ---------------------------------------------------------------------------
842
843/// Context passed to `RosterProvider`.
844#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
845pub struct RosterContext {
846    pub mob_definition: Option<meerkat_mob::MobDefinition>,
847    pub previous_identities: Vec<AgentIdentity>,
848}
849
850/// Context passed to `TopologyProvider`.
851#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
852pub struct TopologyContext {
853    pub roster: Vec<DurableAgentSpec>,
854}
855
856// ---------------------------------------------------------------------------
857// Error types
858// ---------------------------------------------------------------------------
859
860/// Error from the continuity store.
861#[derive(Debug)]
862pub enum ContinuityStoreError {
863    StaleFencingToken {
864        identity: AgentIdentity,
865        presented: FencingToken,
866        current: FencingToken,
867    },
868    StaleCheckpointVersion {
869        identity: AgentIdentity,
870        presented: CheckpointVersion,
871        current: CheckpointVersion,
872    },
873    NotFound {
874        identity: AgentIdentity,
875    },
876    Io(String),
877    Corruption(String),
878}
879
880impl fmt::Display for ContinuityStoreError {
881    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
882        match self {
883            Self::StaleFencingToken {
884                identity,
885                presented,
886                current,
887            } => write!(
888                f,
889                "stale fencing token for {identity}: presented {presented}, current {current}"
890            ),
891            Self::StaleCheckpointVersion {
892                identity,
893                presented,
894                current,
895            } => write!(
896                f,
897                "stale checkpoint version for {identity}: presented {presented}, current {current}"
898            ),
899            Self::NotFound { identity } => {
900                write!(f, "continuity record not found for {identity}")
901            }
902            Self::Io(msg) => write!(f, "continuity store I/O error: {msg}"),
903            Self::Corruption(msg) => write!(f, "continuity store corruption: {msg}"),
904        }
905    }
906}
907
908impl std::error::Error for ContinuityStoreError {}
909
910/// Error from the lease provider.
911#[derive(Debug)]
912pub enum LeaseError {
913    ProviderUnavailable(String),
914    Io(String),
915}
916
917impl fmt::Display for LeaseError {
918    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
919        match self {
920            Self::ProviderUnavailable(msg) => {
921                write!(f, "lease provider unavailable: {msg}")
922            }
923            Self::Io(msg) => write!(f, "lease I/O error: {msg}"),
924        }
925    }
926}
927
928impl std::error::Error for LeaseError {}
929
930/// Error from the roster provider.
931#[derive(Debug)]
932pub enum RosterError {
933    ProviderUnavailable(String),
934    Io(String),
935}
936
937impl fmt::Display for RosterError {
938    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
939        match self {
940            Self::ProviderUnavailable(msg) => {
941                write!(f, "roster provider unavailable: {msg}")
942            }
943            Self::Io(msg) => write!(f, "roster I/O error: {msg}"),
944        }
945    }
946}
947
948impl std::error::Error for RosterError {}
949
950/// Error from the topology provider.
951#[derive(Debug)]
952pub enum TopologyError {
953    InvalidEdge(String),
954    ProviderUnavailable(String),
955}
956
957impl fmt::Display for TopologyError {
958    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
959        match self {
960            Self::InvalidEdge(msg) => write!(f, "invalid topology edge: {msg}"),
961            Self::ProviderUnavailable(msg) => {
962                write!(f, "topology provider unavailable: {msg}")
963            }
964        }
965    }
966}
967
968impl std::error::Error for TopologyError {}
969
970/// Error from the agent customizer.
971#[derive(Debug)]
972pub enum CustomizerError {
973    BuildFailed(String),
974    Io(String),
975}
976
977impl fmt::Display for CustomizerError {
978    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
979        match self {
980            Self::BuildFailed(msg) => write!(f, "customizer build failed: {msg}"),
981            Self::Io(msg) => write!(f, "customizer I/O error: {msg}"),
982        }
983    }
984}
985
986impl std::error::Error for CustomizerError {}