feat(orchestration): multi-agent communication, unified agents, env passthrough

- btmsg: admin role (tier 0), channel messaging (create/list/send/history),
  admin global feed, mark-read conversations
- Rust btmsg module: admin bypass, channels, feed, 8 new Tauri commands
- CommsTab: sidebar chat interface with activity feed, DMs, channels (Ctrl+M)
- Agent unification: Tier 1 agents rendered as ProjectBoxes via agentToProject()
  converter, getAllWorkItems() combines agents + projects in ProjectGrid
- GroupAgentsPanel: click-to-navigate agents to their ProjectBox
- Agent system prompts: generateAgentPrompt() builds comprehensive introductory
  context (role, environment, team, btmsg/bttask docs, workflow instructions)
- AgentSession passes group context to prompt generator via $derived.by()
- BTMSG_AGENT_ID env var passthrough: extra_env field flows through full chain
  (agent-bridge → Rust AgentQueryOptions → NDJSON → sidecar runners → cleanEnv)
- workspace store: updateAgent() for Tier 1 agent config persistence
This commit is contained in:
DexterFromLab 2026-03-11 14:53:39 +01:00
parent 1331d094b3
commit a158ed9544
19 changed files with 1918 additions and 39 deletions

View file

@ -48,6 +48,44 @@ pub struct BtmsgMessage {
pub sender_role: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BtmsgFeedMessage {
pub id: String,
pub from_agent: String,
pub to_agent: String,
pub content: String,
pub created_at: String,
pub reply_to: Option<String>,
pub sender_name: String,
pub sender_role: String,
pub recipient_name: String,
pub recipient_role: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BtmsgChannel {
pub id: String,
pub name: String,
pub group_id: String,
pub created_by: String,
pub member_count: i32,
pub created_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BtmsgChannelMessage {
pub id: String,
pub channel_id: String,
pub from_agent: String,
pub content: String,
pub created_at: String,
pub sender_name: String,
pub sender_role: String,
}
pub fn get_agents(group_id: &str) -> Result<Vec<BtmsgAgent>, String> {
let db = open_db()?;
let mut stmt = db.prepare(
@ -136,22 +174,24 @@ pub fn history(agent_id: &str, other_id: &str, limit: i32) -> Result<Vec<BtmsgMe
pub fn send_message(from_agent: &str, to_agent: &str, content: &str) -> Result<String, String> {
let db = open_db()?;
// Get sender's group
let group_id: String = db.query_row(
"SELECT group_id FROM agents WHERE id = ?",
// Get sender's group and tier
let (group_id, sender_tier): (String, i32) = db.query_row(
"SELECT group_id, tier FROM agents WHERE id = ?",
params![from_agent],
|row| row.get(0),
|row| Ok((row.get(0)?, row.get(1)?)),
).map_err(|e| format!("Sender not found: {e}"))?;
// Check contact permission
let allowed: bool = db.query_row(
"SELECT COUNT(*) > 0 FROM contacts WHERE agent_id = ? AND contact_id = ?",
params![from_agent, to_agent],
|row| row.get(0),
).map_err(|e| format!("Contact check error: {e}"))?;
// Admin (tier 0) bypasses contact restrictions
if sender_tier > 0 {
let allowed: bool = db.query_row(
"SELECT COUNT(*) > 0 FROM contacts WHERE agent_id = ? AND contact_id = ?",
params![from_agent, to_agent],
|row| row.get(0),
).map_err(|e| format!("Contact check error: {e}"))?;
if !allowed {
return Err(format!("Not allowed to message '{to_agent}'"));
if !allowed {
return Err(format!("Not allowed to message '{to_agent}'"));
}
}
let msg_id = uuid::Uuid::new_v4().to_string();
@ -171,3 +211,195 @@ pub fn set_status(agent_id: &str, status: &str) -> Result<(), String> {
).map_err(|e| format!("Update error: {e}"))?;
Ok(())
}
pub fn ensure_admin(group_id: &str) -> Result<(), String> {
let db = open_db()?;
let exists: bool = db.query_row(
"SELECT COUNT(*) > 0 FROM agents WHERE id = 'admin'",
[],
|row| row.get(0),
).map_err(|e| format!("Query error: {e}"))?;
if !exists {
db.execute(
"INSERT INTO agents (id, name, role, group_id, tier, status) \
VALUES ('admin', 'Operator', 'admin', ?, 0, 'active')",
params![group_id],
).map_err(|e| format!("Insert error: {e}"))?;
}
// Ensure admin has bidirectional contacts with ALL agents in the group
let mut stmt = db.prepare(
"SELECT id FROM agents WHERE group_id = ? AND id != 'admin'"
).map_err(|e| format!("Query error: {e}"))?;
let agent_ids: Vec<String> = stmt.query_map(params![group_id], |row| row.get(0))
.map_err(|e| format!("Query error: {e}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("Row error: {e}"))?;
drop(stmt);
for aid in &agent_ids {
db.execute(
"INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES ('admin', ?)",
params![aid],
).map_err(|e| format!("Insert error: {e}"))?;
db.execute(
"INSERT OR IGNORE INTO contacts (agent_id, contact_id) VALUES (?, 'admin')",
params![aid],
).map_err(|e| format!("Insert error: {e}"))?;
}
Ok(())
}
pub fn all_feed(group_id: &str, limit: i32) -> Result<Vec<BtmsgFeedMessage>, String> {
let db = open_db()?;
let mut stmt = db.prepare(
"SELECT m.id, m.from_agent, m.to_agent, m.content, m.created_at, m.reply_to, \
a1.name, a1.role, a2.name, a2.role \
FROM messages m \
JOIN agents a1 ON m.from_agent = a1.id \
JOIN agents a2 ON m.to_agent = a2.id \
WHERE m.group_id = ? \
ORDER BY m.created_at DESC LIMIT ?"
).map_err(|e| format!("Query error: {e}"))?;
let msgs = stmt.query_map(params![group_id, limit], |row| {
Ok(BtmsgFeedMessage {
id: row.get(0)?,
from_agent: row.get(1)?,
to_agent: row.get(2)?,
content: row.get(3)?,
created_at: row.get(4)?,
reply_to: row.get(5)?,
sender_name: row.get(6)?,
sender_role: row.get(7)?,
recipient_name: row.get(8)?,
recipient_role: row.get(9)?,
})
}).map_err(|e| format!("Query error: {e}"))?;
msgs.collect::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
}
pub fn mark_read_conversation(reader_id: &str, sender_id: &str) -> Result<(), String> {
let db = open_db()?;
db.execute(
"UPDATE messages SET read = 1 WHERE to_agent = ? AND from_agent = ? AND read = 0",
params![reader_id, sender_id],
).map_err(|e| format!("Update error: {e}"))?;
Ok(())
}
pub fn get_channels(group_id: &str) -> Result<Vec<BtmsgChannel>, String> {
let db = open_db()?;
let mut stmt = db.prepare(
"SELECT c.id, c.name, c.group_id, c.created_by, \
(SELECT COUNT(*) FROM channel_members cm WHERE cm.channel_id = c.id), \
c.created_at \
FROM channels c WHERE c.group_id = ? ORDER BY c.name"
).map_err(|e| format!("Query error: {e}"))?;
let channels = stmt.query_map(params![group_id], |row| {
Ok(BtmsgChannel {
id: row.get(0)?,
name: row.get(1)?,
group_id: row.get(2)?,
created_by: row.get(3)?,
member_count: row.get(4)?,
created_at: row.get(5)?,
})
}).map_err(|e| format!("Query error: {e}"))?;
channels.collect::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
}
pub fn get_channel_messages(channel_id: &str, limit: i32) -> Result<Vec<BtmsgChannelMessage>, String> {
let db = open_db()?;
let mut stmt = db.prepare(
"SELECT cm.id, cm.channel_id, cm.from_agent, cm.content, cm.created_at, \
a.name, a.role \
FROM channel_messages cm JOIN agents a ON cm.from_agent = a.id \
WHERE cm.channel_id = ? ORDER BY cm.created_at ASC LIMIT ?"
).map_err(|e| format!("Query error: {e}"))?;
let msgs = stmt.query_map(params![channel_id, limit], |row| {
Ok(BtmsgChannelMessage {
id: row.get(0)?,
channel_id: row.get(1)?,
from_agent: row.get(2)?,
content: row.get(3)?,
created_at: row.get(4)?,
sender_name: row.get(5)?,
sender_role: row.get(6)?,
})
}).map_err(|e| format!("Query error: {e}"))?;
msgs.collect::<Result<Vec<_>, _>>().map_err(|e| format!("Row error: {e}"))
}
pub fn send_channel_message(channel_id: &str, from_agent: &str, content: &str) -> Result<String, String> {
let db = open_db()?;
// Verify channel exists
let _: String = db.query_row(
"SELECT id FROM channels WHERE id = ?",
params![channel_id],
|row| row.get(0),
).map_err(|e| format!("Channel not found: {e}"))?;
// Check membership (admin bypasses)
let sender_tier: i32 = db.query_row(
"SELECT tier FROM agents WHERE id = ?",
params![from_agent],
|row| row.get(0),
).map_err(|e| format!("Sender not found: {e}"))?;
if sender_tier > 0 {
let is_member: bool = db.query_row(
"SELECT COUNT(*) > 0 FROM channel_members WHERE channel_id = ? AND agent_id = ?",
params![channel_id, from_agent],
|row| row.get(0),
).map_err(|e| format!("Membership check error: {e}"))?;
if !is_member {
return Err("Not a member of this channel".into());
}
}
let msg_id = uuid::Uuid::new_v4().to_string();
db.execute(
"INSERT INTO channel_messages (id, channel_id, from_agent, content) VALUES (?1, ?2, ?3, ?4)",
params![msg_id, channel_id, from_agent, content],
).map_err(|e| format!("Insert error: {e}"))?;
Ok(msg_id)
}
pub fn create_channel(name: &str, group_id: &str, created_by: &str) -> Result<String, String> {
let db = open_db()?;
let channel_id = uuid::Uuid::new_v4().to_string()[..8].to_string();
db.execute(
"INSERT INTO channels (id, name, group_id, created_by) VALUES (?1, ?2, ?3, ?4)",
params![channel_id, name, group_id, created_by],
).map_err(|e| format!("Insert error: {e}"))?;
// Auto-add creator as member
db.execute(
"INSERT INTO channel_members (channel_id, agent_id) VALUES (?1, ?2)",
params![channel_id, created_by],
).map_err(|e| format!("Insert error: {e}"))?;
Ok(channel_id)
}
pub fn add_channel_member(channel_id: &str, agent_id: &str) -> Result<(), String> {
let db = open_db()?;
db.execute(
"INSERT OR IGNORE INTO channel_members (channel_id, agent_id) VALUES (?1, ?2)",
params![channel_id, agent_id],
).map_err(|e| format!("Insert error: {e}"))?;
Ok(())
}

View file

@ -29,3 +29,43 @@ pub fn btmsg_send(from_agent: String, to_agent: String, content: String) -> Resu
pub fn btmsg_set_status(agent_id: String, status: String) -> Result<(), String> {
btmsg::set_status(&agent_id, &status)
}
#[tauri::command]
pub fn btmsg_ensure_admin(group_id: String) -> Result<(), String> {
btmsg::ensure_admin(&group_id)
}
#[tauri::command]
pub fn btmsg_all_feed(group_id: String, limit: i32) -> Result<Vec<btmsg::BtmsgFeedMessage>, String> {
btmsg::all_feed(&group_id, limit)
}
#[tauri::command]
pub fn btmsg_mark_read(reader_id: String, sender_id: String) -> Result<(), String> {
btmsg::mark_read_conversation(&reader_id, &sender_id)
}
#[tauri::command]
pub fn btmsg_get_channels(group_id: String) -> Result<Vec<btmsg::BtmsgChannel>, String> {
btmsg::get_channels(&group_id)
}
#[tauri::command]
pub fn btmsg_channel_messages(channel_id: String, limit: i32) -> Result<Vec<btmsg::BtmsgChannelMessage>, String> {
btmsg::get_channel_messages(&channel_id, limit)
}
#[tauri::command]
pub fn btmsg_channel_send(channel_id: String, from_agent: String, content: String) -> Result<String, String> {
btmsg::send_channel_message(&channel_id, &from_agent, &content)
}
#[tauri::command]
pub fn btmsg_create_channel(name: String, group_id: String, created_by: String) -> Result<String, String> {
btmsg::create_channel(&name, &group_id, &created_by)
}
#[tauri::command]
pub fn btmsg_add_channel_member(channel_id: String, agent_id: String) -> Result<(), String> {
btmsg::add_channel_member(&channel_id, &agent_id)
}

View file

@ -131,6 +131,14 @@ pub fn run() {
commands::btmsg::btmsg_history,
commands::btmsg::btmsg_send,
commands::btmsg::btmsg_set_status,
commands::btmsg::btmsg_ensure_admin,
commands::btmsg::btmsg_all_feed,
commands::btmsg::btmsg_mark_read,
commands::btmsg::btmsg_get_channels,
commands::btmsg::btmsg_channel_messages,
commands::btmsg::btmsg_channel_send,
commands::btmsg::btmsg_create_channel,
commands::btmsg::btmsg_add_channel_member,
// Misc
commands::misc::cli_get_group,
commands::misc::open_url,