Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function SelectedRunView({
onClearSelectedRun,
banner,
}: Props) {
const { run, preset, isLoading, responseError, httpError } =
const { run, preset, isLoading, responseError, httpError, executionStuck } =
useSelectedRunView(agent.graph_id, runId);

const breakpoint = useBreakpoint();
Expand Down Expand Up @@ -73,6 +73,15 @@ export function SelectedRunView({
);
}

if (executionStuck) {
return (
<ErrorCard
hint="Execution stopped: no updates received. The run may be stuck."
context="run"
/>
);
}

if (isLoading && !run) {
return <LoadingSelectedContent agent={agent} />;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,64 @@

import { useGetV1GetExecutionDetails } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useGetV2GetASpecificPreset } from "@/app/api/__generated__/endpoints/presets/presets";
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";
import { okData } from "@/app/api/helpers";
import { useEffect, useRef, useState } from "react";
import {
EMPTY_EXECUTION_UPDATES_THRESHOLD,
isEmptyExecutionUpdate,
isPollingStatus,
} from "@/lib/executionPollingWatchdog";

export function useSelectedRunView(graphId: string, runId: string) {
const emptyUpdatesCountRef = useRef(0);
const stuckRef = useRef(false);
const setStuckRef = useRef<((stuck: boolean) => void) | null>(null);
const [executionStuck, setExecutionStuck] = useState(false);

useEffect(() => {
emptyUpdatesCountRef.current = 0;
stuckRef.current = false;
setExecutionStuck(false);
}, [graphId, runId]);

const executionQuery = useGetV1GetExecutionDetails(graphId, runId, {
query: {
refetchInterval: (q) => {
const isSuccess = q.state.data?.status === 200;
if (stuckRef.current) return false;

if (!isSuccess) return false;
const rawData = q.state.data;
if (!rawData) return false;

if (isEmptyExecutionUpdate(rawData)) {
emptyUpdatesCountRef.current += 1;
if (
emptyUpdatesCountRef.current >= EMPTY_EXECUTION_UPDATES_THRESHOLD
) {
stuckRef.current = true;
setStuckRef.current?.(true);
return false;
}
} else {
emptyUpdatesCountRef.current = 0;
setStuckRef.current?.(false);
}

const status =
q.state.data?.status === 200 ? q.state.data.data.status : undefined;
(rawData as { status?: number }).status === 200
? (rawData as { data?: { status?: string } }).data?.status
: undefined;

if (!status) return false;
if (
status === AgentExecutionStatus.RUNNING ||
status === AgentExecutionStatus.QUEUED ||
status === AgentExecutionStatus.INCOMPLETE ||
status === AgentExecutionStatus.REVIEW
)
return 1500;
if (isPollingStatus(status)) return 1500;
return false;
},
refetchIntervalInBackground: true,
refetchOnWindowFocus: false,
},
});

setStuckRef.current = setExecutionStuck;

const run = okData(executionQuery.data);
const status = executionQuery.data?.status;

Expand All @@ -54,5 +83,6 @@ export function useSelectedRunView(graphId: string, runId: string) {
isLoading: executionQuery.isLoading || presetQuery.isLoading,
responseError: executionQuery.error || presetQuery.error,
httpError,
executionStuck,
} as const;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { PendingReviewsList } from "@/components/organisms/PendingReviewsList/PendingReviewsList";
import { usePendingReviewsForExecution } from "@/hooks/usePendingReviews";
import { Button } from "@/components/atoms/Button/Button";
Expand All @@ -10,6 +10,11 @@ import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecut
import { okData } from "@/app/api/helpers";
import { useGraphStore } from "@/app/(platform)/build/stores/graphStore";
import { useShallow } from "zustand/react/shallow";
import {
EMPTY_EXECUTION_UPDATES_THRESHOLD,
isEmptyExecutionUpdate,
isPollingStatus,
} from "@/lib/executionPollingWatchdog";

interface FloatingReviewsPanelProps {
executionId?: string;
Expand All @@ -23,6 +28,13 @@ export function FloatingReviewsPanel({
className,
}: FloatingReviewsPanelProps) {
const [isOpen, setIsOpen] = useState(false);
const emptyUpdatesCountRef = useRef(0);
const stuckRef = useRef(false);

useEffect(() => {
emptyUpdatesCountRef.current = 0;
stuckRef.current = false;
}, [graphId, executionId]);

const { data: executionDetails } = useGetV1GetExecutionDetails(
graphId || "",
Expand All @@ -31,26 +43,30 @@ export function FloatingReviewsPanel({
query: {
enabled: !!(graphId && executionId),
select: okData,
// Poll while execution is in progress to detect status changes
refetchInterval: (q) => {
// Note: refetchInterval callback receives raw data before select transform
const rawData = q.state.data as
| { status: number; data?: { status?: string } }
| undefined;
if (rawData?.status !== 200) return false;
if (stuckRef.current) return false;

const rawData = q.state.data;
if (!rawData) return false;

if (isEmptyExecutionUpdate(rawData)) {
emptyUpdatesCountRef.current += 1;
if (
emptyUpdatesCountRef.current >= EMPTY_EXECUTION_UPDATES_THRESHOLD
) {
stuckRef.current = true;
return false;
}
} else {
emptyUpdatesCountRef.current = 0;
}

const status = rawData?.data?.status;
const status =
(rawData as { status?: number }).status === 200
? (rawData as { data?: { status?: string } }).data?.status
: undefined;
if (!status) return false;

// Poll every 2 seconds while running or in review
if (
status === AgentExecutionStatus.RUNNING ||
status === AgentExecutionStatus.QUEUED ||
status === AgentExecutionStatus.INCOMPLETE ||
status === AgentExecutionStatus.REVIEW
) {
return 2000;
}
if (isPollingStatus(status)) return 2000;
return false;
},
refetchIntervalInBackground: true,
Expand Down
27 changes: 27 additions & 0 deletions autogpt_platform/frontend/src/lib/executionPollingWatchdog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AgentExecutionStatus } from "@/app/api/__generated__/models/agentExecutionStatus";

export const EMPTY_EXECUTION_UPDATES_THRESHOLD = 40;

const POLLING_STATUSES = new Set<AgentExecutionStatus>([
AgentExecutionStatus.RUNNING,
AgentExecutionStatus.QUEUED,
AgentExecutionStatus.INCOMPLETE,
AgentExecutionStatus.REVIEW,
]);

export function isEmptyExecutionUpdate(rawData: unknown): boolean {
if (!rawData || typeof rawData !== "object" || !("status" in rawData))
return true;
if ((rawData as { status: unknown }).status !== 200) return false;
const data = (rawData as { data?: unknown }).data;
if (!data || typeof data !== "object" || Array.isArray(data)) return true;
const status = (data as { status?: string }).status;
if (!status || !POLLING_STATUSES.has(status)) return true;
const nodeExecutions = (data as { node_executions?: unknown[] })
.node_executions;
return !Array.isArray(nodeExecutions) || nodeExecutions.length === 0;
}

export function isPollingStatus(status: string | undefined): boolean {
return !!status && POLLING_STATUSES.has(status);
}
Loading