use super::{Capability, CapabilityLocalization, CapabilityStatus};
use crate::tools::{SpawnBackgroundTool, Tool};
pub const BACKGROUND_EXECUTION_CAPABILITY_ID: &str = "background_execution";
pub struct BackgroundExecutionCapability;
impl Capability for BackgroundExecutionCapability {
fn id(&self) -> &str {
BACKGROUND_EXECUTION_CAPABILITY_ID
}
fn name(&self) -> &str {
"Background Execution"
}
fn description(&self) -> &str {
"Run any background-capable built-in tool asynchronously via \
`spawn_background`. Auto-activated whenever the agent has a tool \
that declares `supports_background=true`."
}
fn localizations(&self) -> Vec<CapabilityLocalization> {
vec![CapabilityLocalization::text(
"uk",
"Фонове виконання",
"Запускайте будь-який вбудований інструмент із підтримкою фонового режиму асинхронно через `spawn_background`. Активується автоматично, щойно агент має інструмент, який оголошує `supports_background=true`.",
)]
}
fn status(&self) -> CapabilityStatus {
CapabilityStatus::Available
}
fn icon(&self) -> Option<&str> {
Some("zap")
}
fn category(&self) -> Option<&str> {
Some("Execution")
}
fn tools(&self) -> Vec<Box<dyn Tool>> {
vec![Box::new(SpawnBackgroundTool)]
}
}
pub struct BackgroundToolTaskExecutor;
#[async_trait::async_trait]
impl crate::session_task::TaskExecutor for BackgroundToolTaskExecutor {
fn kind(&self) -> &str {
crate::session_task::TASK_KIND_BACKGROUND_TOOL
}
fn can_reattach_task(&self, task: &crate::session_task::SessionTask) -> bool {
task.spec
.get("reattachable")
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
async fn start(
&self,
task: &crate::session_task::SessionTask,
context: &crate::traits::ToolContext,
) -> crate::error::Result<()> {
crate::tools::reattach_background_run(task, context).await
}
async fn cancel(
&self,
_task: &crate::session_task::SessionTask,
_context: &crate::traits::ToolContext,
) -> crate::error::Result<()> {
Ok(())
}
}
inventory::submit! {
crate::session_task::TaskExecutorPlugin {
executor: || std::sync::Arc::new(BackgroundToolTaskExecutor),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::capabilities::CapabilityRegistry;
#[test]
fn capability_metadata() {
let cap = BackgroundExecutionCapability;
assert_eq!(cap.id(), BACKGROUND_EXECUTION_CAPABILITY_ID);
assert_eq!(cap.id(), "background_execution");
assert_eq!(cap.name(), "Background Execution");
assert_eq!(cap.category(), Some("Execution"));
assert_eq!(cap.icon(), Some("zap"));
assert_eq!(cap.status(), CapabilityStatus::Available);
}
#[test]
fn capability_contributes_spawn_background_tool() {
let cap = BackgroundExecutionCapability;
let tools = cap.tools();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name(), "spawn_background");
}
#[test]
fn capability_tool_definition_carries_spawn_background() {
let cap = BackgroundExecutionCapability;
let defs = cap.tool_definitions();
assert_eq!(defs.len(), 1);
assert_eq!(defs[0].name(), "spawn_background");
}
#[test]
fn capability_registered_in_builtins() {
let registry = CapabilityRegistry::with_builtins();
let cap = registry
.get(BACKGROUND_EXECUTION_CAPABILITY_ID)
.expect("background_execution must be registered as a built-in capability");
assert_eq!(cap.id(), BACKGROUND_EXECUTION_CAPABILITY_ID);
assert_eq!(cap.tools().len(), 1);
}
#[test]
fn can_reattach_task_true_when_spec_reattachable_true() {
use crate::session_task::{SessionTask, SessionTaskState, TaskExecutor, TaskWakePolicy};
use chrono::Utc;
let exec = BackgroundToolTaskExecutor;
let task = SessionTask {
id: "t1".to_string(),
session_id: crate::SessionId::new(),
kind: crate::session_task::TASK_KIND_BACKGROUND_TOOL.to_string(),
display_name: "test".to_string(),
spec: serde_json::json!({ "tool": "get_current_time", "reattachable": true }),
state: SessionTaskState::Running,
state_detail: None,
progress: None,
input_request: None,
cancel_requested_at: None,
summary: None,
result_path: None,
artifacts: vec![],
error: None,
attempt: 1,
worker_id: None,
heartbeat_at: None,
links: crate::session_task::TaskLinks::default(),
wake_policy: TaskWakePolicy::Silent,
created_at: Utc::now(),
started_at: None,
finished_at: None,
updated_at: Utc::now(),
};
assert!(exec.can_reattach_task(&task));
}
#[test]
fn can_reattach_task_false_when_spec_reattachable_false() {
use crate::session_task::{SessionTask, SessionTaskState, TaskExecutor, TaskWakePolicy};
use chrono::Utc;
let exec = BackgroundToolTaskExecutor;
let task = SessionTask {
id: "t2".to_string(),
session_id: crate::SessionId::new(),
kind: crate::session_task::TASK_KIND_BACKGROUND_TOOL.to_string(),
display_name: "test".to_string(),
spec: serde_json::json!({ "tool": "some_side_effecting_tool", "reattachable": false }),
state: SessionTaskState::Running,
state_detail: None,
progress: None,
input_request: None,
cancel_requested_at: None,
summary: None,
result_path: None,
artifacts: vec![],
error: None,
attempt: 1,
worker_id: None,
heartbeat_at: None,
links: crate::session_task::TaskLinks::default(),
wake_policy: TaskWakePolicy::Silent,
created_at: Utc::now(),
started_at: None,
finished_at: None,
updated_at: Utc::now(),
};
assert!(!exec.can_reattach_task(&task));
}
#[test]
fn can_reattach_task_false_when_spec_has_no_reattachable_field() {
use crate::session_task::{SessionTask, SessionTaskState, TaskExecutor, TaskWakePolicy};
use chrono::Utc;
let exec = BackgroundToolTaskExecutor;
let task = SessionTask {
id: "t3".to_string(),
session_id: crate::SessionId::new(),
kind: crate::session_task::TASK_KIND_BACKGROUND_TOOL.to_string(),
display_name: "test".to_string(),
spec: serde_json::json!({ "tool": "old_task_without_reattachable_flag" }),
state: SessionTaskState::Running,
state_detail: None,
progress: None,
input_request: None,
cancel_requested_at: None,
summary: None,
result_path: None,
artifacts: vec![],
error: None,
attempt: 1,
worker_id: None,
heartbeat_at: None,
links: crate::session_task::TaskLinks::default(),
wake_policy: TaskWakePolicy::Silent,
created_at: Utc::now(),
started_at: None,
finished_at: None,
updated_at: Utc::now(),
};
assert!(!exec.can_reattach_task(&task));
}
}