mirror of
https://github.com/RightNow-AI/openfang.git
synced 2026-07-30 06:32:17 +00:00
activate agents
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user