Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/lib/fakeBots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function botHandlers(fake: FakeBotloft): Pick<Handlers, BotMethods> {
color: color ?? AVATAR_PALETTE[index % AVATAR_PALETTE.length] ?? "#FF7A59",
paused: false,
permissionMode: "default",
agent: "claude",
model: model ?? "default",
modelInUse: null,
effort: "default",
Expand Down
17 changes: 15 additions & 2 deletions app/src/lib/protocol.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ export type BotState = "offline" | "launching" | "idle" | "busy" | "needs_approv
*/
export type PermissionMode = "default" | "accept_edits" | "plan" | "auto" | "bypass_permissions";

/**
* Which agent runs a bot (spec 30). Chosen when the bot is created.
*/
export type AgentKind = "claude" | "codex" | "agy";

/**
* Which Claude model a bot runs on (spec 7.4): Claude Code's `--model`
* aliases, or the account's default.
Expand Down Expand Up @@ -233,7 +238,11 @@ slug: string, role: string, instructions: string,
/**
* Avatar color, `#RRGGBB`.
*/
color: string, paused: boolean, permissionMode: PermissionMode, model: BotModel,
color: string, paused: boolean, permissionMode: PermissionMode,
/**
* The agent behind the bot.
*/
agent: AgentKind, model: BotModel,
/**
* The model id Claude Code reported when the bot last started a turn
* (`claude-opus-5-5`); `null` before its first turn.
Expand Down Expand Up @@ -446,7 +455,11 @@ color?: string,
/**
* The account's default when absent.
*/
model?: BotModel, };
model?: BotModel,
/**
* `claude` when absent.
*/
agent?: AgentKind, };

/**
* Fields left out stay unchanged.
Expand Down
1 change: 1 addition & 0 deletions crates/botloft-core/src/protocol/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ fn export_bindings() {
out.decl::<Crew>();
out.decl::<BotState>();
out.decl::<PermissionMode>();
out.decl::<AgentKind>();
out.decl::<BotModel>();
out.decl::<ActivityKind>();
out.decl::<Activity>();
Expand Down
6 changes: 5 additions & 1 deletion crates/botloft-core/src/protocol/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::fmt;

use serde::{Deserialize, Serialize};

use super::{BotModel, DeliveryBacklog, PermissionMode};
use super::{AgentKind, BotModel, DeliveryBacklog, PermissionMode};
use crate::ids::{BotId, CrewId};

/// Identifies the connecting app in `session.hello`.
Expand Down Expand Up @@ -235,6 +235,10 @@ pub struct BotsCreateParams {
#[serde(default)]
#[cfg_attr(test, ts(optional))]
pub model: Option<BotModel>,
/// `claude` when absent.
#[serde(default)]
#[cfg_attr(test, ts(optional))]
pub agent: Option<AgentKind>,
}

/// Fields left out stay unchanged.
Expand Down
21 changes: 21 additions & 0 deletions crates/botloft-core/src/protocol/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,25 @@ impl PermissionMode {
}
}

text_enum!(
/// Which agent runs a bot (spec 30). Chosen when the bot is created.
AgentKind, "agent" {
/// Claude Code.
Claude => "claude",
/// OpenAI's Codex CLI.
Codex => "codex",
/// Google's Antigravity CLI (`agy`).
Agy => "agy",
}
);

impl AgentKind {
/// Whether the daemon can run bots of this agent yet.
pub fn runs(self) -> bool {
matches!(self, Self::Claude)
}
}

text_enum!(
/// Which Claude model a bot runs on (spec 7.4): Claude Code's `--model`
/// aliases, or the account's default.
Expand Down Expand Up @@ -135,6 +154,8 @@ pub struct Bot {
pub color: String,
pub paused: bool,
pub permission_mode: PermissionMode,
/// The agent behind the bot.
pub agent: AgentKind,
pub model: BotModel,
/// The model id Claude Code reported when the bot last started a turn
/// (`claude-opus-5-5`); `null` before its first turn.
Expand Down
2 changes: 2 additions & 0 deletions crates/botloft-store/migrations/0026_bot_agent.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Which agent runs a bot (spec 30): `claude`, `codex` or `agy`.
ALTER TABLE bots ADD COLUMN agent TEXT NOT NULL DEFAULT 'claude';
11 changes: 7 additions & 4 deletions crates/botloft-store/src/bots.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! `bots` table.

use botloft_core::ids::{BotId, CrewId};
use botloft_core::protocol::{BotEffort, BotModel, ModelEffort, PermissionMode};
use botloft_core::protocol::{AgentKind, BotEffort, BotModel, ModelEffort, PermissionMode};
use rusqlite::{OptionalExtension, Row, params};

use crate::{Result, Store, cached_execute, cached_row, parse_column, unique_as_duplicate};
Expand All @@ -20,6 +20,7 @@ pub struct BotRecord {
pub color: String,
pub paused: bool,
pub permission_mode: PermissionMode,
pub agent: AgentKind,
pub model: BotModel,
/// What Claude Code last reported; written by [`Store::set_model_in_use`]
/// and left alone by [`Store::update_bot`].
Expand All @@ -33,7 +34,7 @@ pub struct BotRecord {
}

const COLUMNS: &str = "id, crew_id, name, handle, slug, role, instructions, color, paused, created_at, archived_at, \
permission_mode, model, model_in_use, effort, effort_default";
permission_mode, model, model_in_use, effort, effort_default, agent";

fn from_row(row: &Row<'_>) -> rusqlite::Result<BotRecord> {
Ok(BotRecord {
Expand All @@ -56,6 +57,7 @@ fn from_row(row: &Row<'_>) -> rusqlite::Result<BotRecord> {
effort_default: row
.get::<_, Option<String>>(15)?
.and_then(|level| level.parse().ok()),
agent: parse_column(row, 16)?,
})
}

Expand All @@ -65,7 +67,7 @@ impl Store {
.execute(
&format!(
"INSERT INTO bots ({COLUMNS}) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)"
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)"
),
params![
bot.id.as_str(),
Expand All @@ -83,7 +85,8 @@ impl Store {
bot.model.as_str(),
bot.model_in_use,
bot.effort.as_str(),
bot.effort_default.map(ModelEffort::as_str)
bot.effort_default.map(ModelEffort::as_str),
bot.agent.as_str()
],
)
.map_err(|err| unique_as_duplicate(err, "bot handle or slug"))?;
Expand Down
1 change: 1 addition & 0 deletions crates/botloft-store/src/bots/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ fn bot(crew: &CrewId, handle: &str, created_at: i64) -> BotRecord {
color: "#FF7A59".to_owned(),
paused: false,
permission_mode: PermissionMode::Default,
agent: AgentKind::Claude,
model: BotModel::Default,
model_in_use: None,
effort: BotEffort::Default,
Expand Down
1 change: 1 addition & 0 deletions crates/botloft-store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ pub(crate) mod tests {
color: "#FF7A59".to_owned(),
paused: false,
permission_mode: botloft_core::protocol::PermissionMode::Default,
agent: botloft_core::protocol::AgentKind::Claude,
model: botloft_core::protocol::BotModel::Default,
model_in_use: None,
effort: botloft_core::protocol::BotEffort::Default,
Expand Down
1 change: 1 addition & 0 deletions crates/botloft-store/src/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const MIGRATIONS: &[(u32, &str)] = &[
(23, include_str!("../migrations/0023_plan_share.sql")),
(24, include_str!("../migrations/0024_mcp_servers.sql")),
(25, include_str!("../migrations/0025_crew_color.sql")),
(26, include_str!("../migrations/0026_bot_agent.sql")),
];

/// Schema version after every migration has run.
Expand Down
15 changes: 14 additions & 1 deletion crates/botloftd/src/service/bots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use botloft_core::ids::{BotId, CrewId};
use botloft_core::protocol::{
Bot, BotEffort, BotIdParams, BotModel, BotState, BotsCreateParams, BotsListParams,
AgentKind, Bot, BotEffort, BotIdParams, BotModel, BotState, BotsCreateParams, BotsListParams,
BotsRestartParams, BotsSetPausedParams, BotsUpdateParams, Crew, PermissionMode,
};
use botloft_core::{avatar, now_ms, slug, validate};
Expand Down Expand Up @@ -38,6 +38,7 @@ pub fn create(daemon: &Daemon, params: BotsCreateParams) -> ApiResult<Bot> {
&params.instructions,
params.color,
params.model,
params.agent,
)?;
let store = daemon.store();
let crew = crews::active(&store, &params.crew_id)?;
Expand All @@ -53,6 +54,7 @@ pub(crate) struct NewBot {
pub role: String,
pub instructions: String,
pub color: Option<String>,
pub agent: AgentKind,
pub model: BotModel,
pub effort: BotEffort,
}
Expand All @@ -64,12 +66,21 @@ impl NewBot {
instructions: &str,
color: Option<String>,
model: Option<BotModel>,
agent: Option<AgentKind>,
) -> ApiResult<Self> {
let agent = agent.unwrap_or(AgentKind::Claude);
if !agent.runs() {
return Err(ApiError::validation(format!(
"bots that run on {} are not available yet",
agent.as_str()
)));
}
Ok(Self {
name: validate::name("name", name)?,
role: validate::role(role)?,
instructions: validate::instructions(instructions)?,
color: color.as_deref().map(parse_color).transpose()?,
agent,
model: model.unwrap_or(BotModel::Default),
effort: BotEffort::Default,
})
Expand Down Expand Up @@ -109,6 +120,7 @@ pub(crate) fn insert(
color,
paused: false,
permission_mode: PermissionMode::Default,
agent: new.agent,
model: new.model,
model_in_use: None,
effort: new.effort,
Expand Down Expand Up @@ -231,6 +243,7 @@ pub(crate) fn to_protocol(daemon: &Daemon, store: &Store, crew: &Crew, record: B
color: record.color,
paused: record.paused,
permission_mode: record.permission_mode,
agent: record.agent,
model: record.model,
model_in_use: record.model_in_use,
effort: record.effort,
Expand Down
1 change: 1 addition & 0 deletions crates/botloftd/src/service/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub fn add(daemon: &Daemon, params: CatalogAddParams) -> ApiResult<Bot> {
&template.instructions,
None,
Some(template.model),
None,
)?;
new.effort = template.effort;
let store = daemon.store();
Expand Down
11 changes: 10 additions & 1 deletion crates/botloftd/src/service/crews.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,16 @@ pub fn create(daemon: &Daemon, params: CrewsCreateParams) -> ApiResult<Crew> {

let lead = params
.lead
.map(|lead| NewBot::check(&lead.name, &lead.role, &lead.instructions, None, lead.model))
.map(|lead| {
NewBot::check(
&lead.name,
&lead.role,
&lead.instructions,
None,
lead.model,
None,
)
})
.transpose()?;

let mut crew = Crew {
Expand Down
1 change: 1 addition & 0 deletions crates/botloftd/src/tools/suggest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ fn check(suggestion: &Suggestion) -> Result<NewBot, String> {
&suggestion.instructions,
None,
model,
None,
)
.map_err(explain)
}
Expand Down
1 change: 1 addition & 0 deletions crates/botloftd/src/workspace/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ mod tests {
color: "#FF7A59".to_owned(),
paused: false,
permission_mode: botloft_core::protocol::PermissionMode::Default,
agent: botloft_core::protocol::AgentKind::Claude,
model: botloft_core::protocol::BotModel::Default,
model_in_use: None,
effort: botloft_core::protocol::BotEffort::Default,
Expand Down
1 change: 1 addition & 0 deletions crates/botloftd/src/workspace/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ mod tests {
color: "#FF7A59".to_owned(),
paused: false,
permission_mode: botloft_core::protocol::PermissionMode::Default,
agent: botloft_core::protocol::AgentKind::Claude,
model: botloft_core::protocol::BotModel::Default,
model_in_use: None,
effort: botloft_core::protocol::BotEffort::Default,
Expand Down
1 change: 1 addition & 0 deletions crates/botloftd/tests/common/supervised.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub async fn setup() -> Setup {
instructions: String::new(),
color: None,
model: None,
agent: None,
},
)
.expect("bot");
Expand Down
1 change: 1 addition & 0 deletions crates/botloftd/tests/crew_folders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fn bot(parts: &Parts, crew: &Crew) -> botloft_core::protocol::Bot {
instructions: String::new(),
color: None,
model: None,
agent: None,
},
)
.expect("bot")
Expand Down
2 changes: 2 additions & 0 deletions crates/botloftd/tests/routine_signals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ fn writer(s: &Setup) -> botloft_core::ids::BotId {
instructions: String::new(),
color: None,
model: None,
agent: None,
},
)
.expect("writer")
Expand Down Expand Up @@ -135,6 +136,7 @@ async fn only_enabled_routines_of_the_crew_hear_a_signal() {
instructions: String::new(),
color: None,
model: None,
agent: None,
},
)
.expect("bot");
Expand Down
32 changes: 32 additions & 0 deletions crates/botloftd/tests/rpc_crews_bots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,38 @@ async fn updates_refresh_the_rules_and_keep_the_bot_memory() {
assert_eq!(crew["paused"], true);
}

#[tokio::test]
async fn a_bot_has_an_agent_and_only_claude_runs_yet() {
let daemon = TestDaemon::start().await;
let mut app = daemon.session().await;
let crew = app
.call("crews.create", json!({ "name": "Docs" }))
.await
.expect("crew");
let make = |agent: serde_json::Value| json!({ "crewId": crew["id"], "name": "Scout", "role": "", "instructions": "", "agent": agent });
let bot = app
.call("bots.create", make(json!("claude")))
.await
.expect("claude bot");
assert_eq!(bot["agent"], "claude");
for other in ["codex", "agy"] {
let err = app
.call("bots.create", make(json!(other)))
.await
.expect_err("not available yet");
assert_eq!(err.code, VALIDATION);
assert!(err.message.contains("not available yet"), "{err:?}");
}
let plain = app
.call(
"bots.create",
json!({ "crewId": crew["id"], "name": "Writer", "role": "", "instructions": "" }),
)
.await
.expect("default bot");
assert_eq!(plain["agent"], "claude");
}

#[tokio::test]
async fn archiving_a_crew_archives_its_bots() {
let daemon = TestDaemon::start().await;
Expand Down
1 change: 1 addition & 0 deletions crates/botloftd/tests/supervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ async fn a_failed_spawn_backs_off_and_retries() {
instructions: String::new(),
color: None,
model: None,
agent: None,
};
let bot = bots::create(&daemon, params).expect("bot");
tokio::spawn(supervisor::run(Arc::clone(&daemon)));
Expand Down
1 change: 1 addition & 0 deletions crates/botloftd/tests/supervisor_launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ fn add_bot(daemon: &Daemon) -> BotId {
instructions: String::new(),
color: None,
model: None,
agent: None,
},
)
.expect("bot")
Expand Down
Loading
Loading