1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//! Launch mode and fork context for mob member provisioning.
//!
//! These types define how a member is launched within a mob — fresh, resuming
//! an existing session, or forking from another member's conversation.
//!
//! `ForkContext` is a mob-native type defined here (not in `meerkat-core`)
//! because forking is a mob-level orchestration concept.
use crate::ids::AgentIdentity;
use meerkat_core::types::SessionId;
use serde::{Deserialize, Serialize};
/// How a mob member should be launched.
///
/// Public spawn-policy enum exposed so that external consumers of
/// [`crate::runtime::SpawnMemberSpec`] can configure session adoption
/// (resume an existing bridge session, fork from a sibling member's
/// history) without reaching into `pub(crate)` internals.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "snake_case")]
#[non_exhaustive]
pub enum MemberLaunchMode {
/// Start with a brand-new session (default).
#[default]
Fresh,
/// Resume an existing bridge session binding by ID.
Resume { bridge_session_id: SessionId },
/// Fork from another member's conversation history.
Fork {
source_member_id: AgentIdentity,
#[serde(default)]
fork_context: ForkContext,
},
}
impl MemberLaunchMode {
/// If this launch mode resumes an existing bridge session, return
/// the session id. Returns `None` for `Fresh` and `Fork` modes.
pub fn resume_bridge_session_id(&self) -> Option<&SessionId> {
match self {
Self::Resume { bridge_session_id } => Some(bridge_session_id),
_ => None,
}
}
}
/// Controls how much conversation history is included when forking.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum ForkContext {
/// Full conversation history, read from the source member's session via
/// the `SessionService` and rendered as a TEXT context block prepended to
/// the new member's initial prompt. The child gets a fresh session — no
/// `Session::fork()` copy-on-write is involved.
#[default]
FullHistory,
/// Last N messages from the source session.
LastMessages { count: u32 },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resume_launch_mode_bridge_session_accessors_stay_additive() {
let sid = SessionId::new();
let mode = MemberLaunchMode::Resume {
bridge_session_id: sid.clone(),
};
assert_eq!(mode.resume_bridge_session_id(), Some(&sid));
}
#[test]
fn resume_launch_mode_deserializes_bridge_session_id() {
let sid = SessionId::new();
let payload = serde_json::json!({
"mode": "resume",
"bridge_session_id": sid,
});
let mode: MemberLaunchMode =
serde_json::from_value(payload).expect("resume launch mode should deserialize");
assert_eq!(mode.resume_bridge_session_id(), Some(&sid));
}
/// DELETE_ME A3 + C1 regression: `MemberLaunchMode` and its variants
/// (Fresh, Resume, Fork) must be reachable from outside the crate
/// so external consumers can configure session adoption without
/// reaching into `pub(crate)` internals. This test exercises each
/// variant at the public API level — if anyone ever re-hides the
/// enum or its constructors, this fails to compile instead of
/// breaking downstream crates silently.
#[test]
fn member_launch_mode_public_seam_covers_all_variants() {
use crate::ids::AgentIdentity;
// Fresh is the default.
let fresh = MemberLaunchMode::default();
assert!(fresh.resume_bridge_session_id().is_none());
// Resume carries a SessionId; accessor exposed as pub.
let sid = SessionId::new();
let resume = MemberLaunchMode::Resume {
bridge_session_id: sid.clone(),
};
assert_eq!(resume.resume_bridge_session_id(), Some(&sid));
// Fork carries a source member id (+ fork context). ForkContext
// variants are both reachable — FullHistory (default) and
// LastMessages { count }.
let fork_full = MemberLaunchMode::Fork {
source_member_id: AgentIdentity::from("lead"),
fork_context: ForkContext::default(),
};
assert!(fork_full.resume_bridge_session_id().is_none());
let fork_last = MemberLaunchMode::Fork {
source_member_id: AgentIdentity::from("lead"),
fork_context: ForkContext::LastMessages { count: 5 },
};
assert!(fork_last.resume_bridge_session_id().is_none());
// Round-trip serde on all three shapes to catch accidental
// `#[serde(skip)]` or visibility regressions on constructor
// fields.
for mode in [fresh, resume, fork_full, fork_last] {
let encoded = serde_json::to_value(&mode).expect("mode serializes");
let decoded: MemberLaunchMode =
serde_json::from_value(encoded).expect("mode roundtrips");
assert_eq!(
mode.resume_bridge_session_id().is_some(),
decoded.resume_bridge_session_id().is_some(),
);
}
}
}