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:
parent
1331d094b3
commit
a158ed9544
19 changed files with 1918 additions and 39 deletions
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue