hand stop

This commit is contained in:
jaberjaber23
2026-05-12 15:37:02 +03:00
parent 5cc865e6e6
commit 505a8e8080
3 changed files with 117 additions and 8 deletions
+31
View File
@@ -7077,6 +7077,10 @@ pub async fn compact_session(
}
/// POST /api/agents/{id}/stop — Cancel an agent's current LLM run.
///
/// If the agent is owned by an active hand instance, the hand instance is
/// also deactivated. Otherwise the hand stays registered as `Active` and the
/// user cannot re-activate it via the wizard (issue #1164).
pub async fn stop_agent(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
@@ -7090,6 +7094,33 @@ pub async fn stop_agent(
)
}
};
// If this agent is the agent of an active hand instance, deactivate the
// hand entirely — which also kills the agent and cancels the run. This
// matches what users expect when they click Stop on a hand-owned agent.
if let Some(instance) = state.kernel.hand_registry.find_by_agent(agent_id) {
match state.kernel.deactivate_hand(instance.instance_id) {
Ok(()) => {
return (
StatusCode::OK,
Json(serde_json::json!({
"status": "ok",
"message": "Hand deactivated",
"hand_deactivated": true,
"hand_id": instance.hand_id,
"instance_id": instance.instance_id,
})),
);
}
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": format!("{e}")})),
);
}
}
}
match state.kernel.stop_agent_run(agent_id) {
Ok(true) => (
StatusCode::OK,
+23 -8
View File
@@ -1025,15 +1025,30 @@ async fn handle_command(
serde_json::json!({"type": "error", "content": format!("Compaction failed: {e}")})
}
},
"stop" => match state.kernel.stop_agent_run(agent_id) {
Ok(true) => {
serde_json::json!({"type": "command_result", "command": cmd, "message": "Run cancelled."})
"stop" => {
// If this agent is owned by an active hand instance, deactivate the
// hand entirely so the user can re-activate it (issue #1164).
if let Some(instance) = state.kernel.hand_registry.find_by_agent(agent_id) {
match state.kernel.deactivate_hand(instance.instance_id) {
Ok(()) => serde_json::json!({
"type": "command_result",
"command": cmd,
"message": format!("Hand '{}' deactivated.", instance.hand_id),
}),
Err(e) => serde_json::json!({"type": "error", "content": format!("Stop failed: {e}")}),
}
} else {
match state.kernel.stop_agent_run(agent_id) {
Ok(true) => {
serde_json::json!({"type": "command_result", "command": cmd, "message": "Run cancelled."})
}
Ok(false) => {
serde_json::json!({"type": "command_result", "command": cmd, "message": "No active run to cancel."})
}
Err(e) => serde_json::json!({"type": "error", "content": format!("Stop failed: {e}")}),
}
}
Ok(false) => {
serde_json::json!({"type": "command_result", "command": cmd, "message": "No active run to cancel."})
}
Err(e) => serde_json::json!({"type": "error", "content": format!("Stop failed: {e}")}),
},
}
"model" => {
if args.is_empty() {
if let Some(entry) = state.kernel.registry.get(agent_id) {
+63
View File
@@ -8125,6 +8125,69 @@ mod tests {
kernel.shutdown();
}
// ----------------------------------------------------------------------
// Issue #1164: Agent Stop on a hand-owned agent must also deactivate the
// hand instance, otherwise the hand stays Active and the user cannot
// re-activate it (wizard fails with 400 "Hand already active").
// ----------------------------------------------------------------------
#[test]
fn test_hand_owned_agent_stop_clears_hand_for_reactivation() {
let tmp = tempfile::tempdir().unwrap();
let home_dir = tmp.path().join("openfang-kernel-hand-stop-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");
// Activate a hand and grab its agent id (mirrors what the wizard does).
let instance = kernel
.activate_hand("lead", HashMap::new(), None)
.expect("lead hand should activate");
let agent_id = instance.agent_id.expect("lead hand agent id");
let first_instance_id = instance.instance_id;
// Sanity: hand is Active and re-activation is rejected.
assert!(kernel
.activate_hand("lead", HashMap::new(), None)
.is_err());
// Simulate what POST /api/agents/{id}/stop now does for a hand-owned
// agent: look up the instance and deactivate the hand (which also
// kills the agent and cancels any running task).
let owning = kernel
.hand_registry
.find_by_agent(agent_id)
.expect("active hand owning the agent");
assert_eq!(owning.instance_id, first_instance_id);
kernel
.deactivate_hand(owning.instance_id)
.expect("deactivate via stop path");
// The hand instance must be gone now — re-activation must succeed.
assert!(kernel.hand_registry.find_by_agent(agent_id).is_none());
let active: Vec<_> = kernel
.hand_registry
.list_instances()
.into_iter()
.filter(|i| i.hand_id == "lead")
.collect();
assert!(
active.is_empty(),
"no lead instances should remain after stop",
);
let second = kernel
.activate_hand("lead", HashMap::new(), None)
.expect("hand must be re-activatable after stop");
assert_ne!(second.instance_id, first_instance_id);
kernel.shutdown();
}
// ----------------------------------------------------------------------
// Issue #890: activate_agent — wake up inactive agents
// ----------------------------------------------------------------------