1
0
forked from boos/BoosAPI

chore: sync current workspace changes

This commit is contained in:
boos
2026-07-14 03:21:10 +08:00
parent 1512fa23ce
commit 557914fea8
745 changed files with 124917 additions and 11222 deletions
@@ -0,0 +1,22 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "agent_runs")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub session_id: String,
pub conversation_id: String,
pub status: String,
pub assistant_message_id: Option<String>,
pub error_message: Option<String>,
pub created_at: String,
pub updated_at: String,
pub finished_at: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
@@ -10,6 +10,12 @@ pub struct Model {
pub cwd: Option<String>,
pub permission_mode: String,
pub runtime_status: String,
pub current_run_id: Option<String>,
pub host_access_enabled: bool,
pub revision: i64,
pub context_version: i32,
pub last_provider_id: Option<String>,
pub last_model_id: Option<String>,
pub sdk_context_json: Option<String>,
pub sdk_context_backup_json: Option<String>,
pub total_tokens: i32,
@@ -40,5 +40,6 @@ pub mod retrieval_hits;
pub mod stored_files;
pub mod agent_sessions;
pub mod agent_runs;
pub use sea_orm;
@@ -27,3 +27,4 @@ pub mod skill;
pub mod stored_file;
pub mod tool_execution;
pub mod agent_session;
pub mod agent_run;
@@ -0,0 +1,105 @@
use crate::entity::agent_runs;
use crate::error::{BoosApiError, Result};
use sea_orm::*;
const TERMINAL_STATUSES: &[&str] = &["succeeded", "failed", "cancelled", "interrupted"];
fn now() -> String {
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string()
}
pub async fn create_agent_run(
db: &DatabaseConnection,
run_id: &str,
session_id: &str,
conversation_id: &str,
) -> Result<()> {
let timestamp = now();
agent_runs::ActiveModel {
id: Set(run_id.to_string()),
session_id: Set(session_id.to_string()),
conversation_id: Set(conversation_id.to_string()),
status: Set("starting".to_string()),
assistant_message_id: Set(None),
error_message: Set(None),
created_at: Set(timestamp.clone()),
updated_at: Set(timestamp),
finished_at: Set(None),
}
.insert(db)
.await?;
Ok(())
}
pub async fn update_agent_run_status(
db: &DatabaseConnection,
run_id: &str,
status: &str,
assistant_message_id: Option<&str>,
error_message: Option<&str>,
) -> Result<()> {
let timestamp = now();
let mut update = agent_runs::Entity::update_many()
.col_expr(agent_runs::Column::Status, Expr::value(status.to_string()))
.col_expr(
agent_runs::Column::UpdatedAt,
Expr::value(timestamp.clone()),
)
.filter(agent_runs::Column::Id.eq(run_id))
.filter(agent_runs::Column::Status.is_in([
"starting",
"running",
"waiting_approval",
"waiting_input",
"cancelling",
]));
if let Some(message_id) = assistant_message_id {
update = update.col_expr(
agent_runs::Column::AssistantMessageId,
Expr::value(Some(message_id.to_string())),
);
}
if let Some(error) = error_message {
update = update.col_expr(
agent_runs::Column::ErrorMessage,
Expr::value(Some(error.to_string())),
);
}
if TERMINAL_STATUSES.contains(&status) {
update = update.col_expr(agent_runs::Column::FinishedAt, Expr::value(Some(timestamp)));
}
let result = update.exec(db).await?;
if result.rows_affected != 1 {
return Err(BoosApiError::Validation(format!(
"Agent run {run_id} is no longer active"
)));
}
Ok(())
}
pub async fn interrupt_active_runs(db: &DatabaseConnection) -> Result<u64> {
let timestamp = now();
let result = agent_runs::Entity::update_many()
.col_expr(
agent_runs::Column::Status,
sea_orm::sea_query::Expr::value("interrupted"),
)
.col_expr(
agent_runs::Column::UpdatedAt,
sea_orm::sea_query::Expr::value(timestamp.clone()),
)
.col_expr(
agent_runs::Column::FinishedAt,
sea_orm::sea_query::Expr::value(Some(timestamp)),
)
.filter(agent_runs::Column::Status.is_in([
"starting",
"running",
"waiting_approval",
"waiting_input",
"cancelling",
]))
.exec(db)
.await?;
Ok(result.rows_affected)
}
@@ -12,6 +12,12 @@ fn model_to_agent_session(model: agent_sessions::Model) -> AgentSession {
cwd: model.cwd,
permission_mode: model.permission_mode,
runtime_status: model.runtime_status,
current_run_id: model.current_run_id,
host_access_enabled: model.host_access_enabled,
revision: model.revision,
context_version: model.context_version,
last_provider_id: model.last_provider_id,
last_model_id: model.last_model_id,
sdk_context_json: model.sdk_context_json,
sdk_context_backup_json: model.sdk_context_backup_json,
total_tokens: model.total_tokens,
@@ -55,6 +61,12 @@ pub async fn upsert_agent_session(
cwd: Set(cwd.map(|s| s.to_string())),
permission_mode: Set(permission_mode.unwrap_or("default").to_string()),
runtime_status: Set("idle".to_string()),
current_run_id: Set(None),
host_access_enabled: Set(false),
revision: Set(0),
context_version: Set(1),
last_provider_id: Set(None),
last_model_id: Set(None),
sdk_context_json: Set(None),
sdk_context_backup_json: Set(None),
total_tokens: Set(0),
@@ -99,6 +111,92 @@ pub async fn update_agent_session_status(
Ok(())
}
/// Atomically binds an idle session to a run. The in-process lease is acquired
/// before any await; this database guard prevents stale cleanup from taking
/// ownership of a newer run after a restart.
pub async fn begin_agent_run(
db: &DatabaseConnection,
id: &str,
run_id: &str,
provider_id: &str,
model_id: &str,
) -> Result<()> {
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
let result = agent_sessions::Entity::update_many()
.col_expr(agent_sessions::Column::RuntimeStatus, Expr::value("active"))
.col_expr(
agent_sessions::Column::CurrentRunId,
Expr::value(Some(run_id.to_string())),
)
.col_expr(
agent_sessions::Column::LastProviderId,
Expr::value(Some(provider_id.to_string())),
)
.col_expr(
agent_sessions::Column::LastModelId,
Expr::value(Some(model_id.to_string())),
)
.col_expr(
agent_sessions::Column::Revision,
Expr::col(agent_sessions::Column::Revision).add(1),
)
.col_expr(agent_sessions::Column::UpdatedAt, Expr::value(now))
.filter(agent_sessions::Column::Id.eq(id))
.filter(agent_sessions::Column::CurrentRunId.is_null())
.exec(db)
.await?;
if result.rows_affected != 1 {
return Err(BoosApiError::Validation(
"Agent session already has an active run".to_string(),
));
}
Ok(())
}
pub async fn update_agent_session_status_for_run(
db: &DatabaseConnection,
id: &str,
run_id: &str,
runtime_status: &str,
) -> Result<bool> {
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
let result = agent_sessions::Entity::update_many()
.col_expr(
agent_sessions::Column::RuntimeStatus,
Expr::value(runtime_status.to_string()),
)
.col_expr(agent_sessions::Column::UpdatedAt, Expr::value(now))
.filter(agent_sessions::Column::Id.eq(id))
.filter(agent_sessions::Column::CurrentRunId.eq(run_id))
.exec(db)
.await?;
Ok(result.rows_affected == 1)
}
pub async fn update_agent_session_host_access(
db: &DatabaseConnection,
conversation_id: &str,
enabled: bool,
) -> Result<AgentSession> {
let model = agent_sessions::Entity::find()
.filter(agent_sessions::Column::ConversationId.eq(conversation_id))
.one(db)
.await?
.ok_or_else(|| BoosApiError::NotFound(format!("AgentSession {conversation_id}")))?;
if model.current_run_id.is_some() {
return Err(BoosApiError::Validation(
"Cannot change host access while an agent is running".to_string(),
));
}
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
let next_revision = model.revision + 1;
let mut active: agent_sessions::ActiveModel = model.into();
active.host_access_enabled = Set(enabled);
active.revision = Set(next_revision);
active.updated_at = Set(now);
Ok(model_to_agent_session(active.update(db).await?))
}
/// Update the working directory of an agent session.
pub async fn update_agent_session_cwd(
db: &DatabaseConnection,
@@ -137,22 +235,26 @@ pub async fn update_agent_session_permission_mode(
Ok(())
}
/// Reset all running/waiting_approval sessions to idle (for app startup recovery).
/// Mark sessions interrupted on startup and release any stale run binding.
pub async fn reset_running_sessions(db: &DatabaseConnection) -> Result<u64> {
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
let result = agent_sessions::Entity::update_many()
.col_expr(
agent_sessions::Column::RuntimeStatus,
Expr::value("idle"),
Expr::value("interrupted"),
)
.col_expr(agent_sessions::Column::CurrentRunId, Expr::value(None::<String>))
.col_expr(
agent_sessions::Column::UpdatedAt,
Expr::value(now),
)
.filter(
Condition::any()
.add(agent_sessions::Column::RuntimeStatus.eq("active"))
.add(agent_sessions::Column::RuntimeStatus.eq("running"))
.add(agent_sessions::Column::RuntimeStatus.eq("waiting_approval")),
.add(agent_sessions::Column::RuntimeStatus.eq("waiting_approval"))
.add(agent_sessions::Column::RuntimeStatus.eq("waiting_input"))
.add(agent_sessions::Column::RuntimeStatus.eq("cancelling")),
)
.exec(db)
.await?;
@@ -164,12 +266,14 @@ pub async fn reset_running_sessions(db: &DatabaseConnection) -> Result<u64> {
pub async fn update_agent_session_after_query(
db: &DatabaseConnection,
id: &str,
run_id: &str,
runtime_status: &str,
sdk_context_json: Option<&str>,
tokens_delta: i32,
cost_delta: f64,
) -> Result<()> {
let model = agent_sessions::Entity::find_by_id(id)
.filter(agent_sessions::Column::CurrentRunId.eq(run_id))
.one(db)
.await?
.ok_or_else(|| BoosApiError::NotFound(format!("AgentSession {}", id)))?;
@@ -177,11 +281,13 @@ pub async fn update_agent_session_after_query(
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
let mut am: agent_sessions::ActiveModel = model.clone().into();
am.runtime_status = Set(runtime_status.to_string());
am.current_run_id = Set(None);
if let Some(ctx) = sdk_context_json {
am.sdk_context_json = Set(Some(ctx.to_string()));
}
am.total_tokens = Set(model.total_tokens + tokens_delta);
am.total_cost_usd = Set(model.total_cost_usd + cost_delta);
am.revision = Set(model.revision + 1);
am.updated_at = Set(now);
am.update(db).await?;
Ok(())
@@ -1450,6 +1450,12 @@ pub struct AgentSession {
pub cwd: Option<String>,
pub permission_mode: String,
pub runtime_status: String,
pub current_run_id: Option<String>,
pub host_access_enabled: bool,
pub revision: i64,
pub context_version: i32,
pub last_provider_id: Option<String>,
pub last_model_id: Option<String>,
pub sdk_context_json: Option<String>,
pub sdk_context_backup_json: Option<String>,
pub total_tokens: i32,