activate agents

This commit is contained in:
jaberjaber23
2026-05-12 14:54:22 +03:00
parent a422058049
commit 68bde60fac
5 changed files with 229 additions and 1 deletions
+6
View File
@@ -199,6 +199,12 @@ pub async fn build_router(
"/api/agents/{id}/start",
axum::routing::post(routes::restart_agent),
)
.route(
// Issue #890 — alias so dashboards and external orchestrators can
// wake an inactive agent via a verb that matches the agent_activate tool.
"/api/agents/{id}/activate",
axum::routing::post(routes::restart_agent),
)
.route(
"/api/agents/{id}/message",
axum::routing::post(routes::send_message),
+177
View File
@@ -3482,6 +3482,44 @@ impl OpenFangKernel {
))
}
/// Activate (wake up) an inactive agent — flips Suspended/Crashed/Created
/// state back to Running so it can receive messages and process events again.
///
/// Returns the agent's name on success. `Terminated` agents cannot be
/// activated (they have been removed from the registry). `Running` agents
/// are a no-op (returns name, last_active is refreshed).
///
/// See issue #890 — allows an orchestrator agent to wake other agents.
pub fn activate_agent(&self, agent_id: AgentId) -> KernelResult<String> {
let entry = self.registry.get(agent_id).ok_or_else(|| {
KernelError::OpenFang(OpenFangError::AgentNotFound(agent_id.to_string()))
})?;
if entry.state == AgentState::Terminated {
return Err(KernelError::OpenFang(OpenFangError::Internal(format!(
"Agent {} is Terminated and cannot be activated",
entry.name
))));
}
let was_state = entry.state;
let name = entry.name.clone();
drop(entry);
self.registry
.set_state(agent_id, AgentState::Running)
.map_err(KernelError::OpenFang)?;
info!(
agent = %name,
id = %agent_id,
previous_state = ?was_state,
"Agent activated"
);
Ok(name)
}
/// Kill an agent.
pub fn kill_agent(&self, agent_id: AgentId) -> KernelResult<()> {
let entry = self
@@ -6823,6 +6861,19 @@ impl KernelHandle for OpenFangKernel {
OpenFangKernel::kill_agent(self, id).map_err(|e| format!("Kill failed: {e}"))
}
fn activate_agent(&self, agent_id: &str) -> Result<String, String> {
// Accept UUID or human-readable name.
let id: AgentId = match agent_id.parse() {
Ok(id) => id,
Err(_) => self
.registry
.find_by_name(agent_id)
.map(|e| e.id)
.ok_or_else(|| format!("Agent not found: {agent_id}"))?,
};
OpenFangKernel::activate_agent(self, id).map_err(|e| format!("Activate failed: {e}"))
}
fn memory_store(&self, key: &str, value: serde_json::Value) -> Result<(), String> {
let agent_id = shared_memory_agent_id();
self.memory
@@ -7987,6 +8038,132 @@ mod tests {
kernel.shutdown();
}
// ----------------------------------------------------------------------
// Issue #890: activate_agent — wake up inactive agents
// ----------------------------------------------------------------------
#[test]
fn test_activate_agent_wakes_suspended_and_crashed() {
let tmp = tempfile::tempdir().unwrap();
let home_dir = tmp.path().join("openfang-kernel-activate-test");
std::fs::create_dir_all(&home_dir).unwrap();
let config = KernelConfig {
home_dir: home_dir.clone(),
data_dir: home_dir.join("data"),
..KernelConfig::default()
};
let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots");
// Suspended agent: should flip to Running.
let suspended = register_test_agent(&kernel, "sleepy");
kernel
.registry
.set_state(suspended, AgentState::Suspended)
.unwrap();
let name = kernel
.activate_agent(suspended)
.expect("activate suspended agent");
assert_eq!(name, "sleepy");
assert_eq!(
kernel.registry.get(suspended).unwrap().state,
AgentState::Running
);
// Crashed agent: should also flip to Running.
let crashed = register_test_agent(&kernel, "broken");
kernel
.registry
.set_state(crashed, AgentState::Crashed)
.unwrap();
kernel.activate_agent(crashed).expect("activate crashed");
assert_eq!(
kernel.registry.get(crashed).unwrap().state,
AgentState::Running
);
// Created (never-started) agent: should also flip to Running.
let created = register_test_agent(&kernel, "freshly-baked");
kernel
.registry
.set_state(created, AgentState::Created)
.unwrap();
kernel.activate_agent(created).expect("activate created");
assert_eq!(
kernel.registry.get(created).unwrap().state,
AgentState::Running
);
// Already-running agent: idempotent, stays Running, no error.
kernel.activate_agent(crashed).expect("idempotent activate");
assert_eq!(
kernel.registry.get(crashed).unwrap().state,
AgentState::Running
);
// Terminated agent: rejected.
let dead = register_test_agent(&kernel, "zombie");
kernel
.registry
.set_state(dead, AgentState::Terminated)
.unwrap();
assert!(
kernel.activate_agent(dead).is_err(),
"Terminated agents must not be revivable"
);
// Unknown agent ID: rejected.
assert!(kernel.activate_agent(AgentId::new()).is_err());
kernel.shutdown();
}
#[test]
fn test_activate_agent_handle_accepts_name_and_uuid() {
use openfang_runtime::kernel_handle::KernelHandle;
let tmp = tempfile::tempdir().unwrap();
let home_dir = tmp.path().join("openfang-kernel-activate-handle-test");
std::fs::create_dir_all(&home_dir).unwrap();
let config = KernelConfig {
home_dir: home_dir.clone(),
data_dir: home_dir.join("data"),
..KernelConfig::default()
};
let kernel = OpenFangKernel::boot_with_config(config).expect("kernel boots");
let agent = register_test_agent(&kernel, "worker");
kernel
.registry
.set_state(agent, AgentState::Suspended)
.unwrap();
// Wake by name.
let name = KernelHandle::activate_agent(&kernel, "worker").expect("activate by name");
assert_eq!(name, "worker");
assert_eq!(
kernel.registry.get(agent).unwrap().state,
AgentState::Running
);
// Put it back to sleep then wake by UUID string.
kernel
.registry
.set_state(agent, AgentState::Suspended)
.unwrap();
KernelHandle::activate_agent(&kernel, &agent.to_string()).expect("activate by uuid");
assert_eq!(
kernel.registry.get(agent).unwrap().state,
AgentState::Running
);
// Unknown name returns Err.
assert!(KernelHandle::activate_agent(&kernel, "ghost").is_err());
kernel.shutdown();
}
// ----------------------------------------------------------------------
// Issue #1069: sanitize_cron_job_name + shared-memory schedule migration
// ----------------------------------------------------------------------
@@ -43,6 +43,15 @@ pub trait KernelHandle: Send + Sync {
/// Kill an agent by ID.
fn kill_agent(&self, agent_id: &str) -> Result<(), String>;
/// Activate (wake up) an inactive agent by ID, flipping its state to Running.
/// Used by orchestrator agents to dispatch work to currently inactive agents
/// (Suspended, Crashed, or never-started). Terminated agents cannot be revived.
/// Returns the agent's name on success.
fn activate_agent(&self, agent_id: &str) -> Result<String, String> {
let _ = agent_id;
Err("Agent activation not available".to_string())
}
/// Store a value in shared memory (cross-agent accessible).
fn memory_store(&self, key: &str, value: serde_json::Value) -> Result<(), String>;
@@ -519,7 +519,7 @@ pub fn tool_category(name: &str) -> &'static str {
"memory_store" | "memory_recall" | "memory_delete" | "memory_list" => "Memory",
"agent_send" | "agent_spawn" | "agent_list" | "agent_kill" => "Agents",
"agent_send" | "agent_spawn" | "agent_list" | "agent_kill" | "agent_activate" => "Agents",
"image_describe" | "image_generate" | "audio_transcribe" | "tts_speak" => "Media",
@@ -581,6 +581,7 @@ pub fn tool_hint(name: &str) -> &'static str {
"agent_spawn" => "create a new agent",
"agent_list" => "list running agents",
"agent_kill" => "terminate an agent",
"agent_activate" => "wake up an inactive agent so it can receive work",
// Media
"image_describe" => "describe an image",
@@ -299,6 +299,7 @@ pub async fn execute_tool(
"agent_spawn" => tool_agent_spawn(input, kernel, caller_agent_id).await,
"agent_list" => tool_agent_list(kernel),
"agent_kill" => tool_agent_kill(input, kernel),
"agent_activate" => tool_agent_activate(input, kernel),
// Shared memory tools
"memory_store" => tool_memory_store(input, kernel),
@@ -694,6 +695,24 @@ pub fn builtin_tool_definitions() -> Vec<ToolDefinition> {
"required": ["agent_id"]
}),
},
ToolDefinition {
name: "agent_activate".to_string(),
description: "Activate (wake up) an inactive agent so it can receive messages \
and process events. Use this when agent_list shows an agent in a \
Suspended, Crashed, or Created state and you want to delegate work \
to it via agent_send. Terminated agents cannot be revived."
.to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"agent_id": {
"type": "string",
"description": "The target agent's UUID or human-readable name"
}
},
"required": ["agent_id"]
}),
},
// --- Shared memory tools ---
ToolDefinition {
name: "memory_store".to_string(),
@@ -1696,6 +1715,20 @@ fn tool_agent_kill(
Ok(format!("Agent {agent_id} killed successfully."))
}
fn tool_agent_activate(
input: &serde_json::Value,
kernel: Option<&Arc<dyn KernelHandle>>,
) -> Result<String, String> {
let kh = require_kernel(kernel)?;
let agent_id = input["agent_id"]
.as_str()
.ok_or("Missing 'agent_id' parameter")?;
let name = kh.activate_agent(agent_id)?;
Ok(format!(
"Agent '{name}' activated. It is now Running and ready to receive messages."
))
}
// ---------------------------------------------------------------------------
// Shared memory tools
// ---------------------------------------------------------------------------
@@ -3420,6 +3453,8 @@ mod tests {
assert!(names.contains(&"agent_spawn"));
assert!(names.contains(&"agent_list"));
assert!(names.contains(&"agent_kill"));
// Issue #890 — wake up inactive agents
assert!(names.contains(&"agent_activate"));
assert!(names.contains(&"memory_store"));
assert!(names.contains(&"memory_recall"));
// 6 collaboration tools