Merge branch 'dev' into swiftyos/automat-69-add-youtube-support-to-ayrshire

This commit is contained in:
Swifty
2025-07-31 10:59:15 +02:00
committed by GitHub
14 changed files with 752 additions and 687 deletions

View File

@@ -1141,3 +1141,88 @@ async def oauth_refresh_tokens(
if response.ok:
return OAuthTokenResponse.model_validate(response.json())
raise ValueError(f"Failed to refresh tokens: {response.status} {response.text}")
#################################################################
# Base Management
#################################################################
async def create_base(
credentials: Credentials,
workspace_id: str,
name: str,
tables: list[dict] = [
{
"description": "Default table",
"name": "Default table",
"fields": [
{
"name": "ID",
"type": "number",
"description": "Auto-incrementing ID field",
"options": {"precision": 0},
}
],
}
],
) -> dict:
"""
Create a new base in Airtable.
Args:
credentials: Airtable API credentials
workspace_id: The workspace ID where the base will be created
name: The name of the new base
tables: Optional list of table objects to create in the base
Returns:
dict: Response containing the created base information
"""
params: dict[str, Any] = {
"name": name,
"workspaceId": workspace_id,
}
if tables:
params["tables"] = tables
print(params)
response = await Requests().post(
"https://api.airtable.com/v0/meta/bases",
headers={
"Authorization": credentials.auth_header(),
"Content-Type": "application/json",
},
json=params,
)
return response.json()
async def list_bases(
credentials: Credentials,
offset: str | None = None,
) -> dict:
"""
List all bases that the authenticated user has access to.
Args:
credentials: Airtable API credentials
offset: Optional pagination offset
Returns:
dict: Response containing the list of bases
"""
params = {}
if offset:
params["offset"] = offset
response = await Requests().get(
"https://api.airtable.com/v0/meta/bases",
headers={"Authorization": credentials.auth_header()},
params=params,
)
return response.json()

View File

@@ -9,6 +9,7 @@ from ._api import (
TableFieldType,
WebhookFilters,
WebhookSpecification,
create_base,
create_field,
create_record,
create_table,
@@ -17,6 +18,7 @@ from ._api import (
delete_record,
delete_webhook,
get_record,
list_bases,
list_records,
list_webhook_payloads,
update_field,
@@ -38,7 +40,21 @@ async def test_create_update_table():
api_key=SecretStr(key),
)
postfix = uuid4().hex[:4]
base_id = "appSbaQLkcYiIOqux"
workspace_id = "wsphuHmfllg7V3Brd"
response = await create_base(credentials, workspace_id, "API Testing Base")
assert response is not None, f"Checking create base response: {response}"
assert (
response.get("id") is not None
), f"Checking create base response id: {response}"
base_id = response.get("id")
assert base_id is not None, f"Checking create base response id: {base_id}"
response = await list_bases(credentials)
assert response is not None, f"Checking list bases response: {response}"
assert "API Testing Base" in [
base.get("name") for base in response.get("bases", [])
], f"Checking list bases response bases: {response}"
table_name = f"test_table_{postfix}"
table_fields = [{"name": "test_field", "type": "singleLineText"}]
table = await create_table(credentials, base_id, table_name, table_fields)
@@ -73,7 +89,7 @@ async def test_invalid_field_type():
api_key=SecretStr(key),
)
postfix = uuid4().hex[:4]
base_id = "appSbaQLkcYiIOqux"
base_id = "appZPxegHEU3kDc1S"
table_name = f"test_table_{postfix}"
table_fields = [{"name": "test_field", "type": "notValid"}]
with pytest.raises(AssertionError):
@@ -91,7 +107,7 @@ async def test_create_and_update_field():
api_key=SecretStr(key),
)
postfix = uuid4().hex[:4]
base_id = "appSbaQLkcYiIOqux"
base_id = "appZPxegHEU3kDc1S"
table_name = f"test_table_{postfix}"
table_fields = [{"name": "test_field", "type": "singleLineText"}]
table = await create_table(credentials, base_id, table_name, table_fields)
@@ -133,7 +149,7 @@ async def test_record_management():
api_key=SecretStr(key),
)
postfix = uuid4().hex[:4]
base_id = "appSbaQLkcYiIOqux"
base_id = "appZPxegHEU3kDc1S"
table_name = f"test_table_{postfix}"
table_fields = [{"name": "test_field", "type": "singleLineText"}]
table = await create_table(credentials, base_id, table_name, table_fields)
@@ -261,7 +277,7 @@ async def test_webhook_management():
api_key=SecretStr(key),
)
postfix = uuid4().hex[:4]
base_id = "appSbaQLkcYiIOqux"
base_id = "appZPxegHEU3kDc1S"
table_name = f"test_table_{postfix}"
table_fields = [{"name": "test_field", "type": "singleLineText"}]
table = await create_table(credentials, base_id, table_name, table_fields)

View File

@@ -0,0 +1,122 @@
"""
Airtable base operation blocks.
"""
from typing import Optional
from backend.sdk import (
APIKeyCredentials,
Block,
BlockCategory,
BlockOutput,
BlockSchema,
CredentialsMetaInput,
SchemaField,
)
from ._api import create_base, list_bases
from ._config import airtable
class AirtableCreateBaseBlock(Block):
"""
Creates a new base in an Airtable workspace.
"""
class Input(BlockSchema):
credentials: CredentialsMetaInput = airtable.credentials_field(
description="Airtable API credentials"
)
workspace_id: str = SchemaField(
description="The workspace ID where the base will be created"
)
name: str = SchemaField(description="The name of the new base")
tables: list[dict] = SchemaField(
description="At least one table and field must be specified. Array of table objects to create in the base. Each table should have 'name' and 'fields' properties",
default=[
{
"description": "Default table",
"name": "Default table",
"fields": [
{
"name": "ID",
"type": "number",
"description": "Auto-incrementing ID field",
"options": {"precision": 0},
}
],
}
],
)
class Output(BlockSchema):
base_id: str = SchemaField(description="The ID of the created base")
tables: list[dict] = SchemaField(description="Array of table objects")
table: dict = SchemaField(description="A single table object")
def __init__(self):
super().__init__(
id="f59b88a8-54ce-4676-a508-fd614b4e8dce",
description="Create a new base in Airtable",
categories={BlockCategory.DATA},
input_schema=self.Input,
output_schema=self.Output,
)
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
data = await create_base(
credentials,
input_data.workspace_id,
input_data.name,
input_data.tables,
)
yield "base_id", data.get("id", None)
yield "tables", data.get("tables", [])
for table in data.get("tables", []):
yield "table", table
class AirtableListBasesBlock(Block):
"""
Lists all bases in an Airtable workspace that the user has access to.
"""
class Input(BlockSchema):
credentials: CredentialsMetaInput = airtable.credentials_field(
description="Airtable API credentials"
)
trigger: str = SchemaField(
description="Trigger the block to run - value is ignored", default="manual"
)
offset: str = SchemaField(
description="Pagination offset from previous request", default=""
)
class Output(BlockSchema):
bases: list[dict] = SchemaField(description="Array of base objects")
offset: Optional[str] = SchemaField(
description="Offset for next page (null if no more bases)", default=None
)
def __init__(self):
super().__init__(
id="4bd8d466-ed5d-4e44-8083-97f25a8044e7",
description="List all bases in Airtable",
categories={BlockCategory.DATA},
input_schema=self.Input,
output_schema=self.Output,
)
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
data = await list_bases(
credentials,
offset=input_data.offset if input_data.offset else None,
)
yield "bases", data.get("bases", [])
yield "offset", data.get("offset", None)

View File

@@ -0,0 +1,14 @@
from backend.sdk import APIKeyCredentials, Requests
async def llm_api_call(credentials: APIKeyCredentials, question: str) -> str:
params = {"appid": credentials.api_key.get_secret_value(), "input": question}
response = await Requests().get(
"https://www.wolframalpha.com/api/v1/llm-api", params=params
)
if not response.ok:
raise ValueError(f"API request failed: {response.status} {response.text()}")
answer = response.text() if response.text() else ""
return answer

View File

@@ -0,0 +1,50 @@
from backend.sdk import (
APIKeyCredentials,
Block,
BlockCategory,
BlockCostType,
BlockOutput,
BlockSchema,
CredentialsMetaInput,
ProviderBuilder,
SchemaField,
)
from ._api import llm_api_call
wolfram = (
ProviderBuilder("wolfram")
.with_api_key("WOLFRAM_APP_ID", "Wolfram Alpha App ID")
.with_base_cost(1, BlockCostType.RUN)
.build()
)
class AskWolframBlock(Block):
"""
Ask Wolfram Alpha a question.
"""
class Input(BlockSchema):
credentials: CredentialsMetaInput = wolfram.credentials_field(
description="Wolfram Alpha API credentials"
)
question: str = SchemaField(description="The question to ask")
class Output(BlockSchema):
answer: str = SchemaField(description="The answer to the question")
def __init__(self):
super().__init__(
id="b7710ce4-68ef-4e82-9a2f-f0b874ef9c7d",
description="Ask Wolfram Alpha a question",
categories={BlockCategory.SEARCH},
input_schema=self.Input,
output_schema=self.Output,
)
async def run(
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
) -> BlockOutput:
answer = await llm_api_call(credentials, input_data.question)
yield "answer", answer

File diff suppressed because it is too large Load Diff

View File

@@ -27,8 +27,6 @@ import type { CreatorDetails } from "../../models/creatorDetails";
import type { CreatorsResponse } from "../../models/creatorsResponse";
import type { GetV2GetMyAgentsParams } from "../../models/getV2GetMyAgentsParams";
import type { GetV2ListMySubmissionsParams } from "../../models/getV2ListMySubmissionsParams";
import type { GetV2ListStoreAgentsParams } from "../../models/getV2ListStoreAgentsParams";
@@ -1952,78 +1950,45 @@ export type getV2GetMyAgentsResponse200 = {
status: 200;
};
export type getV2GetMyAgentsResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type getV2GetMyAgentsResponseComposite =
| getV2GetMyAgentsResponse200
| getV2GetMyAgentsResponse422;
export type getV2GetMyAgentsResponseComposite = getV2GetMyAgentsResponse200;
export type getV2GetMyAgentsResponse = getV2GetMyAgentsResponseComposite & {
headers: Headers;
};
export const getGetV2GetMyAgentsUrl = (params?: GetV2GetMyAgentsParams) => {
const normalizedParams = new URLSearchParams();
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? "null" : value.toString());
}
});
const stringifiedParams = normalizedParams.toString();
return stringifiedParams.length > 0
? `/api/store/myagents?${stringifiedParams}`
: `/api/store/myagents`;
export const getGetV2GetMyAgentsUrl = () => {
return `/api/store/myagents`;
};
export const getV2GetMyAgents = async (
params?: GetV2GetMyAgentsParams,
options?: RequestInit,
): Promise<getV2GetMyAgentsResponse> => {
return customMutator<getV2GetMyAgentsResponse>(
getGetV2GetMyAgentsUrl(params),
{
...options,
method: "GET",
},
);
return customMutator<getV2GetMyAgentsResponse>(getGetV2GetMyAgentsUrl(), {
...options,
method: "GET",
});
};
export const getGetV2GetMyAgentsQueryKey = (
params?: GetV2GetMyAgentsParams,
) => {
return [`/api/store/myagents`, ...(params ? [params] : [])] as const;
export const getGetV2GetMyAgentsQueryKey = () => {
return [`/api/store/myagents`] as const;
};
export const getGetV2GetMyAgentsQueryOptions = <
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = HTTPValidationError,
>(
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customMutator>;
},
) => {
TError = unknown,
>(options?: {
query?: Partial<
UseQueryOptions<Awaited<ReturnType<typeof getV2GetMyAgents>>, TError, TData>
>;
request?: SecondParameter<typeof customMutator>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetV2GetMyAgentsQueryKey(params);
const queryKey = queryOptions?.queryKey ?? getGetV2GetMyAgentsQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getV2GetMyAgents>>
> = ({ signal }) => getV2GetMyAgents(params, { signal, ...requestOptions });
> = ({ signal }) => getV2GetMyAgents({ signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getV2GetMyAgents>>,
@@ -2035,13 +2000,12 @@ export const getGetV2GetMyAgentsQueryOptions = <
export type GetV2GetMyAgentsQueryResult = NonNullable<
Awaited<ReturnType<typeof getV2GetMyAgents>>
>;
export type GetV2GetMyAgentsQueryError = HTTPValidationError;
export type GetV2GetMyAgentsQueryError = unknown;
export function useGetV2GetMyAgents<
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = HTTPValidationError,
TError = unknown,
>(
params: undefined | GetV2GetMyAgentsParams,
options: {
query: Partial<
UseQueryOptions<
@@ -2066,9 +2030,8 @@ export function useGetV2GetMyAgents<
};
export function useGetV2GetMyAgents<
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = HTTPValidationError,
TError = unknown,
>(
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -2093,9 +2056,8 @@ export function useGetV2GetMyAgents<
};
export function useGetV2GetMyAgents<
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = HTTPValidationError,
TError = unknown,
>(
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -2116,9 +2078,8 @@ export function useGetV2GetMyAgents<
export function useGetV2GetMyAgents<
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = HTTPValidationError,
TError = unknown,
>(
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -2133,7 +2094,7 @@ export function useGetV2GetMyAgents<
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getGetV2GetMyAgentsQueryOptions(params, options);
const queryOptions = getGetV2GetMyAgentsQueryOptions(options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
@@ -2150,10 +2111,9 @@ export function useGetV2GetMyAgents<
*/
export const prefetchGetV2GetMyAgentsQuery = async <
TData = Awaited<ReturnType<typeof getV2GetMyAgents>>,
TError = HTTPValidationError,
TError = unknown,
>(
queryClient: QueryClient,
params?: GetV2GetMyAgentsParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -2165,7 +2125,7 @@ export const prefetchGetV2GetMyAgentsQuery = async <
request?: SecondParameter<typeof customMutator>;
},
): Promise<QueryClient> => {
const queryOptions = getGetV2GetMyAgentsQueryOptions(params, options);
const queryOptions = getGetV2GetMyAgentsQueryOptions(options);
await queryClient.prefetchQuery(queryOptions);

View File

@@ -2467,30 +2467,6 @@
"tags": ["v2", "store", "private"],
"summary": "Get my agents",
"operationId": "getV2Get my agents",
"parameters": [
{
"name": "page",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"default": 1,
"title": "Page"
}
},
{
"name": "page_size",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"default": 20,
"title": "Page Size"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
@@ -2499,14 +2475,6 @@
"schema": { "$ref": "#/components/schemas/MyAgentsResponse" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}

View File

@@ -75,17 +75,17 @@ export const PublishAgentPopout: React.FC<PublishAgentPopoutProps> = ({
const api = useBackendAPI();
// Use the auto-generated API hook
const { data, error, isLoading, refetch } = useGetV2GetMyAgents(
{
page: currentPage,
page_size: 20,
},
{
query: {
enabled: open, // Only fetch when the popout is open
const { data, error, isLoading, refetch } = useGetV2GetMyAgents({
request: {
params: {
page: currentPage,
page_size: 20,
},
},
);
query: {
enabled: open, // Only fetch when the popout is open
},
});
// Update allAgents when new data arrives
React.useEffect(() => {

View File

@@ -79,18 +79,11 @@ export function ActivityItem({ execution }: Props) {
return "Unknown";
}
const agentId = execution.library_agent_id || execution.graph_id;
const linkUrl = `/library/agents/${agentId}?executionId=${execution.id}`;
const withLibraryId = !!execution.library_agent_id;
const linkUrl = `/library/agents/${execution.library_agent_id}?executionId=${execution.id}`;
const withExecutionLink = execution.library_agent_id && execution.id;
if (!withLibraryId) return null;
return (
<Link
className="block cursor-pointer border-b border-slate-50 px-2 py-3 transition-colors last:border-b-0 hover:bg-lightGrey"
href={linkUrl}
role="button"
>
const content = (
<>
{/* Icon + Agent Name */}
<div className="flex items-center space-x-2">
{getStatusIcon()}
@@ -109,6 +102,20 @@ export function ActivityItem({ execution }: Props) {
{getTimeDisplay()}
</Text>
</div>
</>
);
return withExecutionLink ? (
<Link
className="block cursor-pointer border-b border-slate-50 px-2 py-3 transition-colors last:border-b-0 hover:bg-lightGrey"
href={linkUrl}
role="button"
>
{content}
</Link>
) : (
<div className="block border-b border-slate-50 px-2 py-3 last:border-b-0">
{content}
</div>
);
}

View File

@@ -1,17 +1,14 @@
import { useGetV1GetAllExecutions } from "@/app/api/__generated__/endpoints/graphs/graphs";
import { useGetV2GetMyAgents } from "@/app/api/__generated__/endpoints/store/store";
import { useGetV2ListLibraryAgents } from "@/app/api/__generated__/endpoints/library/library";
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
import { LibraryAgentResponse } from "@/app/api/__generated__/models/libraryAgentResponse";
import { MyAgentsResponse } from "@/app/api/__generated__/models/myAgentsResponse";
import { MyAgent } from "@/app/api/__generated__/models/myAgent";
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 {
NotificationState,
categorizeExecutions,
createAgentInfoMap,
handleExecutionUpdate,
} from "./helpers";
@@ -22,80 +19,55 @@ type AgentInfoMap = Map<
export function useAgentActivityDropdown() {
const [api] = useState(() => new BackendAPI());
const [notifications, setNotifications] = useState<NotificationState>({
activeExecutions: [],
recentCompletions: [],
recentFailures: [],
totalCount: 0,
});
const [isConnected, setIsConnected] = useState(false);
const [agentInfoMap, setAgentInfoMap] = useState<AgentInfoMap>(new Map());
// Get library agents using generated hook
const {
data: myAgentsResponse,
isLoading: isAgentsLoading,
data: agents,
isSuccess: agentsSuccess,
error: agentsError,
} = useGetV2GetMyAgents(
{},
{
// Enable query by default
query: {
enabled: true,
},
},
);
} = useGetV2ListLibraryAgents();
// Get library agents data to map graph_id to library_agent_id
const {
data: libraryAgentsResponse,
isLoading: isLibraryAgentsLoading,
error: libraryAgentsError,
} = useGetV2ListLibraryAgents(
{},
{
query: {
enabled: true,
},
},
);
// Get all executions using generated hook
const {
data: executionsResponse,
isLoading: isExecutionsLoading,
data: executions,
isSuccess: executionsSuccess,
error: executionsError,
} = useGetV1GetAllExecutions({
query: {
enabled: true,
},
});
} = useGetV1GetAllExecutions();
// Update agent info map when both agent data sources change
// Create a map of library agents
useEffect(() => {
if (myAgentsResponse?.data && libraryAgentsResponse?.data) {
const myAgents = myAgentsResponse.data as MyAgentsResponse;
const libraryAgents = libraryAgentsResponse.data as LibraryAgentResponse;
if (agents && agentsSuccess) {
// SafeCast: library agents loaded
const libraryAgents = agents.data as LibraryAgentResponse;
if (myAgents?.agents && libraryAgents?.agents) {
const agentMap = createAgentInfoMap(myAgents.agents);
if (!libraryAgents.agents || !libraryAgents.agents.length) return;
libraryAgents.agents.forEach((libraryAgent: LibraryAgent) => {
if (libraryAgent.graph_id && libraryAgent.id) {
const existingInfo = agentMap.get(libraryAgent.graph_id);
if (existingInfo) {
agentMap.set(libraryAgent.graph_id, {
...existingInfo,
library_agent_id: libraryAgent.id,
});
}
}
});
const agentMap = new Map<
string,
{ name: string; description: string; library_agent_id?: string }
>();
setAgentInfoMap(agentMap);
}
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);
}
}, [myAgentsResponse, libraryAgentsResponse]);
}, [agents, agentsSuccess]);
// Handle real-time execution updates
const handleExecutionEvent = useCallback(
@@ -109,41 +81,27 @@ export function useAgentActivityDropdown() {
// Process initial execution state when data loads
useEffect(() => {
if (
executionsResponse?.data &&
!isExecutionsLoading &&
agentInfoMap.size > 0
) {
const newNotifications = categorizeExecutions(
executionsResponse.data,
agentInfoMap,
);
setNotifications(newNotifications);
if (executions && executionsSuccess && agentInfoMap.size > 0) {
const notifications = categorizeExecutions(executions.data, agentInfoMap);
setNotifications(notifications);
}
}, [executionsResponse, isExecutionsLoading, agentInfoMap]);
}, [executions, executionsSuccess, agentInfoMap]);
// Initialize WebSocket connection for real-time updates
useEffect(() => {
if (!agentInfoMap.size) return;
const connectHandler = api.onWebSocketConnect(() => {
setIsConnected(true);
if (myAgentsResponse?.data) {
const myAgents = myAgentsResponse.data as MyAgentsResponse;
if (myAgents?.agents) {
myAgents.agents.forEach((agent: MyAgent) => {
api
.subscribeToGraphExecutions(agent.agent_id as GraphID)
.catch((error) => {
console.error(
`[AgentNotifications] Failed to subscribe to graph ${agent.agent_id}:`,
error,
);
});
agentInfoMap.forEach((_, graphId) => {
api.subscribeToGraphExecutions(graphId as GraphID).catch((error) => {
Sentry.captureException(error, {
tags: {
graphId,
},
});
}
}
});
});
});
const disconnectHandler = api.onWebSocketDisconnect(() => {
@@ -163,12 +121,12 @@ export function useAgentActivityDropdown() {
messageHandler();
api.disconnectWebSocket();
};
}, [api, handleExecutionEvent, myAgentsResponse]);
}, [api, handleExecutionEvent, agentInfoMap]);
return {
...notifications,
isConnected,
isLoading: isAgentsLoading || isExecutionsLoading || isLibraryAgentsLoading,
error: agentsError || executionsError || libraryAgentsError,
isReady: executionsSuccess && agentsSuccess,
error: executionsError || agentsError,
};
}