use std::collections::BTreeSet;
use async_trait::async_trait;
use meerkat::experimental_gpt_live::PublicGptLiveInstructionsPreface;
use meerkat_core::SessionId;
use meerkat_mob::{AgentIdentity, MobHandle, MobMemberStatus, ProfileBinding};
pub(crate) const PREFACE_MAX_BYTES: usize = 2000;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct PeerCapability {
pub name: String,
pub role: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct MemberCapabilities {
pub identity: String,
pub role: String,
pub mob_id: String,
pub peers: Vec<PeerCapability>,
pub tool_categories: Vec<(String, Vec<String>)>,
pub skills: Vec<String>,
}
impl MemberCapabilities {
pub(crate) async fn gather(handle: &MobHandle, identity: &AgentIdentity) -> Option<Self> {
let members = handle.list_members().await;
let me = members
.iter()
.find(|member| &member.agent_identity == identity)?;
let definition = handle.definition();
let mut peers: Vec<PeerCapability> = if me.wired_to.is_empty() {
members
.iter()
.filter(|member| member.agent_identity != *identity)
.filter(|member| !matches!(member.status, MobMemberStatus::Retiring))
.map(|member| PeerCapability {
name: member.agent_identity.to_string(),
role: member.role.to_string(),
})
.collect()
} else {
me.wired_to
.iter()
.map(|peer| PeerCapability {
name: peer.to_string(),
role: members
.iter()
.find(|member| &member.agent_identity == peer)
.map(|member| member.role.to_string())
.unwrap_or_default(),
})
.collect()
};
peers.sort();
peers.dedup();
let mut tool_categories = Vec::new();
let mut skills = Vec::new();
if let Some(ProfileBinding::Inline(profile)) = definition.profiles.get(&me.role) {
let tools = &profile.tools;
let mut categories: Vec<(&str, bool)> = vec![
("builtins", tools.builtins),
("shell", tools.shell),
("comms", tools.comms),
("memory", tools.memory),
("workgraph", tools.workgraph),
("mob", tools.mob),
("schedule", tools.schedule),
("image_generation", tools.image_generation),
];
categories.retain(|(_, enabled)| *enabled);
for (category, _) in categories {
tool_categories.push((category.to_string(), Vec::new()));
}
if !tools.mcp.is_empty() {
let mut modules: Vec<String> = tools.mcp.clone();
modules.sort();
modules.dedup();
tool_categories.push(("mcp".to_string(), modules));
}
let unique: BTreeSet<String> = profile.skills.iter().cloned().collect();
skills = unique.into_iter().collect();
}
Some(Self {
identity: identity.to_string(),
role: me.role.to_string(),
mob_id: handle.mob_id().to_string(),
peers,
tool_categories,
skills,
})
}
pub(crate) fn preface(&self) -> String {
let mut text = format!(
"You speak for {} ({}) in mob {}.",
self.identity, self.role, self.mob_id
);
if self.peers.is_empty() {
text.push_str(" You are not connected to other agents right now.");
} else {
text.push_str(" You are connected to: ");
text.push_str(
&self
.peers
.iter()
.map(|peer| {
if peer.role.is_empty() {
peer.name.clone()
} else {
format!("{} ({})", peer.name, peer.role)
}
})
.collect::<Vec<_>>()
.join(", "),
);
text.push('.');
}
let closing = " When asked who you are connected to or what you can do, answer from this \
list. Anything that needs these tools runs through the executor and \
returns to you.";
let budget = PREFACE_MAX_BYTES.saturating_sub(text.len() + closing.len());
let mut tools_text = String::new();
let mut omitted = 0usize;
let mut entries: Vec<String> = Vec::new();
for (category, names) in &self.tool_categories {
if names.is_empty() {
entries.push(category.clone());
} else {
entries.push(format!("{}: {}", category, names.join(", ")));
}
}
if !entries.is_empty() {
tools_text.push_str(" You have these tools available through the executor: ");
let mut first = true;
for (index, entry) in entries.iter().enumerate() {
let separator = if first { "" } else { "; " };
let remaining = entries.len() - index;
let marker_reserve = if remaining > 1 { 24 } else { 0 };
if tools_text.len() + separator.len() + entry.len() + 1 + marker_reserve
> budget.saturating_sub(self.skills_text_len())
{
omitted = remaining;
break;
}
tools_text.push_str(separator);
tools_text.push_str(entry);
first = false;
}
if omitted > 0 {
if !first {
tools_text.push_str("; ");
}
tools_text.push_str(&format!("and {omitted} more tool groups"));
}
tools_text.push('.');
}
text.push_str(&tools_text);
if !self.skills.is_empty() {
let skills = format!(" Skills: {}.", self.skills.join(", "));
if text.len() + skills.len() + closing.len() <= PREFACE_MAX_BYTES {
text.push_str(&skills);
} else {
text.push_str(&format!(" Skills: {} skills available.", self.skills.len()));
}
}
text.push_str(closing);
debug_assert!(text.len() <= PREFACE_MAX_BYTES + 64, "preface clamp failed");
text
}
fn skills_text_len(&self) -> usize {
if self.skills.is_empty() {
0
} else {
" Skills: .".len()
+ self
.skills
.iter()
.map(|skill| skill.len() + 2)
.sum::<usize>()
}
}
}
pub(crate) struct MemberPreface {
handle: MobHandle,
}
impl MemberPreface {
pub(crate) fn new(handle: MobHandle) -> Self {
Self { handle }
}
pub(crate) async fn preface_for_session(&self, session_id: &SessionId) -> Option<String> {
for member in self.handle.list_members().await {
if self
.handle
.resolve_bridge_session_id(&member.agent_identity)
.await
.as_ref()
== Some(session_id)
{
return MemberCapabilities::gather(&self.handle, &member.agent_identity)
.await
.map(|capabilities| capabilities.preface());
}
}
None
}
}
#[async_trait]
impl PublicGptLiveInstructionsPreface for MemberPreface {
async fn preface(&self, session_id: &SessionId) -> Option<String> {
self.preface_for_session(session_id).await
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture() -> MemberCapabilities {
MemberCapabilities {
identity: "agent-a".to_string(),
role: "worker".to_string(),
mob_id: "console-voice".to_string(),
peers: vec![
PeerCapability {
name: "agent-b".to_string(),
role: "reviewer".to_string(),
},
PeerCapability {
name: "ops-lead".to_string(),
role: "orchestrator".to_string(),
},
],
tool_categories: vec![
("builtins".to_string(), Vec::new()),
("shell".to_string(), Vec::new()),
(
"mcp".to_string(),
vec!["github".to_string(), "ledger".to_string()],
),
],
skills: vec!["incident-triage".to_string(), "release-notes".to_string()],
}
}
#[test]
fn preface_is_deterministic_and_names_everything() {
let text = fixture().preface();
assert_eq!(
text,
"You speak for agent-a (worker) in mob console-voice. You are connected to: \
agent-b (reviewer), ops-lead (orchestrator). You have these tools available \
through the executor: builtins; shell; mcp: github, ledger. Skills: \
incident-triage, release-notes. When asked who you are connected to or what you \
can do, answer from this list. Anything that needs these tools runs through the \
executor and returns to you."
);
assert_eq!(fixture().preface(), text);
}
#[test]
fn preface_without_peers_says_so() {
let mut caps = fixture();
caps.peers.clear();
assert!(
caps.preface()
.contains("You are not connected to other agents right now.")
);
}
#[test]
fn preface_clamps_tools_and_keeps_identity_and_peers_first() {
let mut caps = fixture();
caps.tool_categories = (0..400)
.map(|index| {
(
format!("category-{index:03}"),
vec![format!("tool-{index:03}")],
)
})
.collect();
caps.skills = (0..200).map(|index| format!("skill-{index:03}")).collect();
let text = caps.preface();
assert!(text.len() <= PREFACE_MAX_BYTES + 64, "{}", text.len());
assert!(text.starts_with("You speak for agent-a (worker) in mob console-voice."));
assert!(text.contains("agent-b (reviewer), ops-lead (orchestrator)."));
assert!(text.contains("and "), "omission marker missing: {text}");
assert!(text.contains("more tool groups"), "{text}");
assert!(text.contains("Skills: 200 skills available."), "{text}");
assert!(text.ends_with("returns to you."));
}
}