Stop execution

This commit is contained in:
Louis Knight-Webb
2025-06-16 20:45:34 -04:00
parent cba8da0816
commit a0aa00f6ba
4 changed files with 156 additions and 26 deletions

View File

@@ -34,13 +34,12 @@ impl Executor for EchoExecutor {
// For demonstration of streaming, we can use a shell command that outputs multiple lines // For demonstration of streaming, we can use a shell command that outputs multiple lines
let script = format!( let script = format!(
r#"echo "Starting task: {}" r#"echo "Starting task: {}"
for i in {{1..5}}; do for i in {{1..50}}; do
echo "Progress line $i" echo "Progress line $i"
sleep 1 sleep 1
done done
echo "Task completed: {}""#, echo "Task completed: {}""#,
task.title, task.title, task.title
task.title
); );
let child = Command::new("sh") let child = Command::new("sh")

View File

@@ -1,4 +1,5 @@
pub mod auth; pub mod auth;
pub mod execution_monitor;
pub mod executor; pub mod executor;
pub mod executors; pub mod executors;
pub mod models; pub mod models;

View File

@@ -281,6 +281,86 @@ pub async fn create_task_attempt_activity(
} }
} }
pub async fn stop_task_attempt(
_auth: AuthUser,
Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>,
Extension(pool): Extension<PgPool>,
Extension(app_state): Extension<crate::execution_monitor::AppState>
) -> Result<ResponseJson<ApiResponse<()>>, StatusCode> {
// Verify task attempt exists and belongs to the correct task
match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await {
Ok(false) => return Err(StatusCode::NOT_FOUND),
Err(e) => {
tracing::error!("Failed to check task attempt existence: {}", e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
Ok(true) => {}
}
// Find and stop the running execution
let mut stopped = false;
{
let mut executions = app_state.running_executions.lock().await;
let mut execution_id_to_remove = None;
// Find the execution for this attempt
for (exec_id, execution) in executions.iter_mut() {
if execution.task_attempt_id == attempt_id {
// Kill the process
match execution.child.kill().await {
Ok(_) => {
stopped = true;
execution_id_to_remove = Some(*exec_id);
tracing::info!("Stopped execution for task attempt {}", attempt_id);
break;
}
Err(e) => {
tracing::error!("Failed to kill process for attempt {}: {}", attempt_id, e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
}
}
}
// Remove the stopped execution from the map
if let Some(exec_id) = execution_id_to_remove {
executions.remove(&exec_id);
}
}
if !stopped {
return Ok(ResponseJson(ApiResponse {
success: true,
data: None,
message: Some("No running execution found for this attempt".to_string()),
}));
}
// Create a new activity record to mark as stopped
let activity_id = Uuid::new_v4();
let create_activity = CreateTaskAttemptActivity {
task_attempt_id: attempt_id,
status: Some(TaskAttemptStatus::Paused),
note: Some("Execution stopped by user".to_string()),
};
if let Err(e) = TaskAttemptActivity::create(
&pool,
&create_activity,
activity_id,
TaskAttemptStatus::Paused,
).await {
tracing::error!("Failed to create stopped activity: {}", e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
Ok(ResponseJson(ApiResponse {
success: true,
data: None,
message: Some("Task attempt stopped successfully".to_string()),
}))
}
pub fn tasks_router() -> Router { pub fn tasks_router() -> Router {
use axum::routing::{post, put, delete}; use axum::routing::{post, put, delete};
@@ -289,6 +369,7 @@ pub fn tasks_router() -> Router {
.route("/projects/:project_id/tasks/:task_id", get(get_task).put(update_task).delete(delete_task)) .route("/projects/:project_id/tasks/:task_id", get(get_task).put(update_task).delete(delete_task))
.route("/projects/:project_id/tasks/:task_id/attempts", get(get_task_attempts).post(create_task_attempt)) .route("/projects/:project_id/tasks/:task_id/attempts", get(get_task_attempts).post(create_task_attempt))
.route("/projects/:project_id/tasks/:task_id/attempts/:attempt_id/activities", get(get_task_attempt_activities).post(create_task_attempt_activity)) .route("/projects/:project_id/tasks/:task_id/attempts/:attempt_id/activities", get(get_task_attempt_activities).post(create_task_attempt_activity))
.route("/projects/:project_id/tasks/:task_id/attempts/:attempt_id/stop", post(stop_task_attempt))
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -75,6 +75,7 @@ export function TaskDetailsDialog({
const [activitiesLoading, setActivitiesLoading] = useState(false); const [activitiesLoading, setActivitiesLoading] = useState(false);
const [selectedExecutor, setSelectedExecutor] = useState<string>("echo"); const [selectedExecutor, setSelectedExecutor] = useState<string>("echo");
const [creatingAttempt, setCreatingAttempt] = useState(false); const [creatingAttempt, setCreatingAttempt] = useState(false);
const [stoppingAttempt, setStoppingAttempt] = useState(false);
// Edit mode state // Edit mode state
const [isEditMode, setIsEditMode] = useState(false); const [isEditMode, setIsEditMode] = useState(false);
@@ -83,6 +84,10 @@ export function TaskDetailsDialog({
const [editedStatus, setEditedStatus] = useState<TaskStatus>("todo"); const [editedStatus, setEditedStatus] = useState<TaskStatus>("todo");
const [savingTask, setSavingTask] = useState(false); const [savingTask, setSavingTask] = useState(false);
// Check if the selected attempt is currently running (latest activity is "inprogress")
const isAttemptRunning = selectedAttempt && attemptActivities.length > 0 &&
attemptActivities[0].status === "inprogress";
useEffect(() => { useEffect(() => {
if (isOpen && task) { if (isOpen && task) {
fetchTaskAttempts(task.id); fetchTaskAttempts(task.id);
@@ -236,6 +241,34 @@ export function TaskDetailsDialog({
} }
}; };
const stopTaskAttempt = async () => {
if (!task || !selectedAttempt) return;
try {
setStoppingAttempt(true);
const response = await makeAuthenticatedRequest(
`/api/projects/${projectId}/tasks/${task.id}/attempts/${selectedAttempt.id}/stop`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
}
);
if (response.ok) {
// Refresh the activities list to show the stopped status
fetchAttemptActivities(selectedAttempt.id);
} else {
onError("Failed to stop task attempt");
}
} catch (err) {
onError("Failed to stop task attempt");
} finally {
setStoppingAttempt(false);
}
};
return ( return (
<Dialog open={isOpen} onOpenChange={onOpenChange} className="max-w-7xl"> <Dialog open={isOpen} onOpenChange={onOpenChange} className="max-w-7xl">
<DialogContent className="max-h-[85vh] overflow-y-auto"> <DialogContent className="max-h-[85vh] overflow-y-auto">
@@ -513,31 +546,47 @@ export function TaskDetailsDialog({
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-xs text-muted-foreground"> <Label className="text-xs text-muted-foreground">
New Attempt Actions
</Label> </Label>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Select {isAttemptRunning && (
value={selectedExecutor} <Button
onValueChange={(value) => onClick={stopTaskAttempt}
setSelectedExecutor(value as "echo" | "claude") disabled={stoppingAttempt}
} size="sm"
> variant="destructive"
<SelectTrigger> className="w-full"
<SelectValue /> >
</SelectTrigger> {stoppingAttempt ? "Stopping..." : "Stop Execution"}
<SelectContent> </Button>
<SelectItem value="echo">Echo</SelectItem> )}
<SelectItem value="claude">Claude</SelectItem> <div className="space-y-2">
</SelectContent> <Label className="text-xs text-muted-foreground">
</Select> New Attempt
<Button </Label>
onClick={createNewAttempt} <Select
disabled={creatingAttempt} value={selectedExecutor}
size="sm" onValueChange={(value) =>
className="w-full" setSelectedExecutor(value as "echo" | "claude")
> }
{creatingAttempt ? "Creating..." : "Create Attempt"} >
</Button> <SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="echo">Echo</SelectItem>
<SelectItem value="claude">Claude</SelectItem>
</SelectContent>
</Select>
<Button
onClick={createNewAttempt}
disabled={creatingAttempt}
size="sm"
className="w-full"
>
{creatingAttempt ? "Creating..." : "Create Attempt"}
</Button>
</div>
</div> </div>
</div> </div>
</div> </div>