fix(frontend): agent activity graph names (#11233)

## Changes 🏗️

We weren't fetching all library agents, just the first 15... to compute
the agent map on the Agent Activity dropdown. We suspect that is causing
some agent executions coming as `Unknown agent`.

In this changes, I'm fetching all the library agents upfront ( _without
blocking page load_ ) and caching them on the browser, so we have all
the details to render the agent runs. This is re-used in the library as
well for fast initial load on the agents list page.

## Checklist 📋

### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
- [x] First request populates cache; subsequent identical requests hit
cache
- [x] Editing an agent invalidates relevant cache keys and serves fresh
data
  - [x] Different query params generate distinct cache entries
  - [x] Cache layer gracefully falls back to live data on errors
  - [x] 404 behavior for unknown agents unchanged

### For configuration changes:

None
This commit is contained in:
Ubbe
2025-10-27 20:08:21 +04:00
committed by GitHub
parent cbe0cee0fc
commit 9316100864
12 changed files with 228 additions and 73 deletions

View File

@@ -64,7 +64,7 @@ import { useRouter, usePathname, useSearchParams } from "next/navigation";
import RunnerUIWrapper, { RunnerUIWrapperRef } from "../RunnerUIWrapper";
import OttoChatWidget from "@/app/(platform)/build/components/legacy-builder/OttoChatWidget";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useCopyPaste } from "../../../../../../hooks/useCopyPaste";
import { useCopyPaste } from "../useCopyPaste";
import NewControlPanel from "@/app/(platform)/build/components/NewControlPanel/NewControlPanel";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { BuildActionBar } from "../BuildActionBar";

View File

@@ -0,0 +1,48 @@
import { getV2ListLibraryAgentsResponse } from "@/app/api/__generated__/endpoints/library/library";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
export function filterAgents(agents: LibraryAgent[], term?: string | null) {
const t = term?.trim().toLowerCase();
if (!t) return agents;
return agents.filter(
(a) =>
a.name.toLowerCase().includes(t) ||
a.description.toLowerCase().includes(t),
);
}
export function getInitialData(
cachedAgents: LibraryAgent[],
searchTerm: string | null,
pageSize: number,
) {
const filtered = filterAgents(
cachedAgents as unknown as LibraryAgent[],
searchTerm,
);
if (!filtered.length) {
return undefined;
}
const firstPageAgents: LibraryAgent[] = filtered.slice(0, pageSize);
const totalItems = filtered.length;
const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
const firstPage: getV2ListLibraryAgentsResponse = {
status: 200,
data: {
agents: firstPageAgents,
pagination: {
total_items: totalItems,
total_pages: totalPages,
current_page: 1,
page_size: pageSize,
},
} satisfies LibraryAgentResponse,
headers: new Headers(),
};
return { pageParams: [1], pages: [firstPage] };
}

View File

@@ -3,9 +3,13 @@
import { useGetV2ListLibraryAgentsInfinite } from "@/app/api/__generated__/endpoints/library/library";
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
import { useLibraryPageContext } from "../state-provider";
import { useLibraryAgentsStore } from "@/hooks/useLibraryAgents/store";
import { getInitialData } from "./helpers";
export const useLibraryAgentList = () => {
const { searchTerm, librarySort } = useLibraryPageContext();
const { agents: cachedAgents } = useLibraryAgentsStore();
const {
data: agents,
fetchNextPage,
@@ -21,6 +25,7 @@ export const useLibraryAgentList = () => {
},
{
query: {
initialData: getInitialData(cachedAgents, searchTerm, 8),
getNextPageParam: (lastPage) => {
const pagination = (lastPage.data as LibraryAgentResponse).pagination;
const isMore =

View File

@@ -1,4 +1,7 @@
import { usePostV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
import {
getGetV2ListLibraryAgentsQueryKey,
usePostV2AddMarketplaceAgent,
} from "@/app/api/__generated__/endpoints/library/library";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { useRouter } from "next/navigation";
import * as Sentry from "@sentry/nextjs";
@@ -6,6 +9,7 @@ import { useGetV2DownloadAgentFile } from "@/app/api/__generated__/endpoints/sto
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
import { analytics } from "@/services/analytics";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { useQueryClient } from "@tanstack/react-query";
interface UseAgentInfoProps {
storeListingVersionId: string;
@@ -15,6 +19,7 @@ export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
const { toast } = useToast();
const router = useRouter();
const { completeStep } = useOnboarding();
const queryClient = useQueryClient();
const {
mutateAsync: addMarketplaceAgentToLibrary,
@@ -46,6 +51,10 @@ export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
if (isAddingAgentFirstTime) {
completeStep("MARKETPLACE_ADD_AGENT");
await queryClient.invalidateQueries({
queryKey: getGetV2ListLibraryAgentsQueryKey(),
});
analytics.sendDatafastEvent("add_to_library", {
name: data.name,
id: data.id,

View File

@@ -5,7 +5,7 @@ import {
} from "@/app/api/__generated__/endpoints/auth/auth";
import { SettingsForm } from "@/app/(platform)/profile/(user)/settings/components/SettingsForm/SettingsForm";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { useTimezoneDetection } from "@/hooks/useTimezoneDetection";
import { useTimezoneDetection } from "@/app/(platform)/profile/(user)/settings/useTimezoneDetection";
import * as React from "react";
import SettingsLoading from "./loading";
import { redirect } from "next/navigation";
@@ -28,6 +28,7 @@ export default function SettingsPage() {
},
},
});
useTimezoneDetection(timezone);
const { user, isUserLoading } = useSupabase();

View File

@@ -1,26 +1,20 @@
import { useGetV1ListAllExecutions } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useGetV2ListLibraryAgents } from "@/app/api/__generated__/endpoints/library/library";
import BackendAPI from "@/lib/autogpt-server-api/client";
import type { GraphExecution, GraphID } from "@/lib/autogpt-server-api/types";
import { useCallback, useEffect, useState } from "react";
import * as Sentry from "@sentry/nextjs";
import { toast } from "sonner";
import {
NotificationState,
categorizeExecutions,
handleExecutionUpdate,
} from "./helpers";
type AgentInfoMap = Map<
string,
{ name: string; description: string; library_agent_id?: string }
>;
import { useLibraryAgents } from "@/hooks/useLibraryAgents/useLibraryAgents";
export function useAgentActivityDropdown() {
const [isOpen, setIsOpen] = useState(false);
const [api] = useState(() => new BackendAPI());
const { agentInfoMap } = useLibraryAgents();
const [notifications, setNotifications] = useState<NotificationState>({
activeExecutions: [],
@@ -30,13 +24,6 @@ export function useAgentActivityDropdown() {
});
const [isConnected, setIsConnected] = useState(false);
const [agentInfoMap, setAgentInfoMap] = useState<AgentInfoMap>(new Map());
const {
data: agents,
isSuccess: agentsSuccess,
error: agentsError,
} = useGetV2ListLibraryAgents();
const {
data: executions,
@@ -46,59 +33,6 @@ export function useAgentActivityDropdown() {
query: { select: (res) => (res.status === 200 ? res.data : null) },
});
// Create a map of library agents
useEffect(() => {
if (agentsError) {
Sentry.captureException(agentsError, {
tags: {
context: "library_agents_fetch",
},
});
toast.error("Failed to load agent information", {
description:
"There was a problem connecting to our servers. Agent activity may be limited.",
});
return;
}
if (agents && agentsSuccess) {
if (agents.status !== 200) {
Sentry.captureException(new Error("Failed to load library agents"), {
extra: {
status: agents.status,
error: agents.data,
},
});
toast.error("Invalid agent data received", {
description:
"The server returned invalid data. Agent activity may be limited.",
});
return;
}
const libraryAgents = agents.data;
if (!libraryAgents.agents || !libraryAgents.agents.length) return;
const agentMap = new Map<
string,
{ name: string; description: string; library_agent_id?: string }
>();
libraryAgents.agents.forEach((agent) => {
if (agent.graph_id && agent.id) {
agentMap.set(agent.graph_id, {
name: agent.name || `Agent ${agent.graph_id.slice(0, 8)}`,
description: agent.description || "",
library_agent_id: agent.id,
});
}
});
setAgentInfoMap(agentMap);
}
}, [agents, agentsSuccess, agentsError]);
// Handle real-time execution updates
const handleExecutionEvent = useCallback(
(execution: GraphExecution) => {
@@ -156,8 +90,8 @@ export function useAgentActivityDropdown() {
return {
...notifications,
isConnected,
isReady: executionsSuccess && agentsSuccess,
error: executionsError || agentsError,
isReady: executionsSuccess,
error: executionsError,
isOpen,
setIsOpen,
};

View File

@@ -32,6 +32,8 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
import { useQueryClient } from "@tanstack/react-query";
import { getGetV2ListLibraryAgentsQueryKey } from "@/app/api/__generated__/endpoints/library/library";
export default function useAgentGraph(
flowID?: GraphID,
@@ -44,6 +46,7 @@ export default function useAgentGraph(
const pathname = usePathname();
const searchParams = useSearchParams();
const api = useBackendAPI();
const queryClient = useQueryClient();
const [isScheduling, setIsScheduling] = useState(false);
const [savedAgent, setSavedAgent] = useState<Graph | null>(null);
@@ -755,6 +758,11 @@ export default function useAgentGraph(
setIsSaving(true);
try {
await _saveAgent();
await queryClient.invalidateQueries({
queryKey: getGetV2ListLibraryAgentsQueryKey(),
});
completeStep("BUILDER_SAVE_AGENT");
} catch (error) {
const errorMessage =

View File

@@ -0,0 +1,128 @@
import { create } from "zustand";
import * as Sentry from "@sentry/nextjs";
import { storage, Key } from "@/services/storage/local-storage";
import {
getV2ListLibraryAgents,
type getV2ListLibraryAgentsResponse,
} from "@/app/api/__generated__/endpoints/library/library";
import type { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
export type AgentInfo = LibraryAgent;
type AgentStore = {
agents: AgentInfo[];
lastUpdatedAt?: number;
isRefreshing: boolean;
error?: unknown;
loadFromCache: () => void;
refreshAll: () => Promise<void>;
};
type CachedAgents = {
agents: LibraryAgent[];
lastUpdatedAt: number;
};
async function fetchAllLibraryAgents() {
const pageSize = 100;
let page = 1;
const all: LibraryAgent[] = [];
let res: getV2ListLibraryAgentsResponse | undefined;
try {
res = await getV2ListLibraryAgents({ page, page_size: pageSize });
} catch (err) {
Sentry.captureException(err, { tags: { context: "library_agents_fetch" } });
throw err;
}
if (!res || res.status !== 200) return all;
const { agents, pagination } = res.data;
all.push(...agents);
const totalPages = pagination?.total_pages ?? 1;
for (page = 2; page <= totalPages; page += 1) {
try {
const next = await getV2ListLibraryAgents({ page, page_size: pageSize });
if (next.status === 200) {
all.push(...next.data.agents);
}
} catch (err) {
Sentry.captureException(err, {
tags: { context: "library_agents_fetch" },
});
}
}
return all;
}
function persistCache(cached: CachedAgents) {
try {
storage.set(Key.LIBRARY_AGENTS_CACHE, JSON.stringify(cached));
} catch (error) {
// Ignore cache failures
// eslint-disable-next-line no-console
console.error("Failed to persist library agents cache", error);
Sentry.captureException(error, {
tags: { context: "library_agents_cache_persist" },
});
}
}
function readCache(): CachedAgents | undefined {
try {
const raw = storage.get(Key.LIBRARY_AGENTS_CACHE);
if (!raw) return;
return JSON.parse(raw) as CachedAgents;
} catch {
return;
}
}
export const useLibraryAgentsStore = create<AgentStore>((set, get) => ({
agents: [],
lastUpdatedAt: undefined,
isRefreshing: false,
error: undefined,
loadFromCache: () => {
const cached = readCache();
if (cached?.agents?.length) {
set({ agents: cached.agents, lastUpdatedAt: cached.lastUpdatedAt });
}
},
refreshAll: async () => {
if (get().isRefreshing) return;
set({ isRefreshing: true, error: undefined });
try {
const agents = await fetchAllLibraryAgents();
const snapshot: CachedAgents = { agents, lastUpdatedAt: Date.now() };
persistCache(snapshot);
set({ agents, lastUpdatedAt: snapshot.lastUpdatedAt });
} catch (error) {
set({ error });
} finally {
set({ isRefreshing: false });
}
},
}));
export function buildAgentInfoMap(agents: AgentInfo[]) {
const map = new Map<
string,
{ name: string; description: string; library_agent_id?: string }
>();
agents.forEach((a) => {
if (a.graph_id && a.id) {
map.set(a.graph_id, {
name:
a.name || (a.graph_id ? `Agent ${a.graph_id.slice(0, 8)}` : "Agent"),
description: a.description || "",
library_agent_id: a.id,
});
}
});
return map;
}

View File

@@ -0,0 +1,21 @@
import { useEffect, useMemo } from "react";
import { buildAgentInfoMap, useLibraryAgentsStore } from "./store";
let initialized = false;
export function useLibraryAgents() {
const { agents, isRefreshing, lastUpdatedAt, loadFromCache, refreshAll } =
useLibraryAgentsStore();
useEffect(() => {
if (!initialized) {
loadFromCache();
void refreshAll();
initialized = true;
}
}, [loadFromCache, refreshAll]);
const agentInfoMap = useMemo(() => buildAgentInfoMap(agents), [agents]);
return { agents, agentInfoMap, isRefreshing, lastUpdatedAt };
}

View File

@@ -7,6 +7,7 @@ export enum Key {
COPIED_FLOW_DATA = "copied-flow-data",
SHEPHERD_TOUR = "shepherd-tour",
WALLET_LAST_SEEN_CREDITS = "wallet-last-seen-credits",
LIBRARY_AGENTS_CACHE = "library-agents-cache",
}
function get(key: Key) {