From 6eaecdd0f8af577a47bc33def6406adfef7f08f7 Mon Sep 17 00:00:00 2001 From: GanJiaKouN16 Date: Sat, 6 Jun 2026 14:15:42 +0800 Subject: [PATCH 1/3] feat: add reset password functionality to workspace UI Wire the existing GenerateResetLinkModal and PasswordResetLinkModal into the Actions dropdown in the workspace members table. - Add 'Reset password' menu item for workspace members (not self) - Add resetPassword API function in profile service - Show confirmation dialog before generating the reset link - Display the generated password reset link with copy functionality Closes #2572 --- .../WorkspaceManage/cellRenderers.tsx | 57 ++++++++++++++++++- web/oss/src/services/profile/index.ts | 14 +++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx b/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx index d689d57135..7be7d3befd 100644 --- a/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx +++ b/web/oss/src/components/pages/settings/WorkspaceManage/cellRenderers.tsx @@ -3,7 +3,7 @@ import {useState} from "react" import type {User} from "@agenta/shared/types" import {message} from "@agenta/ui/app-message" import {EditOutlined, MoreOutlined, SyncOutlined} from "@ant-design/icons" -import {ArrowClockwise, Trash} from "@phosphor-icons/react" +import {ArrowClockwise, Key, Trash} from "@phosphor-icons/react" import {Button, Dropdown, Input, Modal, Space, Tag, Tooltip, Typography} from "antd" import AlertPopup from "@/oss/components/AlertPopup/AlertPopup" @@ -12,7 +12,7 @@ import {isEmailInvitationsEnabled} from "@/oss/lib/helpers/isEE" import {useEntitlements} from "@/oss/lib/helpers/useEntitlements" import {snakeToTitle} from "@/oss/lib/helpers/utils" import {WorkspaceMember} from "@/oss/lib/Types" -import {updateUsername} from "@/oss/services/profile" +import {resetPassword, updateUsername} from "@/oss/services/profile" import { assignWorkspaceRole, removeFromWorkspace, @@ -23,6 +23,9 @@ import {useOrgData} from "@/oss/state/org" import {useProfileData} from "@/oss/state/profile" import {useWorkspaceRoles} from "@/oss/state/workspace" +import GenerateResetLinkModal from "./Modals/GenerateResetLinkModal" +import PasswordResetLinkModal from "./Modals/PasswordResetLinkModal" + export const Actions: React.FC<{ member: WorkspaceMember hidden?: boolean @@ -39,6 +42,10 @@ export const Actions: React.FC<{ const {refetch: refetchProfile} = useProfileData() const [renameOpen, setRenameOpen] = useState(false) const [renameValue, setRenameValue] = useState(user.username || "") + const [generateResetLinkOpen, setGenerateResetLinkOpen] = useState(false) + const [resetLinkOpen, setResetLinkOpen] = useState(false) + const [resetLink, setResetLink] = useState("") + const [resetLoading, setResetLoading] = useState(false) if (hidden && !selfMenu) return null @@ -90,6 +97,24 @@ export const Actions: React.FC<{ } } + const handleResetPassword = async () => { + setResetLoading(true) + try { + const link = await resetPassword(user.id) + setGenerateResetLinkOpen(false) + setResetLink(link) + setResetLinkOpen(true) + } catch (error: any) { + const detail = + error?.response?.data?.detail || + error?.message || + "Unable to generate reset password link" + message.error(detail) + } finally { + setResetLoading(false) + } + } + return ( <> , + onClick: (e: any) => { + e.domEvent.stopPropagation() + setGenerateResetLinkOpen(true) + }, + }, + ] + : []), { key: "remove", label: "Remove", @@ -165,6 +203,21 @@ export const Actions: React.FC<{ placeholder="New username" /> + + setGenerateResetLinkOpen(false)} + onOk={handleResetPassword} + confirmLoading={resetLoading} + /> + + setResetLinkOpen(false)} + /> ) } diff --git a/web/oss/src/services/profile/index.ts b/web/oss/src/services/profile/index.ts index 0b3816c710..0acfcedb48 100644 --- a/web/oss/src/services/profile/index.ts +++ b/web/oss/src/services/profile/index.ts @@ -56,3 +56,17 @@ export const changePassword = async (payload: { body: JSON.stringify(payload), }) } + +/** + * Generate a password reset link for a user (admin action). + * Returns the reset password link string. + */ +export const resetPassword = async (userId: string): Promise => { + const base = getBaseUrl() + const url = new URL("api/profile/reset-password", base) + url.searchParams.set("user_id", userId) + const data = await fetchJson(url, { + method: "POST", + }) + return data +} From 445fc3f700e0ac46c7c4e3fbb653039a56d8bbe6 Mon Sep 17 00:00:00 2001 From: GanJiaKouN16 Date: Sat, 6 Jun 2026 14:36:28 +0800 Subject: [PATCH 2/3] fix: add shouldIgnoreRowClick guard to unprotected tables Several tables with row-level click navigation were missing the shouldIgnoreRowClick guard, causing clicks on interactive elements (checkboxes, dropdowns, buttons) to accidentally trigger row navigation. Changes: - Consolidate shouldIgnoreRowClick with broader selector list (merges EvaluationRunsTablePOC's extra selectors: [role='button'], [role='menuitem'], [role='checkbox'], .ant-btn, etc.) - Export INTERACTIVE_ROW_SELECTORS constant for reuse - Add guard to ObservabilityTable (traces) - Add guard to SessionsTable - Add guard to PromptsPage - Add guard to TestcasesTableShell - Add guard to EntityTable - Replace partial data-ivt-stop-row-click check in ScenarioListView with full shouldIgnoreRowClick - Update useEntityTableState to use consolidated selectors - Remove duplicate shouldIgnoreRowClick from navigationActions.ts - Update EvaluationRunsTablePOC to import from shared utility Closes #3254 --- .../actions/navigationActions.ts | 10 ----- .../components/EvaluationRunsTable/index.tsx | 2 +- .../hooks/useTableManager.tsx | 42 ++++++++++++------- .../components/TestcasesTableShell.tsx | 8 +++- .../components/ObservabilityTable/index.tsx | 7 +++- .../components/SessionsTable/index.tsx | 7 +++- .../components/pages/prompts/PromptsPage.tsx | 6 ++- .../AnnotationSession/ScenarioListView.tsx | 4 +- .../src/shared/EntityTable.tsx | 7 +++- .../hooks/useEntityTableState.ts | 17 +++----- .../hooks/useTableManager.tsx | 42 ++++++++++++------- 11 files changed, 86 insertions(+), 66 deletions(-) diff --git a/web/oss/src/components/EvaluationRunsTablePOC/actions/navigationActions.ts b/web/oss/src/components/EvaluationRunsTablePOC/actions/navigationActions.ts index bf4160796b..8f1ac7dc66 100644 --- a/web/oss/src/components/EvaluationRunsTablePOC/actions/navigationActions.ts +++ b/web/oss/src/components/EvaluationRunsTablePOC/actions/navigationActions.ts @@ -1,5 +1,3 @@ -import type {MouseEvent} from "react" - import {message} from "@agenta/ui/app-message" import {getDefaultStore} from "jotai" import Router from "next/router" @@ -23,14 +21,6 @@ const getUrlState = (): URLState => store.get(urlAtom) as URLState const getActiveAppId = (): string | null => store.get(routerAppIdAtom) -export const shouldIgnoreRowClick = (event: MouseEvent) => { - const target = event.target as HTMLElement | null - if (!target) return false - const interactiveSelector = - "button, a, input, textarea, select, [role='button'], [role='menuitem'], [role='checkbox'], .ant-checkbox, .ant-checkbox-input, .ant-checkbox-inner, .ant-checkbox-wrapper, .ant-btn, .ant-select, .ant-dropdown-trigger" - return Boolean(target.closest(interactiveSelector)) -} - interface NavigateToRunParams { record: EvaluationRunTableRow scope: "app" | "project" diff --git a/web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsTable/index.tsx b/web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsTable/index.tsx index 381c42a3c5..e4aa5e1fd3 100644 --- a/web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsTable/index.tsx +++ b/web/oss/src/components/EvaluationRunsTablePOC/components/EvaluationRunsTable/index.tsx @@ -13,6 +13,7 @@ import {activePreviewProjectIdAtom} from "@/oss/components/EvalRunDetails/atoms/ import {clearAllMetricStatsCaches} from "@/oss/components/EvalRunDetails/atoms/runMetrics" import { InfiniteVirtualTableFeatureShell, + shouldIgnoreRowClick, type TableFeaturePagination, type TableScopeConfig, } from "@/oss/components/InfiniteVirtualTable" @@ -34,7 +35,6 @@ import { } from "@/oss/lib/onboarding" import {useQueryParamState} from "@/oss/state/appState" -import {shouldIgnoreRowClick} from "../../actions/navigationActions" import { evaluationRunsDeleteContextAtom, evaluationRunsTableFetchEnabledAtom, diff --git a/web/oss/src/components/InfiniteVirtualTable/hooks/useTableManager.tsx b/web/oss/src/components/InfiniteVirtualTable/hooks/useTableManager.tsx index be69f2bad5..8c38dce80f 100644 --- a/web/oss/src/components/InfiniteVirtualTable/hooks/useTableManager.tsx +++ b/web/oss/src/components/InfiniteVirtualTable/hooks/useTableManager.tsx @@ -27,26 +27,36 @@ import useTableExport from "./useTableExport" const dummySearchAtom = atom("") /** - * Helper to detect if a click event should be ignored for row navigation + * Default CSS selectors for interactive elements that should not trigger row navigation. + * Consolidated from all table implementations to ensure consistent click-through behavior. + */ +export const INTERACTIVE_ROW_SELECTORS = [ + "button", + "a", + "input", + "textarea", + "select", + "[role='button']", + "[role='menuitem']", + "[role='checkbox']", + "[data-interactive]", + ".ant-dropdown-trigger", + ".ant-checkbox-wrapper", + ".ant-checkbox", + ".ant-checkbox-input", + ".ant-checkbox-inner", + ".ant-btn", + ".ant-select", +].join(", ") + +/** + * Helper to detect if a click event should be ignored for row navigation. * Returns true if the click was on an interactive element (button, link, dropdown, etc.) */ export const shouldIgnoreRowClick = (event: MouseEvent): boolean => { const target = event.target as HTMLElement - - // Check if clicking on interactive elements - if ( - target.closest("button") || - target.closest("a") || - target.closest(".ant-dropdown-trigger") || - target.closest(".ant-checkbox-wrapper") || - target.closest(".ant-select") || - target.closest("input") || - target.closest("textarea") - ) { - return true - } - - return false + if (!target) return false + return Boolean(target.closest(INTERACTIVE_ROW_SELECTORS)) } /** Configuration for built-in search. When provided, the hook manages search state internally. */ diff --git a/web/oss/src/components/TestcasesTableNew/components/TestcasesTableShell.tsx b/web/oss/src/components/TestcasesTableNew/components/TestcasesTableShell.tsx index a046d6ec5c..a9663cd8d5 100644 --- a/web/oss/src/components/TestcasesTableNew/components/TestcasesTableShell.tsx +++ b/web/oss/src/components/TestcasesTableNew/components/TestcasesTableShell.tsx @@ -1,10 +1,11 @@ -import {useCallback, useMemo, useState} from "react" +import React, {useCallback, useMemo, useState} from "react" import { ColumnVisibilityMenuTrigger, defaultHeaderVariant, detectColumnTypes, InfiniteVirtualTableFeatureShell, + shouldIgnoreRowClick, type TableScopeConfig, type TypeChipConfig, useTypeChipFeature, @@ -758,7 +759,10 @@ export function TestcasesTableShell(props: TestcasesTableShellProps) { size: "small" as const, bordered: true, onRow: (record: TestcaseTableRow) => ({ - onClick: () => onRowClick(record), + onClick: (event: React.MouseEvent) => { + if (shouldIgnoreRowClick(event)) return + onRowClick(record) + }, className: "cursor-pointer hover:bg-gray-50", }), }), diff --git a/web/oss/src/components/pages/observability/components/ObservabilityTable/index.tsx b/web/oss/src/components/pages/observability/components/ObservabilityTable/index.tsx index d6a94663ae..9bf9056264 100644 --- a/web/oss/src/components/pages/observability/components/ObservabilityTable/index.tsx +++ b/web/oss/src/components/pages/observability/components/ObservabilityTable/index.tsx @@ -1,6 +1,6 @@ import {type Key, type ReactNode, useCallback, useEffect, useMemo, useState} from "react" -import {InfiniteVirtualTableFeatureShell} from "@agenta/ui/table" +import {InfiniteVirtualTableFeatureShell, shouldIgnoreRowClick} from "@agenta/ui/table" import type {TableFeaturePagination, TableScopeConfig} from "@agenta/ui/table" import {useAtomValue, useSetAtom, useStore} from "jotai" import dynamic from "next/dynamic" @@ -307,7 +307,10 @@ const ObservabilityTable = () => { sticky: true, style: {cursor: "pointer"}, onRow: (record, index) => ({ - onClick: () => handleTraceRowClick(record), + onClick: (event) => { + if (shouldIgnoreRowClick(event)) return + handleTraceRowClick(record) + }, "data-tour": index === 0 ? "trace-row" : undefined, }), }} diff --git a/web/oss/src/components/pages/observability/components/SessionsTable/index.tsx b/web/oss/src/components/pages/observability/components/SessionsTable/index.tsx index 0d15650646..66282cbf3a 100644 --- a/web/oss/src/components/pages/observability/components/SessionsTable/index.tsx +++ b/web/oss/src/components/pages/observability/components/SessionsTable/index.tsx @@ -1,6 +1,6 @@ import {useCallback, useEffect, useMemo, useState} from "react" -import {InfiniteVirtualTableFeatureShell} from "@agenta/ui/table" +import {InfiniteVirtualTableFeatureShell, shouldIgnoreRowClick} from "@agenta/ui/table" import type {TableFeaturePagination, TableScopeConfig} from "@agenta/ui/table" import {useAtomValue, useSetAtom} from "jotai" import dynamic from "next/dynamic" @@ -141,7 +141,10 @@ const SessionsTable: React.FC = () => { bordered: true, loading: isLoading && sessionIds.length === 0, onRow: (record) => ({ - onClick: () => openDrawer({sessionId: record.session_id}), + onClick: (event) => { + if (shouldIgnoreRowClick(event)) return + openDrawer({sessionId: record.session_id}) + }, style: {cursor: "pointer"}, }), }} diff --git a/web/oss/src/components/pages/prompts/PromptsPage.tsx b/web/oss/src/components/pages/prompts/PromptsPage.tsx index f7c12e303c..ff959eb488 100644 --- a/web/oss/src/components/pages/prompts/PromptsPage.tsx +++ b/web/oss/src/components/pages/prompts/PromptsPage.tsx @@ -12,6 +12,7 @@ import type { TableFeaturePagination, TableScopeConfig, } from "@agenta/ui/table" +import {shouldIgnoreRowClick} from "@agenta/ui/table" import {message} from "antd" import type {TableProps} from "antd/es/table" import {useAtomValue, useSetAtom} from "jotai" @@ -686,7 +687,10 @@ const PromptsPage = () => { scroll: {x: "max-content" as const}, expandable: tableExpandableConfig, onRow: (record: PromptsTableRow) => ({ - onClick: () => handleRowClick(record), + onClick: (event: React.MouseEvent) => { + if (shouldIgnoreRowClick(event)) return + handleRowClick(record) + }, className: "cursor-pointer", draggable: true, onDragStart: (event: any) => { diff --git a/web/packages/agenta-annotation-ui/src/components/AnnotationSession/ScenarioListView.tsx b/web/packages/agenta-annotation-ui/src/components/AnnotationSession/ScenarioListView.tsx index 3dec2aebb6..1d33dde571 100644 --- a/web/packages/agenta-annotation-ui/src/components/AnnotationSession/ScenarioListView.tsx +++ b/web/packages/agenta-annotation-ui/src/components/AnnotationSession/ScenarioListView.tsx @@ -38,6 +38,7 @@ import { EXPORT_RESOLVE_SKIP, InfiniteVirtualTableFeatureShell, createActionsColumn, + shouldIgnoreRowClick, type InfiniteVirtualTableRowSelection, type TableScopeConfig, type TableExportColumnContext, @@ -1674,8 +1675,7 @@ const ScenarioListView = memo(function ScenarioListView({ // Row click opens annotation drawer const handleRowClick = useCallback((_event: React.MouseEvent, record: ScenarioTableRow) => { - const target = _event.target as HTMLElement - if (target?.closest("[data-ivt-stop-row-click]")) return + if (shouldIgnoreRowClick(_event)) return setDrawerScenarioId(record.scenarioId) }, []) diff --git a/web/packages/agenta-entity-ui/src/shared/EntityTable.tsx b/web/packages/agenta-entity-ui/src/shared/EntityTable.tsx index e92044c6cf..40066616b9 100644 --- a/web/packages/agenta-entity-ui/src/shared/EntityTable.tsx +++ b/web/packages/agenta-entity-ui/src/shared/EntityTable.tsx @@ -52,6 +52,7 @@ import {bgColors, cn} from "@agenta/ui/styles" import { buildEntityColumns, InfiniteVirtualTableFeatureShell, + shouldIgnoreRowClick, type BuildEntityColumnsOptions, type RowHeightFeatureConfig, type TableScopeConfig, @@ -546,8 +547,10 @@ export function EntityTable< bordered: true, onRow: selectable ? (record) => ({ - onClick: () => - handleRowSelect(record.id, !selectedIdsSet.has(record.id)), + onClick: (event) => { + if (shouldIgnoreRowClick(event)) return + handleRowSelect(record.id, !selectedIdsSet.has(record.id)) + }, className: cn( "cursor-pointer", selectedIdsSet.has(record.id) && bgColors.subtle, diff --git a/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useEntityTableState.ts b/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useEntityTableState.ts index 4ddfe1df5e..37ab19f0aa 100644 --- a/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useEntityTableState.ts +++ b/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useEntityTableState.ts @@ -72,6 +72,8 @@ import type { } from "../paginated/createPaginatedEntityStore" import type {InfiniteTableRowBase} from "../types" +import {INTERACTIVE_ROW_SELECTORS} from "./useTableManager" + // ============================================================================ // TYPES // ============================================================================ @@ -182,19 +184,10 @@ export interface UseEntityTableStateResult { // ============================================================================ /** - * Default selectors for interactive elements that should not trigger row click + * Default selectors for interactive elements that should not trigger row click. + * Uses the consolidated selector string from useTableManager for consistency. */ -const DEFAULT_INTERACTIVE_SELECTORS = [ - "button", - "a", - ".ant-dropdown-trigger", - ".ant-checkbox-wrapper", - ".ant-select", - "input", - "textarea", - "[role='button']", - "[data-interactive]", -] +const DEFAULT_INTERACTIVE_SELECTORS = INTERACTIVE_ROW_SELECTORS.split(", ") // ============================================================================ // HOOK diff --git a/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useTableManager.tsx b/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useTableManager.tsx index 82aafa7963..bc8dde1690 100644 --- a/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useTableManager.tsx +++ b/web/packages/agenta-ui/src/InfiniteVirtualTable/hooks/useTableManager.tsx @@ -28,26 +28,36 @@ import useTableExport from "./useTableExport" const dummySearchAtom = atom("") /** - * Helper to detect if a click event should be ignored for row navigation + * Default CSS selectors for interactive elements that should not trigger row navigation. + * Consolidated from all table implementations to ensure consistent click-through behavior. + */ +export const INTERACTIVE_ROW_SELECTORS = [ + "button", + "a", + "input", + "textarea", + "select", + "[role='button']", + "[role='menuitem']", + "[role='checkbox']", + "[data-interactive]", + ".ant-dropdown-trigger", + ".ant-checkbox-wrapper", + ".ant-checkbox", + ".ant-checkbox-input", + ".ant-checkbox-inner", + ".ant-btn", + ".ant-select", +].join(", ") + +/** + * Helper to detect if a click event should be ignored for row navigation. * Returns true if the click was on an interactive element (button, link, dropdown, etc.) */ export const shouldIgnoreRowClick = (event: MouseEvent): boolean => { const target = event.target as HTMLElement - - // Check if clicking on interactive elements - if ( - target.closest("button") || - target.closest("a") || - target.closest(".ant-dropdown-trigger") || - target.closest(".ant-checkbox-wrapper") || - target.closest(".ant-select") || - target.closest("input") || - target.closest("textarea") - ) { - return true - } - - return false + if (!target) return false + return Boolean(target.closest(INTERACTIVE_ROW_SELECTORS)) } /** Configuration for built-in search. When provided, the hook manages search state internally. */ From 585c6adbd8bf1f94b232b615f32bb1f09e32ae0d Mon Sep 17 00:00:00 2001 From: GanJiaKouN16 Date: Sat, 6 Jun 2026 14:54:06 +0800 Subject: [PATCH 3/3] fix: surface provider error messages in evaluation table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The evaluation table was showing a generic 'too many requests' message instead of the actual provider error because: 1. executeViaFetch never checked for body-level errors on HTTP 200. The Python SDK can return HTTP 200 with a non-200 status.code embedded in the response body (WorkflowBatchResponse.status.code). This path was silently treated as success. 2. Error stacktrace/type/code were not propagated through the pipeline. Even when the HTTP error path was taken, only the message was extracted — the SDK's status.type, status.code, and status.stacktrace were dropped. Changes: - executeViaFetch: detect body-level errors on HTTP 200 by checking responseData.status.code !== 200 and return an error result - executeViaFetch: extract stacktrace (coercing string[] to string), type, and code from both HTTP-error and body-error paths - Add stacktrace and type to ExecutionResult, RunResult, and ExecuteWorkflowRevisionResult error shapes - runInvocationAction: pass stacktrace and type through to upsertStepResultWithInvocation - upsertStepResultWithInvocation: accept type field in error param No UI changes needed — InvocationCell already renders stepError.message and stepError.stacktrace when present; extractStepError already reads error.code, error.type, error.stacktrace from persisted step data. Closes #3324 --- .../atoms/runInvocationAction.ts | 6 ++- .../services/evaluations/invocations/api.ts | 2 +- .../agenta-entities/src/runnable/types.ts | 2 + .../src/executeWorkflowRevision.ts | 2 +- .../src/state/execution/executionRunner.ts | 46 ++++++++++++++++++- .../src/state/execution/types.ts | 2 +- 6 files changed, 54 insertions(+), 6 deletions(-) diff --git a/web/oss/src/components/EvalRunDetails/atoms/runInvocationAction.ts b/web/oss/src/components/EvalRunDetails/atoms/runInvocationAction.ts index 05f72ac6b0..7b618d912a 100644 --- a/web/oss/src/components/EvalRunDetails/atoms/runInvocationAction.ts +++ b/web/oss/src/components/EvalRunDetails/atoms/runInvocationAction.ts @@ -208,7 +208,11 @@ export const triggerRunInvocationAtom = atom( traceId: result.traceId ?? undefined, status: "failure", references, - error: {message: errorMessage}, + error: { + message: errorMessage, + ...(result.error?.stacktrace ? {stacktrace: result.error.stacktrace} : {}), + ...(result.error?.type ? {type: result.error.type} : {}), + }, }) await updateScenarioStatus(scenarioId, EvaluationStatus.FAILURE) diff --git a/web/oss/src/services/evaluations/invocations/api.ts b/web/oss/src/services/evaluations/invocations/api.ts index 7e91e8a437..6563126248 100644 --- a/web/oss/src/services/evaluations/invocations/api.ts +++ b/web/oss/src/services/evaluations/invocations/api.ts @@ -69,7 +69,7 @@ export const upsertStepResultWithInvocation = async ({ status: string references?: InvocationReferences outputs?: unknown - error?: {message: string; stacktrace?: string} + error?: {message: string; stacktrace?: string; type?: string} }): Promise => { const {projectId} = getProjectValues() diff --git a/web/packages/agenta-entities/src/runnable/types.ts b/web/packages/agenta-entities/src/runnable/types.ts index 0247546f57..ee79ec5e01 100644 --- a/web/packages/agenta-entities/src/runnable/types.ts +++ b/web/packages/agenta-entities/src/runnable/types.ts @@ -204,6 +204,8 @@ export interface ExecutionResult { error?: { message: string code?: string + type?: string + stacktrace?: string } trace?: TraceInfo metrics?: ExecutionMetrics diff --git a/web/packages/agenta-playground/src/executeWorkflowRevision.ts b/web/packages/agenta-playground/src/executeWorkflowRevision.ts index cf808fd035..85d5dcaba2 100644 --- a/web/packages/agenta-playground/src/executeWorkflowRevision.ts +++ b/web/packages/agenta-playground/src/executeWorkflowRevision.ts @@ -62,7 +62,7 @@ export interface ExecuteWorkflowRevisionResult { structuredOutput?: unknown traceId?: string | null spanId?: string | null - error?: {message: string; code?: string} + error?: {message: string; code?: string; type?: string; stacktrace?: string} } // ============================================================================ diff --git a/web/packages/agenta-playground/src/state/execution/executionRunner.ts b/web/packages/agenta-playground/src/state/execution/executionRunner.ts index 83339f1478..db14743358 100644 --- a/web/packages/agenta-playground/src/state/execution/executionRunner.ts +++ b/web/packages/agenta-playground/src/state/execution/executionRunner.ts @@ -187,7 +187,7 @@ interface ExecutionSessionLifecycleCallbacks { chainResults?: RunResult["chainResults"] }) => void onComplete: (payload: {result: Partial}) => void - onFail: (payload: {error: {message: string; code?: string}; traceId?: string | null}) => void + onFail: (payload: {error: {message: string; code?: string; type?: string; stacktrace?: string}; traceId?: string | null}) => void onCancel: () => void } @@ -671,6 +671,9 @@ async function executeViaFetch(params: { if (!response.ok) { const errorText = await response.text() let errorMessage = `Request failed with status ${response.status}` + let errorCode: string | undefined + let errorType: string | undefined + let errorStacktrace: string | undefined let traceId: string | null = null try { @@ -678,6 +681,10 @@ async function executeViaFetch(params: { traceId = extractTraceIdFromPayload(errorData) if (errorData?.status?.message) { errorMessage = errorData.status.message + errorCode = errorData.status.code?.toString() + errorType = errorData.status.type + const st = errorData.status.stacktrace + errorStacktrace = Array.isArray(st) ? st.join("\n") : st } else if (errorData?.detail?.message) { errorMessage = errorData.detail.message } else if (typeof errorData?.detail === "string") { @@ -692,13 +699,48 @@ async function executeViaFetch(params: { status: "error", startedAt, completedAt: new Date().toISOString(), - error: {message: errorMessage}, + error: { + message: errorMessage, + ...(errorCode ? {code: errorCode} : {}), + ...(errorType ? {type: errorType} : {}), + ...(errorStacktrace ? {stacktrace: errorStacktrace} : {}), + }, ...(traceId ? {trace: {id: traceId}} : {}), } } const responseData = await response.json() + // Check for body-level error status (SDK returns HTTP 200 with error in body). + // The Python SDK's WorkflowBatchResponse may embed a non-200 status.code + // inside the response body even when the HTTP status is 200. + const bodyStatus = responseData?.status + if (bodyStatus && typeof bodyStatus === "object" && bodyStatus.code && bodyStatus.code !== 200) { + const traceId = extractTraceIdFromPayload(responseData) + const spanId = extractSpanIdFromPayload(responseData) + const st = bodyStatus.stacktrace + return { + executionId, + status: "error", + startedAt, + completedAt: new Date().toISOString(), + error: { + message: bodyStatus.message || "Invocation failed", + ...(bodyStatus.code ? {code: bodyStatus.code.toString()} : {}), + ...(bodyStatus.type ? {type: bodyStatus.type} : {}), + ...(st ? {stacktrace: Array.isArray(st) ? st.join("\n") : st} : {}), + }, + ...(traceId + ? { + trace: { + id: traceId, + ...(spanId ? {spanId} : {}), + }, + } + : {}), + } + } + // Delegate response parsing to entity-level normalizer when provided. // Default: unwrap `data` field if present, extract `trace_id`. const normalized = normalizeResponse diff --git a/web/packages/agenta-playground/src/state/execution/types.ts b/web/packages/agenta-playground/src/state/execution/types.ts index 58e8309dce..df02967cef 100644 --- a/web/packages/agenta-playground/src/state/execution/types.ts +++ b/web/packages/agenta-playground/src/state/execution/types.ts @@ -165,7 +165,7 @@ export interface RunResult { /** Hash of result for comparison (optional) */ resultHash?: string | null /** Error details if status is "error" */ - error?: {message: string; code?: string} | null + error?: {message: string; code?: string; type?: string; stacktrace?: string} | null /** Timestamp when execution started (ms) */ startedAt?: number /** Timestamp when execution completed (ms) */