Task attempt 16132ff6-fd9b-4551-86b9-c6b161df4f57 - Final changes

This commit is contained in:
Louis Knight-Webb
2025-06-25 00:07:38 +01:00
parent 81434f52c1
commit bc82e4c189
5 changed files with 130 additions and 39 deletions

View File

@@ -0,0 +1,50 @@
{
"db_name": "SQLite",
"query": "SELECT \n taa.id as \"activity_id!: Uuid\",\n taa.execution_process_id as \"execution_process_id!: Uuid\",\n taa.status as \"status!: TaskAttemptStatus\",\n taa.note,\n taa.created_at as \"created_at!: DateTime<Utc>\",\n es.prompt\n FROM task_attempt_activities taa\n INNER JOIN execution_processes ep ON taa.execution_process_id = ep.id\n LEFT JOIN executor_sessions es ON es.execution_process_id = ep.id\n WHERE ep.task_attempt_id = $1\n ORDER BY taa.created_at ASC",
"describe": {
"columns": [
{
"name": "activity_id!: Uuid",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "execution_process_id!: Uuid",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "status!: TaskAttemptStatus",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "note",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "created_at!: DateTime<Utc>",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "prompt",
"ordinal": 5,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
true,
false,
true
]
},
"hash": "bd57e47989627ac5e9f44e83f4bacff590d357586b149c7c17ff0e8f349e7025"
}

View File

@@ -24,6 +24,17 @@ pub struct CreateTaskAttemptActivity {
pub note: Option<String>, pub note: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct TaskAttemptActivityWithPrompt {
pub id: Uuid,
pub execution_process_id: Uuid,
pub status: TaskAttemptStatus,
pub note: Option<String>,
pub created_at: DateTime<Utc>,
pub prompt: Option<String>, // From executor_session
}
impl TaskAttemptActivity { impl TaskAttemptActivity {
pub async fn find_by_execution_process_id( pub async fn find_by_execution_process_id(
pool: &SqlitePool, pool: &SqlitePool,
@@ -84,4 +95,40 @@ impl TaskAttemptActivity {
Ok(records.into_iter().map(|r| r.id).collect()) Ok(records.into_iter().map(|r| r.id).collect())
} }
/// Find activities for all execution processes in a task attempt, with executor session prompts
pub async fn find_with_prompts_by_task_attempt_id(
pool: &SqlitePool,
task_attempt_id: Uuid,
) -> Result<Vec<TaskAttemptActivityWithPrompt>, sqlx::Error> {
let records = sqlx::query!(
r#"SELECT
taa.id as "activity_id!: Uuid",
taa.execution_process_id as "execution_process_id!: Uuid",
taa.status as "status!: TaskAttemptStatus",
taa.note,
taa.created_at as "created_at!: DateTime<Utc>",
es.prompt
FROM task_attempt_activities taa
INNER JOIN execution_processes ep ON taa.execution_process_id = ep.id
LEFT JOIN executor_sessions es ON es.execution_process_id = ep.id
WHERE ep.task_attempt_id = $1
ORDER BY taa.created_at ASC"#,
task_attempt_id
)
.fetch_all(pool)
.await?;
Ok(records
.into_iter()
.map(|record| TaskAttemptActivityWithPrompt {
id: record.activity_id,
execution_process_id: record.execution_process_id,
status: record.status,
note: record.note,
created_at: record.created_at,
prompt: record.prompt,
})
.collect())
}
} }

View File

@@ -17,7 +17,7 @@ use crate::models::{
BranchStatus, CreateFollowUpAttempt, CreateTaskAttempt, TaskAttempt, TaskAttemptStatus, BranchStatus, CreateFollowUpAttempt, CreateTaskAttempt, TaskAttempt, TaskAttemptStatus,
WorktreeDiff, WorktreeDiff,
}, },
task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity}, task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity, TaskAttemptActivityWithPrompt},
ApiResponse, ApiResponse,
}; };
@@ -51,7 +51,7 @@ pub async fn get_task_attempts(
pub async fn get_task_attempt_activities( pub async fn get_task_attempt_activities(
Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>, Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>,
Extension(pool): Extension<SqlitePool>, Extension(pool): Extension<SqlitePool>,
) -> Result<ResponseJson<ApiResponse<Vec<TaskAttemptActivity>>>, StatusCode> { ) -> Result<ResponseJson<ApiResponse<Vec<TaskAttemptActivityWithPrompt>>>, StatusCode> {
// Verify task attempt exists and belongs to the correct task // Verify task attempt exists and belongs to the correct task
match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await { match TaskAttempt::exists_for_task(&pool, attempt_id, task_id, project_id).await {
Ok(false) => return Err(StatusCode::NOT_FOUND), Ok(false) => return Err(StatusCode::NOT_FOUND),
@@ -62,40 +62,8 @@ pub async fn get_task_attempt_activities(
Ok(true) => {} Ok(true) => {}
} }
// Get all execution processes for the task attempt // Get activities with prompts for the task attempt
let execution_processes = match TaskAttemptActivity::find_with_prompts_by_task_attempt_id(&pool, attempt_id).await {
match ExecutionProcess::find_by_task_attempt_id(&pool, attempt_id).await {
Ok(processes) => processes,
Err(e) => {
tracing::error!(
"Failed to fetch execution processes for attempt {}: {}",
attempt_id,
e
);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};
// Get activities for all execution processes
let mut all_activities = Vec::new();
for process in execution_processes {
match TaskAttemptActivity::find_by_execution_process_id(&pool, process.id).await {
Ok(mut activities) => all_activities.append(&mut activities),
Err(e) => {
tracing::error!(
"Failed to fetch activities for execution process {}: {}",
process.id,
e
);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
}
}
// Sort activities by created_at
all_activities.sort_by(|a, b| a.created_at.cmp(&b.created_at));
match Ok::<Vec<TaskAttemptActivity>, sqlx::Error>(all_activities) {
Ok(activities) => Ok(ResponseJson(ApiResponse { Ok(activities) => Ok(ResponseJson(ApiResponse {
success: true, success: true,
data: Some(activities), data: Some(activities),

View File

@@ -47,6 +47,7 @@ import type {
TaskStatus, TaskStatus,
TaskAttempt, TaskAttempt,
TaskAttemptActivity, TaskAttemptActivity,
TaskAttemptActivityWithPrompt,
TaskAttemptStatus, TaskAttemptStatus,
ApiResponse, ApiResponse,
TaskWithAttemptStatus, TaskWithAttemptStatus,
@@ -150,7 +151,7 @@ export function TaskDetailsPanel({
); );
// Combined attempt data state // Combined attempt data state
const [attemptData, setAttemptData] = useState<{ const [attemptData, setAttemptData] = useState<{
activities: TaskAttemptActivity[]; activities: TaskAttemptActivityWithPrompt[];
processes: ExecutionProcessSummary[]; processes: ExecutionProcessSummary[];
runningProcessDetails: Record<string, ExecutionProcess>; runningProcessDetails: Record<string, ExecutionProcess>;
}>({ }>({
@@ -421,7 +422,7 @@ export function TaskDetailsPanel({
]); ]);
if (activitiesResponse.ok && processesResponse.ok) { if (activitiesResponse.ok && processesResponse.ok) {
const activitiesResult: ApiResponse<TaskAttemptActivity[]> = const activitiesResult: ApiResponse<TaskAttemptActivityWithPrompt[]> =
await activitiesResponse.json(); await activitiesResponse.json();
const processesResult: ApiResponse<ExecutionProcessSummary[]> = const processesResult: ApiResponse<ExecutionProcessSummary[]> =
await processesResponse.json(); await processesResponse.json();
@@ -1160,7 +1161,30 @@ export function TaskDetailsPanel({
</div> </div>
</div> </div>
{/* Show stdio output for running processes */} {/* Show prompt for coding agent executions */}
{activity.prompt &&
(activity.status === "setuprunning" ||
activity.status === "setupcomplete" ||
activity.status === "setupfailed" ||
activity.status === "executorrunning" ||
activity.status === "executorcomplete" ||
activity.status === "executorfailed") && (
<div className="mt-2 mb-4">
<div className="p-3 bg-blue-50 dark:bg-blue-950/30 rounded-md border border-blue-200 dark:border-blue-800">
<div className="flex items-start gap-2 mb-2">
<Code className="h-4 w-4 text-blue-600 dark:text-blue-400 mt-0.5" />
<span className="text-sm font-medium text-blue-900 dark:text-blue-100">
Prompt
</span>
</div>
<pre className="text-sm text-blue-800 dark:text-blue-200 whitespace-pre-wrap break-words">
{activity.prompt}
</pre>
</div>
</div>
)}
{/* Show stdio output for running processes */}
{(activity.status === "setuprunning" || {(activity.status === "setuprunning" ||
activity.status === "executorrunning") && activity.status === "executorrunning") &&
attemptData.runningProcessDetails[ attemptData.runningProcessDetails[

View File

@@ -62,6 +62,8 @@ export type TaskAttemptActivity = { id: string, execution_process_id: string, st
export type CreateTaskAttemptActivity = { execution_process_id: string, status: TaskAttemptStatus | null, note: string | null, }; export type CreateTaskAttemptActivity = { execution_process_id: string, status: TaskAttemptStatus | null, note: string | null, };
export type TaskAttemptActivityWithPrompt = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, prompt: string | null, };
export type DirectoryEntry = { name: string, path: string, is_directory: boolean, is_git_repo: boolean, }; export type DirectoryEntry = { name: string, path: string, is_directory: boolean, is_git_repo: boolean, };
export type DiffChunkType = "Equal" | "Insert" | "Delete"; export type DiffChunkType = "Equal" | "Insert" | "Delete";